From 653e2c22cf4cdebdfae7c562b62bedc904f5a1fa Mon Sep 17 00:00:00 2001 From: rykuno Date: Mon, 27 May 2024 12:38:59 -0500 Subject: [PATCH] Removed fragmented controllers in favor of hono's native chaining --- README.md | 45 ++++-- src/app.d.ts | 4 +- src/hooks.server.ts | 7 +- .../server/api/controllers/iam.controller.ts | 144 +++++++++--------- src/lib/server/api/dtos/register-email.dto.ts | 18 +++ src/lib/server/api/dtos/signin-email.dto.ts | 18 +++ src/lib/server/api/dtos/update-email.dto.ts | 18 +++ src/lib/server/api/dtos/verify-email.dto.ts | 18 +++ src/lib/server/api/index.ts | 43 ++++-- .../email-templates/welcome.handlebars | 20 +++ .../api/interfaces/controller.interface.ts | 8 + .../api/providers/controller.provider.ts | 15 -- src/lib/server/api/providers/index.ts | 2 +- .../api/repositories/tokens.repository.ts | 20 ++- .../api/repositories/users.repository.ts | 24 ++- src/lib/server/api/services/iam.service.ts | 31 ++-- src/lib/server/api/services/mailer.service.ts | 27 ++++ src/lib/server/api/services/tokens.service.ts | 18 +++ src/lib/types.ts | 15 ++ vite.config.ts | 8 +- 20 files changed, 354 insertions(+), 149 deletions(-) create mode 100644 src/lib/server/api/infrastructure/email-templates/welcome.handlebars create mode 100644 src/lib/server/api/interfaces/controller.interface.ts delete mode 100644 src/lib/server/api/providers/controller.provider.ts create mode 100644 src/lib/types.ts diff --git a/README.md b/README.md index 64d2040..0653517 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Sveltekit - Starter BYOB (Bring your own Backend) +# Sveltekit - Starter BYOT (Bring your own Tech) A scalable, testable, extensible, boilerplate for Sveltekit. @@ -24,7 +24,24 @@ export const DELETE: RequestHandler = ({ request }) => app.fetch(request); export const POST: RequestHandler = ({ request }) => app.fetch(request); ``` -## Library Selection +## Features + +- [x] Email +- [x] E2E Typesafety +- [x] Base Test Coverage +- [x] Authentication + - [x] Email/Passkey + - [ ] OAuth (Implementation varies so maybe not included as base) + - [x] Email verification + - [x] Email updating +- [x] Database + - [x] Migrations + +## Who is this for? + +Me. I created this for myself to bootstrap weekend projects. Its a continous work in progress that I'd like to eventually make ready for public use. That being said, if you see something I could improve I welcome feedback/prs. + +## Opinionated Library Selection My selection of libraries are just what I've found success, **stable** and high cohesion with. This repo should just serve as a guide as to whats possible; not what libary is the best. @@ -42,26 +59,28 @@ My selection of libraries are just what I've found success, **stable** and high #### Frontend -- **[Sveltekit](https://kit.svelte.dev/)**: After trying Vue, React, Next, and pretty much every frotnend framework in the JS ecosystem, its safe to say I vastly prefer Svelte and its priority of building on web standards. +- **[Sveltekit](https://kit.svelte.dev/)**: After trying Vue, React, Next, and pretty much every frotnend framework in the JS ecosystem, its safe to say I vastly prefer Svelte and its priority of building on web standards. #### Dependency Injection - **[TSyringe](https://github.com/microsoft/tsyringe)**: Lightweight dependency injection for JS/TS. If you're familiar with TypeDI, this is essentially the same but actively maintained and used by Microsoft. -## Architecture +## Architecture -We have a few popular architectures for structuring backends -* Technical -* Clean/Onion -* Domain Driven Design(DDD)/Vertical Slice Architecture(VSA) +We have a few popular architectures for structuring traditional backends -I choose to start with organizing my backends by technical imeplementions and evolve into one of the others as the project evolves. You don't have to strictly stick to any specific architecture, but they do serve as good guidelines. Alternatively, move things around until it feels right. +- Technical +- Clean/Onion +- Domain Driven Design(DDD)/Vertical Slice Architecture(VSA) +I choose to start with organizing my backends by **Technical** imeplementions and evolve into one of the others as the project increases in complexity. You don't have to strictly stick to any specific architecture, but they do serve as good guidelines. Alternatively, move things around until it feels right. + +## Abstraction + +Too many boilerplates lock you into technology choices or straight up throw random libraries to advertise they have more features than their competitors. This is a horrific practice. Just look at the feature list of this ["boilerplate for NextJS"](https://github.com/ixartz/Next-js-Boilerplate#getting-started). The instant this boiler it used, your code is already complex, riddled with external services, impossible to test, require signing up with multiple external services, AND YOU HAVENT EVEN STARTED DEVELOPING YOUR PRODUCT. ## Testing -Testing probably isn't first and foremost when creating an app. Thats fine. You shoulkdn't be spending time writing tests if your app is changing and pivoting. - -BUT a good stack should be **testable** when the time to solidify a codebase arrives. I created this stack with that pinciple in mind. I've provided a examples of how to write these tests under `authentication.service.test.ts` or `users.service.test.ts` - +Testing probably isn't first and foremost when creating an app. Thats fine. You probably shouldnt't be spending time writing tests if your app is changing and pivoting. +BUT, a good stack should be **testable** when the time to solidify a codebase arrives. I created this stack with that pinciple in mind. I've provided a examples of how to write these tests under `iam.service.test.ts`. diff --git a/src/app.d.ts b/src/app.d.ts index 27102b2..4b1f114 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -8,8 +8,8 @@ declare global { // interface Error {} interface Locals { api: ApiClient['api']; - getAuthedUser: () => Promise; - getAuthedUserOrThrow: () => Promise; + getAuthedUser: () => Promise | null>; + getAuthedUserOrThrow: () => Promise>; } // interface PageData {} // interface PageState {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 422a3ae..8686f5e 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,10 +1,11 @@ -import { hc, type ClientResponse } from 'hono/client'; +import { hc } from 'hono/client'; import { redirect, type Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import type { ApiRoutes } from '$lib/server/api'; import { parseApiResponse } from '$lib/helpers'; const apiClient: Handle = async ({ event, resolve }) => { + /* ------------------------------ Register api ------------------------------ */ const { api } = hc('/', { fetch: event.fetch, headers: { @@ -12,6 +13,7 @@ const apiClient: Handle = async ({ event, resolve }) => { } }); + /* ----------------------------- Auth functions ----------------------------- */ async function getAuthedUser() { const { data } = await parseApiResponse(api.iam.user.$get()); return data && data.user; @@ -23,11 +25,12 @@ const apiClient: Handle = async ({ event, resolve }) => { return data?.user; } - // set contexts + /* ------------------------------ Set contexts ------------------------------ */ event.locals.api = api; event.locals.getAuthedUser = getAuthedUser; event.locals.getAuthedUserOrThrow = getAuthedUserOrThrow; + /* ----------------------------- Return response ---------------------------- */ const response = await resolve(event); return response; }; diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts index cbcdad0..f66adb4 100644 --- a/src/lib/server/api/controllers/iam.controller.ts +++ b/src/lib/server/api/controllers/iam.controller.ts @@ -1,5 +1,4 @@ import { inject, injectable } from 'tsyringe'; -import { ControllerProvider } from '../providers'; import { zValidator } from '@hono/zod-validator'; import { registerEmailDto } from '../dtos/register-email.dto'; import { IamService } from '../services/iam.service'; @@ -9,93 +8,92 @@ import { LuciaProvider } from '../providers/lucia.provider'; import { requireAuth } from '../middleware/require-auth.middleware'; import { updateEmailDto } from '../dtos/update-email.dto'; import { verifyEmailDto } from '../dtos/verify-email.dto'; +import { Hono } from 'hono'; +import type { HonoTypes } from '../types'; +import type { Controller } from '../interfaces/controller.interface'; + +/* -------------------------------------------------------------------------- */ +/* Controller */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Controllers are responsible for handling incoming requests and returning responses +to a client. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +A controller should generally only handle routing and authorization through +middleware. + +Any business logic should be delegated to a service. This keeps the controller +clean and easy to read. +*/ +/* -------------------------------- Important ------------------------------- */ +/* +Remember to register your controller in the api/index.ts file. +*/ +/* -------------------------------------------------------------------------- */ @injectable() -export class IamController { +export class IamController implements Controller { + controller = new Hono(); + constructor( - @inject(ControllerProvider) private controller: ControllerProvider, @inject(IamService) private iamService: IamService, @inject(LuciaProvider) private lucia: LuciaProvider ) {} - getAuthedUser() { - return this.controller.get('/user', async (c) => { - const user = c.var.user; - return c.json({ user: user }); - }); - } - - registerEmail() { - return this.controller.post( - '/email/register', - zValidator('json', registerEmailDto), - async (c) => { + routes() { + return this.controller + .get('/user', async (c) => { + const user = c.var.user; + return c.json({ user: user }); + }) + .post('/email/register', zValidator('json', registerEmailDto), async (c) => { const { email } = c.req.valid('json'); await this.iamService.registerEmail({ email }); return c.json({ message: 'Verification email sent' }); - } - ); - } - - signInEmail() { - return this.controller.post('/email/signin', zValidator('json', signInEmailDto), async (c) => { - const { email, token } = c.req.valid('json'); - const session = await this.iamService.signinEmail({ email, token }); - const sessionCookie = this.lucia.createSessionCookie(session.id); - setCookie(c, sessionCookie.name, sessionCookie.value, { - path: sessionCookie.attributes.path, - maxAge: sessionCookie.attributes.maxAge, - domain: sessionCookie.attributes.domain, - sameSite: sessionCookie.attributes.sameSite as any, - secure: sessionCookie.attributes.secure, - httpOnly: sessionCookie.attributes.httpOnly, - expires: sessionCookie.attributes.expires - }); - return c.json({ message: 'ok' }); - }); - } - - logout() { - return this.controller.post('/logout', requireAuth, async (c) => { - const sessionId = c.var.session.id; - await this.iamService.logout(sessionId); - const sessionCookie = this.lucia.createBlankSessionCookie(); - setCookie(c, sessionCookie.name, sessionCookie.value, { - path: sessionCookie.attributes.path, - maxAge: sessionCookie.attributes.maxAge, - domain: sessionCookie.attributes.domain, - sameSite: sessionCookie.attributes.sameSite as any, - secure: sessionCookie.attributes.secure, - httpOnly: sessionCookie.attributes.httpOnly, - expires: sessionCookie.attributes.expires - }); - return c.json({ status: 'success' }); - }); - } - - updateEmail() { - return this.controller.post( - '/email/update', - requireAuth, - zValidator('json', updateEmailDto), - async (c) => { + }) + .post('/email/signin', zValidator('json', signInEmailDto), async (c) => { + const { email, token } = c.req.valid('json'); + const session = await this.iamService.signinEmail({ email, token }); + const sessionCookie = this.lucia.createSessionCookie(session.id); + setCookie(c, sessionCookie.name, sessionCookie.value, { + path: sessionCookie.attributes.path, + maxAge: sessionCookie.attributes.maxAge, + domain: sessionCookie.attributes.domain, + sameSite: sessionCookie.attributes.sameSite as any, + secure: sessionCookie.attributes.secure, + httpOnly: sessionCookie.attributes.httpOnly, + expires: sessionCookie.attributes.expires + }); + return c.json({ message: 'ok' }); + }) + .post('/logout', requireAuth, async (c) => { + const sessionId = c.var.session.id; + await this.iamService.logout(sessionId); + const sessionCookie = this.lucia.createBlankSessionCookie(); + setCookie(c, sessionCookie.name, sessionCookie.value, { + path: sessionCookie.attributes.path, + maxAge: sessionCookie.attributes.maxAge, + domain: sessionCookie.attributes.domain, + sameSite: sessionCookie.attributes.sameSite as any, + secure: sessionCookie.attributes.secure, + httpOnly: sessionCookie.attributes.httpOnly, + expires: sessionCookie.attributes.expires + }); + return c.json({ status: 'success' }); + }) + .post('/email/update', requireAuth, zValidator('json', updateEmailDto), async (c) => { const json = c.req.valid('json'); await this.iamService.updateEmail(c.var.user.id, json); return c.json({ message: 'Verification email sent' }); - } - ); - } - - verifyEmail() { - return this.controller.post( - '/email/verify', - requireAuth, - zValidator('json', verifyEmailDto), - async (c) => { + }) + .post('/email/verify', requireAuth, zValidator('json', verifyEmailDto), async (c) => { const json = c.req.valid('json'); await this.iamService.verifyEmail(c.var.user.id, json.token); return c.json({ message: 'Verified and updated' }); - } - ); + }); } } diff --git a/src/lib/server/api/dtos/register-email.dto.ts b/src/lib/server/api/dtos/register-email.dto.ts index 462c82e..d73db49 100644 --- a/src/lib/server/api/dtos/register-email.dto.ts +++ b/src/lib/server/api/dtos/register-email.dto.ts @@ -1,5 +1,23 @@ import { z } from 'zod'; +/* -------------------------------------------------------------------------- */ +/* DTO */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Data Transfer Objects (DTOs) are used to define the shape of data that is passed. +They are used to validate data and ensure that the correct data is being passed +to the correct methods. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +DTO's are pretty flexible. You can use them anywhere you want in this application to +validate or shape data. They are especially useful in API routes and services to +ensure that the correct data is being passed around. +*/ +/* -------------------------------------------------------------------------- */ + export const registerEmailDto = z.object({ email: z.string().email() }); diff --git a/src/lib/server/api/dtos/signin-email.dto.ts b/src/lib/server/api/dtos/signin-email.dto.ts index e04a046..630f614 100644 --- a/src/lib/server/api/dtos/signin-email.dto.ts +++ b/src/lib/server/api/dtos/signin-email.dto.ts @@ -1,5 +1,23 @@ import { z } from 'zod'; +/* -------------------------------------------------------------------------- */ +/* DTO */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Data Transfer Objects (DTOs) are used to define the shape of data that is passed. +They are used to validate data and ensure that the correct data is being passed +to the correct methods. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +DTO's are pretty flexible. You can use them anywhere you want in this application to +validate or shape data. They are especially useful in API routes and services to +ensure that the correct data is being passed around. +*/ +/* -------------------------------------------------------------------------- */ + export const signInEmailDto = z.object({ email: z.string().email(), token: z.string() diff --git a/src/lib/server/api/dtos/update-email.dto.ts b/src/lib/server/api/dtos/update-email.dto.ts index 20d7fed..a4e2f41 100644 --- a/src/lib/server/api/dtos/update-email.dto.ts +++ b/src/lib/server/api/dtos/update-email.dto.ts @@ -1,5 +1,23 @@ import { z } from 'zod'; +/* -------------------------------------------------------------------------- */ +/* DTO */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Data Transfer Objects (DTOs) are used to define the shape of data that is passed. +They are used to validate data and ensure that the correct data is being passed +to the correct methods. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +DTO's are pretty flexible. You can use them anywhere you want in this application to +validate or shape data. They are especially useful in API routes and services to +ensure that the correct data is being passed around. +*/ +/* -------------------------------------------------------------------------- */ + export const updateEmailDto = z.object({ email: z.string() }); diff --git a/src/lib/server/api/dtos/verify-email.dto.ts b/src/lib/server/api/dtos/verify-email.dto.ts index 0366a20..f46f872 100644 --- a/src/lib/server/api/dtos/verify-email.dto.ts +++ b/src/lib/server/api/dtos/verify-email.dto.ts @@ -1,5 +1,23 @@ import { z } from 'zod'; +/* -------------------------------------------------------------------------- */ +/* DTO */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Data Transfer Objects (DTOs) are used to define the shape of data that is passed. +They are used to validate data and ensure that the correct data is being passed +to the correct methods. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +DTO's are pretty flexible. You can use them anywhere you want in this application to +validate or shape data. They are especially useful in API routes and services to +ensure that the correct data is being passed around. +*/ +/* -------------------------------------------------------------------------- */ + export const verifyEmailDto = z.object({ token: z.string() }); diff --git a/src/lib/server/api/index.ts b/src/lib/server/api/index.ts index 00e0d98..a1cf50b 100644 --- a/src/lib/server/api/index.ts +++ b/src/lib/server/api/index.ts @@ -5,30 +5,45 @@ import { hc } from 'hono/client'; import { container } from 'tsyringe'; import { processAuth } from './middleware/process-auth.middleware'; import { IamController } from './controllers/iam.controller'; +import { config } from './common/config'; /* -------------------------------------------------------------------------- */ -/* API */ +/* Client Request */ +/* ------------------------------------ ▲ ----------------------------------- */ +/* ------------------------------------ | ----------------------------------- */ +/* ------------------------------------ ▼ ----------------------------------- */ +/* Controller */ +/* ---------------------------- (Request Routing) --------------------------- */ +/* ------------------------------------ ▲ ----------------------------------- */ +/* ------------------------------------ | ----------------------------------- */ +/* ------------------------------------ ▼ ----------------------------------- */ +/* Service */ +/* ---------------------------- (Business logic) ---------------------------- */ +/* ------------------------------------ ▲ ----------------------------------- */ +/* ------------------------------------ | ----------------------------------- */ +/* ------------------------------------ ▼ ----------------------------------- */ +/* Repository */ +/* ----------------------------- (Data storage) ----------------------------- */ /* -------------------------------------------------------------------------- */ + +/* ----------------------------------- Api ---------------------------------- */ const app = new Hono().basePath('/api'); -/* -------------------------------------------------------------------------- */ -/* Global Middlewares */ -/* -------------------------------------------------------------------------- */ -app.use(processAuth); // all this does is set the session and user variables in the request object -/* -------------------------------------------------------------------------- */ -/* Routes */ -/* -------------------------------------------------------------------------- */ +/* --------------------------- Global Middlewares --------------------------- */ +app.use(processAuth); + +/* --------------------------------- Routes --------------------------------- */ const routes = app - .route('/iam', container.resolve(IamController).registerEmail()) - .route('/iam', container.resolve(IamController).signInEmail()) - .route('/iam', container.resolve(IamController).getAuthedUser()) - .route('/iam', container.resolve(IamController).updateEmail()) - .route('/iam', container.resolve(IamController).verifyEmail()); + .route('/iam', container.resolve(IamController).routes()) + .route('/iam', container.resolve(IamController).routes()) + .route('/iam', container.resolve(IamController).routes()) + .route('/iam', container.resolve(IamController).routes()) + .route('/iam', container.resolve(IamController).routes()); /* -------------------------------------------------------------------------- */ /* Exports */ /* -------------------------------------------------------------------------- */ -export const rpc = hc('http://localhost:5173'); +export const rpc = hc(config.ORIGIN); export type ApiClient = typeof rpc; export type ApiRoutes = typeof routes; export { app }; diff --git a/src/lib/server/api/infrastructure/email-templates/welcome.handlebars b/src/lib/server/api/infrastructure/email-templates/welcome.handlebars new file mode 100644 index 0000000..c61b1e3 --- /dev/null +++ b/src/lib/server/api/infrastructure/email-templates/welcome.handlebars @@ -0,0 +1,20 @@ + + + + + Message + + +

