From dbd4777e0fdafdf7e0d8af85dc24cd5964a9a4e5 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 12 May 2026 10:58:08 +0530 Subject: [PATCH 01/14] queue and db setup for ai suggestions --- debug-ai.ts | 62 + debug-langs.ts | 28 + src/app.ts | 1 + src/db/migrations/0010_ai_suggestions.sql | 32 + src/db/migrations/meta/0010_snapshot.json | 2336 +++++++++++++++++ src/db/migrations/meta/_journal.json | 9 +- src/db/schema.ts | 70 + .../ai-suggestions.repository.ts | 78 + .../ai-suggestions/ai-suggestions.route.ts | 118 + .../ai-suggestions/ai-suggestions.service.ts | 76 + .../ai-suggestions/ai-suggestions.types.ts | 31 + 11 files changed, 2840 insertions(+), 1 deletion(-) create mode 100644 debug-ai.ts create mode 100644 debug-langs.ts create mode 100644 src/db/migrations/0010_ai_suggestions.sql create mode 100644 src/db/migrations/meta/0010_snapshot.json create mode 100644 src/domains/ai-suggestions/ai-suggestions.repository.ts create mode 100644 src/domains/ai-suggestions/ai-suggestions.route.ts create mode 100644 src/domains/ai-suggestions/ai-suggestions.service.ts create mode 100644 src/domains/ai-suggestions/ai-suggestions.types.ts diff --git a/debug-ai.ts b/debug-ai.ts new file mode 100644 index 0000000..c8756c4 --- /dev/null +++ b/debug-ai.ts @@ -0,0 +1,62 @@ +import { eq } from 'drizzle-orm'; + +import { db } from './src/db'; +import { bible_texts, languages, project_units, projects, translated_verses } from './src/db/schema'; + +async function run() { + console.log("=== DB Diagnostic Script ==="); + + try { + // 1. Let's see what projects exist + console.log("\n--- Projects ---"); + const allProjects = await db.select().from(projects); + allProjects.forEach(p => console.log(`Project ${p.id}: ${p.name} (Source: ${p.sourceLanguage}, Target: ${p.targetLanguage})`)); + + // 2. Let's see what languages exist + console.log("\n--- Languages ---"); + const allLanguages = await db.select().from(languages); + allLanguages.forEach(l => console.log(`Lang ${l.id}: ${l.langName}`)); + + // 3. Let's see what project units exist + console.log("\n--- Project Units ---"); + const allUnits = await db.select().from(project_units); + allUnits.forEach(u => console.log(`Unit ${u.id} -> Project ${u.projectId} (Status: ${u.status})`)); + + // 4. Let's count translated verses per project unit + console.log("\n--- Translated Verses Count ---"); + const allTranslations = await db.select({ + id: translated_verses.id, + projectUnitId: translated_verses.projectUnitId, + content: translated_verses.content, + bibleTextId: translated_verses.bibleTextId + }).from(translated_verses); + + console.log(`Total translated verses in DB: ${allTranslations.length}`); + + const unitCounts: Record = {}; + allTranslations.forEach(t => { + if (t.content && t.content.trim() !== '') { + unitCounts[t.projectUnitId] = (unitCounts[t.projectUnitId] || 0) + 1; + } + }); + console.log("Valid translations per Unit ID:", unitCounts); + + // 5. Let's check a sample of valid translations + const validTranslations = allTranslations.filter(t => t.content && t.content.trim() !== ''); + if (validTranslations.length > 0) { + console.log("\n--- Sample valid translation ---"); + const sample = validTranslations[0]; + console.log(sample); + + // Find its bible text + const text = await db.select().from(bible_texts).where(eq(bible_texts.id, sample.bibleTextId)); + console.log("Source text:", text[0]); + } + } catch (error) { + console.error("DB Error:", error); + } finally { + process.exit(0); + } +} + +run().catch(console.error); diff --git a/debug-langs.ts b/debug-langs.ts new file mode 100644 index 0000000..cd868a2 --- /dev/null +++ b/debug-langs.ts @@ -0,0 +1,28 @@ +import { and, eq, isNotNull, ne } from 'drizzle-orm'; + +import { db } from './src/db'; +import { project_units, projects, translated_verses } from './src/db/schema'; + +async function run() { + const res = await db.select({ + content: translated_verses.content, + projName: projects.name + }) + .from(translated_verses) + .innerJoin(project_units, eq(translated_verses.projectUnitId, project_units.id)) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .where(and( + eq(projects.sourceLanguage, 3), + eq(projects.targetLanguage, 5), + isNotNull(translated_verses.content), + ne(translated_verses.content, '') + )) + .limit(10); + + console.log("Samples of Gujarati -> Kachi Koli translations in DB:"); + res.forEach(r => { + console.log(`[${r.projName}] Content: ${r.content.substring(0, 50)}...`); + }); + process.exit(0); +} +run(); diff --git a/src/app.ts b/src/app.ts index e589c3d..d820ea1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,7 @@ import '@/domains/chapter-assignments/editor-state/user-chapter-assignment-edito import '@/domains/projects/users/project-users.route'; import '@/domains/users/projects/user-projects.route'; import '@/domains/chapter-assignments/presence/chapter-assignments-presence.route'; +import '@/domains/ai-suggestions/ai-suggestions.route'; configureOpenAPI(server); export default server; diff --git a/src/db/migrations/0010_ai_suggestions.sql b/src/db/migrations/0010_ai_suggestions.sql new file mode 100644 index 0000000..9bb1426 --- /dev/null +++ b/src/db/migrations/0010_ai_suggestions.sql @@ -0,0 +1,32 @@ +CREATE TYPE "public"."ai_suggestion_job_status" AS ENUM('queued', 'processing', 'completed', 'failed');--> statement-breakpoint +CREATE TABLE "ai_suggestion_jobs" ( + "id" serial PRIMARY KEY NOT NULL, + "project_unit_id" integer NOT NULL, + "bible_id" integer NOT NULL, + "book_code" varchar(50) NOT NULL, + "chapter_number" integer NOT NULL, + "verse_start" integer NOT NULL, + "verse_end" integer NOT NULL, + "status" "ai_suggestion_job_status" DEFAULT 'queued' NOT NULL, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "ai_suggestions" ( + "id" serial PRIMARY KEY NOT NULL, + "bible_text_id" integer NOT NULL, + "project_unit_id" integer NOT NULL, + "suggested_text" varchar NOT NULL, + "model_info" varchar(100), + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +ALTER TABLE "ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_suggestions" ADD CONSTRAINT "ai_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai_suggestions" ADD CONSTRAINT "ai_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_ai_jobs_project_unit" ON "ai_suggestion_jobs" USING btree ("project_unit_id");--> statement-breakpoint +CREATE INDEX "idx_ai_jobs_status" ON "ai_suggestion_jobs" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_jobs_range" ON "ai_suggestion_jobs" USING btree ("project_unit_id","book_code","chapter_number","verse_start","verse_end");--> statement-breakpoint +CREATE INDEX "idx_ai_suggestions_bible_text" ON "ai_suggestions" USING btree ("bible_text_id");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_suggestions_per_text_unit" ON "ai_suggestions" USING btree ("bible_text_id","project_unit_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0010_snapshot.json b/src/db/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..f0947e3 --- /dev/null +++ b/src/db/migrations/meta/0010_snapshot.json @@ -0,0 +1,2336 @@ +{ + "id": "b5e72067-119a-4734-97f0-fe7382fbdb9d", + "prevId": "0e1e1972-53ae-4b50-9df0-af09aa4190cc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.active_chapter_editors": { + "name": "active_chapter_editors", + "schema": "", + "columns": { + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_active_editors_chapter": { + "name": "idx_active_editors_chapter", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "active_chapter_editors", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "active_chapter_editors_user_id_users_id_fk": { + "name": "active_chapter_editors_user_id_users_id_fk", + "tableFrom": "active_chapter_editors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "active_chapter_editors_chapter_assignment_id_user_id_pk": { + "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", + "columns": [ + "chapter_assignment_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_suggestion_jobs": { + "name": "ai_suggestion_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_code": { + "name": "book_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "chapter_number": { + "name": "chapter_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "verse_start": { + "name": "verse_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "verse_end": { + "name": "verse_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "ai_suggestion_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ai_jobs_project_unit": { + "name": "idx_ai_jobs_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_jobs_status": { + "name": "idx_ai_jobs_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_jobs_range": { + "name": "uq_ai_jobs_range", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verse_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verse_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestion_jobs_project_unit_id_project_units_id_fk": { + "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestion_jobs", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestion_jobs_bible_id_bibles_id_fk": { + "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", + "tableFrom": "ai_suggestion_jobs", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_suggestions": { + "name": "ai_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "suggested_text": { + "name": "suggested_text", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "model_info": { + "name": "model_info", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ai_suggestions_bible_text": { + "name": "idx_ai_suggestions_bible_text", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_suggestions_per_text_unit": { + "name": "uq_ai_suggestions_per_text_unit", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestions_bible_text_id_bible_texts_id_fk": { + "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", + "tableFrom": "ai_suggestions", + "tableTo": "bible_texts", + "columnsFrom": [ + "bible_text_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestions_project_unit_id_project_units_id_fk": { + "name": "ai_suggestions_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestions", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bible_books": { + "name": "bible_books", + "schema": "", + "columns": { + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bible_books_bible_id_bibles_id_fk": { + "name": "bible_books_bible_id_bibles_id_fk", + "tableFrom": "bible_books", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bible_books_book_id_books_id_fk": { + "name": "bible_books_book_id_books_id_fk", + "tableFrom": "bible_books", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bible_texts": { + "name": "bible_texts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chapter_number": { + "name": "chapter_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "verse_number": { + "name": "verse_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_bible_texts_bible_book_chapter": { + "name": "idx_bible_texts_bible_book_chapter", + "columns": [ + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bible_texts_bible_book_chapter_verse": { + "name": "idx_bible_texts_bible_book_chapter_verse", + "columns": [ + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verse_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bible_texts_bible_id_bibles_id_fk": { + "name": "bible_texts_bible_id_bibles_id_fk", + "tableFrom": "bible_texts", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bible_texts_book_id_books_id_fk": { + "name": "bible_texts_book_id_books_id_fk", + "tableFrom": "bible_texts", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bibles": { + "name": "bibles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "language_id": { + "name": "language_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bibles_language_id_languages_id_fk": { + "name": "bibles_language_id_languages_id_fk", + "tableFrom": "bibles", + "tableTo": "languages", + "columnsFrom": [ + "language_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "bibles_name_unique": { + "name": "bibles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "bibles_abbreviation_unique": { + "name": "bibles_abbreviation_unique", + "nullsNotDistinct": false, + "columns": [ + "abbreviation" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.books": { + "name": "books", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "eng_display_name": { + "name": "eng_display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignment_assigned_user_history": { + "name": "chapter_assignment_assigned_user_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "assignment_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ca_user_history_assignment": { + "name": "idx_ca_user_history_assignment", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ca_user_history_user": { + "name": "idx_ca_user_history_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "chapter_assignment_assigned_user_history", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk": { + "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignment_assigned_user_history", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignment_snapshots": { + "name": "chapter_assignment_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ca_snapshots_assignment": { + "name": "idx_ca_snapshots_assignment", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ca_snapshots_user": { + "name": "idx_ca_snapshots_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "chapter_assignment_snapshots", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chapter_assignment_snapshots_assigned_user_id_users_id_fk": { + "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignment_snapshots", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignment_status_history": { + "name": "chapter_assignment_status_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ca_status_history_assignment": { + "name": "idx_ca_status_history_assignment", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "chapter_assignment_status_history", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignments": { + "name": "chapter_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chapter_number": { + "name": "chapter_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "peer_checker_id": { + "name": "peer_checker_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "chapter_status": { + "name": "chapter_status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "submitted_time": { + "name": "submitted_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uq_chapter_assignment_per_chapter": { + "name": "uq_chapter_assignment_per_chapter", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_assigned_user": { + "name": "idx_chapter_assignments_assigned_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_peer_checker_status": { + "name": "idx_chapter_assignments_peer_checker_status", + "columns": [ + { + "expression": "peer_checker_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_project_unit": { + "name": "idx_chapter_assignments_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignments_project_unit_id_project_units_id_fk": { + "name": "chapter_assignments_project_unit_id_project_units_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "chapter_assignments_bible_id_bibles_id_fk": { + "name": "chapter_assignments_bible_id_bibles_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_book_id_books_id_fk": { + "name": "chapter_assignments_book_id_books_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_assigned_user_id_users_id_fk": { + "name": "chapter_assignments_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_peer_checker_id_users_id_fk": { + "name": "chapter_assignments_peer_checker_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": [ + "peer_checker_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.languages": { + "name": "languages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "lang_name": { + "name": "lang_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "lang_name_localized": { + "name": "lang_name_localized", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "lang_code_iso_639_3": { + "name": "lang_code_iso_639_3", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "script_direction": { + "name": "script_direction", + "type": "script_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'ltr'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_name_unique": { + "name": "organizations_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "permissions_name_unique": { + "name": "permissions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_unit_bible_books": { + "name": "project_unit_bible_books", + "schema": "", + "columns": { + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_unit_bible_books_project_unit_id_project_units_id_fk": { + "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", + "tableFrom": "project_unit_bible_books", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "project_unit_bible_books_bible_id_bibles_id_fk": { + "name": "project_unit_bible_books_bible_id_bibles_id_fk", + "tableFrom": "project_unit_bible_books", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_unit_bible_books_book_id_books_id_fk": { + "name": "project_unit_bible_books_book_id_books_id_fk", + "tableFrom": "project_unit_bible_books", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_units": { + "name": "project_units", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_units_project_id_projects_id_fk": { + "name": "project_units_project_id_projects_id_fk", + "tableFrom": "project_units", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_users": { + "name": "project_users", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_project_users_project": { + "name": "idx_project_users_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_users_user": { + "name": "idx_project_users_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_users_project_id_projects_id_fk": { + "name": "project_users_project_id_projects_id_fk", + "tableFrom": "project_users", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "project_users_user_id_users_id_fk": { + "name": "project_users_user_id_users_id_fk", + "tableFrom": "project_users", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "project_users_project_id_user_id_pk": { + "name": "project_users_project_id_user_id_pk", + "columns": [ + "project_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_language": { + "name": "source_language", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_language": { + "name": "target_language", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "status": { + "name": "status", + "type": "project_assignment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_assigned'" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_source_language_languages_id_fk": { + "name": "projects_source_language_languages_id_fk", + "tableFrom": "projects", + "tableTo": "languages", + "columnsFrom": [ + "source_language" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_target_language_languages_id_fk": { + "name": "projects_target_language_languages_id_fk", + "tableFrom": "projects", + "tableTo": "languages", + "columnsFrom": [ + "target_language" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_organization_organizations_id_fk": { + "name": "projects_organization_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_created_by_users_id_fk": { + "name": "projects_created_by_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_role_permissions_role": { + "name": "idx_role_permissions_role", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_permissions_role_id_permission_id_pk": { + "name": "role_permissions_role_id_permission_id_pk", + "columns": [ + "role_id", + "permission_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.translated_verses": { + "name": "translated_verses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_translated_verse_per_bible_text": { + "name": "uq_translated_verse_per_bible_text", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "translated_verses_project_unit_id_project_units_id_fk": { + "name": "translated_verses_project_unit_id_project_units_id_fk", + "tableFrom": "translated_verses", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "translated_verses_bible_text_id_bible_texts_id_fk": { + "name": "translated_verses_bible_text_id_bible_texts_id_fk", + "tableFrom": "translated_verses", + "tableTo": "bible_texts", + "columnsFrom": [ + "bible_text_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "translated_verses_assigned_user_id_users_id_fk": { + "name": "translated_verses_assigned_user_id_users_id_fk", + "tableFrom": "translated_verses", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_chapter_assignment_editor_state": { + "name": "user_chapter_assignment_editor_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uq_user_chapter_assignment_editor_state": { + "name": "uq_user_chapter_assignment_editor_state", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_chapter_assignment_editor_state_user_id_users_id_fk": { + "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", + "tableFrom": "user_chapter_assignment_editor_state", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "user_chapter_assignment_editor_state", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_role_roles_id_fk": { + "name": "users_role_roles_id_fk", + "tableFrom": "users", + "tableTo": "roles", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_organization_organizations_id_fk": { + "name": "users_organization_organizations_id_fk", + "tableFrom": "users", + "tableTo": "organizations", + "columnsFrom": [ + "organization" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_created_by_users_id_fk": { + "name": "users_created_by_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ai_suggestion_job_status": { + "name": "ai_suggestion_job_status", + "schema": "public", + "values": [ + "queued", + "processing", + "completed", + "failed" + ] + }, + "public.assignment_role": { + "name": "assignment_role", + "schema": "public", + "values": [ + "drafter", + "peer_checker" + ] + }, + "public.chapter_status": { + "name": "chapter_status", + "schema": "public", + "values": [ + "not_started", + "draft", + "peer_check", + "community_review", + "linguist_check", + "theological_check", + "consultant_check", + "complete" + ] + }, + "public.project_assignment_status": { + "name": "project_assignment_status", + "schema": "public", + "values": [ + "active", + "not_assigned" + ] + }, + "public.project_status": { + "name": "project_status", + "schema": "public", + "values": [ + "not_started", + "in_progress", + "completed" + ] + }, + "public.script_direction": { + "name": "script_direction", + "schema": "public", + "values": [ + "ltr", + "rtl" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "invited", + "verified", + "inactive" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index d368c10..f493e7d 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1774517640951, "tag": "0009_adding_more_project_status_", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1778478819437, + "tag": "0010_ai_suggestions", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index c738700..bcfe111 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -40,6 +40,12 @@ export const chapterStatusEnum = pgEnum('chapter_status', [ 'complete', ]); export const assignmentRoleEnum = pgEnum('assignment_role', ['drafter', 'peer_checker']); +export const aiSuggestionJobStatusEnum = pgEnum('ai_suggestion_job_status', [ + 'queued', + 'processing', + 'completed', + 'failed', +]); export const roles = pgTable('roles', { id: serial('id').primaryKey(), @@ -764,3 +770,67 @@ export const patchProjectsClientSchema = patchProjectsSchema.omit({ export const patchUsersClientSchema = patchUsersSchema.omit({ organization: true, }); + +export const ai_suggestion_jobs = pgTable('ai_suggestion_jobs', { + id: serial('id').primaryKey(), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + bibleId: integer('bible_id') + .notNull() + .references(() => bibles.id), + bookCode: varchar('book_code', { length: 50 }).notNull(), + chapterNumber: integer('chapter_number').notNull(), + verseStart: integer('verse_start').notNull(), + verseEnd: integer('verse_end').notNull(), + status: aiSuggestionJobStatusEnum('status').notNull().default('queued'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => new Date()), +}, (table) => [ + index('idx_ai_jobs_project_unit').on(table.projectUnitId), + index('idx_ai_jobs_status').on(table.status), + uniqueIndex('uq_ai_jobs_range').on(table.projectUnitId, table.bookCode, table.chapterNumber, table.verseStart, table.verseEnd), +]); + +export const ai_suggestions = pgTable('ai_suggestions', { + id: serial('id').primaryKey(), + bibleTextId: integer('bible_text_id') + .notNull() + .references(() => bible_texts.id, { onDelete: 'cascade' }), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + suggestedText: varchar('suggested_text').notNull(), + modelInfo: varchar('model_info', { length: 100 }), + createdAt: timestamp('created_at').defaultNow(), +}, (table) => [ + index('idx_ai_suggestions_bible_text').on(table.bibleTextId), + uniqueIndex('uq_ai_suggestions_per_text_unit').on(table.bibleTextId, table.projectUnitId), +]); + +export const selectAiSuggestionJobsSchema = createSelectSchema(ai_suggestion_jobs); +export const selectAiSuggestionsSchema = createSelectSchema(ai_suggestions); + +export const insertAiSuggestionJobsSchema = createInsertSchema(ai_suggestion_jobs, { + projectUnitId: (schema) => schema.int(), + bibleId: (schema) => schema.int(), + bookCode: (schema) => schema.min(1), + chapterNumber: (schema) => schema.int().min(1), + verseStart: (schema) => schema.int().min(1), + verseEnd: (schema) => schema.int().min(1), +}) +.required({ projectUnitId: true, bibleId: true, bookCode: true, chapterNumber: true, verseStart: true, verseEnd: true }) +.omit({ id: true, status: true, createdAt: true, updatedAt: true }); + +export const insertAiSuggestionsSchema = createInsertSchema(ai_suggestions, { + bibleTextId: (schema) => schema.int(), + projectUnitId: (schema) => schema.int(), + suggestedText: (schema) => schema.min(1), +}) +.required({ bibleTextId: true, projectUnitId: true, suggestedText: true }) +.omit({ id: true, createdAt: true }); + +export const patchAiSuggestionJobsSchema = insertAiSuggestionJobsSchema.partial(); +export const patchAiSuggestionsSchema = insertAiSuggestionsSchema.partial(); diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts new file mode 100644 index 0000000..f08a808 --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -0,0 +1,78 @@ +import { and, eq, inArray } from 'drizzle-orm'; + +import type { DbTransaction, Result } from '@/lib/types'; + +import { db } from '@/db'; +import { ai_suggestion_jobs, ai_suggestions } from '@/db/schema'; +import { logger } from '@/lib/logger'; +import { err, ErrorCode, ok } from '@/lib/types'; + + +export async function queueAiSuggestionJobs( + jobs: { + projectUnitId: number; + bibleId: number; + bookCode: string; + chapterNumber: number; + verseStart: number; + verseEnd: number; + }[], + tx?: DbTransaction +): Promise> { + const database = tx || db; + try { + if (jobs.length === 0) return ok(undefined); + + await database + .insert(ai_suggestion_jobs) + .values(jobs) + .onConflictDoNothing({ + target: [ + ai_suggestion_jobs.projectUnitId, + ai_suggestion_jobs.bookCode, + ai_suggestion_jobs.chapterNumber, + ai_suggestion_jobs.verseStart, + ai_suggestion_jobs.verseEnd, + ], + }); + + return ok(undefined); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to queue AI suggestion jobs', + context: { jobCount: jobs.length }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } +} + +export async function getAiSuggestions( + projectUnitId: number, + bibleTextIds: number[], + tx?: DbTransaction +) { + const database = tx || db; + try { + if (bibleTextIds.length === 0) return ok([]); + + const results = await database + .select() + .from(ai_suggestions) + .where( + and( + eq(ai_suggestions.projectUnitId, projectUnitId), + inArray(ai_suggestions.bibleTextId, bibleTextIds) + ) + ); + + return ok(results); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to fetch AI suggestions', + context: { projectUnitId, textIdsCount: bibleTextIds.length }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/ai-suggestions/ai-suggestions.route.ts b/src/domains/ai-suggestions/ai-suggestions.route.ts new file mode 100644 index 0000000..7420213 --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.route.ts @@ -0,0 +1,118 @@ +import { createRoute } from '@hono/zod-openapi'; +import * as HttpStatusCodes from 'stoker/http-status-codes'; +import * as HttpStatusPhrases from 'stoker/http-status-phrases'; +import { jsonContent } from 'stoker/openapi/helpers'; +import { createMessageObjectSchema } from 'stoker/openapi/schemas'; + +import { PERMISSIONS } from '@/lib/permissions'; +import { getHttpStatus } from '@/lib/types'; +import { authenticateUser, requirePermission } from '@/middlewares/role-auth'; +import { server } from '@/server/server'; + +import * as aiSuggestionsService from './ai-suggestions.service'; +import { + aiSuggestionsListResponseSchema, + getAiSuggestionsQuerySchema, + queueNextVersesRequestSchema, +} from './ai-suggestions.types'; + +// ─── GET /ai-suggestions ────────────────────────────────────────────── + +const getAiSuggestionsRoute = createRoute({ + tags: ['AI Suggestions'], + method: 'get', + path: '/ai-suggestions', + middleware: [authenticateUser, requirePermission(PERMISSIONS.PROJECT_VIEW)] as const, + request: { + query: getAiSuggestionsQuerySchema, + }, + responses: { + [HttpStatusCodes.OK]: jsonContent( + aiSuggestionsListResponseSchema, + 'List of AI suggestions' + ), + [HttpStatusCodes.BAD_REQUEST]: jsonContent( + createMessageObjectSchema('Bad Request'), + 'Validation error' + ), + [HttpStatusCodes.UNAUTHORIZED]: jsonContent( + createMessageObjectSchema('Unauthorized'), + 'Authentication required' + ), + [HttpStatusCodes.FORBIDDEN]: jsonContent( + createMessageObjectSchema('Forbidden'), + 'Permission denied' + ), + [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonContent( + createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR), + 'Internal server error' + ), + }, + summary: 'Get pre-generated AI suggestions', + description: 'Retrieves AI suggestions for the specified bible text IDs.', +}); + +server.openapi(getAiSuggestionsRoute, async (c) => { + const query = c.req.valid('query'); + + const result = await aiSuggestionsService.getAiSuggestions(query); + if (result.ok) { + return c.json(result.data, HttpStatusCodes.OK); + } + + return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); +}); + +// ─── POST /ai-suggestions/queue-next ────────────────────────────────────────── + +const queueNextVersesRoute = createRoute({ + tags: ['AI Suggestions'], + method: 'post', + path: '/ai-suggestions/queue-next', + middleware: [authenticateUser, requirePermission(PERMISSIONS.PROJECT_VIEW)] as const, + request: { + body: jsonContent(queueNextVersesRequestSchema, 'Verses context'), + }, + responses: { + [HttpStatusCodes.OK]: jsonContent( + createMessageObjectSchema('Successfully queued'), + 'Queued' + ), + [HttpStatusCodes.BAD_REQUEST]: jsonContent( + createMessageObjectSchema('Bad Request'), + 'Validation error' + ), + [HttpStatusCodes.UNAUTHORIZED]: jsonContent( + createMessageObjectSchema('Unauthorized'), + 'Authentication required' + ), + [HttpStatusCodes.FORBIDDEN]: jsonContent( + createMessageObjectSchema('Forbidden'), + 'Permission denied' + ), + [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonContent( + createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR), + 'Internal server error' + ), + }, + summary: 'Queue pre-generation of AI suggestions', + description: 'Triggers the queue to generate suggestions for the next few verses ahead of the drafter.', +}); + +server.openapi(queueNextVersesRoute, async (c) => { + const body = c.req.valid('json'); + + const result = await aiSuggestionsService.queueNextVerses( + body.projectUnitId, + body.bibleId, + body.bookCode, + body.chapterNumber, + body.currentVerse, + body.lookahead + ); + if (result.ok) { + return c.json({ message: 'Queued' }, HttpStatusCodes.OK); + } + + return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); +}); diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts new file mode 100644 index 0000000..e119122 --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -0,0 +1,76 @@ +import { and, asc, eq, gt } from 'drizzle-orm'; + +import type {Result} from '@/lib/types'; + +import { db } from '@/db'; +import { bible_texts, books } from '@/db/schema'; +import { logger } from '@/lib/logger'; +import { err, ErrorCode, ok } from '@/lib/types'; + +import type { AiSuggestionsListResponse, GetAiSuggestionsQuery } from './ai-suggestions.types'; + +import { getAiSuggestions as getAiSuggestionsRepo, queueAiSuggestionJobs } from './ai-suggestions.repository'; + +export async function getAiSuggestions( + query: GetAiSuggestionsQuery +): Promise> { + const ids = query.bibleTextIds.split(',').map(id => Number.parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id)); + + const suggestionsResult = await getAiSuggestionsRepo(query.projectUnitId, ids); + + if (!suggestionsResult.ok) { + return suggestionsResult; + } + + const data = suggestionsResult.data.map(suggestion => ({ + bibleTextId: suggestion.bibleTextId, + suggestedText: suggestion.suggestedText, + modelInfo: suggestion.modelInfo, + })); + + return ok({ data }); +} + +export async function queueNextVerses( + projectUnitId: number, + bibleId: number, + bookCode: string, + chapterNumber: number, + currentVerse: number, + lookahead: number = 5 +): Promise> { + try { + // 1. Find the next few verses + const nextVerses = await db + .select({ verseNumber: bible_texts.verseNumber }) + .from(bible_texts) + .innerJoin(books, eq(bible_texts.bookId, books.id)) + .where( + and( + eq(bible_texts.bibleId, bibleId), + eq(books.code, bookCode), + eq(bible_texts.chapterNumber, chapterNumber), + gt(bible_texts.verseNumber, currentVerse) + ) + ) + .orderBy(asc(bible_texts.verseNumber)) + .limit(lookahead); + + if (nextVerses.length === 0) return ok(undefined); + + // 2. Queue them + const jobs = nextVerses.map(v => ({ + projectUnitId, + bibleId, + bookCode, + chapterNumber, + verseStart: v.verseNumber, + verseEnd: v.verseNumber, + })); + + return await queueAiSuggestionJobs(jobs); + } catch (error) { + logger.error(error); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/ai-suggestions/ai-suggestions.types.ts b/src/domains/ai-suggestions/ai-suggestions.types.ts new file mode 100644 index 0000000..62fdfe5 --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.types.ts @@ -0,0 +1,31 @@ +import { z } from '@hono/zod-openapi'; + +export const getAiSuggestionsQuerySchema = z.object({ + projectUnitId: z.coerce.number().int().positive(), + bibleTextIds: z.string().describe('Comma-separated list of bible text IDs'), +}); + +export type GetAiSuggestionsQuery = z.infer; + +export const aiSuggestionResponseSchema = z.object({ + bibleTextId: z.number().int(), + suggestedText: z.string(), + modelInfo: z.string().nullable().optional(), +}); + +export const aiSuggestionsListResponseSchema = z.object({ + data: z.array(aiSuggestionResponseSchema), +}); + +export type AiSuggestionsListResponse = z.infer; + +export const queueNextVersesRequestSchema = z.object({ + projectUnitId: z.number().int().positive(), + bibleId: z.number().int().positive(), + bookCode: z.string().min(3).max(3), + chapterNumber: z.number().int().positive(), + currentVerse: z.number().int().positive(), + lookahead: z.number().int().positive().max(20).default(5), +}); + +export type QueueNextVersesRequest = z.infer; From 92f85b6dd795f4490d7fbfff176458e36a566226 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 12 May 2026 11:09:17 +0530 Subject: [PATCH 02/14] format fix --- debug-ai.ts | 69 +-- debug-langs.ts | 35 +- package-lock.json | 34 +- src/db/migrations/meta/0010_snapshot.json | 407 +++++------------- src/db/migrations/meta/_journal.json | 2 +- src/db/schema.ts | 103 +++-- .../ai-suggestions.repository.ts | 1 - .../ai-suggestions/ai-suggestions.route.ts | 13 +- .../ai-suggestions/ai-suggestions.service.ts | 20 +- 9 files changed, 252 insertions(+), 432 deletions(-) diff --git a/debug-ai.ts b/debug-ai.ts index c8756c4..c48166c 100644 --- a/debug-ai.ts +++ b/debug-ai.ts @@ -1,59 +1,76 @@ import { eq } from 'drizzle-orm'; import { db } from './src/db'; -import { bible_texts, languages, project_units, projects, translated_verses } from './src/db/schema'; +import { + bible_texts, + languages, + project_units, + projects, + translated_verses, +} from './src/db/schema'; async function run() { - console.log("=== DB Diagnostic Script ==="); - + console.log('=== DB Diagnostic Script ==='); + try { // 1. Let's see what projects exist - console.log("\n--- Projects ---"); + console.log('\n--- Projects ---'); const allProjects = await db.select().from(projects); - allProjects.forEach(p => console.log(`Project ${p.id}: ${p.name} (Source: ${p.sourceLanguage}, Target: ${p.targetLanguage})`)); + allProjects.forEach((p) => + console.log( + `Project ${p.id}: ${p.name} (Source: ${p.sourceLanguage}, Target: ${p.targetLanguage})` + ) + ); // 2. Let's see what languages exist - console.log("\n--- Languages ---"); + console.log('\n--- Languages ---'); const allLanguages = await db.select().from(languages); - allLanguages.forEach(l => console.log(`Lang ${l.id}: ${l.langName}`)); + allLanguages.forEach((l) => console.log(`Lang ${l.id}: ${l.langName}`)); // 3. Let's see what project units exist - console.log("\n--- Project Units ---"); + console.log('\n--- Project Units ---'); const allUnits = await db.select().from(project_units); - allUnits.forEach(u => console.log(`Unit ${u.id} -> Project ${u.projectId} (Status: ${u.status})`)); + allUnits.forEach((u) => + console.log(`Unit ${u.id} -> Project ${u.projectId} (Status: ${u.status})`) + ); // 4. Let's count translated verses per project unit - console.log("\n--- Translated Verses Count ---"); - const allTranslations = await db.select({ - id: translated_verses.id, - projectUnitId: translated_verses.projectUnitId, - content: translated_verses.content, - bibleTextId: translated_verses.bibleTextId - }).from(translated_verses); - + console.log('\n--- Translated Verses Count ---'); + const allTranslations = await db + .select({ + id: translated_verses.id, + projectUnitId: translated_verses.projectUnitId, + content: translated_verses.content, + bibleTextId: translated_verses.bibleTextId, + }) + .from(translated_verses); + console.log(`Total translated verses in DB: ${allTranslations.length}`); - + const unitCounts: Record = {}; - allTranslations.forEach(t => { + allTranslations.forEach((t) => { if (t.content && t.content.trim() !== '') { unitCounts[t.projectUnitId] = (unitCounts[t.projectUnitId] || 0) + 1; } }); - console.log("Valid translations per Unit ID:", unitCounts); + console.log('Valid translations per Unit ID:', unitCounts); // 5. Let's check a sample of valid translations - const validTranslations = allTranslations.filter(t => t.content && t.content.trim() !== ''); + const validTranslations = allTranslations.filter((t) => t.content && t.content.trim() !== ''); if (validTranslations.length > 0) { - console.log("\n--- Sample valid translation ---"); + console.log('\n--- Sample valid translation ---'); const sample = validTranslations[0]; console.log(sample); - + // Find its bible text - const text = await db.select().from(bible_texts).where(eq(bible_texts.id, sample.bibleTextId)); - console.log("Source text:", text[0]); + const text = await db + .select() + .from(bible_texts) + .where(eq(bible_texts.id, sample.bibleTextId)); + console.log('Source text:', text[0]); } } catch (error) { - console.error("DB Error:", error); + console.error('DB Error:', error); } finally { process.exit(0); } diff --git a/debug-langs.ts b/debug-langs.ts index cd868a2..8a4240a 100644 --- a/debug-langs.ts +++ b/debug-langs.ts @@ -4,23 +4,26 @@ import { db } from './src/db'; import { project_units, projects, translated_verses } from './src/db/schema'; async function run() { - const res = await db.select({ - content: translated_verses.content, - projName: projects.name - }) - .from(translated_verses) - .innerJoin(project_units, eq(translated_verses.projectUnitId, project_units.id)) - .innerJoin(projects, eq(project_units.projectId, projects.id)) - .where(and( - eq(projects.sourceLanguage, 3), - eq(projects.targetLanguage, 5), - isNotNull(translated_verses.content), - ne(translated_verses.content, '') - )) - .limit(10); + const res = await db + .select({ + content: translated_verses.content, + projName: projects.name, + }) + .from(translated_verses) + .innerJoin(project_units, eq(translated_verses.projectUnitId, project_units.id)) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .where( + and( + eq(projects.sourceLanguage, 3), + eq(projects.targetLanguage, 5), + isNotNull(translated_verses.content), + ne(translated_verses.content, '') + ) + ) + .limit(10); - console.log("Samples of Gujarati -> Kachi Koli translations in DB:"); - res.forEach(r => { + console.log('Samples of Gujarati -> Kachi Koli translations in DB:'); + res.forEach((r) => { console.log(`[${r.projName}] Content: ${r.content.substring(0, 50)}...`); }); process.exit(0); diff --git a/package-lock.json b/package-lock.json index 5a13726..21a697b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -183,7 +183,6 @@ "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-7.3.4.tgz", "integrity": "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==", "license": "MIT", - "peer": true, "dependencies": { "openapi3-ts": "^4.1.2" }, @@ -589,6 +588,7 @@ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -609,6 +609,7 @@ "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.28.0" }, @@ -625,6 +626,7 @@ "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -1905,7 +1907,6 @@ "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-0.19.10.tgz", "integrity": "sha512-dpoS6DenvoJyvxtQ7Kd633FRZ/Qf74+4+o9s+zZI8pEqnbjdF/DtxIib08WDpCaWabMEJOL5TXpMgNEZvb7hpA==", "license": "MIT", - "peer": true, "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.0", "@hono/zod-validator": "^0.7.1", @@ -2078,7 +2079,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -4883,7 +4883,6 @@ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", @@ -5273,6 +5272,7 @@ "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/parser": "^7.28.0", "@vue/shared": "3.5.18", @@ -5287,6 +5287,7 @@ "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-core": "3.5.18", "@vue/shared": "3.5.18" @@ -5317,6 +5318,7 @@ "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.18", "@vue/shared": "3.5.18" @@ -5327,7 +5329,8 @@ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@xmldom/xmldom": { "version": "0.8.12", @@ -5355,7 +5358,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5770,7 +5772,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -6481,7 +6482,6 @@ "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", "integrity": "sha512-r2HV5qFkUICyoaKlBEpLKHjxMXATUf/l+h8UZPGBHGLy4DDiY2sOLcIctax4eRnTw5wH2jTMExLntGPJ8eOJxw==", "license": "MIT", - "peer": true, "dependencies": { "semver": "^7.5.3" } @@ -6556,7 +6556,6 @@ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", @@ -6759,6 +6758,7 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=0.12" }, @@ -6888,7 +6888,6 @@ "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7136,7 +7135,6 @@ "integrity": "sha512-Tdns+CDjS+m7QrM85wwRi2yLae88XiWVdIOXjp9mDII0pmTBQlczPCmjpKnjiUIY3yPZNLqb5Ms/A/JXcBF2Dw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@dprint/formatter": "^0.3.0", "@dprint/markdown": "^0.17.8", @@ -7643,7 +7641,8 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/esutils": { "version": "2.0.3", @@ -8237,7 +8236,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz", "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -8636,7 +8634,6 @@ "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "acorn": "^8.5.0", "eslint-visitor-keys": "^3.0.0", @@ -10177,7 +10174,6 @@ "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", "license": "MIT", - "peer": true, "dependencies": { "yaml": "^2.8.0" } @@ -10364,7 +10360,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -10488,7 +10483,6 @@ "resolved": "https://registry.npmjs.org/pino/-/pino-9.7.0.tgz", "integrity": "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==", "license": "MIT", - "peer": true, "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", @@ -10668,7 +10662,6 @@ "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", "license": "Unlicense", - "peer": true, "engines": { "node": ">=12" }, @@ -12441,7 +12434,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12625,7 +12617,6 @@ "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -13208,7 +13199,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -13289,7 +13279,6 @@ "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", @@ -13676,7 +13665,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/db/migrations/meta/0010_snapshot.json b/src/db/migrations/meta/0010_snapshot.json index f0947e3..09e114f 100644 --- a/src/db/migrations/meta/0010_snapshot.json +++ b/src/db/migrations/meta/0010_snapshot.json @@ -57,12 +57,8 @@ "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -70,12 +66,8 @@ "name": "active_chapter_editors_user_id_users_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -83,10 +75,7 @@ "compositePrimaryKeys": { "active_chapter_editors_chapter_assignment_id_user_id_pk": { "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", - "columns": [ - "chapter_assignment_id", - "user_id" - ] + "columns": ["chapter_assignment_id", "user_id"] } }, "uniqueConstraints": {}, @@ -239,12 +228,8 @@ "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -252,12 +237,8 @@ "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -353,12 +334,8 @@ "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", "tableFrom": "ai_suggestions", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -366,12 +343,8 @@ "name": "ai_suggestions_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestions", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -419,12 +392,8 @@ "name": "bible_books_bible_id_bibles_id_fk", "tableFrom": "bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -432,12 +401,8 @@ "name": "bible_books_book_id_books_id_fk", "tableFrom": "bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -570,12 +535,8 @@ "name": "bible_texts_bible_id_bibles_id_fk", "tableFrom": "bible_texts", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -583,12 +544,8 @@ "name": "bible_texts_book_id_books_id_fk", "tableFrom": "bible_texts", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -648,12 +605,8 @@ "name": "bibles_language_id_languages_id_fk", "tableFrom": "bibles", "tableTo": "languages", - "columnsFrom": [ - "language_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["language_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -663,16 +616,12 @@ "bibles_name_unique": { "name": "bibles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "bibles_abbreviation_unique": { "name": "bibles_abbreviation_unique", "nullsNotDistinct": false, - "columns": [ - "abbreviation" - ] + "columns": ["abbreviation"] } }, "policies": {}, @@ -791,12 +740,8 @@ "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -804,12 +749,8 @@ "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -900,12 +841,8 @@ "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -913,12 +850,8 @@ "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -982,12 +915,8 @@ "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_status_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1164,12 +1093,8 @@ "name": "chapter_assignments_project_unit_id_project_units_id_fk", "tableFrom": "chapter_assignments", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1177,12 +1102,8 @@ "name": "chapter_assignments_bible_id_bibles_id_fk", "tableFrom": "chapter_assignments", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1190,12 +1111,8 @@ "name": "chapter_assignments_book_id_books_id_fk", "tableFrom": "chapter_assignments", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1203,12 +1120,8 @@ "name": "chapter_assignments_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1216,12 +1129,8 @@ "name": "chapter_assignments_peer_checker_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "peer_checker_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["peer_checker_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1329,9 +1238,7 @@ "organizations_name_unique": { "name": "organizations_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1382,9 +1289,7 @@ "permissions_name_unique": { "name": "permissions_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1434,12 +1339,8 @@ "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1447,12 +1348,8 @@ "name": "project_unit_bible_books_bible_id_bibles_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1460,12 +1357,8 @@ "name": "project_unit_bible_books_book_id_books_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1521,12 +1414,8 @@ "name": "project_units_project_id_projects_id_fk", "tableFrom": "project_units", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1598,12 +1487,8 @@ "name": "project_users_project_id_projects_id_fk", "tableFrom": "project_users", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1611,12 +1496,8 @@ "name": "project_users_user_id_users_id_fk", "tableFrom": "project_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1624,10 +1505,7 @@ "compositePrimaryKeys": { "project_users_project_id_user_id_pk": { "name": "project_users_project_id_user_id_pk", - "columns": [ - "project_id", - "user_id" - ] + "columns": ["project_id", "user_id"] } }, "uniqueConstraints": {}, @@ -1718,12 +1596,8 @@ "name": "projects_source_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "source_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1731,12 +1605,8 @@ "name": "projects_target_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "target_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1744,12 +1614,8 @@ "name": "projects_organization_organizations_id_fk", "tableFrom": "projects", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1757,12 +1623,8 @@ "name": "projects_created_by_users_id_fk", "tableFrom": "projects", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1819,12 +1681,8 @@ "name": "role_permissions_role_id_roles_id_fk", "tableFrom": "role_permissions", "tableTo": "roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1832,12 +1690,8 @@ "name": "role_permissions_permission_id_permissions_id_fk", "tableFrom": "role_permissions", "tableTo": "permissions", - "columnsFrom": [ - "permission_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1845,10 +1699,7 @@ "compositePrimaryKeys": { "role_permissions_role_id_permission_id_pk": { "name": "role_permissions_role_id_permission_id_pk", - "columns": [ - "role_id", - "permission_id" - ] + "columns": ["role_id", "permission_id"] } }, "uniqueConstraints": {}, @@ -1894,9 +1745,7 @@ "roles_name_unique": { "name": "roles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1980,12 +1829,8 @@ "name": "translated_verses_project_unit_id_project_units_id_fk", "tableFrom": "translated_verses", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1993,12 +1838,8 @@ "name": "translated_verses_bible_text_id_bible_texts_id_fk", "tableFrom": "translated_verses", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2006,12 +1847,8 @@ "name": "translated_verses_assigned_user_id_users_id_fk", "tableFrom": "translated_verses", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2087,12 +1924,8 @@ "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2100,12 +1933,8 @@ "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2197,12 +2026,8 @@ "name": "users_role_roles_id_fk", "tableFrom": "users", "tableTo": "roles", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2210,12 +2035,8 @@ "name": "users_organization_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2223,12 +2044,8 @@ "name": "users_created_by_users_id_fk", "tableFrom": "users", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2238,16 +2055,12 @@ "users_username_unique": { "name": "users_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] }, "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -2259,20 +2072,12 @@ "public.ai_suggestion_job_status": { "name": "ai_suggestion_job_status", "schema": "public", - "values": [ - "queued", - "processing", - "completed", - "failed" - ] + "values": ["queued", "processing", "completed", "failed"] }, "public.assignment_role": { "name": "assignment_role", "schema": "public", - "values": [ - "drafter", - "peer_checker" - ] + "values": ["drafter", "peer_checker"] }, "public.chapter_status": { "name": "chapter_status", @@ -2291,36 +2096,22 @@ "public.project_assignment_status": { "name": "project_assignment_status", "schema": "public", - "values": [ - "active", - "not_assigned" - ] + "values": ["active", "not_assigned"] }, "public.project_status": { "name": "project_status", "schema": "public", - "values": [ - "not_started", - "in_progress", - "completed" - ] + "values": ["not_started", "in_progress", "completed"] }, "public.script_direction": { "name": "script_direction", "schema": "public", - "values": [ - "ltr", - "rtl" - ] + "values": ["ltr", "rtl"] }, "public.user_status": { "name": "user_status", "schema": "public", - "values": [ - "invited", - "verified", - "inactive" - ] + "values": ["invited", "verified", "inactive"] } }, "schemas": {}, @@ -2333,4 +2124,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index f493e7d..8c7f5f8 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -80,4 +80,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/db/schema.ts b/src/db/schema.ts index bcfe111..8bd6524 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -771,44 +771,58 @@ export const patchUsersClientSchema = patchUsersSchema.omit({ organization: true, }); -export const ai_suggestion_jobs = pgTable('ai_suggestion_jobs', { - id: serial('id').primaryKey(), - projectUnitId: integer('project_unit_id') - .notNull() - .references(() => project_units.id, { onDelete: 'cascade' }), - bibleId: integer('bible_id') - .notNull() - .references(() => bibles.id), - bookCode: varchar('book_code', { length: 50 }).notNull(), - chapterNumber: integer('chapter_number').notNull(), - verseStart: integer('verse_start').notNull(), - verseEnd: integer('verse_end').notNull(), - status: aiSuggestionJobStatusEnum('status').notNull().default('queued'), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at') - .defaultNow() - .$onUpdate(() => new Date()), -}, (table) => [ - index('idx_ai_jobs_project_unit').on(table.projectUnitId), - index('idx_ai_jobs_status').on(table.status), - uniqueIndex('uq_ai_jobs_range').on(table.projectUnitId, table.bookCode, table.chapterNumber, table.verseStart, table.verseEnd), -]); +export const ai_suggestion_jobs = pgTable( + 'ai_suggestion_jobs', + { + id: serial('id').primaryKey(), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + bibleId: integer('bible_id') + .notNull() + .references(() => bibles.id), + bookCode: varchar('book_code', { length: 50 }).notNull(), + chapterNumber: integer('chapter_number').notNull(), + verseStart: integer('verse_start').notNull(), + verseEnd: integer('verse_end').notNull(), + status: aiSuggestionJobStatusEnum('status').notNull().default('queued'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [ + index('idx_ai_jobs_project_unit').on(table.projectUnitId), + index('idx_ai_jobs_status').on(table.status), + uniqueIndex('uq_ai_jobs_range').on( + table.projectUnitId, + table.bookCode, + table.chapterNumber, + table.verseStart, + table.verseEnd + ), + ] +); -export const ai_suggestions = pgTable('ai_suggestions', { - id: serial('id').primaryKey(), - bibleTextId: integer('bible_text_id') - .notNull() - .references(() => bible_texts.id, { onDelete: 'cascade' }), - projectUnitId: integer('project_unit_id') - .notNull() - .references(() => project_units.id, { onDelete: 'cascade' }), - suggestedText: varchar('suggested_text').notNull(), - modelInfo: varchar('model_info', { length: 100 }), - createdAt: timestamp('created_at').defaultNow(), -}, (table) => [ - index('idx_ai_suggestions_bible_text').on(table.bibleTextId), - uniqueIndex('uq_ai_suggestions_per_text_unit').on(table.bibleTextId, table.projectUnitId), -]); +export const ai_suggestions = pgTable( + 'ai_suggestions', + { + id: serial('id').primaryKey(), + bibleTextId: integer('bible_text_id') + .notNull() + .references(() => bible_texts.id, { onDelete: 'cascade' }), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + suggestedText: varchar('suggested_text').notNull(), + modelInfo: varchar('model_info', { length: 100 }), + createdAt: timestamp('created_at').defaultNow(), + }, + (table) => [ + index('idx_ai_suggestions_bible_text').on(table.bibleTextId), + uniqueIndex('uq_ai_suggestions_per_text_unit').on(table.bibleTextId, table.projectUnitId), + ] +); export const selectAiSuggestionJobsSchema = createSelectSchema(ai_suggestion_jobs); export const selectAiSuggestionsSchema = createSelectSchema(ai_suggestions); @@ -821,16 +835,23 @@ export const insertAiSuggestionJobsSchema = createInsertSchema(ai_suggestion_job verseStart: (schema) => schema.int().min(1), verseEnd: (schema) => schema.int().min(1), }) -.required({ projectUnitId: true, bibleId: true, bookCode: true, chapterNumber: true, verseStart: true, verseEnd: true }) -.omit({ id: true, status: true, createdAt: true, updatedAt: true }); + .required({ + projectUnitId: true, + bibleId: true, + bookCode: true, + chapterNumber: true, + verseStart: true, + verseEnd: true, + }) + .omit({ id: true, status: true, createdAt: true, updatedAt: true }); export const insertAiSuggestionsSchema = createInsertSchema(ai_suggestions, { bibleTextId: (schema) => schema.int(), projectUnitId: (schema) => schema.int(), suggestedText: (schema) => schema.min(1), }) -.required({ bibleTextId: true, projectUnitId: true, suggestedText: true }) -.omit({ id: true, createdAt: true }); + .required({ bibleTextId: true, projectUnitId: true, suggestedText: true }) + .omit({ id: true, createdAt: true }); export const patchAiSuggestionJobsSchema = insertAiSuggestionJobsSchema.partial(); export const patchAiSuggestionsSchema = insertAiSuggestionsSchema.partial(); diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts index f08a808..d76769d 100644 --- a/src/domains/ai-suggestions/ai-suggestions.repository.ts +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -7,7 +7,6 @@ import { ai_suggestion_jobs, ai_suggestions } from '@/db/schema'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; - export async function queueAiSuggestionJobs( jobs: { projectUnitId: number; diff --git a/src/domains/ai-suggestions/ai-suggestions.route.ts b/src/domains/ai-suggestions/ai-suggestions.route.ts index 7420213..809a79a 100644 --- a/src/domains/ai-suggestions/ai-suggestions.route.ts +++ b/src/domains/ai-suggestions/ai-suggestions.route.ts @@ -27,10 +27,7 @@ const getAiSuggestionsRoute = createRoute({ query: getAiSuggestionsQuerySchema, }, responses: { - [HttpStatusCodes.OK]: jsonContent( - aiSuggestionsListResponseSchema, - 'List of AI suggestions' - ), + [HttpStatusCodes.OK]: jsonContent(aiSuggestionsListResponseSchema, 'List of AI suggestions'), [HttpStatusCodes.BAD_REQUEST]: jsonContent( createMessageObjectSchema('Bad Request'), 'Validation error' @@ -74,10 +71,7 @@ const queueNextVersesRoute = createRoute({ body: jsonContent(queueNextVersesRequestSchema, 'Verses context'), }, responses: { - [HttpStatusCodes.OK]: jsonContent( - createMessageObjectSchema('Successfully queued'), - 'Queued' - ), + [HttpStatusCodes.OK]: jsonContent(createMessageObjectSchema('Successfully queued'), 'Queued'), [HttpStatusCodes.BAD_REQUEST]: jsonContent( createMessageObjectSchema('Bad Request'), 'Validation error' @@ -96,7 +90,8 @@ const queueNextVersesRoute = createRoute({ ), }, summary: 'Queue pre-generation of AI suggestions', - description: 'Triggers the queue to generate suggestions for the next few verses ahead of the drafter.', + description: + 'Triggers the queue to generate suggestions for the next few verses ahead of the drafter.', }); server.openapi(queueNextVersesRoute, async (c) => { diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index e119122..4780f81 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -1,28 +1,34 @@ import { and, asc, eq, gt } from 'drizzle-orm'; -import type {Result} from '@/lib/types'; +import type { Result } from '@/lib/types'; import { db } from '@/db'; import { bible_texts, books } from '@/db/schema'; import { logger } from '@/lib/logger'; -import { err, ErrorCode, ok } from '@/lib/types'; +import { err, ErrorCode, ok } from '@/lib/types'; import type { AiSuggestionsListResponse, GetAiSuggestionsQuery } from './ai-suggestions.types'; -import { getAiSuggestions as getAiSuggestionsRepo, queueAiSuggestionJobs } from './ai-suggestions.repository'; +import { + getAiSuggestions as getAiSuggestionsRepo, + queueAiSuggestionJobs, +} from './ai-suggestions.repository'; export async function getAiSuggestions( query: GetAiSuggestionsQuery ): Promise> { - const ids = query.bibleTextIds.split(',').map(id => Number.parseInt(id.trim(), 10)).filter(id => !Number.isNaN(id)); - + const ids = query.bibleTextIds + .split(',') + .map((id) => Number.parseInt(id.trim(), 10)) + .filter((id) => !Number.isNaN(id)); + const suggestionsResult = await getAiSuggestionsRepo(query.projectUnitId, ids); if (!suggestionsResult.ok) { return suggestionsResult; } - const data = suggestionsResult.data.map(suggestion => ({ + const data = suggestionsResult.data.map((suggestion) => ({ bibleTextId: suggestion.bibleTextId, suggestedText: suggestion.suggestedText, modelInfo: suggestion.modelInfo, @@ -59,7 +65,7 @@ export async function queueNextVerses( if (nextVerses.length === 0) return ok(undefined); // 2. Queue them - const jobs = nextVerses.map(v => ({ + const jobs = nextVerses.map((v) => ({ projectUnitId, bibleId, bookCode, From 2e1d368d1078fea1061412ac8ba53e8e60d04839 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 13:08:44 +0530 Subject: [PATCH 03/14] updated queue logic, added fts --- debug-ai.ts | 79 --------- debug-langs.ts | 31 ---- src/db/migrations/0010_ai_suggestions.sql | 27 +-- src/db/migrations/0011_ai_usage_tracking.sql | 15 ++ .../0012_project_unit_ai_enabled.sql | 1 + src/db/schema.ts | 43 ++++- .../ai-suggestions.constants.ts | 10 ++ .../ai-suggestions.repository.ts | 36 ++++ .../ai-suggestions/ai-suggestions.route.ts | 50 ++++++ .../ai-suggestions/ai-suggestions.service.ts | 167 ++++++++++++++++-- .../ai-suggestions/ai-suggestions.types.ts | 17 +- .../chapter-assignments.service.ts | 15 ++ .../translated-verses.service.ts | 14 ++ src/scripts/clean-ai-jobs.ts | 35 ++++ 14 files changed, 402 insertions(+), 138 deletions(-) delete mode 100644 debug-ai.ts delete mode 100644 debug-langs.ts create mode 100644 src/db/migrations/0011_ai_usage_tracking.sql create mode 100644 src/db/migrations/0012_project_unit_ai_enabled.sql create mode 100644 src/domains/ai-suggestions/ai-suggestions.constants.ts create mode 100644 src/scripts/clean-ai-jobs.ts diff --git a/debug-ai.ts b/debug-ai.ts deleted file mode 100644 index c48166c..0000000 --- a/debug-ai.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { eq } from 'drizzle-orm'; - -import { db } from './src/db'; -import { - bible_texts, - languages, - project_units, - projects, - translated_verses, -} from './src/db/schema'; - -async function run() { - console.log('=== DB Diagnostic Script ==='); - - try { - // 1. Let's see what projects exist - console.log('\n--- Projects ---'); - const allProjects = await db.select().from(projects); - allProjects.forEach((p) => - console.log( - `Project ${p.id}: ${p.name} (Source: ${p.sourceLanguage}, Target: ${p.targetLanguage})` - ) - ); - - // 2. Let's see what languages exist - console.log('\n--- Languages ---'); - const allLanguages = await db.select().from(languages); - allLanguages.forEach((l) => console.log(`Lang ${l.id}: ${l.langName}`)); - - // 3. Let's see what project units exist - console.log('\n--- Project Units ---'); - const allUnits = await db.select().from(project_units); - allUnits.forEach((u) => - console.log(`Unit ${u.id} -> Project ${u.projectId} (Status: ${u.status})`) - ); - - // 4. Let's count translated verses per project unit - console.log('\n--- Translated Verses Count ---'); - const allTranslations = await db - .select({ - id: translated_verses.id, - projectUnitId: translated_verses.projectUnitId, - content: translated_verses.content, - bibleTextId: translated_verses.bibleTextId, - }) - .from(translated_verses); - - console.log(`Total translated verses in DB: ${allTranslations.length}`); - - const unitCounts: Record = {}; - allTranslations.forEach((t) => { - if (t.content && t.content.trim() !== '') { - unitCounts[t.projectUnitId] = (unitCounts[t.projectUnitId] || 0) + 1; - } - }); - console.log('Valid translations per Unit ID:', unitCounts); - - // 5. Let's check a sample of valid translations - const validTranslations = allTranslations.filter((t) => t.content && t.content.trim() !== ''); - if (validTranslations.length > 0) { - console.log('\n--- Sample valid translation ---'); - const sample = validTranslations[0]; - console.log(sample); - - // Find its bible text - const text = await db - .select() - .from(bible_texts) - .where(eq(bible_texts.id, sample.bibleTextId)); - console.log('Source text:', text[0]); - } - } catch (error) { - console.error('DB Error:', error); - } finally { - process.exit(0); - } -} - -run().catch(console.error); diff --git a/debug-langs.ts b/debug-langs.ts deleted file mode 100644 index 8a4240a..0000000 --- a/debug-langs.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { and, eq, isNotNull, ne } from 'drizzle-orm'; - -import { db } from './src/db'; -import { project_units, projects, translated_verses } from './src/db/schema'; - -async function run() { - const res = await db - .select({ - content: translated_verses.content, - projName: projects.name, - }) - .from(translated_verses) - .innerJoin(project_units, eq(translated_verses.projectUnitId, project_units.id)) - .innerJoin(projects, eq(project_units.projectId, projects.id)) - .where( - and( - eq(projects.sourceLanguage, 3), - eq(projects.targetLanguage, 5), - isNotNull(translated_verses.content), - ne(translated_verses.content, '') - ) - ) - .limit(10); - - console.log('Samples of Gujarati -> Kachi Koli translations in DB:'); - res.forEach((r) => { - console.log(`[${r.projName}] Content: ${r.content.substring(0, 50)}...`); - }); - process.exit(0); -} -run(); diff --git a/src/db/migrations/0010_ai_suggestions.sql b/src/db/migrations/0010_ai_suggestions.sql index 9bb1426..9f990ae 100644 --- a/src/db/migrations/0010_ai_suggestions.sql +++ b/src/db/migrations/0010_ai_suggestions.sql @@ -1,5 +1,6 @@ -CREATE TYPE "public"."ai_suggestion_job_status" AS ENUM('queued', 'processing', 'completed', 'failed');--> statement-breakpoint -CREATE TABLE "ai_suggestion_jobs" ( +CREATE SCHEMA IF NOT EXISTS "ai"; +--> statement-breakpoint +CREATE TABLE "ai"."ai_suggestion_jobs" ( "id" serial PRIMARY KEY NOT NULL, "project_unit_id" integer NOT NULL, "bible_id" integer NOT NULL, @@ -7,12 +8,12 @@ CREATE TABLE "ai_suggestion_jobs" ( "chapter_number" integer NOT NULL, "verse_start" integer NOT NULL, "verse_end" integer NOT NULL, - "status" "ai_suggestion_job_status" DEFAULT 'queued' NOT NULL, + "status" varchar(20) DEFAULT 'queued' NOT NULL, "created_at" timestamp DEFAULT now(), "updated_at" timestamp DEFAULT now() ); --> statement-breakpoint -CREATE TABLE "ai_suggestions" ( +CREATE TABLE "ai"."ai_suggestions" ( "id" serial PRIMARY KEY NOT NULL, "bible_text_id" integer NOT NULL, "project_unit_id" integer NOT NULL, @@ -21,12 +22,12 @@ CREATE TABLE "ai_suggestions" ( "created_at" timestamp DEFAULT now() ); --> statement-breakpoint -ALTER TABLE "ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai_suggestions" ADD CONSTRAINT "ai_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai_suggestions" ADD CONSTRAINT "ai_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_ai_jobs_project_unit" ON "ai_suggestion_jobs" USING btree ("project_unit_id");--> statement-breakpoint -CREATE INDEX "idx_ai_jobs_status" ON "ai_suggestion_jobs" USING btree ("status");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_jobs_range" ON "ai_suggestion_jobs" USING btree ("project_unit_id","book_code","chapter_number","verse_start","verse_end");--> statement-breakpoint -CREATE INDEX "idx_ai_suggestions_bible_text" ON "ai_suggestions" USING btree ("bible_text_id");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_suggestions_per_text_unit" ON "ai_suggestions" USING btree ("bible_text_id","project_unit_id"); \ No newline at end of file +ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_ai_jobs_project_unit" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id");--> statement-breakpoint +CREATE INDEX "idx_ai_jobs_status" ON "ai"."ai_suggestion_jobs" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_jobs_range" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id","book_code","chapter_number","verse_start","verse_end");--> statement-breakpoint +CREATE INDEX "idx_ai_suggestions_bible_text" ON "ai"."ai_suggestions" USING btree ("bible_text_id");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_suggestions_per_text_unit" ON "ai"."ai_suggestions" USING btree ("bible_text_id","project_unit_id"); \ No newline at end of file diff --git a/src/db/migrations/0011_ai_usage_tracking.sql b/src/db/migrations/0011_ai_usage_tracking.sql new file mode 100644 index 0000000..bfecec6 --- /dev/null +++ b/src/db/migrations/0011_ai_usage_tracking.sql @@ -0,0 +1,15 @@ +CREATE TABLE "ai"."ai_suggestion_usage_log" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "bible_text_id" integer NOT NULL, + "project_unit_id" integer NOT NULL, + "was_used" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_ai_usage_user" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_ai_usage_project_unit" ON "ai"."ai_suggestion_usage_log" USING btree ("project_unit_id");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_usage_user_text" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id","bible_text_id"); diff --git a/src/db/migrations/0012_project_unit_ai_enabled.sql b/src/db/migrations/0012_project_unit_ai_enabled.sql new file mode 100644 index 0000000..a46b591 --- /dev/null +++ b/src/db/migrations/0012_project_unit_ai_enabled.sql @@ -0,0 +1 @@ +ALTER TABLE "project_units" ADD COLUMN "is_ai_enabled" boolean DEFAULT false NOT NULL; diff --git a/src/db/schema.ts b/src/db/schema.ts index 8bd6524..324eb43 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -10,6 +10,7 @@ import { json, jsonb, pgEnum, + pgSchema, pgTable, primaryKey, serial, @@ -157,6 +158,7 @@ export const project_units = pgTable('project_units', { .notNull() .references(() => projects.id, { onDelete: 'cascade', onUpdate: 'cascade' }), status: projectStatusEnum('status').notNull().default('not_started'), + isAiEnabled: boolean('is_ai_enabled').default(false).notNull(), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') .defaultNow() @@ -771,7 +773,9 @@ export const patchUsersClientSchema = patchUsersSchema.omit({ organization: true, }); -export const ai_suggestion_jobs = pgTable( +export const aiSchema = pgSchema('ai'); + +export const ai_suggestion_jobs = aiSchema.table( 'ai_suggestion_jobs', { id: serial('id').primaryKey(), @@ -785,7 +789,7 @@ export const ai_suggestion_jobs = pgTable( chapterNumber: integer('chapter_number').notNull(), verseStart: integer('verse_start').notNull(), verseEnd: integer('verse_end').notNull(), - status: aiSuggestionJobStatusEnum('status').notNull().default('queued'), + status: varchar('status', { length: 20 }).notNull().default('queued'), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') .defaultNow() @@ -804,7 +808,7 @@ export const ai_suggestion_jobs = pgTable( ] ); -export const ai_suggestions = pgTable( +export const ai_suggestions = aiSchema.table( 'ai_suggestions', { id: serial('id').primaryKey(), @@ -824,8 +828,32 @@ export const ai_suggestions = pgTable( ] ); +export const ai_suggestion_usage_log = aiSchema.table( + 'ai_suggestion_usage_log', + { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + bibleTextId: integer('bible_text_id') + .notNull() + .references(() => bible_texts.id, { onDelete: 'cascade' }), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + wasUsed: boolean('was_used').notNull().default(false), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + (table) => [ + index('idx_ai_usage_user').on(table.userId), + index('idx_ai_usage_project_unit').on(table.projectUnitId), + uniqueIndex('uq_ai_usage_user_text').on(table.userId, table.bibleTextId), + ] +); + export const selectAiSuggestionJobsSchema = createSelectSchema(ai_suggestion_jobs); export const selectAiSuggestionsSchema = createSelectSchema(ai_suggestions); +export const selectAiSuggestionUsageLogSchema = createSelectSchema(ai_suggestion_usage_log); export const insertAiSuggestionJobsSchema = createInsertSchema(ai_suggestion_jobs, { projectUnitId: (schema) => schema.int(), @@ -853,5 +881,14 @@ export const insertAiSuggestionsSchema = createInsertSchema(ai_suggestions, { .required({ bibleTextId: true, projectUnitId: true, suggestedText: true }) .omit({ id: true, createdAt: true }); +export const insertAiSuggestionUsageLogSchema = createInsertSchema(ai_suggestion_usage_log, { + userId: (schema) => schema.int(), + bibleTextId: (schema) => schema.int(), + projectUnitId: (schema) => schema.int(), +}) + .required({ userId: true, bibleTextId: true, projectUnitId: true, wasUsed: true }) + .omit({ id: true, createdAt: true }); + export const patchAiSuggestionJobsSchema = insertAiSuggestionJobsSchema.partial(); export const patchAiSuggestionsSchema = insertAiSuggestionsSchema.partial(); +export const patchAiSuggestionUsageLogSchema = insertAiSuggestionUsageLogSchema.partial(); diff --git a/src/domains/ai-suggestions/ai-suggestions.constants.ts b/src/domains/ai-suggestions/ai-suggestions.constants.ts new file mode 100644 index 0000000..8269c9c --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.constants.ts @@ -0,0 +1,10 @@ +export const AI_SUGGESTIONS_CONSTANTS = { + // Minimum number of translated verses required before AI suggestions are enabled for a project unit + ACTIVATION_THRESHOLD_VERSES: 500, + + // Number of verses to automatically queue when a drafter is first assigned a chapter + INITIAL_QUEUE_COUNT: 3, + + // Default number of verses to pre-fetch ahead of the drafter's current verse + DEFAULT_LOOKAHEAD: 5, +} as const; diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts index d76769d..d72ef8b 100644 --- a/src/domains/ai-suggestions/ai-suggestions.repository.ts +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -75,3 +75,39 @@ export async function getAiSuggestions( return err(ErrorCode.INTERNAL_ERROR); } } + +export async function logAiSuggestionUsage( + userId: number, + bibleTextId: number, + projectUnitId: number, + wasUsed: boolean, + tx?: DbTransaction +): Promise> { + const database = tx || db; + try { + // Import ai_suggestion_usage_log inside to avoid circular deps if any, + // but better to import it at the top + const { ai_suggestion_usage_log } = await import('@/db/schema'); + await database + .insert(ai_suggestion_usage_log) + .values({ + userId, + bibleTextId, + projectUnitId, + wasUsed, + }) + .onConflictDoUpdate({ + target: [ai_suggestion_usage_log.userId, ai_suggestion_usage_log.bibleTextId], + set: { wasUsed }, // Update if the user later accepts it + }); + + return ok(undefined); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to log AI suggestion usage', + context: { userId, bibleTextId }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/ai-suggestions/ai-suggestions.route.ts b/src/domains/ai-suggestions/ai-suggestions.route.ts index 809a79a..ac02856 100644 --- a/src/domains/ai-suggestions/ai-suggestions.route.ts +++ b/src/domains/ai-suggestions/ai-suggestions.route.ts @@ -14,6 +14,7 @@ import { aiSuggestionsListResponseSchema, getAiSuggestionsQuerySchema, queueNextVersesRequestSchema, + trackUsageRequestSchema, } from './ai-suggestions.types'; // ─── GET /ai-suggestions ────────────────────────────────────────────── @@ -111,3 +112,52 @@ server.openapi(queueNextVersesRoute, async (c) => { return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); }); + +// ─── POST /ai-suggestions/usage ────────────────────────────────────────────── + +const trackUsageRoute = createRoute({ + tags: ['AI Suggestions'], + method: 'post', + path: '/ai-suggestions/usage', + middleware: [authenticateUser, requirePermission(PERMISSIONS.PROJECT_VIEW)] as const, + request: { + body: jsonContent(trackUsageRequestSchema, 'Usage data'), + }, + responses: { + [HttpStatusCodes.OK]: jsonContent(createMessageObjectSchema('Successfully logged'), 'Logged'), + [HttpStatusCodes.BAD_REQUEST]: jsonContent( + createMessageObjectSchema('Bad Request'), + 'Validation error' + ), + [HttpStatusCodes.UNAUTHORIZED]: jsonContent( + createMessageObjectSchema('Unauthorized'), + 'Authentication required' + ), + [HttpStatusCodes.FORBIDDEN]: jsonContent( + createMessageObjectSchema('Forbidden'), + 'Permission denied' + ), + [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonContent( + createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR), + 'Internal server error' + ), + }, + summary: 'Track AI suggestion usage', + description: 'Logs whether an AI suggestion was viewed or used by the user.', +}); + +server.openapi(trackUsageRoute, async (c) => { + const body = c.req.valid('json'); + const user = c.get('user'); + + if (!user?.id) { + return c.json({ message: 'User not found' }, HttpStatusCodes.UNAUTHORIZED); + } + + const result = await aiSuggestionsService.trackUsage(user.id, body); + if (result.ok) { + return c.json({ message: 'Logged' }, HttpStatusCodes.OK); + } + + return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); +}); diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 4780f81..796cd7e 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -1,19 +1,35 @@ -import { and, asc, eq, gt } from 'drizzle-orm'; +import { and, asc, count, eq, gt, isNull } from 'drizzle-orm'; import type { Result } from '@/lib/types'; import { db } from '@/db'; -import { bible_texts, books } from '@/db/schema'; +import { + bible_texts, + books, + chapter_assignments, + project_units, + translated_verses, +} from '@/db/schema'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; -import type { AiSuggestionsListResponse, GetAiSuggestionsQuery } from './ai-suggestions.types'; +import type { + AiSuggestionsListResponse, + GetAiSuggestionsQuery, + TrackUsageRequest, +} from './ai-suggestions.types'; +import { AI_SUGGESTIONS_CONSTANTS } from './ai-suggestions.constants'; import { getAiSuggestions as getAiSuggestionsRepo, + logAiSuggestionUsage, queueAiSuggestionJobs, } from './ai-suggestions.repository'; +export async function trackUsage(userId: number, data: TrackUsageRequest): Promise> { + return logAiSuggestionUsage(userId, data.bibleTextId, data.projectUnitId, data.wasUsed); +} + export async function getAiSuggestions( query: GetAiSuggestionsQuery ): Promise> { @@ -28,11 +44,13 @@ export async function getAiSuggestions( return suggestionsResult; } - const data = suggestionsResult.data.map((suggestion) => ({ - bibleTextId: suggestion.bibleTextId, - suggestedText: suggestion.suggestedText, - modelInfo: suggestion.modelInfo, - })); + const data = suggestionsResult.data.map( + (suggestion: { bibleTextId: number; suggestedText: string; modelInfo: string | null }) => ({ + bibleTextId: suggestion.bibleTextId, + suggestedText: suggestion.suggestedText, + modelInfo: suggestion.modelInfo, + }) + ); return ok({ data }); } @@ -43,20 +61,28 @@ export async function queueNextVerses( bookCode: string, chapterNumber: number, currentVerse: number, - lookahead: number = 5 + lookahead: number = AI_SUGGESTIONS_CONSTANTS.DEFAULT_LOOKAHEAD ): Promise> { try { - // 1. Find the next few verses + // 1. Find the next few verses that haven't been translated yet const nextVerses = await db .select({ verseNumber: bible_texts.verseNumber }) .from(bible_texts) .innerJoin(books, eq(bible_texts.bookId, books.id)) + .leftJoin( + translated_verses, + and( + eq(translated_verses.bibleTextId, bible_texts.id), + eq(translated_verses.projectUnitId, projectUnitId) + ) + ) .where( and( eq(bible_texts.bibleId, bibleId), eq(books.code, bookCode), eq(bible_texts.chapterNumber, chapterNumber), - gt(bible_texts.verseNumber, currentVerse) + gt(bible_texts.verseNumber, currentVerse), + isNull(translated_verses.projectUnitId) ) ) .orderBy(asc(bible_texts.verseNumber)) @@ -80,3 +106,122 @@ export async function queueNextVerses( return err(ErrorCode.INTERNAL_ERROR); } } + +export async function handleChapterAssigned( + projectUnitId: number, + bibleId: number, + bookId: number, + chapterNumber: number +) { + try { + const unit = await db + .select({ isAiEnabled: project_units.isAiEnabled }) + .from(project_units) + .where(eq(project_units.id, projectUnitId)) + .limit(1); + + if (!unit[0]?.isAiEnabled) return; + + const book = await db + .select({ code: books.code }) + .from(books) + .where(eq(books.id, bookId)) + .limit(1); + + if (!book[0]?.code) return; + + await queueNextVerses( + projectUnitId, + bibleId, + book[0].code, + chapterNumber, + 0, + AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT + ); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to trigger initial AI queue on chapter assignment', + context: { projectUnitId, chapterNumber }, + }); + } +} + +export async function handleVerseSaved(projectUnitId: number) { + try { + // 1. Check if AI is already enabled + const unit = await db + .select({ isAiEnabled: project_units.isAiEnabled }) + .from(project_units) + .where(eq(project_units.id, projectUnitId)) + .limit(1); + + if (!unit[0] || unit[0].isAiEnabled) return; + + // 2. Count translated verses for this unit + const result = await db + .select({ value: count() }) + .from(translated_verses) + .where(eq(translated_verses.projectUnitId, projectUnitId)); + + const verseCount = result[0]?.value ?? 0; + + if (verseCount >= AI_SUGGESTIONS_CONSTANTS.ACTIVATION_THRESHOLD_VERSES) { + // 3. Mark AI as enabled + await db + .update(project_units) + .set({ isAiEnabled: true }) + .where(eq(project_units.id, projectUnitId)); + + // 4. Batch queue verses for all assigned chapters concurrently + const assignments = await db + .select({ + bibleId: chapter_assignments.bibleId, + bookId: chapter_assignments.bookId, + chapterNumber: chapter_assignments.chapterNumber, + bookCode: books.code, + }) + .from(chapter_assignments) + .innerJoin(books, eq(chapter_assignments.bookId, books.id)) + .where(eq(chapter_assignments.projectUnitId, projectUnitId)); + + const queueResults = await Promise.all( + assignments.map( + (assignment: { + bibleId: number; + bookId: number; + chapterNumber: number; + bookCode: string; + }) => + queueNextVerses( + projectUnitId, + assignment.bibleId, + assignment.bookCode, + assignment.chapterNumber, + 0, + AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT + ) + ) + ); + + // 5. If any queueing failed, revert the flag so we can try again later + if (queueResults.some((r: Result) => !r.ok)) { + await db + .update(project_units) + .set({ isAiEnabled: false }) + .where(eq(project_units.id, projectUnitId)); + throw new Error( + 'Failed to batch queue initial AI suggestions. Rolled back threshold state.' + ); + } + + logger.info({ projectUnitId }, 'AI Suggestions threshold reached and enabled.'); + } + } catch (error) { + logger.error({ + cause: error, + message: 'Threshold check failed', + context: { projectUnitId }, + }); + } +} diff --git a/src/domains/ai-suggestions/ai-suggestions.types.ts b/src/domains/ai-suggestions/ai-suggestions.types.ts index 62fdfe5..31f63f1 100644 --- a/src/domains/ai-suggestions/ai-suggestions.types.ts +++ b/src/domains/ai-suggestions/ai-suggestions.types.ts @@ -1,5 +1,7 @@ import { z } from '@hono/zod-openapi'; +import { AI_SUGGESTIONS_CONSTANTS } from './ai-suggestions.constants'; + export const getAiSuggestionsQuerySchema = z.object({ projectUnitId: z.coerce.number().int().positive(), bibleTextIds: z.string().describe('Comma-separated list of bible text IDs'), @@ -25,7 +27,20 @@ export const queueNextVersesRequestSchema = z.object({ bookCode: z.string().min(3).max(3), chapterNumber: z.number().int().positive(), currentVerse: z.number().int().positive(), - lookahead: z.number().int().positive().max(20).default(5), + lookahead: z + .number() + .int() + .positive() + .max(20) + .default(AI_SUGGESTIONS_CONSTANTS.DEFAULT_LOOKAHEAD), }); export type QueueNextVersesRequest = z.infer; + +export const trackUsageRequestSchema = z.object({ + bibleTextId: z.number().int().positive(), + projectUnitId: z.number().int().positive(), + wasUsed: z.boolean(), +}); + +export type TrackUsageRequest = z.infer; diff --git a/src/domains/chapter-assignments/chapter-assignments.service.ts b/src/domains/chapter-assignments/chapter-assignments.service.ts index 2d3e9af..e7b9161 100644 --- a/src/domains/chapter-assignments/chapter-assignments.service.ts +++ b/src/domains/chapter-assignments/chapter-assignments.service.ts @@ -1,6 +1,7 @@ import type { DbTransaction, Result } from '@/lib/types'; import { db } from '@/db'; +import { handleChapterAssigned } from '@/domains/ai-suggestions/ai-suggestions.service'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; @@ -79,6 +80,13 @@ export async function createChapterAssignment(data: CreateChapterAssignmentReque 'drafter', CHAPTER_ASSIGNMENT_STATUS.NOT_STARTED ); + // Fire and forget auto-queueing for drafting assignment + handleChapterAssigned( + assignment.projectUnitId, + assignment.bibleId, + assignment.bookId, + assignment.chapterNumber + ); } if (assignment.peerCheckerId) { await repo.insertUserAssignmentHistory( @@ -279,6 +287,13 @@ async function recordUserAssignmentChanges( 'drafter', updated.status as ChapterAssignmentStatus ); + // Fire and forget auto-queueing for drafting assignment + handleChapterAssigned( + updated.projectUnitId, + updated.bibleId, + updated.bookId, + updated.chapterNumber + ); } if ( diff --git a/src/domains/translated-verses/translated-verses.service.ts b/src/domains/translated-verses/translated-verses.service.ts index ff65130..e29f7e1 100644 --- a/src/domains/translated-verses/translated-verses.service.ts +++ b/src/domains/translated-verses/translated-verses.service.ts @@ -1,3 +1,5 @@ +import { handleVerseSaved } from '@/domains/ai-suggestions/ai-suggestions.service'; +import { logger } from '@/lib/logger'; import { ok } from '@/lib/types'; import type { @@ -33,6 +35,12 @@ export async function getTranslatedVerseById(id: number) { export async function createTranslatedVerse(input: CreateTranslatedVerseInput) { const result = await translatedVersesRepo.create(input); if (!result.ok) return result; + + // Fire and forget threshold check + handleVerseSaved(input.projectUnitId).catch((err) => + logger.error({ cause: err, message: 'Threshold check failed' }) + ); + return ok(toTranslatedVerseResponse(result.data)); } @@ -45,6 +53,12 @@ export async function updateTranslatedVerse(id: number, input: UpdateTranslatedV export async function upsertTranslatedVerse(input: CreateTranslatedVerseInput) { const result = await translatedVersesRepo.upsert(input); if (!result.ok) return result; + + // Fire and forget threshold check + handleVerseSaved(input.projectUnitId).catch((err) => + logger.error({ cause: err, message: 'Threshold check failed' }) + ); + return ok(toTranslatedVerseResponse(result.data)); } diff --git a/src/scripts/clean-ai-jobs.ts b/src/scripts/clean-ai-jobs.ts new file mode 100644 index 0000000..148bc7e --- /dev/null +++ b/src/scripts/clean-ai-jobs.ts @@ -0,0 +1,35 @@ +import { and, inArray, lt } from 'drizzle-orm'; +import process from 'node:process'; + +import { db } from '@/db'; +import { ai_suggestion_jobs } from '@/db/schema'; +import { logger } from '@/lib/logger'; + +async function main() { + logger.info('Starting AI Queue Cleanup Job...'); + + try { + // Delete jobs that are 'completed' or 'failed' AND were created more than 7 days ago + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const deleted = await db + .delete(ai_suggestion_jobs) + .where( + and( + inArray(ai_suggestion_jobs.status, ['completed', 'failed']), + lt(ai_suggestion_jobs.createdAt, sevenDaysAgo) + ) + ) + .returning({ id: ai_suggestion_jobs.id }); + + logger.info(`Successfully cleaned up ${deleted.length} old AI jobs.`); + } catch (error) { + logger.error({ cause: error, message: 'Failed to run AI Queue Cleanup' }); + process.exit(1); + } + + process.exit(0); +} + +void main(); From fde3fd1a94db1b37e57eae4e0619ec9cac5505e8 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 17:16:21 +0530 Subject: [PATCH 04/14] updated code and schema for ai suggestions --- .../migrations/0011_harden_ai_suggestions.sql | 17 ++ src/db/migrations/meta/_journal.json | 7 + src/db/schema.ts | 15 +- .../ai-suggestions.constants.ts | 3 + .../ai-suggestions.repository.ts | 1 + .../ai-suggestions/ai-suggestions.route.ts | 15 +- .../ai-suggestions/ai-suggestions.service.ts | 194 ++++++++++++++---- .../ai-suggestions/ai-suggestions.types.ts | 5 +- 8 files changed, 212 insertions(+), 45 deletions(-) create mode 100644 src/db/migrations/0011_harden_ai_suggestions.sql diff --git a/src/db/migrations/0011_harden_ai_suggestions.sql b/src/db/migrations/0011_harden_ai_suggestions.sql new file mode 100644 index 0000000..bce12d0 --- /dev/null +++ b/src/db/migrations/0011_harden_ai_suggestions.sql @@ -0,0 +1,17 @@ +ALTER TABLE "ai"."ai_suggestion_jobs" + ADD COLUMN IF NOT EXISTS "retry_count" integer DEFAULT 0 NOT NULL, + ADD COLUMN IF NOT EXISTS "error_message" text; +--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestions" + ALTER COLUMN "suggested_text" TYPE text; +--> statement-breakpoint +DROP INDEX IF EXISTS "ai"."uq_ai_jobs_range"; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "uq_ai_jobs_range" ON "ai"."ai_suggestion_jobs" USING btree ( + "project_unit_id", + "bible_id", + "book_code", + "chapter_number", + "verse_start", + "verse_end" +); diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 8c7f5f8..8d2b9f7 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1778478819437, "tag": "0010_ai_suggestions", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1779775000000, + "tag": "0011_harden_ai_suggestions", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index 324eb43..38bf712 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -14,6 +14,7 @@ import { pgTable, primaryKey, serial, + text, timestamp, uniqueIndex, varchar, @@ -790,6 +791,8 @@ export const ai_suggestion_jobs = aiSchema.table( verseStart: integer('verse_start').notNull(), verseEnd: integer('verse_end').notNull(), status: varchar('status', { length: 20 }).notNull().default('queued'), + retryCount: integer('retry_count').notNull().default(0), + errorMessage: text('error_message'), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') .defaultNow() @@ -800,6 +803,7 @@ export const ai_suggestion_jobs = aiSchema.table( index('idx_ai_jobs_status').on(table.status), uniqueIndex('uq_ai_jobs_range').on( table.projectUnitId, + table.bibleId, table.bookCode, table.chapterNumber, table.verseStart, @@ -818,7 +822,7 @@ export const ai_suggestions = aiSchema.table( projectUnitId: integer('project_unit_id') .notNull() .references(() => project_units.id, { onDelete: 'cascade' }), - suggestedText: varchar('suggested_text').notNull(), + suggestedText: text('suggested_text').notNull(), modelInfo: varchar('model_info', { length: 100 }), createdAt: timestamp('created_at').defaultNow(), }, @@ -871,7 +875,14 @@ export const insertAiSuggestionJobsSchema = createInsertSchema(ai_suggestion_job verseStart: true, verseEnd: true, }) - .omit({ id: true, status: true, createdAt: true, updatedAt: true }); + .omit({ + id: true, + status: true, + retryCount: true, + errorMessage: true, + createdAt: true, + updatedAt: true, + }); export const insertAiSuggestionsSchema = createInsertSchema(ai_suggestions, { bibleTextId: (schema) => schema.int(), diff --git a/src/domains/ai-suggestions/ai-suggestions.constants.ts b/src/domains/ai-suggestions/ai-suggestions.constants.ts index 8269c9c..0d72c22 100644 --- a/src/domains/ai-suggestions/ai-suggestions.constants.ts +++ b/src/domains/ai-suggestions/ai-suggestions.constants.ts @@ -7,4 +7,7 @@ export const AI_SUGGESTIONS_CONSTANTS = { // Default number of verses to pre-fetch ahead of the drafter's current verse DEFAULT_LOOKAHEAD: 5, + + // Guardrail for GET /ai-suggestions query fan-out + MAX_REQUESTED_BIBLE_TEXT_IDS: 200, } as const; diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts index d72ef8b..95f9ea4 100644 --- a/src/domains/ai-suggestions/ai-suggestions.repository.ts +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -28,6 +28,7 @@ export async function queueAiSuggestionJobs( .onConflictDoNothing({ target: [ ai_suggestion_jobs.projectUnitId, + ai_suggestion_jobs.bibleId, ai_suggestion_jobs.bookCode, ai_suggestion_jobs.chapterNumber, ai_suggestion_jobs.verseStart, diff --git a/src/domains/ai-suggestions/ai-suggestions.route.ts b/src/domains/ai-suggestions/ai-suggestions.route.ts index ac02856..6f25fc9 100644 --- a/src/domains/ai-suggestions/ai-suggestions.route.ts +++ b/src/domains/ai-suggestions/ai-suggestions.route.ts @@ -52,8 +52,13 @@ const getAiSuggestionsRoute = createRoute({ server.openapi(getAiSuggestionsRoute, async (c) => { const query = c.req.valid('query'); + const user = c.get('user'); + + if (!user) { + return c.json({ message: 'User not found' }, HttpStatusCodes.UNAUTHORIZED); + } - const result = await aiSuggestionsService.getAiSuggestions(query); + const result = await aiSuggestionsService.getAiSuggestions(user, query); if (result.ok) { return c.json(result.data, HttpStatusCodes.OK); } @@ -97,8 +102,14 @@ const queueNextVersesRoute = createRoute({ server.openapi(queueNextVersesRoute, async (c) => { const body = c.req.valid('json'); + const user = c.get('user'); + + if (!user) { + return c.json({ message: 'User not found' }, HttpStatusCodes.UNAUTHORIZED); + } const result = await aiSuggestionsService.queueNextVerses( + user, body.projectUnitId, body.bibleId, body.bookCode, @@ -154,7 +165,7 @@ server.openapi(trackUsageRoute, async (c) => { return c.json({ message: 'User not found' }, HttpStatusCodes.UNAUTHORIZED); } - const result = await aiSuggestionsService.trackUsage(user.id, body); + const result = await aiSuggestionsService.trackUsage(user, body); if (result.ok) { return c.json({ message: 'Logged' }, HttpStatusCodes.OK); } diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 796cd7e..145b7bb 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -1,6 +1,6 @@ -import { and, asc, count, eq, gt, isNull } from 'drizzle-orm'; +import { and, asc, count, eq, gt, inArray, isNull } from 'drizzle-orm'; -import type { Result } from '@/lib/types'; +import type { Result, User } from '@/lib/types'; import { db } from '@/db'; import { @@ -8,9 +8,12 @@ import { books, chapter_assignments, project_units, + project_users, + projects, translated_verses, } from '@/db/schema'; import { logger } from '@/lib/logger'; +import { ROLES } from '@/lib/roles'; import { err, ErrorCode, ok } from '@/lib/types'; import type { @@ -26,11 +29,67 @@ import { queueAiSuggestionJobs, } from './ai-suggestions.repository'; -export async function trackUsage(userId: number, data: TrackUsageRequest): Promise> { - return logAiSuggestionUsage(userId, data.bibleTextId, data.projectUnitId, data.wasUsed); +async function userCanAccessProjectUnit(user: User, projectUnitId: number): Promise { + const [record] = await db + .select({ + organization: projects.organization, + memberUserId: project_users.userId, + }) + .from(project_units) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .leftJoin( + project_users, + and(eq(project_users.projectId, projects.id), eq(project_users.userId, user.id)) + ) + .where(eq(project_units.id, projectUnitId)) + .limit(1); + + if (!record) return false; + + if (user.roleName === ROLES.PROJECT_MANAGER) { + return record.organization === user.organization; + } + + if (user.roleName === ROLES.TRANSLATOR) { + return record.memberUserId === user.id; + } + + return false; +} + +async function chapterBelongsToProjectUnit(input: { + projectUnitId: number; + bibleId: number; + bookCode: string; + chapterNumber: number; +}): Promise { + const [record] = await db + .select({ id: chapter_assignments.id }) + .from(chapter_assignments) + .innerJoin(books, eq(chapter_assignments.bookId, books.id)) + .where( + and( + eq(chapter_assignments.projectUnitId, input.projectUnitId), + eq(chapter_assignments.bibleId, input.bibleId), + eq(books.code, input.bookCode), + eq(chapter_assignments.chapterNumber, input.chapterNumber) + ) + ) + .limit(1); + + return Boolean(record); +} + +export async function trackUsage(user: User, data: TrackUsageRequest): Promise> { + if (!(await userCanAccessProjectUnit(user, data.projectUnitId))) { + return err(ErrorCode.FORBIDDEN); + } + + return logAiSuggestionUsage(user.id, data.bibleTextId, data.projectUnitId, data.wasUsed); } export async function getAiSuggestions( + user: User, query: GetAiSuggestionsQuery ): Promise> { const ids = query.bibleTextIds @@ -38,6 +97,27 @@ export async function getAiSuggestions( .map((id) => Number.parseInt(id.trim(), 10)) .filter((id) => !Number.isNaN(id)); + if ( + ids.length === 0 || + ids.length > AI_SUGGESTIONS_CONSTANTS.MAX_REQUESTED_BIBLE_TEXT_IDS || + ids.length !== new Set(ids).size + ) { + return err(ErrorCode.VALIDATION_ERROR); + } + + if (!(await userCanAccessProjectUnit(user, query.projectUnitId))) { + return err(ErrorCode.FORBIDDEN); + } + + const existingIds = await db + .select({ id: bible_texts.id }) + .from(bible_texts) + .where(inArray(bible_texts.id, ids)); + + if (existingIds.length !== ids.length) { + return err(ErrorCode.VALIDATION_ERROR); + } + const suggestionsResult = await getAiSuggestionsRepo(query.projectUnitId, ids); if (!suggestionsResult.ok) { @@ -56,6 +136,7 @@ export async function getAiSuggestions( } export async function queueNextVerses( + user: User, projectUnitId: number, bibleId: number, bookCode: string, @@ -64,49 +145,82 @@ export async function queueNextVerses( lookahead: number = AI_SUGGESTIONS_CONSTANTS.DEFAULT_LOOKAHEAD ): Promise> { try { - // 1. Find the next few verses that haven't been translated yet - const nextVerses = await db - .select({ verseNumber: bible_texts.verseNumber }) - .from(bible_texts) - .innerJoin(books, eq(bible_texts.bookId, books.id)) - .leftJoin( - translated_verses, - and( - eq(translated_verses.bibleTextId, bible_texts.id), - eq(translated_verses.projectUnitId, projectUnitId) - ) - ) - .where( - and( - eq(bible_texts.bibleId, bibleId), - eq(books.code, bookCode), - eq(bible_texts.chapterNumber, chapterNumber), - gt(bible_texts.verseNumber, currentVerse), - isNull(translated_verses.projectUnitId) - ) - ) - .orderBy(asc(bible_texts.verseNumber)) - .limit(lookahead); + if (!(await userCanAccessProjectUnit(user, projectUnitId))) { + return err(ErrorCode.FORBIDDEN); + } - if (nextVerses.length === 0) return ok(undefined); + const normalizedBookCode = bookCode.toLowerCase(); - // 2. Queue them - const jobs = nextVerses.map((v) => ({ + if ( + !(await chapterBelongsToProjectUnit({ + projectUnitId, + bibleId, + bookCode: normalizedBookCode, + chapterNumber, + })) + ) { + return err(ErrorCode.INVALID_REFERENCE); + } + + return await queueNextVersesForAssignment( projectUnitId, bibleId, - bookCode, + normalizedBookCode, chapterNumber, - verseStart: v.verseNumber, - verseEnd: v.verseNumber, - })); - - return await queueAiSuggestionJobs(jobs); + currentVerse, + lookahead + ); } catch (error) { logger.error(error); return err(ErrorCode.INTERNAL_ERROR); } } +async function queueNextVersesForAssignment( + projectUnitId: number, + bibleId: number, + bookCode: string, + chapterNumber: number, + currentVerse: number, + lookahead: number +): Promise> { + const nextVerses = await db + .select({ verseNumber: bible_texts.verseNumber }) + .from(bible_texts) + .innerJoin(books, eq(bible_texts.bookId, books.id)) + .leftJoin( + translated_verses, + and( + eq(translated_verses.bibleTextId, bible_texts.id), + eq(translated_verses.projectUnitId, projectUnitId) + ) + ) + .where( + and( + eq(bible_texts.bibleId, bibleId), + eq(books.code, bookCode), + eq(bible_texts.chapterNumber, chapterNumber), + gt(bible_texts.verseNumber, currentVerse), + isNull(translated_verses.projectUnitId) + ) + ) + .orderBy(asc(bible_texts.verseNumber)) + .limit(lookahead); + + if (nextVerses.length === 0) return ok(undefined); + + const jobs = nextVerses.map((v) => ({ + projectUnitId, + bibleId, + bookCode, + chapterNumber, + verseStart: v.verseNumber, + verseEnd: v.verseNumber, + })); + + return queueAiSuggestionJobs(jobs); +} + export async function handleChapterAssigned( projectUnitId: number, bibleId: number, @@ -130,10 +244,10 @@ export async function handleChapterAssigned( if (!book[0]?.code) return; - await queueNextVerses( + await queueNextVersesForAssignment( projectUnitId, bibleId, - book[0].code, + book[0].code.toLowerCase(), chapterNumber, 0, AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT @@ -193,10 +307,10 @@ export async function handleVerseSaved(projectUnitId: number) { chapterNumber: number; bookCode: string; }) => - queueNextVerses( + queueNextVersesForAssignment( projectUnitId, assignment.bibleId, - assignment.bookCode, + assignment.bookCode.toLowerCase(), assignment.chapterNumber, 0, AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT diff --git a/src/domains/ai-suggestions/ai-suggestions.types.ts b/src/domains/ai-suggestions/ai-suggestions.types.ts index 31f63f1..e5cc8ab 100644 --- a/src/domains/ai-suggestions/ai-suggestions.types.ts +++ b/src/domains/ai-suggestions/ai-suggestions.types.ts @@ -4,7 +4,10 @@ import { AI_SUGGESTIONS_CONSTANTS } from './ai-suggestions.constants'; export const getAiSuggestionsQuerySchema = z.object({ projectUnitId: z.coerce.number().int().positive(), - bibleTextIds: z.string().describe('Comma-separated list of bible text IDs'), + bibleTextIds: z + .string() + .regex(/^\d+(,\d+)*$/, 'Expected comma-separated numeric bible text IDs') + .describe('Comma-separated list of bible text IDs'), }); export type GetAiSuggestionsQuery = z.infer; From 7b384c936324431774b7386295d680d391d43f40 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 18:15:09 +0530 Subject: [PATCH 05/14] updated imports --- src/domains/ai-suggestions/ai-suggestions.repository.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts index 95f9ea4..29abeb2 100644 --- a/src/domains/ai-suggestions/ai-suggestions.repository.ts +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -3,7 +3,7 @@ import { and, eq, inArray } from 'drizzle-orm'; import type { DbTransaction, Result } from '@/lib/types'; import { db } from '@/db'; -import { ai_suggestion_jobs, ai_suggestions } from '@/db/schema'; +import { ai_suggestion_jobs, ai_suggestion_usage_log, ai_suggestions } from '@/db/schema'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; @@ -86,9 +86,6 @@ export async function logAiSuggestionUsage( ): Promise> { const database = tx || db; try { - // Import ai_suggestion_usage_log inside to avoid circular deps if any, - // but better to import it at the top - const { ai_suggestion_usage_log } = await import('@/db/schema'); await database .insert(ai_suggestion_usage_log) .values({ From 20a97cab34dcdb0ec9f27dfcc8f344e6d24c3421 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 18:17:27 +0530 Subject: [PATCH 06/14] format fixes --- src/db/migrations/meta/0010_snapshot.json | 405 +++++-------------- src/db/migrations/meta/0014_snapshot.json | 469 ++++++---------------- 2 files changed, 212 insertions(+), 662 deletions(-) diff --git a/src/db/migrations/meta/0010_snapshot.json b/src/db/migrations/meta/0010_snapshot.json index 49c832d..09e114f 100644 --- a/src/db/migrations/meta/0010_snapshot.json +++ b/src/db/migrations/meta/0010_snapshot.json @@ -57,12 +57,8 @@ "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -70,12 +66,8 @@ "name": "active_chapter_editors_user_id_users_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -83,10 +75,7 @@ "compositePrimaryKeys": { "active_chapter_editors_chapter_assignment_id_user_id_pk": { "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", - "columns": [ - "chapter_assignment_id", - "user_id" - ] + "columns": ["chapter_assignment_id", "user_id"] } }, "uniqueConstraints": {}, @@ -239,12 +228,8 @@ "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -252,12 +237,8 @@ "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -353,12 +334,8 @@ "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", "tableFrom": "ai_suggestions", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -366,12 +343,8 @@ "name": "ai_suggestions_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestions", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -419,12 +392,8 @@ "name": "bible_books_bible_id_bibles_id_fk", "tableFrom": "bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -432,12 +401,8 @@ "name": "bible_books_book_id_books_id_fk", "tableFrom": "bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -570,12 +535,8 @@ "name": "bible_texts_bible_id_bibles_id_fk", "tableFrom": "bible_texts", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -583,12 +544,8 @@ "name": "bible_texts_book_id_books_id_fk", "tableFrom": "bible_texts", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -648,12 +605,8 @@ "name": "bibles_language_id_languages_id_fk", "tableFrom": "bibles", "tableTo": "languages", - "columnsFrom": [ - "language_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["language_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -663,16 +616,12 @@ "bibles_name_unique": { "name": "bibles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "bibles_abbreviation_unique": { "name": "bibles_abbreviation_unique", "nullsNotDistinct": false, - "columns": [ - "abbreviation" - ] + "columns": ["abbreviation"] } }, "policies": {}, @@ -791,12 +740,8 @@ "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -804,12 +749,8 @@ "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -900,12 +841,8 @@ "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -913,12 +850,8 @@ "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -982,12 +915,8 @@ "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_status_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1164,12 +1093,8 @@ "name": "chapter_assignments_project_unit_id_project_units_id_fk", "tableFrom": "chapter_assignments", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1177,12 +1102,8 @@ "name": "chapter_assignments_bible_id_bibles_id_fk", "tableFrom": "chapter_assignments", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1190,12 +1111,8 @@ "name": "chapter_assignments_book_id_books_id_fk", "tableFrom": "chapter_assignments", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1203,12 +1120,8 @@ "name": "chapter_assignments_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1216,12 +1129,8 @@ "name": "chapter_assignments_peer_checker_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "peer_checker_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["peer_checker_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1329,9 +1238,7 @@ "organizations_name_unique": { "name": "organizations_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1382,9 +1289,7 @@ "permissions_name_unique": { "name": "permissions_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1434,12 +1339,8 @@ "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1447,12 +1348,8 @@ "name": "project_unit_bible_books_bible_id_bibles_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1460,12 +1357,8 @@ "name": "project_unit_bible_books_book_id_books_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1521,12 +1414,8 @@ "name": "project_units_project_id_projects_id_fk", "tableFrom": "project_units", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1598,12 +1487,8 @@ "name": "project_users_project_id_projects_id_fk", "tableFrom": "project_users", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1611,12 +1496,8 @@ "name": "project_users_user_id_users_id_fk", "tableFrom": "project_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -1624,10 +1505,7 @@ "compositePrimaryKeys": { "project_users_project_id_user_id_pk": { "name": "project_users_project_id_user_id_pk", - "columns": [ - "project_id", - "user_id" - ] + "columns": ["project_id", "user_id"] } }, "uniqueConstraints": {}, @@ -1718,12 +1596,8 @@ "name": "projects_source_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "source_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1731,12 +1605,8 @@ "name": "projects_target_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "target_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1744,12 +1614,8 @@ "name": "projects_organization_organizations_id_fk", "tableFrom": "projects", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1757,12 +1623,8 @@ "name": "projects_created_by_users_id_fk", "tableFrom": "projects", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1819,12 +1681,8 @@ "name": "role_permissions_role_id_roles_id_fk", "tableFrom": "role_permissions", "tableTo": "roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1832,12 +1690,8 @@ "name": "role_permissions_permission_id_permissions_id_fk", "tableFrom": "role_permissions", "tableTo": "permissions", - "columnsFrom": [ - "permission_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1845,10 +1699,7 @@ "compositePrimaryKeys": { "role_permissions_role_id_permission_id_pk": { "name": "role_permissions_role_id_permission_id_pk", - "columns": [ - "role_id", - "permission_id" - ] + "columns": ["role_id", "permission_id"] } }, "uniqueConstraints": {}, @@ -1894,9 +1745,7 @@ "roles_name_unique": { "name": "roles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1980,12 +1829,8 @@ "name": "translated_verses_project_unit_id_project_units_id_fk", "tableFrom": "translated_verses", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1993,12 +1838,8 @@ "name": "translated_verses_bible_text_id_bible_texts_id_fk", "tableFrom": "translated_verses", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2006,12 +1847,8 @@ "name": "translated_verses_assigned_user_id_users_id_fk", "tableFrom": "translated_verses", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2087,12 +1924,8 @@ "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2100,12 +1933,8 @@ "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2197,12 +2026,8 @@ "name": "users_role_roles_id_fk", "tableFrom": "users", "tableTo": "roles", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2210,12 +2035,8 @@ "name": "users_organization_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2223,12 +2044,8 @@ "name": "users_created_by_users_id_fk", "tableFrom": "users", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2238,16 +2055,12 @@ "users_username_unique": { "name": "users_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] }, "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -2259,20 +2072,12 @@ "public.ai_suggestion_job_status": { "name": "ai_suggestion_job_status", "schema": "public", - "values": [ - "queued", - "processing", - "completed", - "failed" - ] + "values": ["queued", "processing", "completed", "failed"] }, "public.assignment_role": { "name": "assignment_role", "schema": "public", - "values": [ - "drafter", - "peer_checker" - ] + "values": ["drafter", "peer_checker"] }, "public.chapter_status": { "name": "chapter_status", @@ -2291,36 +2096,22 @@ "public.project_assignment_status": { "name": "project_assignment_status", "schema": "public", - "values": [ - "active", - "not_assigned" - ] + "values": ["active", "not_assigned"] }, "public.project_status": { "name": "project_status", "schema": "public", - "values": [ - "not_started", - "in_progress", - "completed" - ] + "values": ["not_started", "in_progress", "completed"] }, "public.script_direction": { "name": "script_direction", "schema": "public", - "values": [ - "ltr", - "rtl" - ] + "values": ["ltr", "rtl"] }, "public.user_status": { "name": "user_status", "schema": "public", - "values": [ - "invited", - "verified", - "inactive" - ] + "values": ["invited", "verified", "inactive"] } }, "schemas": {}, diff --git a/src/db/migrations/meta/0014_snapshot.json b/src/db/migrations/meta/0014_snapshot.json index ab93c68..e5a75ad 100644 --- a/src/db/migrations/meta/0014_snapshot.json +++ b/src/db/migrations/meta/0014_snapshot.json @@ -55,12 +55,8 @@ "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -68,12 +64,8 @@ "name": "active_chapter_editors_user_id_users_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -81,10 +73,7 @@ "compositePrimaryKeys": { "active_chapter_editors_chapter_assignment_id_user_id_pk": { "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", - "columns": [ - "chapter_assignment_id", - "user_id" - ] + "columns": ["chapter_assignment_id", "user_id"] } }, "uniqueConstraints": {}, @@ -255,12 +244,8 @@ "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -268,12 +253,8 @@ "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -385,12 +366,8 @@ "name": "ai_suggestion_usage_log_user_id_users_id_fk", "tableFrom": "ai_suggestion_usage_log", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -398,12 +375,8 @@ "name": "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk", "tableFrom": "ai_suggestion_usage_log", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -411,12 +384,8 @@ "name": "ai_suggestion_usage_log_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestion_usage_log", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -512,12 +481,8 @@ "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", "tableFrom": "ai_suggestions", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -525,12 +490,8 @@ "name": "ai_suggestions_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestions", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -632,12 +593,8 @@ "name": "auth_account_user_id_auth_user_id_fk", "tableFrom": "auth_account", "tableTo": "auth_user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -749,12 +706,8 @@ "name": "auth_audit_log_user_id_auth_user_id_fk", "tableFrom": "auth_audit_log", "tableTo": "auth_user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -826,12 +779,8 @@ "name": "auth_session_user_id_auth_user_id_fk", "tableFrom": "auth_session", "tableTo": "auth_user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -841,9 +790,7 @@ "auth_session_token_unique": { "name": "auth_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -914,9 +861,7 @@ "auth_user_email_unique": { "name": "auth_user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1011,12 +956,8 @@ "name": "bible_books_bible_id_bibles_id_fk", "tableFrom": "bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1024,12 +965,8 @@ "name": "bible_books_book_id_books_id_fk", "tableFrom": "bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1162,12 +1099,8 @@ "name": "bible_texts_bible_id_bibles_id_fk", "tableFrom": "bible_texts", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1175,12 +1108,8 @@ "name": "bible_texts_book_id_books_id_fk", "tableFrom": "bible_texts", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1240,12 +1169,8 @@ "name": "bibles_language_id_languages_id_fk", "tableFrom": "bibles", "tableTo": "languages", - "columnsFrom": [ - "language_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["language_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1255,16 +1180,12 @@ "bibles_name_unique": { "name": "bibles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "bibles_abbreviation_unique": { "name": "bibles_abbreviation_unique", "nullsNotDistinct": false, - "columns": [ - "abbreviation" - ] + "columns": ["abbreviation"] } }, "policies": {}, @@ -1383,12 +1304,8 @@ "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1396,12 +1313,8 @@ "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1492,12 +1405,8 @@ "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1505,12 +1414,8 @@ "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1574,12 +1479,8 @@ "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_status_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1756,12 +1657,8 @@ "name": "chapter_assignments_project_unit_id_project_units_id_fk", "tableFrom": "chapter_assignments", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1769,12 +1666,8 @@ "name": "chapter_assignments_bible_id_bibles_id_fk", "tableFrom": "chapter_assignments", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1782,12 +1675,8 @@ "name": "chapter_assignments_book_id_books_id_fk", "tableFrom": "chapter_assignments", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1795,12 +1684,8 @@ "name": "chapter_assignments_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1808,12 +1693,8 @@ "name": "chapter_assignments_peer_checker_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "peer_checker_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["peer_checker_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1921,9 +1802,7 @@ "organizations_name_unique": { "name": "organizations_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1974,9 +1853,7 @@ "permissions_name_unique": { "name": "permissions_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -2026,12 +1903,8 @@ "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2039,12 +1912,8 @@ "name": "project_unit_bible_books_bible_id_bibles_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2052,12 +1921,8 @@ "name": "project_unit_bible_books_book_id_books_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2120,12 +1985,8 @@ "name": "project_units_project_id_projects_id_fk", "tableFrom": "project_units", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -2197,12 +2058,8 @@ "name": "project_users_project_id_projects_id_fk", "tableFrom": "project_users", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2210,12 +2067,8 @@ "name": "project_users_user_id_users_id_fk", "tableFrom": "project_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -2223,10 +2076,7 @@ "compositePrimaryKeys": { "project_users_project_id_user_id_pk": { "name": "project_users_project_id_user_id_pk", - "columns": [ - "project_id", - "user_id" - ] + "columns": ["project_id", "user_id"] } }, "uniqueConstraints": {}, @@ -2317,12 +2167,8 @@ "name": "projects_source_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "source_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2330,12 +2176,8 @@ "name": "projects_target_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "target_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2343,12 +2185,8 @@ "name": "projects_organization_organizations_id_fk", "tableFrom": "projects", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2356,12 +2194,8 @@ "name": "projects_created_by_users_id_fk", "tableFrom": "projects", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2418,12 +2252,8 @@ "name": "role_permissions_role_id_roles_id_fk", "tableFrom": "role_permissions", "tableTo": "roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2431,12 +2261,8 @@ "name": "role_permissions_permission_id_permissions_id_fk", "tableFrom": "role_permissions", "tableTo": "permissions", - "columnsFrom": [ - "permission_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2444,10 +2270,7 @@ "compositePrimaryKeys": { "role_permissions_role_id_permission_id_pk": { "name": "role_permissions_role_id_permission_id_pk", - "columns": [ - "role_id", - "permission_id" - ] + "columns": ["role_id", "permission_id"] } }, "uniqueConstraints": {}, @@ -2493,9 +2316,7 @@ "roles_name_unique": { "name": "roles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -2579,12 +2400,8 @@ "name": "translated_verses_project_unit_id_project_units_id_fk", "tableFrom": "translated_verses", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2592,12 +2409,8 @@ "name": "translated_verses_bible_text_id_bible_texts_id_fk", "tableFrom": "translated_verses", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2605,12 +2418,8 @@ "name": "translated_verses_assigned_user_id_users_id_fk", "tableFrom": "translated_verses", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2686,12 +2495,8 @@ "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2699,12 +2504,8 @@ "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2802,12 +2603,8 @@ "name": "users_auth_user_id_auth_user_id_fk", "tableFrom": "users", "tableTo": "auth_user", - "columnsFrom": [ - "auth_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["auth_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2815,12 +2612,8 @@ "name": "users_role_roles_id_fk", "tableFrom": "users", "tableTo": "roles", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2828,12 +2621,8 @@ "name": "users_organization_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2841,12 +2630,8 @@ "name": "users_created_by_users_id_fk", "tableFrom": "users", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2856,16 +2641,12 @@ "users_username_unique": { "name": "users_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] }, "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -2877,20 +2658,12 @@ "public.ai_suggestion_job_status": { "name": "ai_suggestion_job_status", "schema": "public", - "values": [ - "queued", - "processing", - "completed", - "failed" - ] + "values": ["queued", "processing", "completed", "failed"] }, "public.assignment_role": { "name": "assignment_role", "schema": "public", - "values": [ - "drafter", - "peer_checker" - ] + "values": ["drafter", "peer_checker"] }, "public.chapter_status": { "name": "chapter_status", @@ -2909,36 +2682,22 @@ "public.project_assignment_status": { "name": "project_assignment_status", "schema": "public", - "values": [ - "active", - "not_assigned" - ] + "values": ["active", "not_assigned"] }, "public.project_status": { "name": "project_status", "schema": "public", - "values": [ - "not_started", - "in_progress", - "completed" - ] + "values": ["not_started", "in_progress", "completed"] }, "public.script_direction": { "name": "script_direction", "schema": "public", - "values": [ - "ltr", - "rtl" - ] + "values": ["ltr", "rtl"] }, "public.user_status": { "name": "user_status", "schema": "public", - "values": [ - "invited", - "verified", - "inactive" - ] + "values": ["invited", "verified", "inactive"] } }, "schemas": { From 327f8e298b6eaadd1cbe54e9764f9cb97d828835 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 19:26:17 +0530 Subject: [PATCH 07/14] fixed book code error --- src/domains/ai-suggestions/ai-suggestions.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 145b7bb..9bfd2fb 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -149,7 +149,7 @@ export async function queueNextVerses( return err(ErrorCode.FORBIDDEN); } - const normalizedBookCode = bookCode.toLowerCase(); + const normalizedBookCode = bookCode.toUpperCase(); if ( !(await chapterBelongsToProjectUnit({ @@ -247,7 +247,7 @@ export async function handleChapterAssigned( await queueNextVersesForAssignment( projectUnitId, bibleId, - book[0].code.toLowerCase(), + book[0].code.toUpperCase(), chapterNumber, 0, AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT From 96c2ee82c52bcd99617d8450d2cc8a4cd8cbf5d0 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 20:00:41 +0530 Subject: [PATCH 08/14] fixed migration data and schema --- src/db/migrations/0010_add_better_auth.sql | 67 + .../migrations/0011_added_ai_usggestions.sql | 52 + src/db/migrations/meta/0010_snapshot.json | 461 ++- src/db/migrations/meta/0011_snapshot.json | 2958 +++++++++++++++++ src/db/migrations/meta/_journal.json | 31 +- src/db/schema.ts | 2 +- 6 files changed, 3391 insertions(+), 180 deletions(-) create mode 100644 src/db/migrations/0010_add_better_auth.sql create mode 100644 src/db/migrations/0011_added_ai_usggestions.sql create mode 100644 src/db/migrations/meta/0011_snapshot.json diff --git a/src/db/migrations/0010_add_better_auth.sql b/src/db/migrations/0010_add_better_auth.sql new file mode 100644 index 0000000..593a332 --- /dev/null +++ b/src/db/migrations/0010_add_better_auth.sql @@ -0,0 +1,67 @@ +CREATE TABLE "auth_account" ( + "id" varchar(36) PRIMARY KEY NOT NULL, + "account_id" varchar(255) NOT NULL, + "provider_id" varchar(255) NOT NULL, + "user_id" varchar(36) NOT NULL, + "access_token" varchar, + "refresh_token" varchar, + "id_token" varchar, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" varchar(255), + "password" varchar(255), + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "auth_audit_log" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" varchar(36), + "event" varchar(100) NOT NULL, + "ip_address" varchar(255), + "user_agent" varchar, + "metadata" jsonb DEFAULT '{}'::jsonb, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "auth_session" ( + "id" varchar(36) PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" varchar(255) NOT NULL, + "ip_address" varchar(255), + "user_agent" varchar, + "user_id" varchar(36) NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "auth_session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "auth_user" ( + "id" varchar(36) PRIMARY KEY NOT NULL, + "name" varchar(255) NOT NULL, + "email" varchar(255) NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" varchar(255), + "two_factor_enabled" boolean DEFAULT false, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "auth_user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "auth_verification" ( + "id" varchar(36) PRIMARY KEY NOT NULL, + "identifier" varchar(255) NOT NULL, + "value" varchar(255) NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "auth_user_id" varchar(36);--> statement-breakpoint +ALTER TABLE "auth_account" ADD CONSTRAINT "auth_account_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth_audit_log" ADD CONSTRAINT "auth_audit_log_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth_session" ADD CONSTRAINT "auth_session_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_audit_log_user" ON "auth_audit_log" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_audit_log_event" ON "auth_audit_log" USING btree ("event");--> statement-breakpoint +CREATE INDEX "idx_audit_log_created" ON "auth_audit_log" USING btree ("created_at");--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_auth_user_id_auth_user_id_fk" FOREIGN KEY ("auth_user_id") REFERENCES "public"."auth_user"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/0011_added_ai_usggestions.sql b/src/db/migrations/0011_added_ai_usggestions.sql new file mode 100644 index 0000000..c043a5a --- /dev/null +++ b/src/db/migrations/0011_added_ai_usggestions.sql @@ -0,0 +1,52 @@ +CREATE SCHEMA "ai"; +--> statement-breakpoint +CREATE TYPE "public"."ai_suggestion_job_status" AS ENUM('queued', 'processing', 'completed', 'failed');--> statement-breakpoint +CREATE TABLE "ai"."ai_suggestion_jobs" ( + "id" serial PRIMARY KEY NOT NULL, + "project_unit_id" integer NOT NULL, + "bible_id" integer NOT NULL, + "book_code" varchar(50) NOT NULL, + "chapter_number" integer NOT NULL, + "verse_start" integer NOT NULL, + "verse_end" integer NOT NULL, + "status" varchar(20) DEFAULT 'queued' NOT NULL, + "retry_count" integer DEFAULT 0 NOT NULL, + "error_message" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "ai"."ai_suggestion_usage_log" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "bible_text_id" integer NOT NULL, + "project_unit_id" integer NOT NULL, + "was_used" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ai"."ai_suggestions" ( + "id" serial PRIMARY KEY NOT NULL, + "bible_text_id" integer NOT NULL, + "project_unit_id" integer NOT NULL, + "suggested_text" text NOT NULL, + "model_info" varchar(100), + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +ALTER TABLE "chapter_assignments" ADD COLUMN "is_ai_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_ai_jobs_project_unit" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id");--> statement-breakpoint +CREATE INDEX "idx_ai_jobs_status" ON "ai"."ai_suggestion_jobs" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_jobs_range" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id","bible_id","book_code","chapter_number","verse_start","verse_end");--> statement-breakpoint +CREATE INDEX "idx_ai_usage_user" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_ai_usage_project_unit" ON "ai"."ai_suggestion_usage_log" USING btree ("project_unit_id");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_usage_user_text" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id","bible_text_id");--> statement-breakpoint +CREATE INDEX "idx_ai_suggestions_bible_text" ON "ai"."ai_suggestions" USING btree ("bible_text_id");--> statement-breakpoint +CREATE UNIQUE INDEX "uq_ai_suggestions_per_text_unit" ON "ai"."ai_suggestions" USING btree ("bible_text_id","project_unit_id"); \ No newline at end of file diff --git a/src/db/migrations/meta/0010_snapshot.json b/src/db/migrations/meta/0010_snapshot.json index 09e114f..fcd8bac 100644 --- a/src/db/migrations/meta/0010_snapshot.json +++ b/src/db/migrations/meta/0010_snapshot.json @@ -1,5 +1,5 @@ { - "id": "b5e72067-119a-4734-97f0-fe7382fbdb9d", + "id": "215ac9fb-e170-4975-9f11-77b471673625", "prevId": "0e1e1972-53ae-4b50-9df0-af09aa4190cc", "version": "7", "dialect": "postgresql", @@ -83,81 +83,164 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.ai_suggestion_jobs": { - "name": "ai_suggestion_jobs", + "public.auth_account": { + "name": "auth_account", "schema": "", "columns": { "id": { "name": "id", - "type": "serial", + "type": "varchar(36)", "primaryKey": true, "notNull": true }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", + "account_id": { + "name": "account_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "bible_id": { - "name": "bible_id", - "type": "integer", + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "book_code": { - "name": "book_code", - "type": "varchar(50)", + "user_id": { + "name": "user_id", + "type": "varchar(36)", "primaryKey": false, "notNull": true }, - "chapter_number": { - "name": "chapter_number", - "type": "integer", + "access_token": { + "name": "access_token", + "type": "varchar", "primaryKey": false, - "notNull": true + "notNull": false }, - "verse_start": { - "name": "verse_start", - "type": "integer", + "refresh_token": { + "name": "refresh_token", + "type": "varchar", "primaryKey": false, - "notNull": true + "notNull": false }, - "verse_end": { - "name": "verse_end", - "type": "integer", + "id_token": { + "name": "id_token", + "type": "varchar", "primaryKey": false, - "notNull": true + "notNull": false }, - "status": { - "name": "status", - "type": "ai_suggestion_job_status", - "typeSchema": "public", + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", "primaryKey": false, - "notNull": true, - "default": "'queued'" + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, - "notNull": false, + "notNull": true, "default": "now()" }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_audit_log": { + "name": "auth_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, "default": "now()" } }, "indexes": { - "idx_ai_jobs_project_unit": { - "name": "idx_ai_jobs_project_unit", + "idx_audit_log_user": { + "name": "idx_audit_log_user", "columns": [ { - "expression": "project_unit_id", + "expression": "user_id", "isExpression": false, "asc": true, "nulls": "last" @@ -168,11 +251,11 @@ "method": "btree", "with": {} }, - "idx_ai_jobs_status": { - "name": "idx_ai_jobs_status", + "idx_audit_log_event": { + "name": "idx_audit_log_event", "columns": [ { - "expression": "status", + "expression": "event", "isExpression": false, "asc": true, "nulls": "last" @@ -183,63 +266,30 @@ "method": "btree", "with": {} }, - "uq_ai_jobs_range": { - "name": "uq_ai_jobs_range", + "idx_audit_log_created": { + "name": "idx_audit_log_created", "columns": [ { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "book_code", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_number", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_start", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_end", + "expression": "created_at", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": true, + "isUnique": false, "concurrently": false, "method": "btree", "with": {} } }, "foreignKeys": { - "ai_suggestion_jobs_project_unit_id_project_units_id_fk": { - "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestion_jobs", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_jobs_bible_id_bibles_id_fk": { - "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", - "tableFrom": "ai_suggestion_jobs", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], + "auth_audit_log_user_id_auth_user_id_fk": { + "name": "auth_audit_log_user_id_auth_user_id_fk", + "tableFrom": "auth_audit_log", + "tableTo": "auth_user", + "columnsFrom": ["user_id"], "columnsTo": ["id"], - "onDelete": "no action", + "onDelete": "set null", "onUpdate": "no action" } }, @@ -249,106 +299,201 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.ai_suggestions": { - "name": "ai_suggestions", + "public.auth_session": { + "name": "auth_session", "schema": "", "columns": { "id": { "name": "id", - "type": "serial", + "type": "varchar(36)", "primaryKey": true, "notNull": true }, - "bible_text_id": { - "name": "bible_text_id", - "type": "integer", + "expires_at": { + "name": "expires_at", + "type": "timestamp", "primaryKey": false, "notNull": true }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", + "token": { + "name": "token", + "type": "varchar(255)", "primaryKey": false, "notNull": true }, - "suggested_text": { - "name": "suggested_text", - "type": "varchar", + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", "primaryKey": false, - "notNull": true + "notNull": false }, - "model_info": { - "name": "model_info", - "type": "varchar(100)", + "user_agent": { + "name": "user_agent", + "type": "varchar", "primaryKey": false, "notNull": false }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true + }, "created_at": { "name": "created_at", "type": "timestamp", "primaryKey": false, - "notNull": false, + "notNull": true, "default": "now()" - } - }, - "indexes": { - "idx_ai_suggestions_bible_text": { - "name": "idx_ai_suggestions_bible_text", - "columns": [ - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} }, - "uq_ai_suggestions_per_text_unit": { - "name": "uq_ai_suggestions_per_text_unit", - "columns": [ - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" } }, + "indexes": {}, "foreignKeys": { - "ai_suggestions_bible_text_id_bible_texts_id_fk": { - "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", - "tableFrom": "ai_suggestions", - "tableTo": "bible_texts", - "columnsFrom": ["bible_text_id"], + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": ["user_id"], "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true }, - "ai_suggestions_project_unit_id_project_units_id_fk": { - "name": "ai_suggestions_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestions", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" } }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, @@ -1955,6 +2100,12 @@ "primaryKey": true, "notNull": true }, + "auth_user_id": { + "name": "auth_user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, "username": { "name": "username", "type": "varchar(100)", @@ -2022,6 +2173,15 @@ }, "indexes": {}, "foreignKeys": { + "users_auth_user_id_auth_user_id_fk": { + "name": "users_auth_user_id_auth_user_id_fk", + "tableFrom": "users", + "tableTo": "auth_user", + "columnsFrom": ["auth_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, "users_role_roles_id_fk": { "name": "users_role_roles_id_fk", "tableFrom": "users", @@ -2069,11 +2229,6 @@ } }, "enums": { - "public.ai_suggestion_job_status": { - "name": "ai_suggestion_job_status", - "schema": "public", - "values": ["queued", "processing", "completed", "failed"] - }, "public.assignment_role": { "name": "assignment_role", "schema": "public", diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..e8ba905 --- /dev/null +++ b/src/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,2958 @@ +{ + "id": "30a2d1df-803f-4c34-a8ae-8d01b8bc3cf8", + "prevId": "215ac9fb-e170-4975-9f11-77b471673625", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.active_chapter_editors": { + "name": "active_chapter_editors", + "schema": "", + "columns": { + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_active_editors_chapter": { + "name": "idx_active_editors_chapter", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "active_chapter_editors", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "active_chapter_editors_user_id_users_id_fk": { + "name": "active_chapter_editors_user_id_users_id_fk", + "tableFrom": "active_chapter_editors", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "active_chapter_editors_chapter_assignment_id_user_id_pk": { + "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", + "columns": [ + "chapter_assignment_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "ai.ai_suggestion_jobs": { + "name": "ai_suggestion_jobs", + "schema": "ai", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_code": { + "name": "book_code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "chapter_number": { + "name": "chapter_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "verse_start": { + "name": "verse_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "verse_end": { + "name": "verse_end", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ai_jobs_project_unit": { + "name": "idx_ai_jobs_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_jobs_status": { + "name": "idx_ai_jobs_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_jobs_range": { + "name": "uq_ai_jobs_range", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verse_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verse_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestion_jobs_project_unit_id_project_units_id_fk": { + "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestion_jobs", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestion_jobs_bible_id_bibles_id_fk": { + "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", + "tableFrom": "ai_suggestion_jobs", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "ai.ai_suggestion_usage_log": { + "name": "ai_suggestion_usage_log", + "schema": "ai", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "was_used": { + "name": "was_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ai_usage_user": { + "name": "idx_ai_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_project_unit": { + "name": "idx_ai_usage_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_usage_user_text": { + "name": "uq_ai_usage_user_text", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestion_usage_log_user_id_users_id_fk": { + "name": "ai_suggestion_usage_log_user_id_users_id_fk", + "tableFrom": "ai_suggestion_usage_log", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk": { + "name": "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk", + "tableFrom": "ai_suggestion_usage_log", + "tableTo": "bible_texts", + "columnsFrom": [ + "bible_text_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestion_usage_log_project_unit_id_project_units_id_fk": { + "name": "ai_suggestion_usage_log_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestion_usage_log", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "ai.ai_suggestions": { + "name": "ai_suggestions", + "schema": "ai", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "suggested_text": { + "name": "suggested_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_info": { + "name": "model_info", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ai_suggestions_bible_text": { + "name": "idx_ai_suggestions_bible_text", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_suggestions_per_text_unit": { + "name": "uq_ai_suggestions_per_text_unit", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestions_bible_text_id_bible_texts_id_fk": { + "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", + "tableFrom": "ai_suggestions", + "tableTo": "bible_texts", + "columnsFrom": [ + "bible_text_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestions_project_unit_id_project_units_id_fk": { + "name": "ai_suggestions_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestions", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_audit_log": { + "name": "auth_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_audit_log_user": { + "name": "idx_audit_log_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_event": { + "name": "idx_audit_log_event", + "columns": [ + { + "expression": "event", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_created": { + "name": "idx_audit_log_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_audit_log_user_id_auth_user_id_fk": { + "name": "auth_audit_log_user_id_auth_user_id_fk", + "tableFrom": "auth_audit_log", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bible_books": { + "name": "bible_books", + "schema": "", + "columns": { + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bible_books_bible_id_bibles_id_fk": { + "name": "bible_books_bible_id_bibles_id_fk", + "tableFrom": "bible_books", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bible_books_book_id_books_id_fk": { + "name": "bible_books_book_id_books_id_fk", + "tableFrom": "bible_books", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bible_texts": { + "name": "bible_texts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chapter_number": { + "name": "chapter_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "verse_number": { + "name": "verse_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_bible_texts_bible_book_chapter": { + "name": "idx_bible_texts_bible_book_chapter", + "columns": [ + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_bible_texts_bible_book_chapter_verse": { + "name": "idx_bible_texts_bible_book_chapter_verse", + "columns": [ + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verse_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bible_texts_bible_id_bibles_id_fk": { + "name": "bible_texts_bible_id_bibles_id_fk", + "tableFrom": "bible_texts", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bible_texts_book_id_books_id_fk": { + "name": "bible_texts_book_id_books_id_fk", + "tableFrom": "bible_texts", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bibles": { + "name": "bibles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "language_id": { + "name": "language_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bibles_language_id_languages_id_fk": { + "name": "bibles_language_id_languages_id_fk", + "tableFrom": "bibles", + "tableTo": "languages", + "columnsFrom": [ + "language_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "bibles_name_unique": { + "name": "bibles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "bibles_abbreviation_unique": { + "name": "bibles_abbreviation_unique", + "nullsNotDistinct": false, + "columns": [ + "abbreviation" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.books": { + "name": "books", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "eng_display_name": { + "name": "eng_display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignment_assigned_user_history": { + "name": "chapter_assignment_assigned_user_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "assignment_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ca_user_history_assignment": { + "name": "idx_ca_user_history_assignment", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ca_user_history_user": { + "name": "idx_ca_user_history_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "chapter_assignment_assigned_user_history", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk": { + "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignment_assigned_user_history", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignment_snapshots": { + "name": "chapter_assignment_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ca_snapshots_assignment": { + "name": "idx_ca_snapshots_assignment", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ca_snapshots_user": { + "name": "idx_ca_snapshots_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "chapter_assignment_snapshots", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chapter_assignment_snapshots_assigned_user_id_users_id_fk": { + "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignment_snapshots", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignment_status_history": { + "name": "chapter_assignment_status_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ca_status_history_assignment": { + "name": "idx_ca_status_history_assignment", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "chapter_assignment_status_history", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chapter_assignments": { + "name": "chapter_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chapter_number": { + "name": "chapter_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "peer_checker_id": { + "name": "peer_checker_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "chapter_status": { + "name": "chapter_status", + "type": "chapter_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "is_ai_enabled": { + "name": "is_ai_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "submitted_time": { + "name": "submitted_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uq_chapter_assignment_per_chapter": { + "name": "uq_chapter_assignment_per_chapter", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_assigned_user": { + "name": "idx_chapter_assignments_assigned_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_peer_checker_status": { + "name": "idx_chapter_assignments_peer_checker_status", + "columns": [ + { + "expression": "peer_checker_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_project_unit": { + "name": "idx_chapter_assignments_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignments_project_unit_id_project_units_id_fk": { + "name": "chapter_assignments_project_unit_id_project_units_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "chapter_assignments_bible_id_bibles_id_fk": { + "name": "chapter_assignments_bible_id_bibles_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_book_id_books_id_fk": { + "name": "chapter_assignments_book_id_books_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_assigned_user_id_users_id_fk": { + "name": "chapter_assignments_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_peer_checker_id_users_id_fk": { + "name": "chapter_assignments_peer_checker_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": [ + "peer_checker_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.languages": { + "name": "languages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "lang_name": { + "name": "lang_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "lang_name_localized": { + "name": "lang_name_localized", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "lang_code_iso_639_3": { + "name": "lang_code_iso_639_3", + "type": "varchar(3)", + "primaryKey": false, + "notNull": false + }, + "script_direction": { + "name": "script_direction", + "type": "script_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'ltr'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_name_unique": { + "name": "organizations_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "permissions_name_unique": { + "name": "permissions_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_unit_bible_books": { + "name": "project_unit_bible_books", + "schema": "", + "columns": { + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_id": { + "name": "bible_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "book_id": { + "name": "book_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_unit_bible_books_project_unit_id_project_units_id_fk": { + "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", + "tableFrom": "project_unit_bible_books", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "project_unit_bible_books_bible_id_bibles_id_fk": { + "name": "project_unit_bible_books_bible_id_bibles_id_fk", + "tableFrom": "project_unit_bible_books", + "tableTo": "bibles", + "columnsFrom": [ + "bible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_unit_bible_books_book_id_books_id_fk": { + "name": "project_unit_bible_books_book_id_books_id_fk", + "tableFrom": "project_unit_bible_books", + "tableTo": "books", + "columnsFrom": [ + "book_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_units": { + "name": "project_units", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "project_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_started'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_units_project_id_projects_id_fk": { + "name": "project_units_project_id_projects_id_fk", + "tableFrom": "project_units", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_users": { + "name": "project_users", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_project_users_project": { + "name": "idx_project_users_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_users_user": { + "name": "idx_project_users_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_users_project_id_projects_id_fk": { + "name": "project_users_project_id_projects_id_fk", + "tableFrom": "project_users", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "project_users_user_id_users_id_fk": { + "name": "project_users_user_id_users_id_fk", + "tableFrom": "project_users", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "project_users_project_id_user_id_pk": { + "name": "project_users_project_id_user_id_pk", + "columns": [ + "project_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "source_language": { + "name": "source_language", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_language": { + "name": "target_language", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "status": { + "name": "status", + "type": "project_assignment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'not_assigned'" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_source_language_languages_id_fk": { + "name": "projects_source_language_languages_id_fk", + "tableFrom": "projects", + "tableTo": "languages", + "columnsFrom": [ + "source_language" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_target_language_languages_id_fk": { + "name": "projects_target_language_languages_id_fk", + "tableFrom": "projects", + "tableTo": "languages", + "columnsFrom": [ + "target_language" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_organization_organizations_id_fk": { + "name": "projects_organization_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "columnsFrom": [ + "organization" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_created_by_users_id_fk": { + "name": "projects_created_by_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_role_permissions_role": { + "name": "idx_role_permissions_role", + "columns": [ + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_permissions_role_id_permission_id_pk": { + "name": "role_permissions_role_id_permission_id_pk", + "columns": [ + "role_id", + "permission_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.translated_verses": { + "name": "translated_verses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assigned_user_id": { + "name": "assigned_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "uq_translated_verse_per_bible_text": { + "name": "uq_translated_verse_per_bible_text", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "translated_verses_project_unit_id_project_units_id_fk": { + "name": "translated_verses_project_unit_id_project_units_id_fk", + "tableFrom": "translated_verses", + "tableTo": "project_units", + "columnsFrom": [ + "project_unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "translated_verses_bible_text_id_bible_texts_id_fk": { + "name": "translated_verses_bible_text_id_bible_texts_id_fk", + "tableFrom": "translated_verses", + "tableTo": "bible_texts", + "columnsFrom": [ + "bible_text_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "translated_verses_assigned_user_id_users_id_fk": { + "name": "translated_verses_assigned_user_id_users_id_fk", + "tableFrom": "translated_verses", + "tableTo": "users", + "columnsFrom": [ + "assigned_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_chapter_assignment_editor_state": { + "name": "user_chapter_assignment_editor_state", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uq_user_chapter_assignment_editor_state": { + "name": "uq_user_chapter_assignment_editor_state", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_chapter_assignment_editor_state_user_id_users_id_fk": { + "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", + "tableFrom": "user_chapter_assignment_editor_state", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "user_chapter_assignment_editor_state", + "tableTo": "chapter_assignments", + "columnsFrom": [ + "chapter_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_user_id": { + "name": "auth_user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization": { + "name": "organization", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "users_auth_user_id_auth_user_id_fk": { + "name": "users_auth_user_id_auth_user_id_fk", + "tableFrom": "users", + "tableTo": "auth_user", + "columnsFrom": [ + "auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "users_role_roles_id_fk": { + "name": "users_role_roles_id_fk", + "tableFrom": "users", + "tableTo": "roles", + "columnsFrom": [ + "role" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_organization_organizations_id_fk": { + "name": "users_organization_organizations_id_fk", + "tableFrom": "users", + "tableTo": "organizations", + "columnsFrom": [ + "organization" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "users_created_by_users_id_fk": { + "name": "users_created_by_users_id_fk", + "tableFrom": "users", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.ai_suggestion_job_status": { + "name": "ai_suggestion_job_status", + "schema": "public", + "values": [ + "queued", + "processing", + "completed", + "failed" + ] + }, + "public.assignment_role": { + "name": "assignment_role", + "schema": "public", + "values": [ + "drafter", + "peer_checker" + ] + }, + "public.chapter_status": { + "name": "chapter_status", + "schema": "public", + "values": [ + "not_started", + "draft", + "peer_check", + "community_review", + "linguist_check", + "theological_check", + "consultant_check", + "complete" + ] + }, + "public.project_assignment_status": { + "name": "project_assignment_status", + "schema": "public", + "values": [ + "active", + "not_assigned" + ] + }, + "public.project_status": { + "name": "project_status", + "schema": "public", + "values": [ + "not_started", + "in_progress", + "completed" + ] + }, + "public.script_direction": { + "name": "script_direction", + "schema": "public", + "values": [ + "ltr", + "rtl" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "invited", + "verified", + "inactive" + ] + } + }, + "schemas": { + "ai": "ai" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 09e723f..f5c73ee 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -75,37 +75,16 @@ { "idx": 10, "version": "7", - "when": 1778478819437, - "tag": "0010_ai_suggestions", + "when": 1778225352600, + "tag": "0010_add_better_auth", "breakpoints": true }, { "idx": 11, "version": "7", - "when": 1779775000000, - "tag": "0011_harden_ai_suggestions", - "breakpoints": true - }, - { - "idx": 12, - "version": "7", - "when": 1779775100000, - "tag": "0012_ai_usage_tracking", - "breakpoints": true - }, - { - "idx": 13, - "version": "7", - "when": 1779775200000, - "tag": "0013_project_unit_ai_enabled", - "breakpoints": true - }, - { - "idx": 14, - "version": "7", - "when": 1779775300000, - "tag": "0014_add_better_auth", + "when": 1779805709340, + "tag": "0011_added_ai_usggestions", "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 5eb1ddb..2a497f8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -237,7 +237,6 @@ export const project_units = pgTable('project_units', { .notNull() .references(() => projects.id, { onDelete: 'cascade', onUpdate: 'cascade' }), status: projectStatusEnum('status').notNull().default('not_started'), - isAiEnabled: boolean('is_ai_enabled').default(false).notNull(), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') .defaultNow() @@ -333,6 +332,7 @@ export const chapter_assignments = pgTable( assignedUserId: integer('assigned_user_id').references(() => users.id), peerCheckerId: integer('peer_checker_id').references(() => users.id), status: chapterStatusEnum('chapter_status').notNull().default('not_started'), + isAiEnabled: boolean('is_ai_enabled').default(false).notNull(), submittedTime: timestamp('submitted_time'), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') From 4deb5751b78f4f44795fd277b9f9e203de6451c6 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Tue, 26 May 2026 20:04:43 +0530 Subject: [PATCH 09/14] removed extra code, set ai enabled in chapter assignment --- src/db/migrations/0010_ai_suggestions.sql | 33 - .../migrations/0011_harden_ai_suggestions.sql | 17 - src/db/migrations/0012_ai_usage_tracking.sql | 15 - .../0013_project_unit_ai_enabled.sql | 1 - src/db/migrations/0014_add_better_auth.sql | 67 - src/db/migrations/meta/0011_snapshot.json | 471 +-- src/db/migrations/meta/0014_snapshot.json | 2717 ----------------- src/db/migrations/meta/_journal.json | 2 +- .../ai-suggestions/ai-suggestions.service.ts | 148 +- .../chapter-assignments.policy.ts | 22 + .../chapter-assignments.repository.ts | 2 + .../chapter-assignments.route.ts | 60 +- .../chapter-assignments.service.ts | 60 +- .../chapter-assignments.types.ts | 5 + .../project-chapter-assignments.repository.ts | 1 + .../translated-verses.service.ts | 14 - 16 files changed, 294 insertions(+), 3341 deletions(-) delete mode 100644 src/db/migrations/0010_ai_suggestions.sql delete mode 100644 src/db/migrations/0011_harden_ai_suggestions.sql delete mode 100644 src/db/migrations/0012_ai_usage_tracking.sql delete mode 100644 src/db/migrations/0013_project_unit_ai_enabled.sql delete mode 100644 src/db/migrations/0014_add_better_auth.sql delete mode 100644 src/db/migrations/meta/0014_snapshot.json diff --git a/src/db/migrations/0010_ai_suggestions.sql b/src/db/migrations/0010_ai_suggestions.sql deleted file mode 100644 index 9f990ae..0000000 --- a/src/db/migrations/0010_ai_suggestions.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS "ai"; ---> statement-breakpoint -CREATE TABLE "ai"."ai_suggestion_jobs" ( - "id" serial PRIMARY KEY NOT NULL, - "project_unit_id" integer NOT NULL, - "bible_id" integer NOT NULL, - "book_code" varchar(50) NOT NULL, - "chapter_number" integer NOT NULL, - "verse_start" integer NOT NULL, - "verse_end" integer NOT NULL, - "status" varchar(20) DEFAULT 'queued' NOT NULL, - "created_at" timestamp DEFAULT now(), - "updated_at" timestamp DEFAULT now() -); ---> statement-breakpoint -CREATE TABLE "ai"."ai_suggestions" ( - "id" serial PRIMARY KEY NOT NULL, - "bible_text_id" integer NOT NULL, - "project_unit_id" integer NOT NULL, - "suggested_text" varchar NOT NULL, - "model_info" varchar(100), - "created_at" timestamp DEFAULT now() -); ---> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_ai_jobs_project_unit" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id");--> statement-breakpoint -CREATE INDEX "idx_ai_jobs_status" ON "ai"."ai_suggestion_jobs" USING btree ("status");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_jobs_range" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id","book_code","chapter_number","verse_start","verse_end");--> statement-breakpoint -CREATE INDEX "idx_ai_suggestions_bible_text" ON "ai"."ai_suggestions" USING btree ("bible_text_id");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_suggestions_per_text_unit" ON "ai"."ai_suggestions" USING btree ("bible_text_id","project_unit_id"); \ No newline at end of file diff --git a/src/db/migrations/0011_harden_ai_suggestions.sql b/src/db/migrations/0011_harden_ai_suggestions.sql deleted file mode 100644 index bce12d0..0000000 --- a/src/db/migrations/0011_harden_ai_suggestions.sql +++ /dev/null @@ -1,17 +0,0 @@ -ALTER TABLE "ai"."ai_suggestion_jobs" - ADD COLUMN IF NOT EXISTS "retry_count" integer DEFAULT 0 NOT NULL, - ADD COLUMN IF NOT EXISTS "error_message" text; ---> statement-breakpoint -ALTER TABLE "ai"."ai_suggestions" - ALTER COLUMN "suggested_text" TYPE text; ---> statement-breakpoint -DROP INDEX IF EXISTS "ai"."uq_ai_jobs_range"; ---> statement-breakpoint -CREATE UNIQUE INDEX IF NOT EXISTS "uq_ai_jobs_range" ON "ai"."ai_suggestion_jobs" USING btree ( - "project_unit_id", - "bible_id", - "book_code", - "chapter_number", - "verse_start", - "verse_end" -); diff --git a/src/db/migrations/0012_ai_usage_tracking.sql b/src/db/migrations/0012_ai_usage_tracking.sql deleted file mode 100644 index bfecec6..0000000 --- a/src/db/migrations/0012_ai_usage_tracking.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE "ai"."ai_suggestion_usage_log" ( - "id" serial PRIMARY KEY NOT NULL, - "user_id" integer NOT NULL, - "bible_text_id" integer NOT NULL, - "project_unit_id" integer NOT NULL, - "was_used" boolean DEFAULT false NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_ai_usage_user" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "idx_ai_usage_project_unit" ON "ai"."ai_suggestion_usage_log" USING btree ("project_unit_id");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_usage_user_text" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id","bible_text_id"); diff --git a/src/db/migrations/0013_project_unit_ai_enabled.sql b/src/db/migrations/0013_project_unit_ai_enabled.sql deleted file mode 100644 index a46b591..0000000 --- a/src/db/migrations/0013_project_unit_ai_enabled.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "project_units" ADD COLUMN "is_ai_enabled" boolean DEFAULT false NOT NULL; diff --git a/src/db/migrations/0014_add_better_auth.sql b/src/db/migrations/0014_add_better_auth.sql deleted file mode 100644 index 593a332..0000000 --- a/src/db/migrations/0014_add_better_auth.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE TABLE "auth_account" ( - "id" varchar(36) PRIMARY KEY NOT NULL, - "account_id" varchar(255) NOT NULL, - "provider_id" varchar(255) NOT NULL, - "user_id" varchar(36) NOT NULL, - "access_token" varchar, - "refresh_token" varchar, - "id_token" varchar, - "access_token_expires_at" timestamp, - "refresh_token_expires_at" timestamp, - "scope" varchar(255), - "password" varchar(255), - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "auth_audit_log" ( - "id" serial PRIMARY KEY NOT NULL, - "user_id" varchar(36), - "event" varchar(100) NOT NULL, - "ip_address" varchar(255), - "user_agent" varchar, - "metadata" jsonb DEFAULT '{}'::jsonb, - "created_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "auth_session" ( - "id" varchar(36) PRIMARY KEY NOT NULL, - "expires_at" timestamp NOT NULL, - "token" varchar(255) NOT NULL, - "ip_address" varchar(255), - "user_agent" varchar, - "user_id" varchar(36) NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "auth_session_token_unique" UNIQUE("token") -); ---> statement-breakpoint -CREATE TABLE "auth_user" ( - "id" varchar(36) PRIMARY KEY NOT NULL, - "name" varchar(255) NOT NULL, - "email" varchar(255) NOT NULL, - "email_verified" boolean DEFAULT false NOT NULL, - "image" varchar(255), - "two_factor_enabled" boolean DEFAULT false, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "auth_user_email_unique" UNIQUE("email") -); ---> statement-breakpoint -CREATE TABLE "auth_verification" ( - "id" varchar(36) PRIMARY KEY NOT NULL, - "identifier" varchar(255) NOT NULL, - "value" varchar(255) NOT NULL, - "expires_at" timestamp NOT NULL, - "created_at" timestamp DEFAULT now(), - "updated_at" timestamp DEFAULT now() -); ---> statement-breakpoint -ALTER TABLE "users" ADD COLUMN "auth_user_id" varchar(36);--> statement-breakpoint -ALTER TABLE "auth_account" ADD CONSTRAINT "auth_account_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "auth_audit_log" ADD CONSTRAINT "auth_audit_log_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "auth_session" ADD CONSTRAINT "auth_session_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_audit_log_user" ON "auth_audit_log" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "idx_audit_log_event" ON "auth_audit_log" USING btree ("event");--> statement-breakpoint -CREATE INDEX "idx_audit_log_created" ON "auth_audit_log" USING btree ("created_at");--> statement-breakpoint -ALTER TABLE "users" ADD CONSTRAINT "users_auth_user_id_auth_user_id_fk" FOREIGN KEY ("auth_user_id") REFERENCES "public"."auth_user"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json index e8ba905..edb90f7 100644 --- a/src/db/migrations/meta/0011_snapshot.json +++ b/src/db/migrations/meta/0011_snapshot.json @@ -57,12 +57,8 @@ "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -70,12 +66,8 @@ "name": "active_chapter_editors_user_id_users_id_fk", "tableFrom": "active_chapter_editors", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -83,10 +75,7 @@ "compositePrimaryKeys": { "active_chapter_editors_chapter_assignment_id_user_id_pk": { "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", - "columns": [ - "chapter_assignment_id", - "user_id" - ] + "columns": ["chapter_assignment_id", "user_id"] } }, "uniqueConstraints": {}, @@ -257,12 +246,8 @@ "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -270,12 +255,8 @@ "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", "tableFrom": "ai_suggestion_jobs", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -387,12 +368,8 @@ "name": "ai_suggestion_usage_log_user_id_users_id_fk", "tableFrom": "ai_suggestion_usage_log", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -400,12 +377,8 @@ "name": "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk", "tableFrom": "ai_suggestion_usage_log", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -413,12 +386,8 @@ "name": "ai_suggestion_usage_log_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestion_usage_log", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -514,12 +483,8 @@ "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", "tableFrom": "ai_suggestions", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -527,12 +492,8 @@ "name": "ai_suggestions_project_unit_id_project_units_id_fk", "tableFrom": "ai_suggestions", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -634,12 +595,8 @@ "name": "auth_account_user_id_auth_user_id_fk", "tableFrom": "auth_account", "tableTo": "auth_user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -751,12 +708,8 @@ "name": "auth_audit_log_user_id_auth_user_id_fk", "tableFrom": "auth_audit_log", "tableTo": "auth_user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -828,12 +781,8 @@ "name": "auth_session_user_id_auth_user_id_fk", "tableFrom": "auth_session", "tableTo": "auth_user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -843,9 +792,7 @@ "auth_session_token_unique": { "name": "auth_session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -916,9 +863,7 @@ "auth_user_email_unique": { "name": "auth_user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -1013,12 +958,8 @@ "name": "bible_books_bible_id_bibles_id_fk", "tableFrom": "bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1026,12 +967,8 @@ "name": "bible_books_book_id_books_id_fk", "tableFrom": "bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1164,12 +1101,8 @@ "name": "bible_texts_bible_id_bibles_id_fk", "tableFrom": "bible_texts", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1177,12 +1110,8 @@ "name": "bible_texts_book_id_books_id_fk", "tableFrom": "bible_texts", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1242,12 +1171,8 @@ "name": "bibles_language_id_languages_id_fk", "tableFrom": "bibles", "tableTo": "languages", - "columnsFrom": [ - "language_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["language_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1257,16 +1182,12 @@ "bibles_name_unique": { "name": "bibles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] }, "bibles_abbreviation_unique": { "name": "bibles_abbreviation_unique", "nullsNotDistinct": false, - "columns": [ - "abbreviation" - ] + "columns": ["abbreviation"] } }, "policies": {}, @@ -1385,12 +1306,8 @@ "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1398,12 +1315,8 @@ "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_assigned_user_history", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1494,12 +1407,8 @@ "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1507,12 +1416,8 @@ "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignment_snapshots", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1576,12 +1481,8 @@ "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "chapter_assignment_status_history", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1765,12 +1666,8 @@ "name": "chapter_assignments_project_unit_id_project_units_id_fk", "tableFrom": "chapter_assignments", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -1778,12 +1675,8 @@ "name": "chapter_assignments_bible_id_bibles_id_fk", "tableFrom": "chapter_assignments", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1791,12 +1684,8 @@ "name": "chapter_assignments_book_id_books_id_fk", "tableFrom": "chapter_assignments", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1804,12 +1693,8 @@ "name": "chapter_assignments_assigned_user_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -1817,12 +1702,8 @@ "name": "chapter_assignments_peer_checker_id_users_id_fk", "tableFrom": "chapter_assignments", "tableTo": "users", - "columnsFrom": [ - "peer_checker_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["peer_checker_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -1930,9 +1811,7 @@ "organizations_name_unique": { "name": "organizations_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -1983,9 +1862,7 @@ "permissions_name_unique": { "name": "permissions_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -2035,12 +1912,8 @@ "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2048,12 +1921,8 @@ "name": "project_unit_bible_books_bible_id_bibles_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "bibles", - "columnsFrom": [ - "bible_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2061,12 +1930,8 @@ "name": "project_unit_bible_books_book_id_books_id_fk", "tableFrom": "project_unit_bible_books", "tableTo": "books", - "columnsFrom": [ - "book_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["book_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2122,12 +1987,8 @@ "name": "project_units_project_id_projects_id_fk", "tableFrom": "project_units", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -2199,12 +2060,8 @@ "name": "project_users_project_id_projects_id_fk", "tableFrom": "project_users", "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2212,12 +2069,8 @@ "name": "project_users_user_id_users_id_fk", "tableFrom": "project_users", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" } @@ -2225,10 +2078,7 @@ "compositePrimaryKeys": { "project_users_project_id_user_id_pk": { "name": "project_users_project_id_user_id_pk", - "columns": [ - "project_id", - "user_id" - ] + "columns": ["project_id", "user_id"] } }, "uniqueConstraints": {}, @@ -2319,12 +2169,8 @@ "name": "projects_source_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "source_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2332,12 +2178,8 @@ "name": "projects_target_language_languages_id_fk", "tableFrom": "projects", "tableTo": "languages", - "columnsFrom": [ - "target_language" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_language"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2345,12 +2187,8 @@ "name": "projects_organization_organizations_id_fk", "tableFrom": "projects", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2358,12 +2196,8 @@ "name": "projects_created_by_users_id_fk", "tableFrom": "projects", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2420,12 +2254,8 @@ "name": "role_permissions_role_id_roles_id_fk", "tableFrom": "role_permissions", "tableTo": "roles", - "columnsFrom": [ - "role_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2433,12 +2263,8 @@ "name": "role_permissions_permission_id_permissions_id_fk", "tableFrom": "role_permissions", "tableTo": "permissions", - "columnsFrom": [ - "permission_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2446,10 +2272,7 @@ "compositePrimaryKeys": { "role_permissions_role_id_permission_id_pk": { "name": "role_permissions_role_id_permission_id_pk", - "columns": [ - "role_id", - "permission_id" - ] + "columns": ["role_id", "permission_id"] } }, "uniqueConstraints": {}, @@ -2495,9 +2318,7 @@ "roles_name_unique": { "name": "roles_name_unique", "nullsNotDistinct": false, - "columns": [ - "name" - ] + "columns": ["name"] } }, "policies": {}, @@ -2581,12 +2402,8 @@ "name": "translated_verses_project_unit_id_project_units_id_fk", "tableFrom": "translated_verses", "tableTo": "project_units", - "columnsFrom": [ - "project_unit_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "cascade" }, @@ -2594,12 +2411,8 @@ "name": "translated_verses_bible_text_id_bible_texts_id_fk", "tableFrom": "translated_verses", "tableTo": "bible_texts", - "columnsFrom": [ - "bible_text_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2607,12 +2420,8 @@ "name": "translated_verses_assigned_user_id_users_id_fk", "tableFrom": "translated_verses", "tableTo": "users", - "columnsFrom": [ - "assigned_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2688,12 +2497,8 @@ "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "users", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2701,12 +2506,8 @@ "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", "tableFrom": "user_chapter_assignment_editor_state", "tableTo": "chapter_assignments", - "columnsFrom": [ - "chapter_assignment_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2804,12 +2605,8 @@ "name": "users_auth_user_id_auth_user_id_fk", "tableFrom": "users", "tableTo": "auth_user", - "columnsFrom": [ - "auth_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["auth_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -2817,12 +2614,8 @@ "name": "users_role_roles_id_fk", "tableFrom": "users", "tableTo": "roles", - "columnsFrom": [ - "role" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["role"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2830,12 +2623,8 @@ "name": "users_organization_organizations_id_fk", "tableFrom": "users", "tableTo": "organizations", - "columnsFrom": [ - "organization" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -2843,12 +2632,8 @@ "name": "users_created_by_users_id_fk", "tableFrom": "users", "tableTo": "users", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -2858,16 +2643,12 @@ "users_username_unique": { "name": "users_username_unique", "nullsNotDistinct": false, - "columns": [ - "username" - ] + "columns": ["username"] }, "users_email_unique": { "name": "users_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -2879,20 +2660,12 @@ "public.ai_suggestion_job_status": { "name": "ai_suggestion_job_status", "schema": "public", - "values": [ - "queued", - "processing", - "completed", - "failed" - ] + "values": ["queued", "processing", "completed", "failed"] }, "public.assignment_role": { "name": "assignment_role", "schema": "public", - "values": [ - "drafter", - "peer_checker" - ] + "values": ["drafter", "peer_checker"] }, "public.chapter_status": { "name": "chapter_status", @@ -2911,36 +2684,22 @@ "public.project_assignment_status": { "name": "project_assignment_status", "schema": "public", - "values": [ - "active", - "not_assigned" - ] + "values": ["active", "not_assigned"] }, "public.project_status": { "name": "project_status", "schema": "public", - "values": [ - "not_started", - "in_progress", - "completed" - ] + "values": ["not_started", "in_progress", "completed"] }, "public.script_direction": { "name": "script_direction", "schema": "public", - "values": [ - "ltr", - "rtl" - ] + "values": ["ltr", "rtl"] }, "public.user_status": { "name": "user_status", "schema": "public", - "values": [ - "invited", - "verified", - "inactive" - ] + "values": ["invited", "verified", "inactive"] } }, "schemas": { @@ -2955,4 +2714,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/src/db/migrations/meta/0014_snapshot.json b/src/db/migrations/meta/0014_snapshot.json deleted file mode 100644 index e5a75ad..0000000 --- a/src/db/migrations/meta/0014_snapshot.json +++ /dev/null @@ -1,2717 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "tables": { - "public.active_chapter_editors": { - "name": "active_chapter_editors", - "schema": "", - "columns": { - "chapter_assignment_id": { - "name": "chapter_assignment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "started_at": { - "name": "started_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "last_heartbeat": { - "name": "last_heartbeat", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_active_editors_chapter": { - "name": "idx_active_editors_chapter", - "columns": [ - { - "expression": "chapter_assignment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk": { - "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", - "tableFrom": "active_chapter_editors", - "tableTo": "chapter_assignments", - "columnsFrom": ["chapter_assignment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "active_chapter_editors_user_id_users_id_fk": { - "name": "active_chapter_editors_user_id_users_id_fk", - "tableFrom": "active_chapter_editors", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "active_chapter_editors_chapter_assignment_id_user_id_pk": { - "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", - "columns": ["chapter_assignment_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "ai.ai_suggestion_jobs": { - "name": "ai_suggestion_jobs", - "schema": "ai", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "bible_id": { - "name": "bible_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "book_code": { - "name": "book_code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "chapter_number": { - "name": "chapter_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "verse_start": { - "name": "verse_start", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "verse_end": { - "name": "verse_end", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true, - "default": "'queued'" - }, - "retry_count": { - "name": "retry_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "error_message": { - "name": "error_message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ai_jobs_project_unit": { - "name": "idx_ai_jobs_project_unit", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_ai_jobs_status": { - "name": "idx_ai_jobs_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "uq_ai_jobs_range": { - "name": "uq_ai_jobs_range", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "bible_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "book_code", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_number", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_start", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_end", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_suggestion_jobs_project_unit_id_project_units_id_fk": { - "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestion_jobs", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_jobs_bible_id_bibles_id_fk": { - "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", - "tableFrom": "ai_suggestion_jobs", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "ai.ai_suggestion_usage_log": { - "name": "ai_suggestion_usage_log", - "schema": "ai", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "bible_text_id": { - "name": "bible_text_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "was_used": { - "name": "was_used", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_ai_usage_user": { - "name": "idx_ai_usage_user", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_ai_usage_project_unit": { - "name": "idx_ai_usage_project_unit", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "uq_ai_usage_user_text": { - "name": "uq_ai_usage_user_text", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_suggestion_usage_log_user_id_users_id_fk": { - "name": "ai_suggestion_usage_log_user_id_users_id_fk", - "tableFrom": "ai_suggestion_usage_log", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk": { - "name": "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk", - "tableFrom": "ai_suggestion_usage_log", - "tableTo": "bible_texts", - "columnsFrom": ["bible_text_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_usage_log_project_unit_id_project_units_id_fk": { - "name": "ai_suggestion_usage_log_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestion_usage_log", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "ai.ai_suggestions": { - "name": "ai_suggestions", - "schema": "ai", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "bible_text_id": { - "name": "bible_text_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "suggested_text": { - "name": "suggested_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "model_info": { - "name": "model_info", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ai_suggestions_bible_text": { - "name": "idx_ai_suggestions_bible_text", - "columns": [ - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "uq_ai_suggestions_per_text_unit": { - "name": "uq_ai_suggestions_per_text_unit", - "columns": [ - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_suggestions_bible_text_id_bible_texts_id_fk": { - "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", - "tableFrom": "ai_suggestions", - "tableTo": "bible_texts", - "columnsFrom": ["bible_text_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestions_project_unit_id_project_units_id_fk": { - "name": "ai_suggestions_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestions", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth_account": { - "name": "auth_account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar(36)", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "varchar(36)", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "auth_account_user_id_auth_user_id_fk": { - "name": "auth_account_user_id_auth_user_id_fk", - "tableFrom": "auth_account", - "tableTo": "auth_user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth_audit_log": { - "name": "auth_audit_log", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "varchar(36)", - "primaryKey": false, - "notNull": false - }, - "event": { - "name": "event", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_audit_log_user": { - "name": "idx_audit_log_user", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_audit_log_event": { - "name": "idx_audit_log_event", - "columns": [ - { - "expression": "event", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_audit_log_created": { - "name": "idx_audit_log_created", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "auth_audit_log_user_id_auth_user_id_fk": { - "name": "auth_audit_log_user_id_auth_user_id_fk", - "tableFrom": "auth_audit_log", - "tableTo": "auth_user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth_session": { - "name": "auth_session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar(36)", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "varchar(36)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "auth_session_user_id_auth_user_id_fk": { - "name": "auth_session_user_id_auth_user_id_fk", - "tableFrom": "auth_session", - "tableTo": "auth_user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "auth_session_token_unique": { - "name": "auth_session_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth_user": { - "name": "auth_user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar(36)", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "image": { - "name": "image", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "two_factor_enabled": { - "name": "two_factor_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "auth_user_email_unique": { - "name": "auth_user_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth_verification": { - "name": "auth_verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "varchar(36)", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.bible_books": { - "name": "bible_books", - "schema": "", - "columns": { - "bible_id": { - "name": "bible_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "book_id": { - "name": "book_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "bible_books_bible_id_bibles_id_fk": { - "name": "bible_books_bible_id_bibles_id_fk", - "tableFrom": "bible_books", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "bible_books_book_id_books_id_fk": { - "name": "bible_books_book_id_books_id_fk", - "tableFrom": "bible_books", - "tableTo": "books", - "columnsFrom": ["book_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.bible_texts": { - "name": "bible_texts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "bible_id": { - "name": "bible_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "book_id": { - "name": "book_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "chapter_number": { - "name": "chapter_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "verse_number": { - "name": "verse_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "text": { - "name": "text", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_bible_texts_bible_book_chapter": { - "name": "idx_bible_texts_bible_book_chapter", - "columns": [ - { - "expression": "bible_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "book_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_bible_texts_bible_book_chapter_verse": { - "name": "idx_bible_texts_bible_book_chapter_verse", - "columns": [ - { - "expression": "bible_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "book_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_number", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "bible_texts_bible_id_bibles_id_fk": { - "name": "bible_texts_bible_id_bibles_id_fk", - "tableFrom": "bible_texts", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "bible_texts_book_id_books_id_fk": { - "name": "bible_texts_book_id_books_id_fk", - "tableFrom": "bible_texts", - "tableTo": "books", - "columnsFrom": ["book_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.bibles": { - "name": "bibles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "language_id": { - "name": "language_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "abbreviation": { - "name": "abbreviation", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "bibles_language_id_languages_id_fk": { - "name": "bibles_language_id_languages_id_fk", - "tableFrom": "bibles", - "tableTo": "languages", - "columnsFrom": ["language_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "bibles_name_unique": { - "name": "bibles_name_unique", - "nullsNotDistinct": false, - "columns": ["name"] - }, - "bibles_abbreviation_unique": { - "name": "bibles_abbreviation_unique", - "nullsNotDistinct": false, - "columns": ["abbreviation"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.books": { - "name": "books", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "code": { - "name": "code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "eng_display_name": { - "name": "eng_display_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chapter_assignment_assigned_user_history": { - "name": "chapter_assignment_assigned_user_history", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "chapter_assignment_id": { - "name": "chapter_assignment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "assigned_user_id": { - "name": "assigned_user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "assignment_role", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "chapter_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ca_user_history_assignment": { - "name": "idx_ca_user_history_assignment", - "columns": [ - { - "expression": "chapter_assignment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_ca_user_history_user": { - "name": "idx_ca_user_history_user", - "columns": [ - { - "expression": "assigned_user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk": { - "name": "chapter_assignment_assigned_user_history_chapter_assignment_id_chapter_assignments_id_fk", - "tableFrom": "chapter_assignment_assigned_user_history", - "tableTo": "chapter_assignments", - "columnsFrom": ["chapter_assignment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk": { - "name": "chapter_assignment_assigned_user_history_assigned_user_id_users_id_fk", - "tableFrom": "chapter_assignment_assigned_user_history", - "tableTo": "users", - "columnsFrom": ["assigned_user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chapter_assignment_snapshots": { - "name": "chapter_assignment_snapshots", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "chapter_assignment_id": { - "name": "chapter_assignment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "chapter_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "assigned_user_id": { - "name": "assigned_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "json", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ca_snapshots_assignment": { - "name": "idx_ca_snapshots_assignment", - "columns": [ - { - "expression": "chapter_assignment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_ca_snapshots_user": { - "name": "idx_ca_snapshots_user", - "columns": [ - { - "expression": "assigned_user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk": { - "name": "chapter_assignment_snapshots_chapter_assignment_id_chapter_assignments_id_fk", - "tableFrom": "chapter_assignment_snapshots", - "tableTo": "chapter_assignments", - "columnsFrom": ["chapter_assignment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "chapter_assignment_snapshots_assigned_user_id_users_id_fk": { - "name": "chapter_assignment_snapshots_assigned_user_id_users_id_fk", - "tableFrom": "chapter_assignment_snapshots", - "tableTo": "users", - "columnsFrom": ["assigned_user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chapter_assignment_status_history": { - "name": "chapter_assignment_status_history", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "chapter_assignment_id": { - "name": "chapter_assignment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "chapter_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ca_status_history_assignment": { - "name": "idx_ca_status_history_assignment", - "columns": [ - { - "expression": "chapter_assignment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk": { - "name": "chapter_assignment_status_history_chapter_assignment_id_chapter_assignments_id_fk", - "tableFrom": "chapter_assignment_status_history", - "tableTo": "chapter_assignments", - "columnsFrom": ["chapter_assignment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chapter_assignments": { - "name": "chapter_assignments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "bible_id": { - "name": "bible_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "book_id": { - "name": "book_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "chapter_number": { - "name": "chapter_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "assigned_user_id": { - "name": "assigned_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "peer_checker_id": { - "name": "peer_checker_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "chapter_status": { - "name": "chapter_status", - "type": "chapter_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'not_started'" - }, - "submitted_time": { - "name": "submitted_time", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "uq_chapter_assignment_per_chapter": { - "name": "uq_chapter_assignment_per_chapter", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "bible_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "book_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_number", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_chapter_assignments_assigned_user": { - "name": "idx_chapter_assignments_assigned_user", - "columns": [ - { - "expression": "assigned_user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_chapter_assignments_peer_checker_status": { - "name": "idx_chapter_assignments_peer_checker_status", - "columns": [ - { - "expression": "peer_checker_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_chapter_assignments_project_unit": { - "name": "idx_chapter_assignments_project_unit", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "chapter_assignments_project_unit_id_project_units_id_fk": { - "name": "chapter_assignments_project_unit_id_project_units_id_fk", - "tableFrom": "chapter_assignments", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "chapter_assignments_bible_id_bibles_id_fk": { - "name": "chapter_assignments_bible_id_bibles_id_fk", - "tableFrom": "chapter_assignments", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "chapter_assignments_book_id_books_id_fk": { - "name": "chapter_assignments_book_id_books_id_fk", - "tableFrom": "chapter_assignments", - "tableTo": "books", - "columnsFrom": ["book_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "chapter_assignments_assigned_user_id_users_id_fk": { - "name": "chapter_assignments_assigned_user_id_users_id_fk", - "tableFrom": "chapter_assignments", - "tableTo": "users", - "columnsFrom": ["assigned_user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "chapter_assignments_peer_checker_id_users_id_fk": { - "name": "chapter_assignments_peer_checker_id_users_id_fk", - "tableFrom": "chapter_assignments", - "tableTo": "users", - "columnsFrom": ["peer_checker_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.languages": { - "name": "languages", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "lang_name": { - "name": "lang_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "lang_name_localized": { - "name": "lang_name_localized", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "lang_code_iso_639_3": { - "name": "lang_code_iso_639_3", - "type": "varchar(3)", - "primaryKey": false, - "notNull": false - }, - "script_direction": { - "name": "script_direction", - "type": "script_direction", - "typeSchema": "public", - "primaryKey": false, - "notNull": false, - "default": "'ltr'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.organizations": { - "name": "organizations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "organizations_name_unique": { - "name": "organizations_name_unique", - "nullsNotDistinct": false, - "columns": ["name"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.permissions": { - "name": "permissions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "varchar(255)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "permissions_name_unique": { - "name": "permissions_name_unique", - "nullsNotDistinct": false, - "columns": ["name"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.project_unit_bible_books": { - "name": "project_unit_bible_books", - "schema": "", - "columns": { - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "bible_id": { - "name": "bible_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "book_id": { - "name": "book_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "project_unit_bible_books_project_unit_id_project_units_id_fk": { - "name": "project_unit_bible_books_project_unit_id_project_units_id_fk", - "tableFrom": "project_unit_bible_books", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "project_unit_bible_books_bible_id_bibles_id_fk": { - "name": "project_unit_bible_books_bible_id_bibles_id_fk", - "tableFrom": "project_unit_bible_books", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "project_unit_bible_books_book_id_books_id_fk": { - "name": "project_unit_bible_books_book_id_books_id_fk", - "tableFrom": "project_unit_bible_books", - "tableTo": "books", - "columnsFrom": ["book_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.project_units": { - "name": "project_units", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "project_id": { - "name": "project_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "project_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'not_started'" - }, - "is_ai_enabled": { - "name": "is_ai_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "project_units_project_id_projects_id_fk": { - "name": "project_units_project_id_projects_id_fk", - "tableFrom": "project_units", - "tableTo": "projects", - "columnsFrom": ["project_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.project_users": { - "name": "project_users", - "schema": "", - "columns": { - "project_id": { - "name": "project_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_project_users_project": { - "name": "idx_project_users_project", - "columns": [ - { - "expression": "project_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_project_users_user": { - "name": "idx_project_users_user", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "project_users_project_id_projects_id_fk": { - "name": "project_users_project_id_projects_id_fk", - "tableFrom": "project_users", - "tableTo": "projects", - "columnsFrom": ["project_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "project_users_user_id_users_id_fk": { - "name": "project_users_user_id_users_id_fk", - "tableFrom": "project_users", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": { - "project_users_project_id_user_id_pk": { - "name": "project_users_project_id_user_id_pk", - "columns": ["project_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.projects": { - "name": "projects", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "source_language": { - "name": "source_language", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "target_language": { - "name": "target_language", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "organization": { - "name": "organization", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": true - }, - "status": { - "name": "status", - "type": "project_assignment_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'not_assigned'" - }, - "created_by": { - "name": "created_by", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - } - }, - "indexes": {}, - "foreignKeys": { - "projects_source_language_languages_id_fk": { - "name": "projects_source_language_languages_id_fk", - "tableFrom": "projects", - "tableTo": "languages", - "columnsFrom": ["source_language"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "projects_target_language_languages_id_fk": { - "name": "projects_target_language_languages_id_fk", - "tableFrom": "projects", - "tableTo": "languages", - "columnsFrom": ["target_language"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "projects_organization_organizations_id_fk": { - "name": "projects_organization_organizations_id_fk", - "tableFrom": "projects", - "tableTo": "organizations", - "columnsFrom": ["organization"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "projects_created_by_users_id_fk": { - "name": "projects_created_by_users_id_fk", - "tableFrom": "projects", - "tableTo": "users", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.role_permissions": { - "name": "role_permissions", - "schema": "", - "columns": { - "role_id": { - "name": "role_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "permission_id": { - "name": "permission_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_role_permissions_role": { - "name": "idx_role_permissions_role", - "columns": [ - { - "expression": "role_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "role_permissions_role_id_roles_id_fk": { - "name": "role_permissions_role_id_roles_id_fk", - "tableFrom": "role_permissions", - "tableTo": "roles", - "columnsFrom": ["role_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "role_permissions_permission_id_permissions_id_fk": { - "name": "role_permissions_permission_id_permissions_id_fk", - "tableFrom": "role_permissions", - "tableTo": "permissions", - "columnsFrom": ["permission_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "role_permissions_role_id_permission_id_pk": { - "name": "role_permissions_role_id_permission_id_pk", - "columns": ["role_id", "permission_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.roles": { - "name": "roles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "roles_name_unique": { - "name": "roles_name_unique", - "nullsNotDistinct": false, - "columns": ["name"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.translated_verses": { - "name": "translated_verses", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "varchar", - "primaryKey": false, - "notNull": true - }, - "bible_text_id": { - "name": "bible_text_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "assigned_user_id": { - "name": "assigned_user_id", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "uq_translated_verse_per_bible_text": { - "name": "uq_translated_verse_per_bible_text", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "translated_verses_project_unit_id_project_units_id_fk": { - "name": "translated_verses_project_unit_id_project_units_id_fk", - "tableFrom": "translated_verses", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "translated_verses_bible_text_id_bible_texts_id_fk": { - "name": "translated_verses_bible_text_id_bible_texts_id_fk", - "tableFrom": "translated_verses", - "tableTo": "bible_texts", - "columnsFrom": ["bible_text_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "translated_verses_assigned_user_id_users_id_fk": { - "name": "translated_verses_assigned_user_id_users_id_fk", - "tableFrom": "translated_verses", - "tableTo": "users", - "columnsFrom": ["assigned_user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_chapter_assignment_editor_state": { - "name": "user_chapter_assignment_editor_state", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "chapter_assignment_id": { - "name": "chapter_assignment_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "resources": { - "name": "resources", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "uq_user_chapter_assignment_editor_state": { - "name": "uq_user_chapter_assignment_editor_state", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_assignment_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "user_chapter_assignment_editor_state_user_id_users_id_fk": { - "name": "user_chapter_assignment_editor_state_user_id_users_id_fk", - "tableFrom": "user_chapter_assignment_editor_state", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk": { - "name": "user_chapter_assignment_editor_state_chapter_assignment_id_chapter_assignments_id_fk", - "tableFrom": "user_chapter_assignment_editor_state", - "tableTo": "chapter_assignments", - "columnsFrom": ["chapter_assignment_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "auth_user_id": { - "name": "auth_user_id", - "type": "varchar(36)", - "primaryKey": false, - "notNull": false - }, - "username": { - "name": "username", - "type": "varchar(100)", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "last_name": { - "name": "last_name", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "organization": { - "name": "organization", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "user_status", - "typeSchema": "public", - "primaryKey": false, - "notNull": true, - "default": "'invited'" - }, - "created_by": { - "name": "created_by", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "users_auth_user_id_auth_user_id_fk": { - "name": "users_auth_user_id_auth_user_id_fk", - "tableFrom": "users", - "tableTo": "auth_user", - "columnsFrom": ["auth_user_id"], - "columnsTo": ["id"], - "onDelete": "set null", - "onUpdate": "no action" - }, - "users_role_roles_id_fk": { - "name": "users_role_roles_id_fk", - "tableFrom": "users", - "tableTo": "roles", - "columnsFrom": ["role"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "users_organization_organizations_id_fk": { - "name": "users_organization_organizations_id_fk", - "tableFrom": "users", - "tableTo": "organizations", - "columnsFrom": ["organization"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "users_created_by_users_id_fk": { - "name": "users_created_by_users_id_fk", - "tableFrom": "users", - "tableTo": "users", - "columnsFrom": ["created_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": ["username"] - }, - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": { - "public.ai_suggestion_job_status": { - "name": "ai_suggestion_job_status", - "schema": "public", - "values": ["queued", "processing", "completed", "failed"] - }, - "public.assignment_role": { - "name": "assignment_role", - "schema": "public", - "values": ["drafter", "peer_checker"] - }, - "public.chapter_status": { - "name": "chapter_status", - "schema": "public", - "values": [ - "not_started", - "draft", - "peer_check", - "community_review", - "linguist_check", - "theological_check", - "consultant_check", - "complete" - ] - }, - "public.project_assignment_status": { - "name": "project_assignment_status", - "schema": "public", - "values": ["active", "not_assigned"] - }, - "public.project_status": { - "name": "project_status", - "schema": "public", - "values": ["not_started", "in_progress", "completed"] - }, - "public.script_direction": { - "name": "script_direction", - "schema": "public", - "values": ["ltr", "rtl"] - }, - "public.user_status": { - "name": "user_status", - "schema": "public", - "values": ["invited", "verified", "inactive"] - } - }, - "schemas": { - "ai": "ai" - }, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "id": "ea6e35fd-aa98-4f44-84d8-b4f529f993d9", - "prevId": "b5e72067-119a-4734-97f0-fe7382fbdb9d" -} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index f5c73ee..ae539cb 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -87,4 +87,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 9bfd2fb..45cabd1 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -1,4 +1,4 @@ -import { and, asc, count, eq, gt, inArray, isNull } from 'drizzle-orm'; +import { and, asc, eq, gt, inArray, isNull } from 'drizzle-orm'; import type { Result, User } from '@/lib/types'; @@ -57,29 +57,6 @@ async function userCanAccessProjectUnit(user: User, projectUnitId: number): Prom return false; } -async function chapterBelongsToProjectUnit(input: { - projectUnitId: number; - bibleId: number; - bookCode: string; - chapterNumber: number; -}): Promise { - const [record] = await db - .select({ id: chapter_assignments.id }) - .from(chapter_assignments) - .innerJoin(books, eq(chapter_assignments.bookId, books.id)) - .where( - and( - eq(chapter_assignments.projectUnitId, input.projectUnitId), - eq(chapter_assignments.bibleId, input.bibleId), - eq(books.code, input.bookCode), - eq(chapter_assignments.chapterNumber, input.chapterNumber) - ) - ) - .limit(1); - - return Boolean(record); -} - export async function trackUsage(user: User, data: TrackUsageRequest): Promise> { if (!(await userCanAccessProjectUnit(user, data.projectUnitId))) { return err(ErrorCode.FORBIDDEN); @@ -151,17 +128,28 @@ export async function queueNextVerses( const normalizedBookCode = bookCode.toUpperCase(); - if ( - !(await chapterBelongsToProjectUnit({ - projectUnitId, - bibleId, - bookCode: normalizedBookCode, - chapterNumber, - })) - ) { + const assignment = await db + .select({ isAiEnabled: chapter_assignments.isAiEnabled }) + .from(chapter_assignments) + .innerJoin(books, eq(chapter_assignments.bookId, books.id)) + .where( + and( + eq(chapter_assignments.projectUnitId, projectUnitId), + eq(chapter_assignments.bibleId, bibleId), + eq(books.code, normalizedBookCode), + eq(chapter_assignments.chapterNumber, chapterNumber) + ) + ) + .limit(1); + + if (!assignment[0]) { return err(ErrorCode.INVALID_REFERENCE); } + if (!assignment[0].isAiEnabled) { + return ok(undefined); + } + return await queueNextVersesForAssignment( projectUnitId, bibleId, @@ -228,13 +216,20 @@ export async function handleChapterAssigned( chapterNumber: number ) { try { - const unit = await db - .select({ isAiEnabled: project_units.isAiEnabled }) - .from(project_units) - .where(eq(project_units.id, projectUnitId)) + const assignment = await db + .select({ isAiEnabled: chapter_assignments.isAiEnabled }) + .from(chapter_assignments) + .where( + and( + eq(chapter_assignments.projectUnitId, projectUnitId), + eq(chapter_assignments.bibleId, bibleId), + eq(chapter_assignments.bookId, bookId), + eq(chapter_assignments.chapterNumber, chapterNumber) + ) + ) .limit(1); - if (!unit[0]?.isAiEnabled) return; + if (!assignment[0]?.isAiEnabled) return; const book = await db .select({ code: books.code }) @@ -260,82 +255,3 @@ export async function handleChapterAssigned( }); } } - -export async function handleVerseSaved(projectUnitId: number) { - try { - // 1. Check if AI is already enabled - const unit = await db - .select({ isAiEnabled: project_units.isAiEnabled }) - .from(project_units) - .where(eq(project_units.id, projectUnitId)) - .limit(1); - - if (!unit[0] || unit[0].isAiEnabled) return; - - // 2. Count translated verses for this unit - const result = await db - .select({ value: count() }) - .from(translated_verses) - .where(eq(translated_verses.projectUnitId, projectUnitId)); - - const verseCount = result[0]?.value ?? 0; - - if (verseCount >= AI_SUGGESTIONS_CONSTANTS.ACTIVATION_THRESHOLD_VERSES) { - // 3. Mark AI as enabled - await db - .update(project_units) - .set({ isAiEnabled: true }) - .where(eq(project_units.id, projectUnitId)); - - // 4. Batch queue verses for all assigned chapters concurrently - const assignments = await db - .select({ - bibleId: chapter_assignments.bibleId, - bookId: chapter_assignments.bookId, - chapterNumber: chapter_assignments.chapterNumber, - bookCode: books.code, - }) - .from(chapter_assignments) - .innerJoin(books, eq(chapter_assignments.bookId, books.id)) - .where(eq(chapter_assignments.projectUnitId, projectUnitId)); - - const queueResults = await Promise.all( - assignments.map( - (assignment: { - bibleId: number; - bookId: number; - chapterNumber: number; - bookCode: string; - }) => - queueNextVersesForAssignment( - projectUnitId, - assignment.bibleId, - assignment.bookCode.toLowerCase(), - assignment.chapterNumber, - 0, - AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT - ) - ) - ); - - // 5. If any queueing failed, revert the flag so we can try again later - if (queueResults.some((r: Result) => !r.ok)) { - await db - .update(project_units) - .set({ isAiEnabled: false }) - .where(eq(project_units.id, projectUnitId)); - throw new Error( - 'Failed to batch queue initial AI suggestions. Rolled back threshold state.' - ); - } - - logger.info({ projectUnitId }, 'AI Suggestions threshold reached and enabled.'); - } - } catch (error) { - logger.error({ - cause: error, - message: 'Threshold check failed', - context: { projectUnitId }, - }); - } -} diff --git a/src/domains/chapter-assignments/chapter-assignments.policy.ts b/src/domains/chapter-assignments/chapter-assignments.policy.ts index 9ce015e..b2a69a8 100644 --- a/src/domains/chapter-assignments/chapter-assignments.policy.ts +++ b/src/domains/chapter-assignments/chapter-assignments.policy.ts @@ -184,6 +184,28 @@ export const ChapterAssignmentPolicy = { } }, + /** + * Can this user toggle AI for this assignment? + */ + toggleAi(user: AppPolicyUser, assignment: PolicyChapterAssignment): boolean { + if (user.organization !== assignment.organizationId) { + return false; + } + + if (user.roleName === ROLES.PROJECT_MANAGER) { + return true; + } + + if (user.roleName === ROLES.TRANSLATOR) { + // The assigned user can toggle AI only when they are the primary drafter + if (assignment.status === CHAPTER_ASSIGNMENT_STATUS.DRAFT) { + return assignment.assignedUserId === user.id; + } + } + + return false; + }, + /** * Is this user a direct participant in this assignment? */ diff --git a/src/domains/chapter-assignments/chapter-assignments.repository.ts b/src/domains/chapter-assignments/chapter-assignments.repository.ts index ae4eac4..9478894 100644 --- a/src/domains/chapter-assignments/chapter-assignments.repository.ts +++ b/src/domains/chapter-assignments/chapter-assignments.repository.ts @@ -69,6 +69,7 @@ export async function findByIdWithOrg(id: number): Promise { if (result.ok) return c.body(null, HttpStatusCodes.NO_CONTENT); return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); }); + +// ─── PATCH /projects/:id/units/:unitId/assignments/:chapterAssignmentId/ai-status ────── + +const updateAiStatusRoute = createRoute({ + tags: ['Chapter Assignments'], + method: 'patch', + path: '/projects/{id}/units/{unitId}/assignments/{chapterAssignmentId}/ai-status', + middleware: [ + authenticateUser, + requirePermission(PERMISSIONS.CONTENT_UPDATE), + requireChapterAssignmentAccess(CHAPTER_ASSIGNMENT_ACTIONS.TOGGLE_AI), + ] as const, + summary: 'Toggle AI Suggestion status for Chapter Assignment', + description: + 'Project Manager only. Enables or disables AI translation suggestions for a specific chapter assignment.', + request: { + params: z.object({ + id: z.coerce.number().openapi({ param: { name: 'id', in: 'path', required: true } }), + unitId: z.coerce.number().openapi({ param: { name: 'unitId', in: 'path', required: true } }), + chapterAssignmentId: z.coerce + .number() + .openapi({ param: { name: 'chapterAssignmentId', in: 'path', required: true } }), + }), + body: jsonContentRequired(updateChapterAssignmentAiStatusSchema, 'AI Status update'), + }, + responses: { + [HttpStatusCodes.OK]: jsonContent(createMessageObjectSchema('Updated successfully'), 'Success'), + [HttpStatusCodes.UNAUTHORIZED]: jsonContent( + createMessageObjectSchema('Unauthorized'), + 'Authentication required' + ), + [HttpStatusCodes.FORBIDDEN]: jsonContent( + createMessageObjectSchema('Forbidden'), + 'Insufficient permissions' + ), + [HttpStatusCodes.NOT_FOUND]: jsonContent( + createMessageObjectSchema('Not Found'), + 'Chapter assignment not found' + ), + [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonContent( + createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR), + 'Internal server error' + ), + }, +}); + +server.openapi(updateAiStatusRoute, async (c) => { + const { chapterAssignmentId } = c.req.valid('param'); + const { isAiEnabled } = c.req.valid('json'); + + const result = await chapterAssignmentService.toggleChapterAssignmentAiStatus( + chapterAssignmentId, + isAiEnabled + ); + if (result.ok) return c.json({ message: 'Updated successfully' }, HttpStatusCodes.OK); + return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); +}); diff --git a/src/domains/chapter-assignments/chapter-assignments.service.ts b/src/domains/chapter-assignments/chapter-assignments.service.ts index e7b9161..8cdccfc 100644 --- a/src/domains/chapter-assignments/chapter-assignments.service.ts +++ b/src/domains/chapter-assignments/chapter-assignments.service.ts @@ -1,7 +1,10 @@ +import { eq } from 'drizzle-orm'; + import type { DbTransaction, Result } from '@/lib/types'; import { db } from '@/db'; -import { handleChapterAssigned } from '@/domains/ai-suggestions/ai-suggestions.service'; +import { chapter_assignments } from '@/db/schema'; +import * as aiSuggestionsService from '@/domains/ai-suggestions/ai-suggestions.service'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; @@ -81,7 +84,7 @@ export async function createChapterAssignment(data: CreateChapterAssignmentReque CHAPTER_ASSIGNMENT_STATUS.NOT_STARTED ); // Fire and forget auto-queueing for drafting assignment - handleChapterAssigned( + aiSuggestionsService.handleChapterAssigned( assignment.projectUnitId, assignment.bibleId, assignment.bookId, @@ -288,7 +291,7 @@ async function recordUserAssignmentChanges( updated.status as ChapterAssignmentStatus ); // Fire and forget auto-queueing for drafting assignment - handleChapterAssigned( + aiSuggestionsService.handleChapterAssigned( updated.projectUnitId, updated.bibleId, updated.bookId, @@ -310,3 +313,54 @@ async function recordUserAssignmentChanges( ); } } + +export async function toggleChapterAssignmentAiStatus( + assignmentId: number, + isAiEnabled: boolean +): Promise> { + try { + const assignment = await db + .select({ + isAiEnabled: chapter_assignments.isAiEnabled, + projectUnitId: chapter_assignments.projectUnitId, + bibleId: chapter_assignments.bibleId, + bookId: chapter_assignments.bookId, + chapterNumber: chapter_assignments.chapterNumber, + }) + .from(chapter_assignments) + .where(eq(chapter_assignments.id, assignmentId)) + .limit(1); + + if (!assignment[0]) { + return err(ErrorCode.NOT_FOUND); + } + + if (assignment[0].isAiEnabled === isAiEnabled) { + return ok(undefined); + } + + await db + .update(chapter_assignments) + .set({ isAiEnabled }) + .where(eq(chapter_assignments.id, assignmentId)); + + if (isAiEnabled) { + // Fire and forget initial queue + aiSuggestionsService.handleChapterAssigned( + assignment[0].projectUnitId, + assignment[0].bibleId, + assignment[0].bookId, + assignment[0].chapterNumber + ); + } + + return ok(undefined); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to toggle AI status for chapter assignment', + context: { assignmentId, isAiEnabled }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/chapter-assignments/chapter-assignments.types.ts b/src/domains/chapter-assignments/chapter-assignments.types.ts index 767b785..434fc9f 100644 --- a/src/domains/chapter-assignments/chapter-assignments.types.ts +++ b/src/domains/chapter-assignments/chapter-assignments.types.ts @@ -10,6 +10,7 @@ export const CHAPTER_ASSIGNMENT_ACTIONS = { SUBMIT: 'submit', DELETE: 'delete', IS_PARTICIPANT: 'isParticipant', + TOGGLE_AI: 'toggleAi', } as const; export type ChapterAssignmentAction = @@ -80,6 +81,10 @@ export interface UpdateChapterAssignmentRequestData { submittedTime?: Date; } +export const updateChapterAssignmentAiStatusSchema = z.object({ + isAiEnabled: z.boolean(), +}); + // ─── API response schema ────────────────────────────────────────────────────── export const chapterAssignmentResponseSchema = z.object({ diff --git a/src/domains/projects/chapter-assignments/project-chapter-assignments.repository.ts b/src/domains/projects/chapter-assignments/project-chapter-assignments.repository.ts index 56e9ef5..ae27a65 100644 --- a/src/domains/projects/chapter-assignments/project-chapter-assignments.repository.ts +++ b/src/domains/projects/chapter-assignments/project-chapter-assignments.repository.ts @@ -21,6 +21,7 @@ export async function getByProject(projectId: number): Promise - logger.error({ cause: err, message: 'Threshold check failed' }) - ); - return ok(toTranslatedVerseResponse(result.data)); } @@ -53,12 +45,6 @@ export async function updateTranslatedVerse(id: number, input: UpdateTranslatedV export async function upsertTranslatedVerse(input: CreateTranslatedVerseInput) { const result = await translatedVersesRepo.upsert(input); if (!result.ok) return result; - - // Fire and forget threshold check - handleVerseSaved(input.projectUnitId).catch((err) => - logger.error({ cause: err, message: 'Threshold check failed' }) - ); - return ok(toTranslatedVerseResponse(result.data)); } From be2b8ff3108fbeedae14dc5d1b25fe1ace9cc007 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Wed, 27 May 2026 18:16:47 +0530 Subject: [PATCH 10/14] changed initial queueing setup --- .../ai-suggestions/ai-suggestions.constants.ts | 2 +- .../ai-suggestions/ai-suggestions.service.ts | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/domains/ai-suggestions/ai-suggestions.constants.ts b/src/domains/ai-suggestions/ai-suggestions.constants.ts index 0d72c22..b36652f 100644 --- a/src/domains/ai-suggestions/ai-suggestions.constants.ts +++ b/src/domains/ai-suggestions/ai-suggestions.constants.ts @@ -6,7 +6,7 @@ export const AI_SUGGESTIONS_CONSTANTS = { INITIAL_QUEUE_COUNT: 3, // Default number of verses to pre-fetch ahead of the drafter's current verse - DEFAULT_LOOKAHEAD: 5, + DEFAULT_LOOKAHEAD: 1, // Guardrail for GET /ai-suggestions query fan-out MAX_REQUESTED_BIBLE_TEXT_IDS: 200, diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 45cabd1..c166667 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -216,21 +216,6 @@ export async function handleChapterAssigned( chapterNumber: number ) { try { - const assignment = await db - .select({ isAiEnabled: chapter_assignments.isAiEnabled }) - .from(chapter_assignments) - .where( - and( - eq(chapter_assignments.projectUnitId, projectUnitId), - eq(chapter_assignments.bibleId, bibleId), - eq(chapter_assignments.bookId, bookId), - eq(chapter_assignments.chapterNumber, chapterNumber) - ) - ) - .limit(1); - - if (!assignment[0]?.isAiEnabled) return; - const book = await db .select({ code: books.code }) .from(books) From 23153ac7f801f9e59a0a2739f3f03dc5ed3dc8a8 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Wed, 27 May 2026 21:58:28 +0530 Subject: [PATCH 11/14] remove ai schema from drizzle migration, updated schema --- .../migrations/0011_added_ai_usggestions.sql | 38 +-- src/db/migrations/meta/0011_snapshot.json | 296 +----------------- src/db/schema.ts | 11 +- 3 files changed, 11 insertions(+), 334 deletions(-) diff --git a/src/db/migrations/0011_added_ai_usggestions.sql b/src/db/migrations/0011_added_ai_usggestions.sql index c043a5a..797eee9 100644 --- a/src/db/migrations/0011_added_ai_usggestions.sql +++ b/src/db/migrations/0011_added_ai_usggestions.sql @@ -1,21 +1,3 @@ -CREATE SCHEMA "ai"; ---> statement-breakpoint -CREATE TYPE "public"."ai_suggestion_job_status" AS ENUM('queued', 'processing', 'completed', 'failed');--> statement-breakpoint -CREATE TABLE "ai"."ai_suggestion_jobs" ( - "id" serial PRIMARY KEY NOT NULL, - "project_unit_id" integer NOT NULL, - "bible_id" integer NOT NULL, - "book_code" varchar(50) NOT NULL, - "chapter_number" integer NOT NULL, - "verse_start" integer NOT NULL, - "verse_end" integer NOT NULL, - "status" varchar(20) DEFAULT 'queued' NOT NULL, - "retry_count" integer DEFAULT 0 NOT NULL, - "error_message" text, - "created_at" timestamp DEFAULT now(), - "updated_at" timestamp DEFAULT now() -); ---> statement-breakpoint CREATE TABLE "ai"."ai_suggestion_usage_log" ( "id" serial PRIMARY KEY NOT NULL, "user_id" integer NOT NULL, @@ -25,28 +7,12 @@ CREATE TABLE "ai"."ai_suggestion_usage_log" ( "created_at" timestamp DEFAULT now() NOT NULL ); --> statement-breakpoint -CREATE TABLE "ai"."ai_suggestions" ( - "id" serial PRIMARY KEY NOT NULL, - "bible_text_id" integer NOT NULL, - "project_unit_id" integer NOT NULL, - "suggested_text" text NOT NULL, - "model_info" varchar(100), - "created_at" timestamp DEFAULT now() -); ---> statement-breakpoint ALTER TABLE "chapter_assignments" ADD COLUMN "is_ai_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_jobs" ADD CONSTRAINT "ai_suggestion_jobs_bible_id_bibles_id_fk" FOREIGN KEY ("bible_id") REFERENCES "public"."bibles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestions" ADD CONSTRAINT "ai_suggestions_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_ai_jobs_project_unit" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id");--> statement-breakpoint -CREATE INDEX "idx_ai_jobs_status" ON "ai"."ai_suggestion_jobs" USING btree ("status");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_jobs_range" ON "ai"."ai_suggestion_jobs" USING btree ("project_unit_id","bible_id","book_code","chapter_number","verse_start","verse_end");--> statement-breakpoint CREATE INDEX "idx_ai_usage_user" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id");--> statement-breakpoint CREATE INDEX "idx_ai_usage_project_unit" ON "ai"."ai_suggestion_usage_log" USING btree ("project_unit_id");--> statement-breakpoint CREATE UNIQUE INDEX "uq_ai_usage_user_text" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id","bible_text_id");--> statement-breakpoint -CREATE INDEX "idx_ai_suggestions_bible_text" ON "ai"."ai_suggestions" USING btree ("bible_text_id");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_suggestions_per_text_unit" ON "ai"."ai_suggestions" USING btree ("bible_text_id","project_unit_id"); \ No newline at end of file +GRANT SELECT, INSERT, UPDATE, DELETE ON "ai"."ai_suggestion_usage_log" TO role_web_data;--> statement-breakpoint +GRANT USAGE, SELECT ON SEQUENCE "ai"."ai_suggestion_usage_log_id_seq" TO role_web_data; \ No newline at end of file diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json index edb90f7..10e9f8d 100644 --- a/src/db/migrations/meta/0011_snapshot.json +++ b/src/db/migrations/meta/0011_snapshot.json @@ -83,190 +83,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "ai.ai_suggestion_jobs": { - "name": "ai_suggestion_jobs", - "schema": "ai", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "bible_id": { - "name": "bible_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "book_code": { - "name": "book_code", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true - }, - "chapter_number": { - "name": "chapter_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "verse_start": { - "name": "verse_start", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "verse_end": { - "name": "verse_end", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "varchar(20)", - "primaryKey": false, - "notNull": true, - "default": "'queued'" - }, - "retry_count": { - "name": "retry_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "error_message": { - "name": "error_message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ai_jobs_project_unit": { - "name": "idx_ai_jobs_project_unit", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_ai_jobs_status": { - "name": "idx_ai_jobs_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "uq_ai_jobs_range": { - "name": "uq_ai_jobs_range", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "bible_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "book_code", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "chapter_number", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_start", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "verse_end", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_suggestion_jobs_project_unit_id_project_units_id_fk": { - "name": "ai_suggestion_jobs_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestion_jobs", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_jobs_bible_id_bibles_id_fk": { - "name": "ai_suggestion_jobs_bible_id_bibles_id_fk", - "tableFrom": "ai_suggestion_jobs", - "tableTo": "bibles", - "columnsFrom": ["bible_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "ai.ai_suggestion_usage_log": { "name": "ai_suggestion_usage_log", "schema": "ai", @@ -398,112 +214,7 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "ai.ai_suggestions": { - "name": "ai_suggestions", - "schema": "ai", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "bible_text_id": { - "name": "bible_text_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "suggested_text": { - "name": "suggested_text", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "model_info": { - "name": "model_info", - "type": "varchar(100)", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_ai_suggestions_bible_text": { - "name": "idx_ai_suggestions_bible_text", - "columns": [ - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "uq_ai_suggestions_per_text_unit": { - "name": "uq_ai_suggestions_per_text_unit", - "columns": [ - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_suggestions_bible_text_id_bible_texts_id_fk": { - "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", - "tableFrom": "ai_suggestions", - "tableTo": "bible_texts", - "columnsFrom": ["bible_text_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestions_project_unit_id_project_units_id_fk": { - "name": "ai_suggestions_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestions", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, + "public.auth_account": { "name": "auth_account", "schema": "", @@ -2657,11 +2368,6 @@ } }, "enums": { - "public.ai_suggestion_job_status": { - "name": "ai_suggestion_job_status", - "schema": "public", - "values": ["queued", "processing", "completed", "failed"] - }, "public.assignment_role": { "name": "assignment_role", "schema": "public", diff --git a/src/db/schema.ts b/src/db/schema.ts index 2a497f8..0b84aae 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -877,8 +877,13 @@ export const ai_suggestion_jobs = aiSchema.table( .$onUpdate(() => new Date()), }, (table) => [ - index('idx_ai_jobs_project_unit').on(table.projectUnitId), - index('idx_ai_jobs_status').on(table.status), + index('idx_ai_suggestion_jobs_dedup').on( + table.projectUnitId, + table.bibleId, + table.bookCode, + table.chapterNumber + ), + index('idx_ai_suggestion_jobs_status_created').on(table.status, table.createdAt), uniqueIndex('uq_ai_jobs_range').on( table.projectUnitId, table.bibleId, @@ -905,7 +910,7 @@ export const ai_suggestions = aiSchema.table( createdAt: timestamp('created_at').defaultNow(), }, (table) => [ - index('idx_ai_suggestions_bible_text').on(table.bibleTextId), + index('idx_ai_suggestions_lookup').on(table.projectUnitId, table.bibleTextId), uniqueIndex('uq_ai_suggestions_per_text_unit').on(table.bibleTextId, table.projectUnitId), ] ); From e92bb27960375ea41f334036c0da883c9b280db4 Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Thu, 28 May 2026 01:28:32 +0530 Subject: [PATCH 12/14] standardize domain architecture, extract db queries, and isolate external schemas --- docs/cross-schema-types.md | 56 ++++++ src/db/external/ai-schema.ts | 164 +++++++++++++++++ ...d_is_ai_enabled_to_chapter_assignments.sql | 1 + .../migrations/0011_added_ai_usggestions.sql | 18 -- src/db/migrations/meta/0011_snapshot.json | 138 +------------- src/db/migrations/meta/_journal.json | 4 +- src/db/schema.ts | 144 --------------- .../ai-suggestions.auth.middleware.ts | 42 +++++ .../ai-suggestions.constants.ts | 13 -- .../ai-suggestions/ai-suggestions.policy.ts | 31 ++++ .../ai-suggestions.repository.ts | 121 ++++++++++++- .../ai-suggestions/ai-suggestions.route.ts | 25 ++- .../ai-suggestions/ai-suggestions.service.ts | 170 +++++------------- .../ai-suggestions/ai-suggestions.types.ts | 12 +- src/env.ts | 6 + src/scripts/clean-ai-jobs.ts | 2 +- 16 files changed, 495 insertions(+), 452 deletions(-) create mode 100644 docs/cross-schema-types.md create mode 100644 src/db/external/ai-schema.ts create mode 100644 src/db/migrations/0011_add_is_ai_enabled_to_chapter_assignments.sql delete mode 100644 src/db/migrations/0011_added_ai_usggestions.sql create mode 100644 src/domains/ai-suggestions/ai-suggestions.auth.middleware.ts delete mode 100644 src/domains/ai-suggestions/ai-suggestions.constants.ts create mode 100644 src/domains/ai-suggestions/ai-suggestions.policy.ts diff --git a/docs/cross-schema-types.md b/docs/cross-schema-types.md new file mode 100644 index 0000000..739c424 --- /dev/null +++ b/docs/cross-schema-types.md @@ -0,0 +1,56 @@ +# Cross-Schema Type Safety + +## Problem + +`fluent-api` (TypeScript / Drizzle) and `fluent-ai` (Python / SQLAlchemy + Alembic) share a single PostgreSQL database. Each service owns a distinct schema: + +- `fluent-api` owns `public`, `pgboss`, `drizzle` +- `fluent-ai` owns `ai` + +When `fluent-api` needs to read or write tables in the `ai` schema (e.g. `ai_suggestion_jobs`, `ai_suggestions`), Drizzle's query builder requires table definitions in the codebase. Previously these definitions were inlined in `src/db/schema.ts`, which created three problems: + +1. **Schema ownership was unclear** — AI tables sat alongside public tables in the same file. +2. **Drizzle Kit generated migrations for AI tables** — even though they are managed by Alembic in `fluent-ai`. +3. **Schema drift risk** — changes in `fluent-ai` could silently diverge from the Drizzle stubs in `fluent-api`. + +## Decision + +Extract externally-owned schema stubs into a dedicated `src/db/external/` directory. + +### What changed + +- `src/db/external/ai-schema.ts` — read-only Drizzle stubs for the `ai` schema. +- `src/db/schema.ts` — no longer contains AI table definitions or the unused `aiSuggestionJobStatusEnum`. +- Consumers (`ai-suggestions.repository.ts`, `clean-ai-jobs.ts`) import AI tables from `@/db/external/ai-schema`. + +### Why this approach + +We evaluated several options: + +| Approach | Verdict | +| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **HTTP API boundary** | Overkill for same-DB data. Adds network hops, serialization, auth, and latency to solve a type-definition problem. | +| **Raw SQL + Zod** | Loses Drizzle joins and relational inference. More boilerplate, not less. | +| **Shared npm package** | Impossible — `fluent-ai` is Python, not Node.js. | +| **Views in `public`** | Breaks inserts/updates on `ai` tables that `fluent-api` legitimately writes to. | +| **Co-located stubs (`src/db/external/`)** | Keeps Drizzle type safety with minimal complexity. Makes ownership explicit. No new infrastructure. | + +### Trade-offs + +- **Staleness risk**: If `fluent-ai` changes a column, `fluent-api`'s stub becomes stale. Mitigation: regenerate from the database when schema changes occur. +- **Regeneration is manual today**: We do not yet automate introspection in CI. This is acceptable because the `ai` schema changes infrequently. + +## Regenerating stubs + +When `fluent-ai` migrates the `ai` schema, update the stubs: + +```bash +npx drizzle-kit introspect --tables='ai.*' --out=./src/db/external/ai-schema.ts +``` + +Review the diff, commit, and run `npm run typecheck` before merging. + +## Future considerations + +- If the ecosystem grows and services split onto separate databases, the natural next step is an HTTP API boundary (or async events via PgBoss) rather than cross-database queries. +- If schema drift becomes painful, automate introspection in CI: run it after `fluent-ai` migrations, diff against the checked-in file, and fail the build on unexpected changes. diff --git a/src/db/external/ai-schema.ts b/src/db/external/ai-schema.ts new file mode 100644 index 0000000..9a4bff1 --- /dev/null +++ b/src/db/external/ai-schema.ts @@ -0,0 +1,164 @@ +import { z } from '@hono/zod-openapi'; +/** + * EXTERNALLY OWNED — `ai` schema stubs for Drizzle type safety. + * + * These tables live in the `ai` schema and are owned by `fluent-ai` + * (Alembic-managed). `fluent-api` needs read/write access to them via + * `role_web_data` / `role_pgboss_user` grants, so we maintain lightweight + * Drizzle stubs here. + * + * DO NOT hand-edit column definitions. When `fluent-ai` changes the + * `ai` schema, regenerate this file from the database: + * + * npx drizzle-kit introspect --tables='ai.*' --out=./src/db/external/ai-schema.ts + * + * Then review the diff and commit. + */ +import { + boolean, + index, + integer, + pgSchema, + serial, + text, + timestamp, + uniqueIndex, + varchar, +} from 'drizzle-orm/pg-core'; +import { createSchemaFactory } from 'drizzle-zod'; + +import { bible_texts, bibles, project_units, users } from '@/db/schema'; + +export const aiSchema = pgSchema('ai'); + +export const ai_suggestion_jobs = aiSchema.table( + 'ai_suggestion_jobs', + { + id: serial('id').primaryKey(), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + bibleId: integer('bible_id') + .notNull() + .references(() => bibles.id), + bookCode: varchar('book_code', { length: 50 }).notNull(), + chapterNumber: integer('chapter_number').notNull(), + verseStart: integer('verse_start').notNull(), + verseEnd: integer('verse_end').notNull(), + status: varchar('status', { length: 20 }).notNull().default('queued'), + retryCount: integer('retry_count').notNull().default(0), + errorMessage: text('error_message'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at') + .defaultNow() + .$onUpdate(() => new Date()), + }, + (table) => [ + index('idx_ai_jobs_project_unit').on(table.projectUnitId), + index('idx_ai_jobs_status').on(table.status), + uniqueIndex('uq_ai_jobs_range').on( + table.projectUnitId, + table.bibleId, + table.bookCode, + table.chapterNumber, + table.verseStart, + table.verseEnd + ), + ] +); + +export const ai_suggestions = aiSchema.table( + 'ai_suggestions', + { + id: serial('id').primaryKey(), + bibleTextId: integer('bible_text_id') + .notNull() + .references(() => bible_texts.id, { onDelete: 'cascade' }), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + suggestedText: text('suggested_text').notNull(), + modelInfo: varchar('model_info', { length: 100 }), + createdAt: timestamp('created_at').defaultNow(), + }, + (table) => [ + index('idx_ai_suggestions_bible_text').on(table.bibleTextId), + uniqueIndex('uq_ai_suggestions_per_text_unit').on(table.bibleTextId, table.projectUnitId), + ] +); + +export const ai_suggestion_usage_log = aiSchema.table( + 'ai_suggestion_usage_log', + { + id: serial('id').primaryKey(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + bibleTextId: integer('bible_text_id') + .notNull() + .references(() => bible_texts.id, { onDelete: 'cascade' }), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + wasUsed: boolean('was_used').notNull().default(false), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + (table) => [ + index('idx_ai_usage_user').on(table.userId), + index('idx_ai_usage_project_unit').on(table.projectUnitId), + uniqueIndex('uq_ai_usage_user_text').on(table.userId, table.bibleTextId), + ] +); + +const { createInsertSchema, createSelectSchema } = createSchemaFactory({ + zodInstance: z, +}); + +export const selectAiSuggestionJobsSchema = createSelectSchema(ai_suggestion_jobs); +export const selectAiSuggestionsSchema = createSelectSchema(ai_suggestions); +export const selectAiSuggestionUsageLogSchema = createSelectSchema(ai_suggestion_usage_log); + +export const insertAiSuggestionJobsSchema = createInsertSchema(ai_suggestion_jobs, { + projectUnitId: (schema) => schema.int(), + bibleId: (schema) => schema.int(), + bookCode: (schema) => schema.min(1), + chapterNumber: (schema) => schema.int().min(1), + verseStart: (schema) => schema.int().min(1), + verseEnd: (schema) => schema.int().min(1), +}) + .required({ + projectUnitId: true, + bibleId: true, + bookCode: true, + chapterNumber: true, + verseStart: true, + verseEnd: true, + }) + .omit({ + id: true, + status: true, + retryCount: true, + errorMessage: true, + createdAt: true, + updatedAt: true, + }); + +export const insertAiSuggestionsSchema = createInsertSchema(ai_suggestions, { + bibleTextId: (schema) => schema.int(), + projectUnitId: (schema) => schema.int(), + suggestedText: (schema) => schema.min(1), +}) + .required({ bibleTextId: true, projectUnitId: true, suggestedText: true }) + .omit({ id: true, createdAt: true }); + +export const insertAiSuggestionUsageLogSchema = createInsertSchema(ai_suggestion_usage_log, { + userId: (schema) => schema.int(), + bibleTextId: (schema) => schema.int(), + projectUnitId: (schema) => schema.int(), +}) + .required({ userId: true, bibleTextId: true, projectUnitId: true, wasUsed: true }) + .omit({ id: true, createdAt: true }); + +export const patchAiSuggestionJobsSchema = insertAiSuggestionJobsSchema.partial(); +export const patchAiSuggestionsSchema = insertAiSuggestionsSchema.partial(); +export const patchAiSuggestionUsageLogSchema = insertAiSuggestionUsageLogSchema.partial(); diff --git a/src/db/migrations/0011_add_is_ai_enabled_to_chapter_assignments.sql b/src/db/migrations/0011_add_is_ai_enabled_to_chapter_assignments.sql new file mode 100644 index 0000000..5c8e332 --- /dev/null +++ b/src/db/migrations/0011_add_is_ai_enabled_to_chapter_assignments.sql @@ -0,0 +1 @@ +ALTER TABLE "chapter_assignments" ADD COLUMN "is_ai_enabled" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/0011_added_ai_usggestions.sql b/src/db/migrations/0011_added_ai_usggestions.sql deleted file mode 100644 index 797eee9..0000000 --- a/src/db/migrations/0011_added_ai_usggestions.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE "ai"."ai_suggestion_usage_log" ( - "id" serial PRIMARY KEY NOT NULL, - "user_id" integer NOT NULL, - "bible_text_id" integer NOT NULL, - "project_unit_id" integer NOT NULL, - "was_used" boolean DEFAULT false NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "chapter_assignments" ADD COLUMN "is_ai_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "ai"."ai_suggestion_usage_log" ADD CONSTRAINT "ai_suggestion_usage_log_project_unit_id_project_units_id_fk" FOREIGN KEY ("project_unit_id") REFERENCES "public"."project_units"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_ai_usage_user" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX "idx_ai_usage_project_unit" ON "ai"."ai_suggestion_usage_log" USING btree ("project_unit_id");--> statement-breakpoint -CREATE UNIQUE INDEX "uq_ai_usage_user_text" ON "ai"."ai_suggestion_usage_log" USING btree ("user_id","bible_text_id");--> statement-breakpoint -GRANT SELECT, INSERT, UPDATE, DELETE ON "ai"."ai_suggestion_usage_log" TO role_web_data;--> statement-breakpoint -GRANT USAGE, SELECT ON SEQUENCE "ai"."ai_suggestion_usage_log_id_seq" TO role_web_data; \ No newline at end of file diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json index 10e9f8d..3d5c7a8 100644 --- a/src/db/migrations/meta/0011_snapshot.json +++ b/src/db/migrations/meta/0011_snapshot.json @@ -1,5 +1,5 @@ { - "id": "30a2d1df-803f-4c34-a8ae-8d01b8bc3cf8", + "id": "d0d20803-a8ad-41e1-98d8-eed5fcf618a5", "prevId": "215ac9fb-e170-4975-9f11-77b471673625", "version": "7", "dialect": "postgresql", @@ -83,138 +83,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "ai.ai_suggestion_usage_log": { - "name": "ai_suggestion_usage_log", - "schema": "ai", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "bible_text_id": { - "name": "bible_text_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "project_unit_id": { - "name": "project_unit_id", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "was_used": { - "name": "was_used", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_ai_usage_user": { - "name": "idx_ai_usage_user", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_ai_usage_project_unit": { - "name": "idx_ai_usage_project_unit", - "columns": [ - { - "expression": "project_unit_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "uq_ai_usage_user_text": { - "name": "uq_ai_usage_user_text", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "bible_text_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "ai_suggestion_usage_log_user_id_users_id_fk": { - "name": "ai_suggestion_usage_log_user_id_users_id_fk", - "tableFrom": "ai_suggestion_usage_log", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk": { - "name": "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk", - "tableFrom": "ai_suggestion_usage_log", - "tableTo": "bible_texts", - "columnsFrom": ["bible_text_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "ai_suggestion_usage_log_project_unit_id_project_units_id_fk": { - "name": "ai_suggestion_usage_log_project_unit_id_project_units_id_fk", - "tableFrom": "ai_suggestion_usage_log", - "tableTo": "project_units", - "columnsFrom": ["project_unit_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auth_account": { "name": "auth_account", "schema": "", @@ -2408,9 +2276,7 @@ "values": ["invited", "verified", "inactive"] } }, - "schemas": { - "ai": "ai" - }, + "schemas": {}, "sequences": {}, "roles": {}, "policies": {}, diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index ae539cb..a2a2a42 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -82,8 +82,8 @@ { "idx": 11, "version": "7", - "when": 1779805709340, - "tag": "0011_added_ai_usggestions", + "when": 1779911602173, + "tag": "0011_add_is_ai_enabled_to_chapter_assignments", "breakpoints": true } ] diff --git a/src/db/schema.ts b/src/db/schema.ts index 0b84aae..19a5fd0 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -10,11 +10,9 @@ import { json, jsonb, pgEnum, - pgSchema, pgTable, primaryKey, serial, - text, timestamp, uniqueIndex, varchar, @@ -42,13 +40,6 @@ export const chapterStatusEnum = pgEnum('chapter_status', [ 'complete', ]); export const assignmentRoleEnum = pgEnum('assignment_role', ['drafter', 'peer_checker']); -export const aiSuggestionJobStatusEnum = pgEnum('ai_suggestion_job_status', [ - 'queued', - 'processing', - 'completed', - 'failed', -]); - export const roles = pgTable('roles', { id: serial('id').primaryKey(), name: varchar('name', { length: 255 }).notNull().unique(), @@ -851,138 +842,3 @@ export const patchProjectsClientSchema = patchProjectsSchema.omit({ export const patchUsersClientSchema = patchUsersSchema.omit({ organization: true, }); - -export const aiSchema = pgSchema('ai'); - -export const ai_suggestion_jobs = aiSchema.table( - 'ai_suggestion_jobs', - { - id: serial('id').primaryKey(), - projectUnitId: integer('project_unit_id') - .notNull() - .references(() => project_units.id, { onDelete: 'cascade' }), - bibleId: integer('bible_id') - .notNull() - .references(() => bibles.id), - bookCode: varchar('book_code', { length: 50 }).notNull(), - chapterNumber: integer('chapter_number').notNull(), - verseStart: integer('verse_start').notNull(), - verseEnd: integer('verse_end').notNull(), - status: varchar('status', { length: 20 }).notNull().default('queued'), - retryCount: integer('retry_count').notNull().default(0), - errorMessage: text('error_message'), - createdAt: timestamp('created_at').defaultNow(), - updatedAt: timestamp('updated_at') - .defaultNow() - .$onUpdate(() => new Date()), - }, - (table) => [ - index('idx_ai_suggestion_jobs_dedup').on( - table.projectUnitId, - table.bibleId, - table.bookCode, - table.chapterNumber - ), - index('idx_ai_suggestion_jobs_status_created').on(table.status, table.createdAt), - uniqueIndex('uq_ai_jobs_range').on( - table.projectUnitId, - table.bibleId, - table.bookCode, - table.chapterNumber, - table.verseStart, - table.verseEnd - ), - ] -); - -export const ai_suggestions = aiSchema.table( - 'ai_suggestions', - { - id: serial('id').primaryKey(), - bibleTextId: integer('bible_text_id') - .notNull() - .references(() => bible_texts.id, { onDelete: 'cascade' }), - projectUnitId: integer('project_unit_id') - .notNull() - .references(() => project_units.id, { onDelete: 'cascade' }), - suggestedText: text('suggested_text').notNull(), - modelInfo: varchar('model_info', { length: 100 }), - createdAt: timestamp('created_at').defaultNow(), - }, - (table) => [ - index('idx_ai_suggestions_lookup').on(table.projectUnitId, table.bibleTextId), - uniqueIndex('uq_ai_suggestions_per_text_unit').on(table.bibleTextId, table.projectUnitId), - ] -); - -export const ai_suggestion_usage_log = aiSchema.table( - 'ai_suggestion_usage_log', - { - id: serial('id').primaryKey(), - userId: integer('user_id') - .notNull() - .references(() => users.id, { onDelete: 'cascade' }), - bibleTextId: integer('bible_text_id') - .notNull() - .references(() => bible_texts.id, { onDelete: 'cascade' }), - projectUnitId: integer('project_unit_id') - .notNull() - .references(() => project_units.id, { onDelete: 'cascade' }), - wasUsed: boolean('was_used').notNull().default(false), - createdAt: timestamp('created_at').defaultNow().notNull(), - }, - (table) => [ - index('idx_ai_usage_user').on(table.userId), - index('idx_ai_usage_project_unit').on(table.projectUnitId), - uniqueIndex('uq_ai_usage_user_text').on(table.userId, table.bibleTextId), - ] -); - -export const selectAiSuggestionJobsSchema = createSelectSchema(ai_suggestion_jobs); -export const selectAiSuggestionsSchema = createSelectSchema(ai_suggestions); -export const selectAiSuggestionUsageLogSchema = createSelectSchema(ai_suggestion_usage_log); - -export const insertAiSuggestionJobsSchema = createInsertSchema(ai_suggestion_jobs, { - projectUnitId: (schema) => schema.int(), - bibleId: (schema) => schema.int(), - bookCode: (schema) => schema.min(1), - chapterNumber: (schema) => schema.int().min(1), - verseStart: (schema) => schema.int().min(1), - verseEnd: (schema) => schema.int().min(1), -}) - .required({ - projectUnitId: true, - bibleId: true, - bookCode: true, - chapterNumber: true, - verseStart: true, - verseEnd: true, - }) - .omit({ - id: true, - status: true, - retryCount: true, - errorMessage: true, - createdAt: true, - updatedAt: true, - }); - -export const insertAiSuggestionsSchema = createInsertSchema(ai_suggestions, { - bibleTextId: (schema) => schema.int(), - projectUnitId: (schema) => schema.int(), - suggestedText: (schema) => schema.min(1), -}) - .required({ bibleTextId: true, projectUnitId: true, suggestedText: true }) - .omit({ id: true, createdAt: true }); - -export const insertAiSuggestionUsageLogSchema = createInsertSchema(ai_suggestion_usage_log, { - userId: (schema) => schema.int(), - bibleTextId: (schema) => schema.int(), - projectUnitId: (schema) => schema.int(), -}) - .required({ userId: true, bibleTextId: true, projectUnitId: true, wasUsed: true }) - .omit({ id: true, createdAt: true }); - -export const patchAiSuggestionJobsSchema = insertAiSuggestionJobsSchema.partial(); -export const patchAiSuggestionsSchema = insertAiSuggestionsSchema.partial(); -export const patchAiSuggestionUsageLogSchema = insertAiSuggestionUsageLogSchema.partial(); diff --git a/src/domains/ai-suggestions/ai-suggestions.auth.middleware.ts b/src/domains/ai-suggestions/ai-suggestions.auth.middleware.ts new file mode 100644 index 0000000..6c3e0f1 --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.auth.middleware.ts @@ -0,0 +1,42 @@ +import { createMiddleware } from 'hono/factory'; +import * as HttpStatusCodes from 'stoker/http-status-codes'; + +import type { AppEnv } from '@/server/context.types'; + +import { AiSuggestionsPolicy } from './ai-suggestions.policy'; +import { findProjectUnitAuthContext } from './ai-suggestions.repository'; + +/** + * Middleware that verifies the authenticated user has access to a specific project unit + * according to the AI Suggestions authorization policy. + * + * @param getProjectUnitId A function to extract the projectUnitId from the request context + */ +export const requireProjectUnitAccess = ( + getProjectUnitId: ( + c: Parameters>[0]>[0] + ) => number | Promise +) => + createMiddleware(async (c, next) => { + const user = c.get('user'); + if (!user) { + return c.json({ message: 'Unauthorized' }, HttpStatusCodes.UNAUTHORIZED); + } + + const projectUnitId = await getProjectUnitId(c); + if (!projectUnitId || Number.isNaN(projectUnitId)) { + return c.json({ message: 'Invalid project unit ID' }, HttpStatusCodes.BAD_REQUEST); + } + + const context = await findProjectUnitAuthContext(projectUnitId); + if (!context) { + return c.json({ message: 'Project unit not found' }, HttpStatusCodes.NOT_FOUND); + } + + const isAuthorized = AiSuggestionsPolicy.canAccessProjectUnit(user, context); + if (!isAuthorized) { + return c.json({ message: 'Forbidden' }, HttpStatusCodes.FORBIDDEN); + } + + await next(); + }); diff --git a/src/domains/ai-suggestions/ai-suggestions.constants.ts b/src/domains/ai-suggestions/ai-suggestions.constants.ts deleted file mode 100644 index b36652f..0000000 --- a/src/domains/ai-suggestions/ai-suggestions.constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const AI_SUGGESTIONS_CONSTANTS = { - // Minimum number of translated verses required before AI suggestions are enabled for a project unit - ACTIVATION_THRESHOLD_VERSES: 500, - - // Number of verses to automatically queue when a drafter is first assigned a chapter - INITIAL_QUEUE_COUNT: 3, - - // Default number of verses to pre-fetch ahead of the drafter's current verse - DEFAULT_LOOKAHEAD: 1, - - // Guardrail for GET /ai-suggestions query fan-out - MAX_REQUESTED_BIBLE_TEXT_IDS: 200, -} as const; diff --git a/src/domains/ai-suggestions/ai-suggestions.policy.ts b/src/domains/ai-suggestions/ai-suggestions.policy.ts new file mode 100644 index 0000000..a93cc6a --- /dev/null +++ b/src/domains/ai-suggestions/ai-suggestions.policy.ts @@ -0,0 +1,31 @@ +import { ROLES } from '@/lib/roles'; + +export interface PolicyUser { + id: number; + roleName: string; + organization: number; +} + +export interface ProjectUnitAuthContext { + organizationId: number; + memberUserIds: number[]; +} + +export class AiSuggestionsPolicy { + /** + * Determines if a user can access AI suggestions for a specific project unit. + * Project Managers can access if they belong to the same organization. + * Translators can access if they are assigned as a member of the project. + */ + static canAccessProjectUnit(user: PolicyUser, context: ProjectUnitAuthContext): boolean { + if (user.roleName === ROLES.PROJECT_MANAGER) { + return context.organizationId === user.organization; + } + + if (user.roleName === ROLES.TRANSLATOR) { + return context.memberUserIds.includes(user.id); + } + + return false; + } +} diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts index 29abeb2..6024ebf 100644 --- a/src/domains/ai-suggestions/ai-suggestions.repository.ts +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -1,12 +1,95 @@ -import { and, eq, inArray } from 'drizzle-orm'; +import { and, asc, eq, gt, inArray, isNull } from 'drizzle-orm'; import type { DbTransaction, Result } from '@/lib/types'; import { db } from '@/db'; -import { ai_suggestion_jobs, ai_suggestion_usage_log, ai_suggestions } from '@/db/schema'; +import { + ai_suggestion_jobs, + ai_suggestion_usage_log, + ai_suggestions, +} from '@/db/external/ai-schema'; +import { + bible_texts, + books, + chapter_assignments, + project_units, + project_users, + projects, + translated_verses, +} from '@/db/schema'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; +import type { ProjectUnitAuthContext } from './ai-suggestions.policy'; + +export async function findProjectUnitAuthContext( + projectUnitId: number +): Promise { + const records = await db + .select({ + organizationId: projects.organization, + memberUserId: project_users.userId, + }) + .from(project_units) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .leftJoin(project_users, eq(project_users.projectId, projects.id)) + .where(eq(project_units.id, projectUnitId)); + + if (records.length === 0) return null; + + return { + organizationId: records[0].organizationId, + memberUserIds: records.map((r) => r.memberUserId).filter((id): id is number => id !== null), + }; +} + +export async function checkBibleTextsExist(ids: number[]): Promise { + if (ids.length === 0) return true; + + const existingIds = await db + .select({ id: bible_texts.id }) + .from(bible_texts) + .where(inArray(bible_texts.id, ids)); + + return existingIds.length === ids.length; +} + +export async function getChapterAssignmentAiStatus( + projectUnitId: number, + bibleId: number, + bookCode: string, + chapterNumber: number +): Promise { + const normalizedBookCode = bookCode.toUpperCase(); + + const assignment = await db + .select({ isAiEnabled: chapter_assignments.isAiEnabled }) + .from(chapter_assignments) + .innerJoin(books, eq(chapter_assignments.bookId, books.id)) + .where( + and( + eq(chapter_assignments.projectUnitId, projectUnitId), + eq(chapter_assignments.bibleId, bibleId), + eq(books.code, normalizedBookCode), + eq(chapter_assignments.chapterNumber, chapterNumber) + ) + ) + .limit(1); + + if (!assignment[0]) return null; + return assignment[0].isAiEnabled; +} + +export async function getBookCodeById(bookId: number): Promise { + const book = await db + .select({ code: books.code }) + .from(books) + .where(eq(books.id, bookId)) + .limit(1); + + return book[0]?.code ?? null; +} + export async function queueAiSuggestionJobs( jobs: { projectUnitId: number; @@ -109,3 +192,37 @@ export async function logAiSuggestionUsage( return err(ErrorCode.INTERNAL_ERROR); } } + +export async function findNextUntranslatedVerses( + projectUnitId: number, + bibleId: number, + bookCode: string, + chapterNumber: number, + currentVerse: number, + lookahead: number +): Promise { + const nextVerses = await db + .select({ verseNumber: bible_texts.verseNumber }) + .from(bible_texts) + .innerJoin(books, eq(bible_texts.bookId, books.id)) + .leftJoin( + translated_verses, + and( + eq(translated_verses.bibleTextId, bible_texts.id), + eq(translated_verses.projectUnitId, projectUnitId) + ) + ) + .where( + and( + eq(bible_texts.bibleId, bibleId), + eq(books.code, bookCode), + eq(bible_texts.chapterNumber, chapterNumber), + gt(bible_texts.verseNumber, currentVerse), + isNull(translated_verses.projectUnitId) + ) + ) + .orderBy(asc(bible_texts.verseNumber)) + .limit(lookahead); + + return nextVerses.map((v) => v.verseNumber); +} diff --git a/src/domains/ai-suggestions/ai-suggestions.route.ts b/src/domains/ai-suggestions/ai-suggestions.route.ts index 6f25fc9..04d48f5 100644 --- a/src/domains/ai-suggestions/ai-suggestions.route.ts +++ b/src/domains/ai-suggestions/ai-suggestions.route.ts @@ -9,6 +9,7 @@ import { getHttpStatus } from '@/lib/types'; import { authenticateUser, requirePermission } from '@/middlewares/role-auth'; import { server } from '@/server/server'; +import { requireProjectUnitAccess } from './ai-suggestions.auth.middleware'; import * as aiSuggestionsService from './ai-suggestions.service'; import { aiSuggestionsListResponseSchema, @@ -23,7 +24,11 @@ const getAiSuggestionsRoute = createRoute({ tags: ['AI Suggestions'], method: 'get', path: '/ai-suggestions', - middleware: [authenticateUser, requirePermission(PERMISSIONS.PROJECT_VIEW)] as const, + middleware: [ + authenticateUser, + requirePermission(PERMISSIONS.PROJECT_VIEW), + requireProjectUnitAccess((c) => Number(c.req.query('projectUnitId'))), + ] as const, request: { query: getAiSuggestionsQuerySchema, }, @@ -72,7 +77,14 @@ const queueNextVersesRoute = createRoute({ tags: ['AI Suggestions'], method: 'post', path: '/ai-suggestions/queue-next', - middleware: [authenticateUser, requirePermission(PERMISSIONS.PROJECT_VIEW)] as const, + middleware: [ + authenticateUser, + requirePermission(PERMISSIONS.PROJECT_VIEW), + requireProjectUnitAccess(async (c) => { + const body = await c.req.json(); + return body.projectUnitId; + }), + ] as const, request: { body: jsonContent(queueNextVersesRequestSchema, 'Verses context'), }, @@ -130,7 +142,14 @@ const trackUsageRoute = createRoute({ tags: ['AI Suggestions'], method: 'post', path: '/ai-suggestions/usage', - middleware: [authenticateUser, requirePermission(PERMISSIONS.PROJECT_VIEW)] as const, + middleware: [ + authenticateUser, + requirePermission(PERMISSIONS.PROJECT_VIEW), + requireProjectUnitAccess(async (c) => { + const body = await c.req.json(); + return body.projectUnitId; + }), + ] as const, request: { body: jsonContent(trackUsageRequestSchema, 'Usage data'), }, diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index c166667..4acd7c8 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -1,19 +1,7 @@ -import { and, asc, eq, gt, inArray, isNull } from 'drizzle-orm'; - import type { Result, User } from '@/lib/types'; -import { db } from '@/db'; -import { - bible_texts, - books, - chapter_assignments, - project_units, - project_users, - projects, - translated_verses, -} from '@/db/schema'; +import env from '@/env'; import { logger } from '@/lib/logger'; -import { ROLES } from '@/lib/roles'; import { err, ErrorCode, ok } from '@/lib/types'; import type { @@ -22,46 +10,17 @@ import type { TrackUsageRequest, } from './ai-suggestions.types'; -import { AI_SUGGESTIONS_CONSTANTS } from './ai-suggestions.constants'; import { + checkBibleTextsExist, + findNextUntranslatedVerses, getAiSuggestions as getAiSuggestionsRepo, + getBookCodeById, + getChapterAssignmentAiStatus, logAiSuggestionUsage, queueAiSuggestionJobs, } from './ai-suggestions.repository'; -async function userCanAccessProjectUnit(user: User, projectUnitId: number): Promise { - const [record] = await db - .select({ - organization: projects.organization, - memberUserId: project_users.userId, - }) - .from(project_units) - .innerJoin(projects, eq(project_units.projectId, projects.id)) - .leftJoin( - project_users, - and(eq(project_users.projectId, projects.id), eq(project_users.userId, user.id)) - ) - .where(eq(project_units.id, projectUnitId)) - .limit(1); - - if (!record) return false; - - if (user.roleName === ROLES.PROJECT_MANAGER) { - return record.organization === user.organization; - } - - if (user.roleName === ROLES.TRANSLATOR) { - return record.memberUserId === user.id; - } - - return false; -} - export async function trackUsage(user: User, data: TrackUsageRequest): Promise> { - if (!(await userCanAccessProjectUnit(user, data.projectUnitId))) { - return err(ErrorCode.FORBIDDEN); - } - return logAiSuggestionUsage(user.id, data.bibleTextId, data.projectUnitId, data.wasUsed); } @@ -69,29 +28,18 @@ export async function getAiSuggestions( user: User, query: GetAiSuggestionsQuery ): Promise> { - const ids = query.bibleTextIds - .split(',') - .map((id) => Number.parseInt(id.trim(), 10)) - .filter((id) => !Number.isNaN(id)); + const ids = query.bibleTextIds; if ( ids.length === 0 || - ids.length > AI_SUGGESTIONS_CONSTANTS.MAX_REQUESTED_BIBLE_TEXT_IDS || + ids.length > env.AI_MAX_REQUESTED_BIBLE_TEXT_IDS || ids.length !== new Set(ids).size ) { return err(ErrorCode.VALIDATION_ERROR); } - if (!(await userCanAccessProjectUnit(user, query.projectUnitId))) { - return err(ErrorCode.FORBIDDEN); - } - - const existingIds = await db - .select({ id: bible_texts.id }) - .from(bible_texts) - .where(inArray(bible_texts.id, ids)); - - if (existingIds.length !== ids.length) { + const allExist = await checkBibleTextsExist(ids); + if (!allExist) { return err(ErrorCode.VALIDATION_ERROR); } @@ -101,13 +49,11 @@ export async function getAiSuggestions( return suggestionsResult; } - const data = suggestionsResult.data.map( - (suggestion: { bibleTextId: number; suggestedText: string; modelInfo: string | null }) => ({ - bibleTextId: suggestion.bibleTextId, - suggestedText: suggestion.suggestedText, - modelInfo: suggestion.modelInfo, - }) - ); + const data = suggestionsResult.data.map((suggestion) => ({ + bibleTextId: suggestion.bibleTextId, + suggestedText: suggestion.suggestedText, + modelInfo: suggestion.modelInfo, + })); return ok({ data }); } @@ -119,41 +65,28 @@ export async function queueNextVerses( bookCode: string, chapterNumber: number, currentVerse: number, - lookahead: number = AI_SUGGESTIONS_CONSTANTS.DEFAULT_LOOKAHEAD + lookahead: number = env.AI_DEFAULT_LOOKAHEAD ): Promise> { try { - if (!(await userCanAccessProjectUnit(user, projectUnitId))) { - return err(ErrorCode.FORBIDDEN); - } + const isAiEnabled = await getChapterAssignmentAiStatus( + projectUnitId, + bibleId, + bookCode, + chapterNumber + ); - const normalizedBookCode = bookCode.toUpperCase(); - - const assignment = await db - .select({ isAiEnabled: chapter_assignments.isAiEnabled }) - .from(chapter_assignments) - .innerJoin(books, eq(chapter_assignments.bookId, books.id)) - .where( - and( - eq(chapter_assignments.projectUnitId, projectUnitId), - eq(chapter_assignments.bibleId, bibleId), - eq(books.code, normalizedBookCode), - eq(chapter_assignments.chapterNumber, chapterNumber) - ) - ) - .limit(1); - - if (!assignment[0]) { + if (isAiEnabled === null) { return err(ErrorCode.INVALID_REFERENCE); } - if (!assignment[0].isAiEnabled) { + if (!isAiEnabled) { return ok(undefined); } return await queueNextVersesForAssignment( projectUnitId, bibleId, - normalizedBookCode, + bookCode.toUpperCase(), chapterNumber, currentVerse, lookahead @@ -172,38 +105,24 @@ async function queueNextVersesForAssignment( currentVerse: number, lookahead: number ): Promise> { - const nextVerses = await db - .select({ verseNumber: bible_texts.verseNumber }) - .from(bible_texts) - .innerJoin(books, eq(bible_texts.bookId, books.id)) - .leftJoin( - translated_verses, - and( - eq(translated_verses.bibleTextId, bible_texts.id), - eq(translated_verses.projectUnitId, projectUnitId) - ) - ) - .where( - and( - eq(bible_texts.bibleId, bibleId), - eq(books.code, bookCode), - eq(bible_texts.chapterNumber, chapterNumber), - gt(bible_texts.verseNumber, currentVerse), - isNull(translated_verses.projectUnitId) - ) - ) - .orderBy(asc(bible_texts.verseNumber)) - .limit(lookahead); + const nextVerses = await findNextUntranslatedVerses( + projectUnitId, + bibleId, + bookCode, + chapterNumber, + currentVerse, + lookahead + ); if (nextVerses.length === 0) return ok(undefined); - const jobs = nextVerses.map((v) => ({ + const jobs = nextVerses.map((verseNumber) => ({ projectUnitId, bibleId, bookCode, chapterNumber, - verseStart: v.verseNumber, - verseEnd: v.verseNumber, + verseStart: verseNumber, + verseEnd: verseNumber, })); return queueAiSuggestionJobs(jobs); @@ -214,29 +133,30 @@ export async function handleChapterAssigned( bibleId: number, bookId: number, chapterNumber: number -) { +): Promise> { try { - const book = await db - .select({ code: books.code }) - .from(books) - .where(eq(books.id, bookId)) - .limit(1); + const bookCode = await getBookCodeById(bookId); - if (!book[0]?.code) return; + if (!bookCode) { + return ok(undefined); + } await queueNextVersesForAssignment( projectUnitId, bibleId, - book[0].code.toUpperCase(), + bookCode.toUpperCase(), chapterNumber, 0, - AI_SUGGESTIONS_CONSTANTS.INITIAL_QUEUE_COUNT + env.AI_INITIAL_QUEUE_COUNT ); + + return ok(undefined); } catch (error) { logger.error({ cause: error, message: 'Failed to trigger initial AI queue on chapter assignment', context: { projectUnitId, chapterNumber }, }); + return err(ErrorCode.INTERNAL_ERROR); } } diff --git a/src/domains/ai-suggestions/ai-suggestions.types.ts b/src/domains/ai-suggestions/ai-suggestions.types.ts index e5cc8ab..bcb5520 100644 --- a/src/domains/ai-suggestions/ai-suggestions.types.ts +++ b/src/domains/ai-suggestions/ai-suggestions.types.ts @@ -1,13 +1,14 @@ import { z } from '@hono/zod-openapi'; -import { AI_SUGGESTIONS_CONSTANTS } from './ai-suggestions.constants'; +import env from '@/env'; export const getAiSuggestionsQuerySchema = z.object({ projectUnitId: z.coerce.number().int().positive(), bibleTextIds: z .string() .regex(/^\d+(,\d+)*$/, 'Expected comma-separated numeric bible text IDs') - .describe('Comma-separated list of bible text IDs'), + .describe('Comma-separated list of bible text IDs') + .transform((val) => val.split(',').map((id) => Number.parseInt(id.trim(), 10))), }); export type GetAiSuggestionsQuery = z.infer; @@ -30,12 +31,7 @@ export const queueNextVersesRequestSchema = z.object({ bookCode: z.string().min(3).max(3), chapterNumber: z.number().int().positive(), currentVerse: z.number().int().positive(), - lookahead: z - .number() - .int() - .positive() - .max(20) - .default(AI_SUGGESTIONS_CONSTANTS.DEFAULT_LOOKAHEAD), + lookahead: z.number().int().positive().max(20).default(env.AI_DEFAULT_LOOKAHEAD), }); export type QueueNextVersesRequest = z.infer; diff --git a/src/env.ts b/src/env.ts index 533ba27..b49263b 100644 --- a/src/env.ts +++ b/src/env.ts @@ -27,6 +27,12 @@ const EnvSchema = z.object({ EMAIL_SERVICE_DOMAIN: z.string(), EMAIL_SERVICE_SENDER: z.string(), FRONTEND_URL: z.string(), + + // AI suggestion tunables + AI_ACTIVATION_THRESHOLD_VERSES: z.coerce.number().int().positive().default(500), + AI_INITIAL_QUEUE_COUNT: z.coerce.number().int().positive().default(3), + AI_DEFAULT_LOOKAHEAD: z.coerce.number().int().positive().default(1), + AI_MAX_REQUESTED_BIBLE_TEXT_IDS: z.coerce.number().int().positive().default(200), }); export type env = z.infer; diff --git a/src/scripts/clean-ai-jobs.ts b/src/scripts/clean-ai-jobs.ts index 148bc7e..a05a49b 100644 --- a/src/scripts/clean-ai-jobs.ts +++ b/src/scripts/clean-ai-jobs.ts @@ -2,7 +2,7 @@ import { and, inArray, lt } from 'drizzle-orm'; import process from 'node:process'; import { db } from '@/db'; -import { ai_suggestion_jobs } from '@/db/schema'; +import { ai_suggestion_jobs } from '@/db/external/ai-schema'; import { logger } from '@/lib/logger'; async function main() { From 2fb04a2cc64ca6bc799ba29b0d9a3a6928d679fe Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Thu, 28 May 2026 13:41:57 +0530 Subject: [PATCH 13/14] refactor queue orchestration and optimize translation memory threshold checks --- .../ai-suggestions.repository.ts | 40 +++++++++++++++- .../ai-suggestions/ai-suggestions.route.ts | 24 ++++------ .../ai-suggestions/ai-suggestions.service.ts | 48 +++++++++++-------- .../ai-suggestions/ai-suggestions.types.ts | 12 +++-- 4 files changed, 82 insertions(+), 42 deletions(-) diff --git a/src/domains/ai-suggestions/ai-suggestions.repository.ts b/src/domains/ai-suggestions/ai-suggestions.repository.ts index 6024ebf..6f6cbf4 100644 --- a/src/domains/ai-suggestions/ai-suggestions.repository.ts +++ b/src/domains/ai-suggestions/ai-suggestions.repository.ts @@ -1,4 +1,4 @@ -import { and, asc, eq, gt, inArray, isNull } from 'drizzle-orm'; +import { and, asc, eq, gt, inArray, isNull, sql } from 'drizzle-orm'; import type { DbTransaction, Result } from '@/lib/types'; @@ -226,3 +226,41 @@ export async function findNextUntranslatedVerses( return nextVerses.map((v) => v.verseNumber); } + +export async function hasReachedAiActivationThreshold( + projectUnitId: number, + threshold: number +): Promise { + const projectInfo = await db + .select({ + sourceLanguage: projects.sourceLanguage, + targetLanguage: projects.targetLanguage, + organization: projects.organization, + }) + .from(project_units) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .where(eq(project_units.id, projectUnitId)) + .limit(1); + + if (!projectInfo[0]) return false; + + const { sourceLanguage, targetLanguage, organization } = projectInfo[0]; + + const result = await db + .select({ id: translated_verses.id }) + .from(translated_verses) + .innerJoin(project_units, eq(translated_verses.projectUnitId, project_units.id)) + .innerJoin(projects, eq(project_units.projectId, projects.id)) + .where( + and( + eq(projects.sourceLanguage, sourceLanguage), + eq(projects.targetLanguage, targetLanguage), + eq(projects.organization, organization), + sql`length(trim(${translated_verses.content})) > 0` + ) + ) + .limit(1) + .offset(threshold - 1); + + return result.length > 0; +} diff --git a/src/domains/ai-suggestions/ai-suggestions.route.ts b/src/domains/ai-suggestions/ai-suggestions.route.ts index 04d48f5..b5cd6b3 100644 --- a/src/domains/ai-suggestions/ai-suggestions.route.ts +++ b/src/domains/ai-suggestions/ai-suggestions.route.ts @@ -15,6 +15,7 @@ import { aiSuggestionsListResponseSchema, getAiSuggestionsQuerySchema, queueNextVersesRequestSchema, + queueNextVersesResponseSchema, trackUsageRequestSchema, } from './ai-suggestions.types'; @@ -57,13 +58,8 @@ const getAiSuggestionsRoute = createRoute({ server.openapi(getAiSuggestionsRoute, async (c) => { const query = c.req.valid('query'); - const user = c.get('user'); - if (!user) { - return c.json({ message: 'User not found' }, HttpStatusCodes.UNAUTHORIZED); - } - - const result = await aiSuggestionsService.getAiSuggestions(user, query); + const result = await aiSuggestionsService.getAiSuggestions(query); if (result.ok) { return c.json(result.data, HttpStatusCodes.OK); } @@ -89,7 +85,10 @@ const queueNextVersesRoute = createRoute({ body: jsonContent(queueNextVersesRequestSchema, 'Verses context'), }, responses: { - [HttpStatusCodes.OK]: jsonContent(createMessageObjectSchema('Successfully queued'), 'Queued'), + [HttpStatusCodes.OK]: jsonContent( + queueNextVersesResponseSchema, + 'Queueing status and threshold state' + ), [HttpStatusCodes.BAD_REQUEST]: jsonContent( createMessageObjectSchema('Bad Request'), 'Validation error' @@ -114,23 +113,16 @@ const queueNextVersesRoute = createRoute({ server.openapi(queueNextVersesRoute, async (c) => { const body = c.req.valid('json'); - const user = c.get('user'); - - if (!user) { - return c.json({ message: 'User not found' }, HttpStatusCodes.UNAUTHORIZED); - } const result = await aiSuggestionsService.queueNextVerses( - user, body.projectUnitId, body.bibleId, body.bookCode, body.chapterNumber, - body.currentVerse, - body.lookahead + body.currentVerse ); if (result.ok) { - return c.json({ message: 'Queued' }, HttpStatusCodes.OK); + return c.json(result.data, HttpStatusCodes.OK); } return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); diff --git a/src/domains/ai-suggestions/ai-suggestions.service.ts b/src/domains/ai-suggestions/ai-suggestions.service.ts index 4acd7c8..40fb278 100644 --- a/src/domains/ai-suggestions/ai-suggestions.service.ts +++ b/src/domains/ai-suggestions/ai-suggestions.service.ts @@ -7,6 +7,7 @@ import { err, ErrorCode, ok } from '@/lib/types'; import type { AiSuggestionsListResponse, GetAiSuggestionsQuery, + QueueNextVersesResponse, TrackUsageRequest, } from './ai-suggestions.types'; @@ -16,6 +17,7 @@ import { getAiSuggestions as getAiSuggestionsRepo, getBookCodeById, getChapterAssignmentAiStatus, + hasReachedAiActivationThreshold, logAiSuggestionUsage, queueAiSuggestionJobs, } from './ai-suggestions.repository'; @@ -25,7 +27,6 @@ export async function trackUsage(user: User, data: TrackUsageRequest): Promise> { const ids = query.bibleTextIds; @@ -59,38 +60,36 @@ export async function getAiSuggestions( } export async function queueNextVerses( - user: User, projectUnitId: number, bibleId: number, bookCode: string, chapterNumber: number, - currentVerse: number, - lookahead: number = env.AI_DEFAULT_LOOKAHEAD -): Promise> { + currentVerse: number +): Promise> { try { - const isAiEnabled = await getChapterAssignmentAiStatus( - projectUnitId, - bibleId, - bookCode, - chapterNumber - ); + const [isThresholdMet, isAiEnabled] = await Promise.all([ + hasReachedAiActivationThreshold(projectUnitId, env.AI_ACTIVATION_THRESHOLD_VERSES), + getChapterAssignmentAiStatus(projectUnitId, bibleId, bookCode, chapterNumber), + ]); if (isAiEnabled === null) { return err(ErrorCode.INVALID_REFERENCE); } - if (!isAiEnabled) { - return ok(undefined); + if (!isThresholdMet || !isAiEnabled) { + return ok({ queued: false, thresholdMet: isThresholdMet }); } - return await queueNextVersesForAssignment( + await queueNextVersesForAssignment( projectUnitId, bibleId, bookCode.toUpperCase(), chapterNumber, currentVerse, - lookahead + env.AI_DEFAULT_LOOKAHEAD ); + + return ok({ queued: true, thresholdMet: true }); } catch (error) { logger.error(error); return err(ErrorCode.INTERNAL_ERROR); @@ -141,15 +140,22 @@ export async function handleChapterAssigned( return ok(undefined); } - await queueNextVersesForAssignment( + const isThresholdMet = await hasReachedAiActivationThreshold( projectUnitId, - bibleId, - bookCode.toUpperCase(), - chapterNumber, - 0, - env.AI_INITIAL_QUEUE_COUNT + env.AI_ACTIVATION_THRESHOLD_VERSES ); + if (isThresholdMet) { + await queueNextVersesForAssignment( + projectUnitId, + bibleId, + bookCode.toUpperCase(), + chapterNumber, + 0, + env.AI_INITIAL_QUEUE_COUNT + ); + } + return ok(undefined); } catch (error) { logger.error({ diff --git a/src/domains/ai-suggestions/ai-suggestions.types.ts b/src/domains/ai-suggestions/ai-suggestions.types.ts index bcb5520..4da7baa 100644 --- a/src/domains/ai-suggestions/ai-suggestions.types.ts +++ b/src/domains/ai-suggestions/ai-suggestions.types.ts @@ -1,7 +1,5 @@ import { z } from '@hono/zod-openapi'; -import env from '@/env'; - export const getAiSuggestionsQuerySchema = z.object({ projectUnitId: z.coerce.number().int().positive(), bibleTextIds: z @@ -28,14 +26,20 @@ export type AiSuggestionsListResponse = z.infer; +export const queueNextVersesResponseSchema = z.object({ + queued: z.boolean(), + thresholdMet: z.boolean(), +}); + +export type QueueNextVersesResponse = z.infer; + export const trackUsageRequestSchema = z.object({ bibleTextId: z.number().int().positive(), projectUnitId: z.number().int().positive(), From 0c90357176db50e4ed36462c82518802203273ea Mon Sep 17 00:00:00 2001 From: AnuMonachan Date: Thu, 28 May 2026 16:53:25 +0530 Subject: [PATCH 14/14] updated ai enable code --- .../chapter-assignments.repository.ts | 1 + .../chapter-assignments.route.ts | 13 ++----- .../chapter-assignments.service.ts | 38 ++++++------------- .../chapter-assignments.types.ts | 2 + .../project-chapter-assignments.service.ts | 2 + .../project-chapter-assignments.types.ts | 1 + .../users-chapter-assignments.service.ts | 1 + .../users-chapter-assignments.types.ts | 2 + 8 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/domains/chapter-assignments/chapter-assignments.repository.ts b/src/domains/chapter-assignments/chapter-assignments.repository.ts index 9478894..002fd8c 100644 --- a/src/domains/chapter-assignments/chapter-assignments.repository.ts +++ b/src/domains/chapter-assignments/chapter-assignments.repository.ts @@ -398,6 +398,7 @@ export async function findAssignmentsProgress( submittedTime: chapter_assignments.submittedTime, createdAt: chapter_assignments.createdAt, updatedAt: chapter_assignments.updatedAt, + isAiEnabled: chapter_assignments.isAiEnabled, }) .from(chapter_assignments) .innerJoin(project_units, eq(chapter_assignments.projectUnitId, project_units.id)) diff --git a/src/domains/chapter-assignments/chapter-assignments.route.ts b/src/domains/chapter-assignments/chapter-assignments.route.ts index c986ad7..56e7937 100644 --- a/src/domains/chapter-assignments/chapter-assignments.route.ts +++ b/src/domains/chapter-assignments/chapter-assignments.route.ts @@ -301,12 +301,11 @@ server.openapi(deleteChapterAssignmentRoute, async (c) => { return c.json({ message: result.error.message }, getHttpStatus(result.error) as never); }); -// ─── PATCH /projects/:id/units/:unitId/assignments/:chapterAssignmentId/ai-status ────── - +// ─── PATCH /chapter-assignments/:chapterAssignmentId/ai-status ──────────────── const updateAiStatusRoute = createRoute({ tags: ['Chapter Assignments'], method: 'patch', - path: '/projects/{id}/units/{unitId}/assignments/{chapterAssignmentId}/ai-status', + path: '/chapter-assignments/{chapterAssignmentId}/ai-status', middleware: [ authenticateUser, requirePermission(PERMISSIONS.CONTENT_UPDATE), @@ -316,13 +315,7 @@ const updateAiStatusRoute = createRoute({ description: 'Project Manager only. Enables or disables AI translation suggestions for a specific chapter assignment.', request: { - params: z.object({ - id: z.coerce.number().openapi({ param: { name: 'id', in: 'path', required: true } }), - unitId: z.coerce.number().openapi({ param: { name: 'unitId', in: 'path', required: true } }), - chapterAssignmentId: z.coerce - .number() - .openapi({ param: { name: 'chapterAssignmentId', in: 'path', required: true } }), - }), + params: chapterAssignmentIdParam, body: jsonContentRequired(updateChapterAssignmentAiStatusSchema, 'AI Status update'), }, responses: { diff --git a/src/domains/chapter-assignments/chapter-assignments.service.ts b/src/domains/chapter-assignments/chapter-assignments.service.ts index 8cdccfc..410976a 100644 --- a/src/domains/chapter-assignments/chapter-assignments.service.ts +++ b/src/domains/chapter-assignments/chapter-assignments.service.ts @@ -1,9 +1,6 @@ -import { eq } from 'drizzle-orm'; - import type { DbTransaction, Result } from '@/lib/types'; import { db } from '@/db'; -import { chapter_assignments } from '@/db/schema'; import * as aiSuggestionsService from '@/domains/ai-suggestions/ai-suggestions.service'; import { logger } from '@/lib/logger'; import { err, ErrorCode, ok } from '@/lib/types'; @@ -319,38 +316,27 @@ export async function toggleChapterAssignmentAiStatus( isAiEnabled: boolean ): Promise> { try { - const assignment = await db - .select({ - isAiEnabled: chapter_assignments.isAiEnabled, - projectUnitId: chapter_assignments.projectUnitId, - bibleId: chapter_assignments.bibleId, - bookId: chapter_assignments.bookId, - chapterNumber: chapter_assignments.chapterNumber, - }) - .from(chapter_assignments) - .where(eq(chapter_assignments.id, assignmentId)) - .limit(1); - - if (!assignment[0]) { - return err(ErrorCode.NOT_FOUND); + const assignment = await repo.findById(assignmentId); + + if (!assignment) { + return err(ErrorCode.CHAPTER_ASSIGNMENT_NOT_FOUND); } - if (assignment[0].isAiEnabled === isAiEnabled) { + if (assignment.isAiEnabled === isAiEnabled) { return ok(undefined); } - await db - .update(chapter_assignments) - .set({ isAiEnabled }) - .where(eq(chapter_assignments.id, assignmentId)); + await db.transaction(async (tx) => { + await repo.update(assignmentId, { isAiEnabled }, tx); + }); if (isAiEnabled) { // Fire and forget initial queue aiSuggestionsService.handleChapterAssigned( - assignment[0].projectUnitId, - assignment[0].bibleId, - assignment[0].bookId, - assignment[0].chapterNumber + assignment.projectUnitId, + assignment.bibleId, + assignment.bookId, + assignment.chapterNumber ); } diff --git a/src/domains/chapter-assignments/chapter-assignments.types.ts b/src/domains/chapter-assignments/chapter-assignments.types.ts index 434fc9f..da2d7ae 100644 --- a/src/domains/chapter-assignments/chapter-assignments.types.ts +++ b/src/domains/chapter-assignments/chapter-assignments.types.ts @@ -61,6 +61,7 @@ export interface ChapterAssignmentProgressInfo { submittedTime: Date | null; createdAt: Date | null; updatedAt: Date | null; + isAiEnabled: boolean; } // ─── Service input types ────────────────────────────────────────────────────── @@ -79,6 +80,7 @@ export interface UpdateChapterAssignmentRequestData { peerCheckerId?: number | null; status?: ChapterAssignmentStatus; submittedTime?: Date; + isAiEnabled?: boolean; } export const updateChapterAssignmentAiStatusSchema = z.object({ diff --git a/src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts b/src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts index 65a66a9..0a61f4a 100644 --- a/src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts +++ b/src/domains/projects/chapter-assignments/project-chapter-assignments.service.ts @@ -52,6 +52,7 @@ export async function getChapterAssignmentProgressByProject(projectId: number) { createdAt: info.createdAt, updatedAt: info.updatedAt, submittedTime: info.submittedTime, + isAiEnabled: info.isAiEnabled, })); return ok(mapped); } @@ -190,6 +191,7 @@ export async function assignSelectedChapters( createdAt: info.createdAt, updatedAt: info.updatedAt, submittedTime: info.submittedTime, + isAiEnabled: info.isAiEnabled, })); return ok(mapped); diff --git a/src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts b/src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts index 4b03cf8..36a095d 100644 --- a/src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts +++ b/src/domains/projects/chapter-assignments/project-chapter-assignments.types.ts @@ -28,6 +28,7 @@ export const chapterAssignmentProgressResponseSchema = z.object({ submittedTime: z.date().nullable(), createdAt: z.date().nullable(), updatedAt: z.date().nullable(), + isAiEnabled: z.boolean(), }); // ─── Assign-all input ───────────────────────────────────────────────────────── diff --git a/src/domains/users/chapter-assignments/users-chapter-assignments.service.ts b/src/domains/users/chapter-assignments/users-chapter-assignments.service.ts index ffbc354..cc18329 100644 --- a/src/domains/users/chapter-assignments/users-chapter-assignments.service.ts +++ b/src/domains/users/chapter-assignments/users-chapter-assignments.service.ts @@ -33,6 +33,7 @@ export function toResponse( assignedUserId: assignment.assignedUserId, peerCheckerId: assignment.peerCheckerId, updatedAt: assignment.updatedAt?.toISOString() ?? null, + isAiEnabled: assignment.isAiEnabled, }; } diff --git a/src/domains/users/chapter-assignments/users-chapter-assignments.types.ts b/src/domains/users/chapter-assignments/users-chapter-assignments.types.ts index 3eb8a70..1c6de95 100644 --- a/src/domains/users/chapter-assignments/users-chapter-assignments.types.ts +++ b/src/domains/users/chapter-assignments/users-chapter-assignments.types.ts @@ -21,6 +21,7 @@ export interface UserChapterAssignment { assignedUserId: number | null; peerCheckerId: number | null; updatedAt: string | null; + isAiEnabled: boolean; } // ─── API response schema ────────────────────────────────────────────────────── @@ -44,6 +45,7 @@ export const userChapterAssignmentResponseSchema = z.object({ assignedUserId: z.number().int().nullable(), peerCheckerId: z.number().int().nullable(), updatedAt: z.string().nullable(), + isAiEnabled: z.boolean(), }); export const userChapterAssignmentsByUserResponseSchema = z.object({