Skip to content

Commit

Permalink
feat: use NestJS design patterns (#37)
Browse files Browse the repository at this point in the history
* Working postgres integration

* Remove optional fields from user create

* Move credentials to an env file

* chore: add convenience db scripts

* refactor: linting

* fix: make migrate.ts work from root

+ this fixes the necessity to cd to src/db to apply migrations and allows executing the script from anywhere

* refactor: move things around to mimic nestjs

Move things around to a NestJS-like resource based structure, based on plural and not singular

* refactor: move one more file

* fix: remove bad script

* fix: db/config.ts catch all schemas

* feat: use decorators

* refactor: index.ts > main.ts

this is more inline with nestjs standards

* refactor: denest 1 level

* refactor: denest 1 level

* refactor: abstract db from service

---------

Co-authored-by: Hajbo <hajbodev@gmail.com>
  • Loading branch information
yamcodes and Hajbo committed Sep 25, 2023
1 parent 041383c commit f4e73fb
Show file tree
Hide file tree
Showing 17 changed files with 127 additions and 46 deletions.
Binary file modified bun.lockb
Binary file not shown.
11 changes: 10 additions & 1 deletion src/config.ts → db/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Config } from "drizzle-kit";
export const dbCredentials = {
host: process.env.POSTGRES_HOST || "0.0.0.0",
port: parseInt(process.env.POSTGRES_PORT || '5432'),
Expand All @@ -6,4 +7,12 @@ export const dbCredentials = {
database: process.env.POSTGRES_DB || "medium"
}

export const dbCredentialsString = `postgres://${dbCredentials.user}:${dbCredentials.password}@${dbCredentials.host}:${dbCredentials.port}/${dbCredentials.database}`
export const dbCredentialsString = `postgres://${dbCredentials.user}:${dbCredentials.password}@${dbCredentials.host}:${dbCredentials.port}/${dbCredentials.database}`;

export default {
out: "./src/db/migrations",
schema: "**/*.schema.ts",
breakpoints: false,
driver: "pg",
dbCredentials
} satisfies Config;
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions db/migrations/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {drizzle} from "drizzle-orm/postgres-js";
import {migrate} from "drizzle-orm/postgres-js/migrator";
import {migrationClient} from "@/database.providers";

await migrate(drizzle(migrationClient), {migrationsFolder: `${import.meta.dir}`});
8 changes: 3 additions & 5 deletions src/db/seed.ts → db/seed.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { exit } from 'process';
import { db } from './index';
import { users } from './schemas/users';
import { db } from '@/database.providers';
import {users} from "@/users/users.schema";


console.log("Migrations complete.")
const data = {
id: users.id.default,
email: 'test@email.com',
Expand All @@ -19,4 +17,4 @@ console.log("User inserted")
const userResult = await db.select().from(users);
console.log("User result: ", userResult);

exit(0);
exit(0);
10 changes: 0 additions & 10 deletions drizzle.config.ts

This file was deleted.

15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@
"name": "elysia-realworld-example-app",
"version": "1.0.50",
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run --watch src/index.ts",
"start": "bun run src/main.ts",
"dev": "bun run --watch src/main.ts",
"test": "echo \"Error: no test specified\" && exit 1",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"db:up": "docker-compose up -d",
"db:generate": "bun drizzle-kit generate:pg --config=db/config.ts",
"db:migrate": "bun run db/migrations/migrate.ts",
"db:push": "bun drizzle-kit push:pg --config=db/config.ts",
"db:seed": "bun run db/seed.ts",
"db:studio": "bun drizzle-kit studio --config=db/config.ts"
},
"dependencies": {
"drizzle-orm": "^0.28.6",
"drizzle-typebox": "^0.1.1",
"elysia": "latest",
"postgres": "^3.3.5"
},
Expand All @@ -25,4 +32,4 @@
},
"module": "src/index.ts",
"type": "module"
}
}
18 changes: 18 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// the file name is in the spirit of NestJS, where app module is the device in charge of putting together all the pieces of the app
// see: https://docs.nestjs.com/modules

