chore: initial user and team tables

This commit is contained in:
Anthony Potdevin 2026-04-01 17:31:01 -06:00
parent 11178793b9
commit c63ea0d76f
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
15 changed files with 462 additions and 14 deletions

View file

@ -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!,
},
});

View file

@ -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;

View file

@ -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": {}
}
}

View file

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1775074089419,
"tag": "0000_new_adam_warlock",
"breakpoints": true
}
]
}

View file

@ -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`);

View file

@ -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": {}
}
}

View file

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1775074036636,
"tag": "0000_premium_pixie",
"breakpoints": true
}
]
}

11
scripts/generateMigrations.sh Executable file
View file

@ -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/"

View file

@ -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<typeof drizzlePg>
| null;
const logger = new Logger('DatabaseProvider');
export const DrizzleProvider: FactoryProvider = {
provide: DRIZZLE,
inject: [ConfigService],
useFactory: (config: ConfigService): DrizzleDB => {
useFactory: async (config: ConfigService): Promise<DrizzleDB> => {
const dbType = config.get<string>('database.type');
if (!dbType) return null;
const migrationsRoot = join(process.cwd(), 'drizzle');
if (dbType === 'postgres') {
const url = config.get<string>('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}`);

View file

@ -0,0 +1,2 @@
export { teams } from './teams';
export { users } from './users';

View file

@ -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(),
});

View file

@ -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(),
});

View file

@ -0,0 +1,2 @@
export { teams } from './teams';
export { users } from './users';

View file

@ -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'))`),
});

View file

@ -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'))`),
});