diff --git a/bun.lockb b/bun.lockb index 6637b0d..0cb9afc 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/config.ts b/db/config.ts similarity index 63% rename from src/config.ts rename to db/config.ts index 8be5b43..83dda5b 100644 --- a/src/config.ts +++ b/db/config.ts @@ -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'), @@ -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}` \ No newline at end of file +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; diff --git a/src/db/migrations/0000_bored_warstar.sql b/db/migrations/0000_bored_warstar.sql similarity index 100% rename from src/db/migrations/0000_bored_warstar.sql rename to db/migrations/0000_bored_warstar.sql diff --git a/src/db/migrations/meta/0000_snapshot.json b/db/migrations/meta/0000_snapshot.json similarity index 100% rename from src/db/migrations/meta/0000_snapshot.json rename to db/migrations/meta/0000_snapshot.json diff --git a/src/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json similarity index 100% rename from src/db/migrations/meta/_journal.json rename to db/migrations/meta/_journal.json diff --git a/db/migrations/migrate.ts b/db/migrations/migrate.ts new file mode 100644 index 0000000..49ffe7b --- /dev/null +++ b/db/migrations/migrate.ts @@ -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}`}); diff --git a/src/db/seed.ts b/db/seed.ts similarity index 76% rename from src/db/seed.ts rename to db/seed.ts index 6185b2a..01c482c 100644 --- a/src/db/seed.ts +++ b/db/seed.ts @@ -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', @@ -19,4 +17,4 @@ console.log("User inserted") const userResult = await db.select().from(users); console.log("User result: ", userResult); -exit(0); \ No newline at end of file +exit(0); diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index a9a535d..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Config } from "drizzle-kit"; -import { dbCredentials } from "./src/config"; - -export default { - out: "./src/db/migrations", - schema: "./src/db/schemas/*.ts", - breakpoints: false, - driver: "pg", - dbCredentials: dbCredentials -} satisfies Config; \ No newline at end of file diff --git a/package.json b/package.json index 0ec59b5..c46e590 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -25,4 +32,4 @@ }, "module": "src/index.ts", "type": "module" -} \ No newline at end of file +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..2fb2de9 --- /dev/null +++ b/src/app.module.ts @@ -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) +} diff --git a/src/database.providers.ts b/src/database.providers.ts new file mode 100644 index 0000000..b411912 --- /dev/null +++ b/src/database.providers.ts @@ -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); diff --git a/src/db/index.ts b/src/db/index.ts deleted file mode 100644 index 44183c6..0000000 --- a/src/db/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { migrate } from 'drizzle-orm/postgres-js/migrator'; -import postgres from 'postgres'; - -import { dbCredentialsString } from '../config'; - -// for migrations -const migrationClient = postgres(dbCredentialsString, { max: 1 }); -migrate(drizzle(migrationClient), {migrationsFolder: './migrations'}) - -// for query purposes -const queryClient = postgres(dbCredentialsString); -export const db: PostgresJsDatabase = drizzle(queryClient); \ No newline at end of file diff --git a/src/index.ts b/src/main.ts similarity index 56% rename from src/index.ts rename to src/main.ts index bb84a52..a5fca71 100644 --- a/src/index.ts +++ b/src/main.ts @@ -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}` diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts new file mode 100644 index 0000000..fd10015 --- /dev/null +++ b/src/users/users.controller.ts @@ -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(); + }) diff --git a/src/db/schemas/users.ts b/src/users/users.schema.ts similarity index 92% rename from src/db/schemas/users.ts rename to src/users/users.schema.ts index 13af4e7..20e8ea5 100644 --- a/src/db/schemas/users.ts +++ b/src/users/users.schema.ts @@ -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(), @@ -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); \ No newline at end of file +export const selectUserSchema = createSelectSchema(users); diff --git a/src/users/users.service.ts b/src/users/users.service.ts new file mode 100644 index 0000000..c960496 --- /dev/null +++ b/src/users/users.service.ts @@ -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); diff --git a/tsconfig.json b/tsconfig.json index 1ca2350..c7f8799 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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. */