import { Elysia } from "elysia";
import UsersService from "./users/users.service";
import UsersController from "./users/users.controller";
import { db } from "@/database.providers";

// the word 'setup' (instead of e.g. 'bootstrap') is in correspondence with the official elysiajs docs
// see: https://elysiajs.com/patterns/dependency-injection.html#dependency-injection

export const setup = () => {
const usersService = UsersService(db);
const usersController = UsersController(usersService);

return new Elysia()
.use(usersController)
}
11 changes: 11 additions & 0 deletions src/database.providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { dbCredentialsString } from '@db/config';

// for migrations
export const migrationClient = postgres(dbCredentialsString, { max: 1 });

// for query purposes
export const queryClient = postgres(dbCredentialsString);

export const db: PostgresJsDatabase = drizzle(queryClient);
13 changes: 0 additions & 13 deletions src/db/index.ts

This file was deleted.

7 changes: 4 additions & 3 deletions src/index.ts → src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Elysia } from "elysia";
import { setup } from "@/app.module";



const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
const app = new Elysia()
.use(setup)
.listen(3000);

console.log(
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
Expand Down
38 changes: 38 additions & 0 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Elysia } from "elysia";
import {UsersService} from "@/users/users.service";

// Below are some ideas for how to structure the controller-service relationship.

// Idea 1: use 'Dependency Injection' to inject the service into the controller
// Pros: follows ElysiaJS docs (https://elysiajs.com/patterns/dependency-injection.html)
// Cons: we must explicitly refer to the service in every single route

// export default new Elysia({prefix: '/users'})
// .decorate("usersService", new UsersService())
// .get("/", async ({usersService}) => {
// return usersService.findAll();
// })

// Idea 2: use a class to wrap the controller and service
// Pros: follows NestJS conventions (https://github.com/lujakob/nestjs-realworld-example-app/blob/master/src/user/user.controller.ts)
// Cons: too noisy, too nested (ha), requires calling the controller with an awkward 'controller.controller' syntax

// export class UsersController {
// constructor(private readonly usersService: UsersService) {}
//
// get controller() {
// return new Elysia({prefix: '/users'})
// .get("/", () => {
// return this.usersService.findAll();
// })
// }
// }

// Idea 3: use a factory function to wrap the controller and service
// Pros: simple, supports 'Method Chaining', follows NestJS conventions (in broad strokes)
// Cons: none

export default (usersService: UsersService) => new Elysia({prefix: '/users'})
.get("/", () => {
return usersService.findAll();
})
9 changes: 2 additions & 7 deletions src/db/schemas/users.ts → src/users/users.schema.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@

import { sql } from "drizzle-orm";
import { pgTable, text, date, serial} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox';
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox';
import { Type } from '@sinclair/typebox';



export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull(),
Expand All @@ -17,11 +14,9 @@ export const users = pgTable('users', {
updated_at: date('updated_at').default(sql`CURRENT_DATE`),
});



// Schema for inserting a user - can be used to validate API requests
const insertUserSchemaRaw = createInsertSchema(users);
export const insertUserSchema = Type.Omit(insertUserSchemaRaw, ['id', 'created_at', 'updated_at']);

// Schema for selecting a user - can be used to validate API responses
export const selectUserSchema = createSelectSchema(users);
export const selectUserSchema = createSelectSchema(users);
19 changes: 19 additions & 0 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {users} from "./users.schema";
import {PostgresJsDatabase} from "drizzle-orm/postgres-js";

// note that we should specifically NOT import the db and use it here
// so we can mock it in tests and switch it out as needed
// also, this would hurt the single responsibility principle.

export class UsersService {

// the type here is
constructor(private readonly db: PostgresJsDatabase) {}

async findAll() {
return this.db.select().from(users);
}
}

// export a factory for consistency with other providers (like the controller)
export default (db: PostgresJsDatabase) => new UsersService(db);
9 changes: 6 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */

/* Modules */
"module": "ES2022", /* Specify what module code is generated. */
"module": "ES2022", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@db/*": ["./db/*"],
"@/*": ["./src/*"],
},
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
"types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */
Expand Down

0 comments on commit f4e73fb

Please sign in to comment.