Welcome to Example

+

+ Thanks for using example.com. We want to make sure it's really you. Please enter the following + verification code when prompted. If you don't have an exmaple.com an account, you can ignore + this message.

+ + + \ No newline at end of file diff --git a/src/lib/server/api/interfaces/controller.interface.ts b/src/lib/server/api/interfaces/controller.interface.ts new file mode 100644 index 0000000..852e695 --- /dev/null +++ b/src/lib/server/api/interfaces/controller.interface.ts @@ -0,0 +1,8 @@ +import { Hono } from 'hono'; +import type { HonoTypes } from '../types'; +import type { BlankSchema } from 'hono/types'; + +export interface Controller { + controller: Hono; + routes(): any; +} diff --git a/src/lib/server/api/providers/controller.provider.ts b/src/lib/server/api/providers/controller.provider.ts deleted file mode 100644 index a95da60..0000000 --- a/src/lib/server/api/providers/controller.provider.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { container } from 'tsyringe'; -import { Hono } from 'hono'; -import type { HonoTypes } from '../types'; - -// Symbol -export const ControllerProvider = Symbol('CONTROLLER_PROVIDER'); - -// Type -export type ControllerProvider = typeof controller; - -// Value -const controller = new Hono(); - -// Register -container.register(ControllerProvider, { useValue: controller }); diff --git a/src/lib/server/api/providers/index.ts b/src/lib/server/api/providers/index.ts index faf0fcc..0f06262 100644 --- a/src/lib/server/api/providers/index.ts +++ b/src/lib/server/api/providers/index.ts @@ -1,2 +1,2 @@ export * from './database.provider'; -export * from './controller.provider' +export * from './lucia.provider'; diff --git a/src/lib/server/api/repositories/tokens.repository.ts b/src/lib/server/api/repositories/tokens.repository.ts index 11d620c..3daff11 100644 --- a/src/lib/server/api/repositories/tokens.repository.ts +++ b/src/lib/server/api/repositories/tokens.repository.ts @@ -5,14 +5,24 @@ import { tokensTable } from '../infrastructure/database/tables'; import { takeFirstOrThrow } from '../infrastructure/database/utils'; import type { Repository } from '../interfaces/repository.interface'; -/* -------------------------------------------------------------------------- */ -/* Types */ -/* -------------------------------------------------------------------------- */ -export type InsertToken = InferInsertModel; - /* -------------------------------------------------------------------------- */ /* Repository */ /* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Repositories are the layer that interacts with the database. They are responsible for retrieving and +storing data. They should not contain any business logic, only database queries. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* + Repositories should only contain methods for CRUD operations and any other database interactions. + Any complex logic should be delegated to a service. If a repository method requires a transaction, + it should be passed in as an argument or the class should have a method to set the transaction. + In our case the method 'trxHost' is used to set the transaction context. +*/ + +export type InsertToken = InferInsertModel; + @injectable() export class TokensRepository implements Repository { constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {} diff --git a/src/lib/server/api/repositories/users.repository.ts b/src/lib/server/api/repositories/users.repository.ts index 6c5dc5e..659d3de 100644 --- a/src/lib/server/api/repositories/users.repository.ts +++ b/src/lib/server/api/repositories/users.repository.ts @@ -1,19 +1,29 @@ import { inject, injectable } from 'tsyringe'; import type { Repository } from '../interfaces/repository.interface'; -import { DatabaseProvider, type DatabaseProvider } from '../providers'; +import { DatabaseProvider } from '../providers'; import { eq, type InferInsertModel } from 'drizzle-orm'; import { usersTable } from '../infrastructure/database/tables/users.table'; import { takeFirstOrThrow } from '../infrastructure/database/utils'; -/* -------------------------------------------------------------------------- */ -/* Types */ -/* -------------------------------------------------------------------------- */ -export type CreateUser = Pick, 'avatar' | 'email' | 'verified'>; -export type UpdateUser = Partial; - /* -------------------------------------------------------------------------- */ /* Repository */ /* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Repositories are the layer that interacts with the database. They are responsible for retrieving and +storing data. They should not contain any business logic, only database queries. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* + Repositories should only contain methods for CRUD operations and any other database interactions. + Any complex logic should be delegated to a service. If a repository method requires a transaction, + it should be passed in as an argument or the class should have a method to set the transaction. + In our case the method 'trxHost' is used to set the transaction context. +*/ + +export type CreateUser = InferInsertModel; +export type UpdateUser = Partial; + @injectable() export class UsersRepository implements Repository { constructor(@inject(DatabaseProvider) private db: DatabaseProvider) {} diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts index 35a87f3..eaaa246 100644 --- a/src/lib/server/api/services/iam.service.ts +++ b/src/lib/server/api/services/iam.service.ts @@ -7,7 +7,24 @@ import type { SignInEmailDto } from '../dtos/signin-email.dto'; import { BadRequest } from '../common/errors'; import { LuciaProvider } from '../providers/lucia.provider'; import type { UpdateEmailDto } from '../dtos/update-email.dto'; -import type { VerifyEmailDto } from '../dtos/verify-email.dto'; + +/* -------------------------------------------------------------------------- */ +/* Service */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Services are responsible for handling business logic and data manipulation. +They genreally call on repositories or other services to complete a use-case. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +Services should be kept as clean and simple as possible. + +Create private functions to handle complex logic and keep the public methods as +simple as possible. This makes the service easier to read, test and understand. +*/ +/* -------------------------------------------------------------------------- */ @injectable() export class IamService { @@ -45,10 +62,9 @@ export class IamService { // if this is a new unverified user, send a welcome email and update the user if (!user.verified) { await this.usersRepository.update(user.id, { verified: true }); - await this.mailerService.send({ + this.mailerService.sendWelcomeEmail({ to: user.email, - subject: 'Welcome!', - html: 'Welcome to example.com' + props: null }); } @@ -81,14 +97,9 @@ export class IamService { private async createValidationReuqest(userId: string, email: string) { const validationToken = await this.tokensService.create(userId, email); - await this.mailerService.sendEmailVerification({ + this.mailerService.sendEmailVerification({ to: email, props: { token: validationToken.token } }); - // return await this.mailerService.send({ - // to: email, - // subject: 'Verify your email', - // html: `Your token is ${validationToken.token}` - // }); } } diff --git a/src/lib/server/api/services/mailer.service.ts b/src/lib/server/api/services/mailer.service.ts index 2f54b0a..5441b04 100644 --- a/src/lib/server/api/services/mailer.service.ts +++ b/src/lib/server/api/services/mailer.service.ts @@ -5,6 +5,24 @@ import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; +/* -------------------------------------------------------------------------- */ +/* Service */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Services are responsible for handling business logic and data manipulation. +They genreally call on repositories or other services to complete a use-case. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +Services should be kept as clean and simple as possible. + +Create private functions to handle complex logic and keep the public methods as +simple as possible. This makes the service easier to read, test and understand. +*/ +/* -------------------------------------------------------------------------- */ + type SendMail = { to: string | string[]; subject: string; @@ -37,6 +55,15 @@ export class MailerService { }); } + sendWelcomeEmail(data: SendTemplate) { + const template = handlebars.compile(this.getTemplate('welcome')); + return this.send({ + to: data.to, + subject: 'Welcome!', + html: template(null) + }); + } + private async send({ to, subject, html }: SendMail) { const message = await this.nodemailer.sendMail({ from: '"Example" ', // sender address diff --git a/src/lib/server/api/services/tokens.service.ts b/src/lib/server/api/services/tokens.service.ts index 0ccc8de..6282aac 100644 --- a/src/lib/server/api/services/tokens.service.ts +++ b/src/lib/server/api/services/tokens.service.ts @@ -4,6 +4,24 @@ import dayjs from 'dayjs'; import { customAlphabet } from 'nanoid'; import { DatabaseProvider } from '../providers'; +/* -------------------------------------------------------------------------- */ +/* Service */ +/* -------------------------------------------------------------------------- */ +/* -------------------------------------------------------------------------- */ +/* ---------------------------------- About --------------------------------- */ +/* +Services are responsible for handling business logic and data manipulation. +They genreally call on repositories or other services to complete a use-case. +*/ +/* ---------------------------------- Notes --------------------------------- */ +/* +Services should be kept as clean and simple as possible. + +Create private functions to handle complex logic and keep the public methods as +simple as possible. This makes the service easier to read, test and understand. +*/ +/* -------------------------------------------------------------------------- */ + @injectable() export class TokensService { constructor( diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..d06e983 --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,15 @@ +export type SwapDatesWithStrings = { + [k in keyof T]: T[k] extends Date | undefined + ? string + : T[k] extends object + ? SwapDatesWithStrings + : T[k]; +}; + +export type Returned = { + [k in keyof T]: T[k] extends Date | undefined + ? string + : T[k] extends object + ? SwapDatesWithStrings + : T[k]; +}; diff --git a/vite.config.ts b/vite.config.ts index 0927cde..37b6a84 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,8 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vitest/config'; -import ViteRestart from 'vite-plugin-restart'; export default defineConfig({ - plugins: [ - sveltekit(), - ViteRestart({ - restart: ['./src/lib/server/api/**'] - }) - ], + plugins: [sveltekit()], test: { include: ['src/**/*.{test,spec}.{js,ts}'] }