From c63ea0d76ffe956c4706d79b58bd24541ec6ac80 Mon Sep 17 00:00:00 2001 From: Anthony Potdevin Date: Wed, 1 Apr 2026 17:31:01 -0600 Subject: [PATCH] chore: initial user and team tables --- drizzle.config.ts | 14 +- drizzle/pg/0000_new_adam_warlock.sql | 19 +++ drizzle/pg/meta/0000_snapshot.json | 140 +++++++++++++++++ drizzle/pg/meta/_journal.json | 13 ++ drizzle/sqlite/0000_premium_pixie.sql | 19 +++ drizzle/sqlite/meta/0000_snapshot.json | 142 ++++++++++++++++++ drizzle/sqlite/meta/_journal.json | 13 ++ scripts/generateMigrations.sh | 11 ++ .../modules/database/drizzle.provider.ts | 25 ++- .../modules/database/schema/pg/index.ts | 2 + .../modules/database/schema/pg/teams.ts | 15 ++ .../modules/database/schema/pg/users.ts | 23 +++ .../modules/database/schema/sqlite/index.ts | 2 + .../modules/database/schema/sqlite/teams.ts | 15 ++ .../modules/database/schema/sqlite/users.ts | 23 +++ 15 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 drizzle/pg/0000_new_adam_warlock.sql create mode 100644 drizzle/pg/meta/0000_snapshot.json create mode 100644 drizzle/pg/meta/_journal.json create mode 100644 drizzle/sqlite/0000_premium_pixie.sql create mode 100644 drizzle/sqlite/meta/0000_snapshot.json create mode 100644 drizzle/sqlite/meta/_journal.json create mode 100755 scripts/generateMigrations.sh create mode 100644 src/server/modules/database/schema/pg/index.ts create mode 100644 src/server/modules/database/schema/pg/teams.ts create mode 100644 src/server/modules/database/schema/pg/users.ts create mode 100644 src/server/modules/database/schema/sqlite/index.ts create mode 100644 src/server/modules/database/schema/sqlite/teams.ts create mode 100644 src/server/modules/database/schema/sqlite/users.ts diff --git a/drizzle.config.ts b/drizzle.config.ts index 009dc350..929ae661 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -4,18 +4,12 @@ const dbType = process.env.DB_TYPE || 'sqlite'; export default dbType === 'postgres' ? defineConfig({ - schema: './src/server/modules/database/schema/*', - out: './drizzle', + schema: './src/server/modules/database/schema/pg/*', + out: './drizzle/pg', dialect: 'postgresql', - dbCredentials: { - url: process.env.DB_POSTGRES_URL!, - }, }) : defineConfig({ - schema: './src/server/modules/database/schema/*', - out: './drizzle', + schema: './src/server/modules/database/schema/sqlite/*', + out: './drizzle/sqlite', dialect: 'sqlite', - dbCredentials: { - url: process.env.DB_SQLITE_PATH!, - }, }); diff --git a/drizzle/pg/0000_new_adam_warlock.sql b/drizzle/pg/0000_new_adam_warlock.sql new file mode 100644 index 00000000..0e2d9e07 --- /dev/null +++ b/drizzle/pg/0000_new_adam_warlock.sql @@ -0,0 +1,19 @@ +CREATE TABLE "teams" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + "created_at" timestamp(6) DEFAULT now() NOT NULL, + "updated_at" timestamp(6) DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "email" text NOT NULL, + "password_hash" text NOT NULL, + "role" text DEFAULT 'member' NOT NULL, + "team_id" uuid NOT NULL, + "created_at" timestamp(6) DEFAULT now() NOT NULL, + "updated_at" timestamp(6) DEFAULT now() NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); +--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/pg/meta/0000_snapshot.json b/drizzle/pg/meta/0000_snapshot.json new file mode 100644 index 00000000..cc6bc195 --- /dev/null +++ b/drizzle/pg/meta/0000_snapshot.json @@ -0,0 +1,140 @@ +{ + "id": "f2a5cebb-ac67-464a-bfc5-d0fa612c3552", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(6)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_team_id_teams_id_fk": { + "name": "users_team_id_teams_id_fk", + "tableFrom": "users", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/pg/meta/_journal.json b/drizzle/pg/meta/_journal.json new file mode 100644 index 00000000..b8d81d98 --- /dev/null +++ b/drizzle/pg/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775074089419, + "tag": "0000_new_adam_warlock", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/sqlite/0000_premium_pixie.sql b/drizzle/sqlite/0000_premium_pixie.sql new file mode 100644 index 00000000..dbb9e6a1 --- /dev/null +++ b/drizzle/sqlite/0000_premium_pixie.sql @@ -0,0 +1,19 @@ +CREATE TABLE `teams` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` text PRIMARY KEY NOT NULL, + `email` text NOT NULL, + `password_hash` text NOT NULL, + `role` text DEFAULT 'member' NOT NULL, + `team_id` text NOT NULL, + `created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + `updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL, + FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/drizzle/sqlite/meta/0000_snapshot.json b/drizzle/sqlite/meta/0000_snapshot.json new file mode 100644 index 00000000..1a9e82b0 --- /dev/null +++ b/drizzle/sqlite/meta/0000_snapshot.json @@ -0,0 +1,142 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "296005fb-1644-4912-8e82-1ebb647066e2", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "teams": { + "name": "teams", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "users_team_id_teams_id_fk": { + "name": "users_team_id_teams_id_fk", + "tableFrom": "users", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json new file mode 100644 index 00000000..a12194cf --- /dev/null +++ b/drizzle/sqlite/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1775074036636, + "tag": "0000_premium_pixie", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/scripts/generateMigrations.sh b/scripts/generateMigrations.sh new file mode 100755 index 00000000..3eb4d85d --- /dev/null +++ b/scripts/generateMigrations.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +echo "Generating SQLite migrations..." +DB_TYPE=sqlite npx drizzle-kit generate + +echo "" +echo "Generating PostgreSQL migrations..." +DB_TYPE=postgres npx drizzle-kit generate + +echo "" +echo "Done. Migrations generated in drizzle/sqlite/ and drizzle/pg/" diff --git a/src/server/modules/database/drizzle.provider.ts b/src/server/modules/database/drizzle.provider.ts index b8624684..2baac57f 100644 --- a/src/server/modules/database/drizzle.provider.ts +++ b/src/server/modules/database/drizzle.provider.ts @@ -1,9 +1,12 @@ -import { FactoryProvider } from '@nestjs/common'; +import { FactoryProvider, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3'; import { drizzle as drizzlePg } from 'drizzle-orm/postgres-js'; +import { migrate as migrateSqlite } from 'drizzle-orm/better-sqlite3/migrator'; +import { migrate as migratePg } from 'drizzle-orm/postgres-js/migrator'; import Database from 'better-sqlite3'; import postgres from 'postgres'; +import { join } from 'path'; export const DRIZZLE = Symbol('DRIZZLE'); @@ -12,20 +15,28 @@ export type DrizzleDB = | ReturnType | null; +const logger = new Logger('DatabaseProvider'); + export const DrizzleProvider: FactoryProvider = { provide: DRIZZLE, inject: [ConfigService], - useFactory: (config: ConfigService): DrizzleDB => { + useFactory: async (config: ConfigService): Promise => { const dbType = config.get('database.type'); if (!dbType) return null; + const migrationsRoot = join(process.cwd(), 'drizzle'); + if (dbType === 'postgres') { const url = config.get('database.url'); if (!url) { throw new Error('DB_POSTGRES_URL is required when DB_TYPE is postgres'); } - return drizzlePg(postgres(url)); + const db = drizzlePg(postgres(url)); + logger.log('Running PostgreSQL migrations...'); + await migratePg(db, { migrationsFolder: join(migrationsRoot, 'pg') }); + logger.log('PostgreSQL migrations complete.'); + return db; } if (dbType === 'sqlite') { @@ -33,7 +44,13 @@ export const DrizzleProvider: FactoryProvider = { if (!path) { throw new Error('DB_SQLITE_PATH is required when DB_TYPE is sqlite'); } - return drizzleSqlite(new Database(path)); + const db = drizzleSqlite(new Database(path)); + logger.log('Running SQLite migrations...'); + migrateSqlite(db, { + migrationsFolder: join(migrationsRoot, 'sqlite'), + }); + logger.log('SQLite migrations complete.'); + return db; } throw new Error(`Unsupported DB_TYPE: ${dbType}`); diff --git a/src/server/modules/database/schema/pg/index.ts b/src/server/modules/database/schema/pg/index.ts new file mode 100644 index 00000000..568611a8 --- /dev/null +++ b/src/server/modules/database/schema/pg/index.ts @@ -0,0 +1,2 @@ +export { teams } from './teams'; +export { users } from './users'; diff --git a/src/server/modules/database/schema/pg/teams.ts b/src/server/modules/database/schema/pg/teams.ts new file mode 100644 index 00000000..ffd164f8 --- /dev/null +++ b/src/server/modules/database/schema/pg/teams.ts @@ -0,0 +1,15 @@ +import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const teams = pgTable('teams', { + id: uuid('id') + .primaryKey() + .default(sql`gen_random_uuid()`), + name: text('name').notNull(), + created_at: timestamp('created_at', { precision: 6, mode: 'string' }) + .notNull() + .defaultNow(), + updated_at: timestamp('updated_at', { precision: 6, mode: 'string' }) + .notNull() + .defaultNow(), +}); diff --git a/src/server/modules/database/schema/pg/users.ts b/src/server/modules/database/schema/pg/users.ts new file mode 100644 index 00000000..0ee61267 --- /dev/null +++ b/src/server/modules/database/schema/pg/users.ts @@ -0,0 +1,23 @@ +import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; +import { teams } from './teams'; + +export const users = pgTable('users', { + id: uuid('id') + .primaryKey() + .default(sql`gen_random_uuid()`), + email: text('email').notNull().unique(), + password_hash: text('password_hash').notNull(), + role: text('role', { enum: ['owner', 'admin', 'member'] }) + .notNull() + .default('member'), + team_id: uuid('team_id') + .notNull() + .references(() => teams.id), + created_at: timestamp('created_at', { precision: 6, mode: 'string' }) + .notNull() + .defaultNow(), + updated_at: timestamp('updated_at', { precision: 6, mode: 'string' }) + .notNull() + .defaultNow(), +}); diff --git a/src/server/modules/database/schema/sqlite/index.ts b/src/server/modules/database/schema/sqlite/index.ts new file mode 100644 index 00000000..568611a8 --- /dev/null +++ b/src/server/modules/database/schema/sqlite/index.ts @@ -0,0 +1,2 @@ +export { teams } from './teams'; +export { users } from './users'; diff --git a/src/server/modules/database/schema/sqlite/teams.ts b/src/server/modules/database/schema/sqlite/teams.ts new file mode 100644 index 00000000..fade75d5 --- /dev/null +++ b/src/server/modules/database/schema/sqlite/teams.ts @@ -0,0 +1,15 @@ +import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { sql } from 'drizzle-orm'; + +export const teams = sqliteTable('teams', { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + name: text('name').notNull(), + created_at: text('created_at') + .notNull() + .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), + updated_at: text('updated_at') + .notNull() + .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), +}); diff --git a/src/server/modules/database/schema/sqlite/users.ts b/src/server/modules/database/schema/sqlite/users.ts new file mode 100644 index 00000000..7df7674e --- /dev/null +++ b/src/server/modules/database/schema/sqlite/users.ts @@ -0,0 +1,23 @@ +import { sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { sql } from 'drizzle-orm'; +import { teams } from './teams'; + +export const users = sqliteTable('users', { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + email: text('email').notNull().unique(), + password_hash: text('password_hash').notNull(), + role: text('role', { enum: ['owner', 'admin', 'member'] }) + .notNull() + .default('member'), + team_id: text('team_id') + .notNull() + .references(() => teams.id), + created_at: text('created_at') + .notNull() + .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), + updated_at: text('updated_at') + .notNull() + .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), +});