diff --git a/drizzle/postgres/0000_complex_ricochet.sql b/drizzle/postgres/0000_stormy_zaladane.sql similarity index 81% rename from drizzle/postgres/0000_complex_ricochet.sql rename to drizzle/postgres/0000_stormy_zaladane.sql index dbd9ea5..032e794 100644 --- a/drizzle/postgres/0000_complex_ricochet.sql +++ b/drizzle/postgres/0000_stormy_zaladane.sql @@ -133,6 +133,12 @@ CREATE TABLE "verification" ( "updated_at" timestamp DEFAULT now() NOT NULL ); --> statement-breakpoint +ALTER TABLE "audiobook_chapters" ADD CONSTRAINT "audiobook_chapters_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "audiobooks" ADD CONSTRAINT "audiobooks_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "documents" ADD CONSTRAINT "documents_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_document_progress" ADD CONSTRAINT "user_document_progress_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_tts_chars" ADD CONSTRAINT "user_tts_chars_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint CREATE INDEX "idx_document_previews_status_lease" ON "document_previews" USING btree ("status","lease_until_ms");--> statement-breakpoint diff --git a/drizzle/postgres/meta/0000_snapshot.json b/drizzle/postgres/meta/0000_snapshot.json index 053d7d7..f8fc122 100644 --- a/drizzle/postgres/meta/0000_snapshot.json +++ b/drizzle/postgres/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "1c067f9b-aa2c-4099-a3b0-021e1a1c7ecb", + "id": "369ec851-a528-4b18-a4c9-b8ea703283ca", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -59,7 +59,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "audiobook_chapters_user_id_user_id_fk": { + "name": "audiobook_chapters_user_id_user_id_fk", + "tableFrom": "audiobook_chapters", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "audiobook_chapters_id_user_id_pk": { "name": "audiobook_chapters_id_user_id_pk", @@ -130,7 +144,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "audiobooks_user_id_user_id_fk": { + "name": "audiobooks_user_id_user_id_fk", + "tableFrom": "audiobooks", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "audiobooks_id_user_id_pk": { "name": "audiobooks_id_user_id_pk", @@ -392,7 +420,21 @@ "with": {} } }, - "foreignKeys": {}, + "foreignKeys": { + "documents_user_id_user_id_fk": { + "name": "documents_user_id_user_id_fk", + "tableFrom": "documents", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "documents_id_user_id_pk": { "name": "documents_id_user_id_pk", @@ -486,7 +528,21 @@ "with": {} } }, - "foreignKeys": {}, + "foreignKeys": { + "user_document_progress_user_id_user_id_fk": { + "name": "user_document_progress_user_id_user_id_fk", + "tableFrom": "user_document_progress", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "user_document_progress_user_id_document_id_pk": { "name": "user_document_progress_user_id_document_id_pk", @@ -541,7 +597,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "user_preferences_user_id_user_id_fk": { + "name": "user_preferences_user_id_user_id_fk", + "tableFrom": "user_preferences", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, @@ -603,7 +673,21 @@ "with": {} } }, - "foreignKeys": {}, + "foreignKeys": { + "user_tts_chars_user_id_user_id_fk": { + "name": "user_tts_chars_user_id_user_id_fk", + "tableFrom": "user_tts_chars", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "user_tts_chars_user_id_date_pk": { "name": "user_tts_chars_user_id_date_pk", diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index 174caec..37531a2 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1771184128245, - "tag": "0000_complex_ricochet", + "when": 1771185356470, + "tag": "0000_stormy_zaladane", "breakpoints": true } ] diff --git a/drizzle/scripts/migrate.mjs b/drizzle/scripts/migrate.mjs index bcc4154..01990a0 100644 --- a/drizzle/scripts/migrate.mjs +++ b/drizzle/scripts/migrate.mjs @@ -26,6 +26,16 @@ const hasConfigArg = extraArgs.includes('--config'); const configFile = process.env.POSTGRES_URL ? 'drizzle.config.pg.ts' : 'drizzle.config.sqlite.ts'; const configArgs = hasConfigArg ? [] : ['--config', configFile]; +// Ensure the docstore directory exists for SQLite migrations. +// drizzle-kit opens the database file directly and will fail if the parent +// directory is missing (e.g. in a fresh CI checkout). +if (!process.env.POSTGRES_URL) { + const dbDir = path.join(process.cwd(), 'docstore'); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } +} + const result = spawnSync('drizzle-kit', ['migrate', ...configArgs, ...extraArgs], { stdio: 'inherit', env: process.env, diff --git a/drizzle/sqlite/0000_eager_rick_jones.sql b/drizzle/sqlite/0000_furry_dragon_man.sql similarity index 88% rename from drizzle/sqlite/0000_eager_rick_jones.sql rename to drizzle/sqlite/0000_furry_dragon_man.sql index 0edbce5..66e71af 100644 --- a/drizzle/sqlite/0000_eager_rick_jones.sql +++ b/drizzle/sqlite/0000_furry_dragon_man.sql @@ -7,7 +7,8 @@ CREATE TABLE `audiobook_chapters` ( `duration` real DEFAULT 0, `file_path` text NOT NULL, `format` text NOT NULL, - PRIMARY KEY(`id`, `user_id`) + PRIMARY KEY(`id`, `user_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE TABLE `audiobooks` ( @@ -19,7 +20,8 @@ CREATE TABLE `audiobooks` ( `cover_path` text, `duration` real DEFAULT 0, `created_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), - PRIMARY KEY(`id`, `user_id`) + PRIMARY KEY(`id`, `user_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE TABLE `document_previews` ( @@ -53,7 +55,8 @@ CREATE TABLE `documents` ( `last_modified` integer NOT NULL, `file_path` text NOT NULL, `created_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), - PRIMARY KEY(`id`, `user_id`) + PRIMARY KEY(`id`, `user_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE INDEX `idx_documents_user_id` ON `documents` (`user_id`);--> statement-breakpoint @@ -67,7 +70,8 @@ CREATE TABLE `user_document_progress` ( `client_updated_at_ms` integer DEFAULT 0 NOT NULL, `created_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), `updated_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), - PRIMARY KEY(`user_id`, `document_id`) + PRIMARY KEY(`user_id`, `document_id`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE INDEX `idx_user_document_progress_user_id_updated_at` ON `user_document_progress` (`user_id`,`updated_at`);--> statement-breakpoint @@ -76,7 +80,8 @@ CREATE TABLE `user_preferences` ( `data_json` text DEFAULT '{}' NOT NULL, `client_updated_at_ms` integer DEFAULT 0 NOT NULL, `created_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), - `updated_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000) + `updated_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE TABLE `user_tts_chars` ( @@ -85,7 +90,8 @@ CREATE TABLE `user_tts_chars` ( `char_count` integer DEFAULT 0, `created_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), `updated_at` integer DEFAULT (cast(strftime('%s','now') as int) * 1000), - PRIMARY KEY(`user_id`, `date`) + PRIMARY KEY(`user_id`, `date`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint CREATE INDEX `idx_user_tts_chars_date` ON `user_tts_chars` (`date`);--> statement-breakpoint diff --git a/drizzle/sqlite/meta/0000_snapshot.json b/drizzle/sqlite/meta/0000_snapshot.json index aea1e12..459b7d5 100644 --- a/drizzle/sqlite/meta/0000_snapshot.json +++ b/drizzle/sqlite/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "b45e86c4-7d98-4cfb-9b38-5d1423b17b5e", + "id": "a55d11c6-847c-4e19-9a7c-fbc2401df061", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "audiobook_chapters": { @@ -66,7 +66,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "audiobook_chapters_user_id_user_id_fk": { + "name": "audiobook_chapters_user_id_user_id_fk", + "tableFrom": "audiobook_chapters", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "audiobook_chapters_id_user_id_pk": { "columns": [ @@ -142,7 +156,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "audiobooks_user_id_user_id_fk": { + "name": "audiobooks_user_id_user_id_fk", + "tableFrom": "audiobooks", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "audiobooks_id_user_id_pk": { "columns": [ @@ -389,7 +417,21 @@ "isUnique": false } }, - "foreignKeys": {}, + "foreignKeys": { + "documents_user_id_user_id_fk": { + "name": "documents_user_id_user_id_fk", + "tableFrom": "documents", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "documents_id_user_id_pk": { "columns": [ @@ -475,7 +517,21 @@ "isUnique": false } }, - "foreignKeys": {}, + "foreignKeys": { + "user_document_progress_user_id_user_id_fk": { + "name": "user_document_progress_user_id_user_id_fk", + "tableFrom": "user_document_progress", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "user_document_progress_user_id_document_id_pk": { "columns": [ @@ -532,7 +588,21 @@ } }, "indexes": {}, - "foreignKeys": {}, + "foreignKeys": { + "user_preferences_user_id_user_id_fk": { + "name": "user_preferences_user_id_user_id_fk", + "tableFrom": "user_preferences", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "checkConstraints": {} @@ -588,7 +658,21 @@ "isUnique": false } }, - "foreignKeys": {}, + "foreignKeys": { + "user_tts_chars_user_id_user_id_fk": { + "name": "user_tts_chars_user_id_user_id_fk", + "tableFrom": "user_tts_chars", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, "compositePrimaryKeys": { "user_tts_chars_user_id_date_pk": { "columns": [ diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 710f047..82b0b5f 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1771184127967, - "tag": "0000_eager_rick_jones", + "when": 1771185356176, + "tag": "0000_furry_dragon_man", "breakpoints": true } ] diff --git a/src/db/index.ts b/src/db/index.ts index 556bd76..e54c9d1 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -17,7 +17,6 @@ function getDrizzleDB() { if (process.env.POSTGRES_URL) { const pool = new Pool({ connectionString: process.env.POSTGRES_URL, - ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false, }); dbInstance = drizzlePg(pool, { schema: { ...schema, ...authSchemaPostgres } }); } else { diff --git a/src/db/schema_postgres.ts b/src/db/schema_postgres.ts index 6c7d9b8..585e7f8 100644 --- a/src/db/schema_postgres.ts +++ b/src/db/schema_postgres.ts @@ -1,8 +1,9 @@ import { pgTable, text, integer, real, timestamp, date, bigint, primaryKey, index, jsonb } from 'drizzle-orm/pg-core'; +import { user } from './schema_auth_postgres'; export const documents = pgTable('documents', { id: text('id').notNull(), - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), name: text('name').notNull(), type: text('type').notNull(), // pdf, epub, docx, html size: bigint('size', { mode: 'number' }).notNull(), @@ -17,7 +18,7 @@ export const documents = pgTable('documents', { export const audiobooks = pgTable('audiobooks', { id: text('id').notNull(), - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), title: text('title').notNull(), author: text('author'), description: text('description'), @@ -31,7 +32,7 @@ export const audiobooks = pgTable('audiobooks', { export const audiobookChapters = pgTable('audiobook_chapters', { id: text('id').notNull(), bookId: text('book_id').notNull(), - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), chapterIndex: integer('chapter_index').notNull(), title: text('title').notNull(), duration: real('duration').default(0), @@ -46,7 +47,7 @@ export const audiobookChapters = pgTable('audiobook_chapters', { // defined here. Only application-specific tables belong in this file. export const userTtsChars = pgTable("user_tts_chars", { - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), date: date('date').notNull(), charCount: bigint('char_count', { mode: 'number' }).default(0), createdAt: timestamp('created_at').defaultNow(), @@ -57,7 +58,7 @@ export const userTtsChars = pgTable("user_tts_chars", { })); export const userPreferences = pgTable('user_preferences', { - userId: text('user_id').primaryKey(), + userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }), dataJson: jsonb('data_json').notNull().default({}), clientUpdatedAtMs: bigint('client_updated_at_ms', { mode: 'number' }).notNull().default(0), createdAt: timestamp('created_at').defaultNow(), @@ -65,7 +66,7 @@ export const userPreferences = pgTable('user_preferences', { }); export const userDocumentProgress = pgTable('user_document_progress', { - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), documentId: text('document_id').notNull(), readerType: text('reader_type').notNull(), // pdf, epub, html location: text('location').notNull(), diff --git a/src/db/schema_sqlite.ts b/src/db/schema_sqlite.ts index e373121..e453a96 100644 --- a/src/db/schema_sqlite.ts +++ b/src/db/schema_sqlite.ts @@ -1,9 +1,10 @@ import { sqliteTable, text, integer, real, primaryKey, index } from 'drizzle-orm/sqlite-core'; import { sql } from 'drizzle-orm'; +import { user } from './schema_auth_sqlite'; export const documents = sqliteTable('documents', { id: text('id').notNull(), - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), name: text('name').notNull(), type: text('type').notNull(), // pdf, epub, docx, html size: integer('size').notNull(), @@ -18,7 +19,7 @@ export const documents = sqliteTable('documents', { export const audiobooks = sqliteTable('audiobooks', { id: text('id').notNull(), - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), title: text('title').notNull(), author: text('author'), description: text('description'), @@ -32,7 +33,7 @@ export const audiobooks = sqliteTable('audiobooks', { export const audiobookChapters = sqliteTable('audiobook_chapters', { id: text('id').notNull(), bookId: text('book_id').notNull(), - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), chapterIndex: integer('chapter_index').notNull(), title: text('title').notNull(), duration: real('duration').default(0), @@ -47,7 +48,7 @@ export const audiobookChapters = sqliteTable('audiobook_chapters', { // defined here. Only application-specific tables belong in this file. export const userTtsChars = sqliteTable("user_tts_chars", { - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), date: text('date').notNull(), // SQLite doesn't have native DATE type, text YYYY-MM-DD is standard charCount: integer('char_count').default(0), createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`), @@ -58,7 +59,7 @@ export const userTtsChars = sqliteTable("user_tts_chars", { })); export const userPreferences = sqliteTable('user_preferences', { - userId: text('user_id').primaryKey(), + userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }), dataJson: text('data_json').notNull().default('{}'), clientUpdatedAtMs: integer('client_updated_at_ms').notNull().default(0), createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`), @@ -66,7 +67,7 @@ export const userPreferences = sqliteTable('user_preferences', { }); export const userDocumentProgress = sqliteTable('user_document_progress', { - userId: text('user_id').notNull(), + userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }), documentId: text('document_id').notNull(), readerType: text('reader_type').notNull(), // pdf, epub, html location: text('location').notNull(),