diff --git a/.env.example b/.env.example index a51ddf59..6faeac56 100644 --- a/.env.example +++ b/.env.example @@ -36,3 +36,14 @@ FLUENT_AI_KEY=fai_dev_admin # (e.g. POST /tools/greek-room/repeated-words), so leave this empty. If/when # fluent-ai adopts versioned routing, set this to /api/v1 — no code change needed. FLUENT_AI_API_PREFIX= + +# Cloudflare R2 — audio recording storage +# Account ID found in Cloudflare dashboard → R2 → Overview +R2_ACCOUNT_ID=your-cloudflare-account-id +# S3-compatible Access Key ID generated in R2 → Manage R2 API Tokens +R2_ACCESS_KEY_ID=your-r2-access-key-id +# Corresponding secret access key +R2_SECRET_ACCESS_KEY=your-r2-secret-access-key +# Name of the R2 bucket to store audio recordings +R2_BUCKET_NAME=your-bucket-name + diff --git a/.env.test b/.env.test index a5c6c0a9..6f08ac46 100644 --- a/.env.test +++ b/.env.test @@ -13,3 +13,9 @@ EMAIL_SERVICE_SENDER=no-reply@test.example.com FRONTEND_URL=http://localhost:5173 FLUENT_AI_URL=http://localhost:8200 FLUENT_AI_KEY=test-only-dummy-fluent-ai-key + +# R2 credentials for testing +R2_ACCOUNT_ID=test-r2-account-id +R2_ACCESS_KEY_ID=test-r2-access-key-id +R2_SECRET_ACCESS_KEY=test-r2-secret-access-key +R2_BUCKET_NAME=test-r2-bucket-name diff --git a/src/app.ts b/src/app.ts index c88eff93..8466bd42 100644 --- a/src/app.ts +++ b/src/app.ts @@ -21,6 +21,7 @@ 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-tools/ai-tools.route'; +import '@/domains/recordings/recordings.route'; configureOpenAPI(server); export default server; diff --git a/src/db/migrations/0012_add_recordings.sql b/src/db/migrations/0012_add_recordings.sql new file mode 100644 index 00000000..4d1e207c --- /dev/null +++ b/src/db/migrations/0012_add_recordings.sql @@ -0,0 +1,16 @@ +CREATE TABLE "recordings" ( + "id" serial PRIMARY KEY NOT NULL, + "project_unit_id" integer NOT NULL, + "bible_text_id" integer NOT NULL, + "relative_path" varchar NOT NULL, + "recorded_by_user_id" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "recordings_relative_path_unique" UNIQUE("relative_path") +); +--> statement-breakpoint +ALTER TABLE "recordings" ADD CONSTRAINT "recordings_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 "recordings" ADD CONSTRAINT "recordings_bible_text_id_bible_texts_id_fk" FOREIGN KEY ("bible_text_id") REFERENCES "public"."bible_texts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "recordings" ADD CONSTRAINT "recordings_recorded_by_user_id_users_id_fk" FOREIGN KEY ("recorded_by_user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_recordings_project_unit" ON "recordings" USING btree ("project_unit_id");--> statement-breakpoint +CREATE INDEX "idx_recordings_bible_text" ON "recordings" USING btree ("bible_text_id");--> statement-breakpoint +CREATE INDEX "idx_recordings_user" ON "recordings" USING btree ("recorded_by_user_id"); \ No newline at end of file diff --git a/src/db/migrations/0013_add_metadata_to_recordings.sql b/src/db/migrations/0013_add_metadata_to_recordings.sql new file mode 100644 index 00000000..b20c05eb --- /dev/null +++ b/src/db/migrations/0013_add_metadata_to_recordings.sql @@ -0,0 +1 @@ +ALTER TABLE "recordings" ADD COLUMN "metadata" jsonb; \ No newline at end of file diff --git a/src/db/migrations/meta/0012_snapshot.json b/src/db/migrations/meta/0012_snapshot.json new file mode 100644 index 00000000..e5100e4e --- /dev/null +++ b/src/db/migrations/meta/0012_snapshot.json @@ -0,0 +1,2419 @@ +{ + "id": "097dc57b-deea-4274-bafe-4b854a0ad1ff", + "prevId": "f6a47766-9387-4565-93c4-9237d431a0e4", + "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.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()" + }, + "is_mobile": { + "name": "is_mobile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "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'" + }, + "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.recordings": { + "name": "recordings", + "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_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "relative_path": { + "name": "relative_path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "recorded_by_user_id": { + "name": "recorded_by_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_recordings_project_unit": { + "name": "idx_recordings_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recordings_bible_text": { + "name": "idx_recordings_bible_text", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recordings_user": { + "name": "idx_recordings_user", + "columns": [ + { + "expression": "recorded_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recordings_project_unit_id_project_units_id_fk": { + "name": "recordings_project_unit_id_project_units_id_fk", + "tableFrom": "recordings", + "tableTo": "project_units", + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recordings_bible_text_id_bible_texts_id_fk": { + "name": "recordings_bible_text_id_bible_texts_id_fk", + "tableFrom": "recordings", + "tableTo": "bible_texts", + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "recordings_recorded_by_user_id_users_id_fk": { + "name": "recordings_recorded_by_user_id_users_id_fk", + "tableFrom": "recordings", + "tableTo": "users", + "columnsFrom": ["recorded_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "recordings_relative_path_unique": { + "name": "recordings_relative_path_unique", + "nullsNotDistinct": false, + "columns": ["relative_path"] + } + }, + "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.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": {} + } +} diff --git a/src/db/migrations/meta/0013_snapshot.json b/src/db/migrations/meta/0013_snapshot.json new file mode 100644 index 00000000..a60b29bc --- /dev/null +++ b/src/db/migrations/meta/0013_snapshot.json @@ -0,0 +1,2425 @@ +{ + "id": "32c46bec-1f67-49cb-86e7-a95f202ff5b4", + "prevId": "097dc57b-deea-4274-bafe-4b854a0ad1ff", + "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.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()" + }, + "is_mobile": { + "name": "is_mobile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "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'" + }, + "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.recordings": { + "name": "recordings", + "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_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "relative_path": { + "name": "relative_path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "recorded_by_user_id": { + "name": "recorded_by_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_recordings_project_unit": { + "name": "idx_recordings_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recordings_bible_text": { + "name": "idx_recordings_bible_text", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_recordings_user": { + "name": "idx_recordings_user", + "columns": [ + { + "expression": "recorded_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recordings_project_unit_id_project_units_id_fk": { + "name": "recordings_project_unit_id_project_units_id_fk", + "tableFrom": "recordings", + "tableTo": "project_units", + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "recordings_bible_text_id_bible_texts_id_fk": { + "name": "recordings_bible_text_id_bible_texts_id_fk", + "tableFrom": "recordings", + "tableTo": "bible_texts", + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "recordings_recorded_by_user_id_users_id_fk": { + "name": "recordings_recorded_by_user_id_users_id_fk", + "tableFrom": "recordings", + "tableTo": "users", + "columnsFrom": ["recorded_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "recordings_relative_path_unique": { + "name": "recordings_relative_path_unique", + "nullsNotDistinct": false, + "columns": ["relative_path"] + } + }, + "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.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": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 0eb2cd70..8f6c248e 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -85,6 +85,20 @@ "when": 1779441453059, "tag": "0011_add_is_mobile_to_auth_session", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1781609375158, + "tag": "0012_add_recordings", + "breakpoints": true + }, + { + "idx": 13, + "version": "7", + "when": 1781682962147, + "tag": "0013_add_metadata_to_recordings", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index a0799fcb..74df1cd3 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -496,6 +496,35 @@ export const active_chapter_editors = pgTable( ] ); +// ─── Recordings ─────────────────────────────────────────────────────────────── +// Each row represents one audio recording uploaded from the mobile app. +// `relative_path` is unique and serves as the Cloudflare R2 object key. + +export const recordings = pgTable( + 'recordings', + { + id: serial('id').primaryKey(), + projectUnitId: integer('project_unit_id') + .notNull() + .references(() => project_units.id, { onDelete: 'cascade' }), + bibleTextId: integer('bible_text_id') + .notNull() + .references(() => bible_texts.id), + // R2 object key — e.g. "4-Hindi_NT_2024/audio/MAT/1/3.m4a" + relativePath: varchar('relative_path').notNull().unique(), + recordedByUserId: integer('recorded_by_user_id') + .notNull() + .references(() => users.id), + metadata: jsonb('metadata').$type<{ size: number | null; recorded_at: string }>(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + index('idx_recordings_project_unit').on(table.projectUnitId), + index('idx_recordings_bible_text').on(table.bibleTextId), + index('idx_recordings_user').on(table.recordedByUserId), + ] +); + const { createInsertSchema, createSelectSchema } = createSchemaFactory({ zodInstance: z, }); @@ -529,6 +558,25 @@ export const selectProjectUsersSchema = createSelectSchema(project_users); export const selectPermissionsSchema = createSelectSchema(permissions); export const selectRolePermissionsSchema = createSelectSchema(role_permissions); export const selectActiveChapterEditorsSchema = createSelectSchema(active_chapter_editors); +export const selectRecordingsSchema = createSelectSchema(recordings); + +export const insertRecordingsSchema = createInsertSchema(recordings, { + projectUnitId: (schema) => schema.int().positive(), + bibleTextId: (schema) => schema.int().positive(), + relativePath: (schema) => schema.min(1), + recordedByUserId: (schema) => schema.int().positive(), + metadata: () => + z + .object({ + size: z.number().nullable(), + recorded_at: z.string(), + }) + .nullable() + .optional(), +}).omit({ + id: true, + createdAt: true, +}); export const insertUsersSchema = createInsertSchema(users, { username: (schema) => schema.min(1).max(100), diff --git a/src/domains/recordings/recordings.repository.ts b/src/domains/recordings/recordings.repository.ts new file mode 100644 index 00000000..2e3ce800 --- /dev/null +++ b/src/domains/recordings/recordings.repository.ts @@ -0,0 +1,69 @@ +import { sql } from 'drizzle-orm'; + +import { db } from '@/db'; +import * as schema from '@/db/schema'; +import { logger } from '@/lib/logger'; + +import type { UpsertRecordingData } from './recordings.types'; + +// ─── Queries ────────────────────────────────────────────────────────────────── + +export async function upsertRecording( + data: UpsertRecordingData +): Promise<{ relativePath: string }> { + const { projectUnitId, bibleTextId, relativePath, recordedByUserId, metadata } = data; + + await db + .insert(schema.recordings) + .values({ + projectUnitId, + bibleTextId, + relativePath, + recordedByUserId, + metadata: metadata ?? null, + }) + .onConflictDoUpdate({ + target: schema.recordings.relativePath, + set: { + metadata: sql`excluded.metadata`, + recordedByUserId: sql`excluded.recorded_by_user_id`, + }, + }); + + logger.info('Recording upserted in DB', { relativePath, projectUnitId, bibleTextId }); + + return { relativePath }; +} + +// ─── COMMENTED OUT — assignment validation (pending model clarification) ─────── +// +// Validates that the calling user is the assigned translator for the chapter +// that contains the given bible_text_id + project_unit_id pair. +// +// export async function validateAssignment( +// projectUnitId: number, +// bibleTextId: number, +// userId: number, +// ): Promise { +// const rows = await db +// .select({ id: schema.chapter_assignments.id }) +// .from(schema.chapter_assignments) +// .innerJoin( +// schema.bible_texts, +// and( +// eq(schema.bible_texts.bibleId, schema.chapter_assignments.bibleId), +// eq(schema.bible_texts.bookId, schema.chapter_assignments.bookId), +// eq(schema.bible_texts.chapterNumber, schema.chapter_assignments.chapterNumber), +// ), +// ) +// .where( +// and( +// eq(schema.chapter_assignments.projectUnitId, projectUnitId), +// eq(schema.bible_texts.id, bibleTextId), +// eq(schema.chapter_assignments.assignedUserId, userId), +// ), +// ) +// .limit(1); +// +// return rows.length > 0; +// } diff --git a/src/domains/recordings/recordings.route.ts b/src/domains/recordings/recordings.route.ts new file mode 100644 index 00000000..b986f3b9 --- /dev/null +++ b/src/domains/recordings/recordings.route.ts @@ -0,0 +1,151 @@ +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 { logger } from '@/lib/logger'; +import { authenticateUser } from '@/middlewares/role-auth'; +import { server } from '@/server/server'; + +import * as service from './recordings.service'; +import { syncSuccessSchema } from './recordings.types'; + +// ─── Route definition ───────────────────────────────────────────────────────── + +const syncRecordingRoute = createRoute({ + tags: ['Recordings'], + method: 'post', + path: '/recordings/sync', + middleware: [authenticateUser] as const, + responses: { + [HttpStatusCodes.OK]: jsonContent( + syncSuccessSchema, + 'Recording uploaded to Cloudflare R2 and registered in the database.' + ), + [HttpStatusCodes.BAD_REQUEST]: jsonContent( + createMessageObjectSchema('Bad Request'), + 'Missing or invalid form fields.' + ), + [HttpStatusCodes.UNAUTHORIZED]: jsonContent( + createMessageObjectSchema('Unauthorized'), + 'Missing or invalid Bearer token.' + ), + [HttpStatusCodes.FORBIDDEN]: jsonContent( + createMessageObjectSchema('Forbidden'), + 'User account is inactive.' + ), + [HttpStatusCodes.NOT_FOUND]: jsonContent( + createMessageObjectSchema('Not Found'), + 'Project unit not found.' + ), + [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonContent( + createMessageObjectSchema(HttpStatusPhrases.INTERNAL_SERVER_ERROR), + 'R2 upload or database error.' + ), + }, + summary: 'Sync a mobile audio recording to cloud storage', + description: + 'Accepts multipart/form-data with fields: `project_unit_id` (text), ' + + '`bible_text_id` (text), `relative_path` (text), `file` (binary .m4a), ' + + '`file_size` (text, optional), `recorded_at` (ISO 8601 text, optional). ' + + "The caller's identity is resolved entirely from the Authorization Bearer token — " + + 'no user_id field is accepted in the request body.', +}); + +// ─── Handler ────────────────────────────────────────────────────────────────── + +server.openapi(syncRecordingRoute, async (c) => { + const user = c.get('user')!; + + // ── 1. Parse multipart body ──────────────────────────────────────────────── + let body: Record; + try { + body = await c.req.parseBody(); + } catch { + logger.warn('Failed to parse multipart body', { userId: user.id }); + return c.json({ message: 'Invalid multipart request body.' }, HttpStatusCodes.BAD_REQUEST); + } + + const projectUnitIdRaw = (body as any).project_unit_id; + const bibleTextIdRaw = (body as any).bible_text_id; + const relativePath = (body as any).relative_path; + const file = (body as any).file; + const fileSizeRaw = (body as any).file_size; + const recordedAtRaw = (body as any).recorded_at; + + // ── 2. Validate required fields ──────────────────────────────────────────── + if ( + typeof projectUnitIdRaw !== 'string' || + typeof bibleTextIdRaw !== 'string' || + typeof relativePath !== 'string' || + !(file instanceof Blob) + ) { + return c.json( + { message: 'Missing required fields: project_unit_id, bible_text_id, relative_path, file.' }, + HttpStatusCodes.BAD_REQUEST + ); + } + + const projectUnitId = Number(projectUnitIdRaw); + const bibleTextId = Number(bibleTextIdRaw); + + if (!Number.isInteger(projectUnitId) || projectUnitId <= 0) { + return c.json( + { message: 'project_unit_id must be a positive integer.' }, + HttpStatusCodes.BAD_REQUEST + ); + } + + if (!Number.isInteger(bibleTextId) || bibleTextId <= 0) { + return c.json( + { message: 'bible_text_id must be a positive integer.' }, + HttpStatusCodes.BAD_REQUEST + ); + } + + if (!relativePath.trim()) { + return c.json({ message: 'relative_path must not be empty.' }, HttpStatusCodes.BAD_REQUEST); + } + + // ── 3. Delegate to service ───────────────────────────────────────────────── + try { + await service.syncRecording({ + projectUnitId, + bibleTextId, + relativePath, + file, + fileSizeRaw, + recordedAtRaw, + userId: user.id, + }); + + return c.json( + { + success: true as const, + message: 'Audio recording synchronized and saved cleanly.', + data: { relative_path: relativePath }, + }, + HttpStatusCodes.OK + ); + } catch (error: any) { + if (error?.code === 'NOT_FOUND') { + return c.json({ message: error.message }, HttpStatusCodes.NOT_FOUND); + } + if (error?.code === 'INVALID_PATH') { + return c.json({ message: error.message }, HttpStatusCodes.BAD_REQUEST); + } + if (error?.code === 'FORBIDDEN') { + return c.json({ message: error.message }, HttpStatusCodes.FORBIDDEN); + } + logger.error('Recording sync failed', { + userId: user.id, + projectUnitId, + error: error instanceof Error ? error.message : String(error), + }); + return c.json( + { message: 'An unexpected error occurred.' }, + HttpStatusCodes.INTERNAL_SERVER_ERROR + ); + } +}); diff --git a/src/domains/recordings/recordings.service.ts b/src/domains/recordings/recordings.service.ts new file mode 100644 index 00000000..5e008e23 --- /dev/null +++ b/src/domains/recordings/recordings.service.ts @@ -0,0 +1,166 @@ +import { eq } from 'drizzle-orm'; +import { Buffer } from 'node:buffer'; + +import { db } from '@/db'; +import * as schema from '@/db/schema'; +import { logger } from '@/lib/logger'; +import { uploadToR2 } from '@/lib/r2-upload'; + +import type { RecordingMetadata, UpsertRecordingData } from './recordings.types'; + +import * as repo from './recordings.repository'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface SyncRecordingInput { + projectUnitId: number; + bibleTextId: number; + relativePath: string; + file: Blob; + fileSizeRaw: unknown; + recordedAtRaw: unknown; + userId: number; +} + +export interface SyncRecordingResult { + transformedRelativePath: string; +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Transforms the client-supplied relative path into a server-side R2 key: + * "/" → "-/" + */ +export function buildR2Key(relativePath: string, projectUnitId: number): string | null { + const cleanPath = relativePath.replace(/^\/+/, ''); + const firstSlashIdx = cleanPath.indexOf('/'); + if (firstSlashIdx === -1) return null; + + const projectName = cleanPath.substring(0, firstSlashIdx); + const rest = cleanPath.substring(firstSlashIdx + 1); + if (!rest.trim()) return null; + return `${projectUnitId}-${projectName}/${rest}`; +} + +/** + * Builds the optional device metadata object from raw form-field values. + */ +export function buildMetadata(fileSizeRaw: unknown, recordedAtRaw: unknown): RecordingMetadata { + const fileSize = fileSizeRaw != null ? Number(fileSizeRaw) : null; + const recordedAt = + typeof recordedAtRaw === 'string' && recordedAtRaw.trim() + ? recordedAtRaw.trim() + : new Date().toISOString(); + + return { size: Number.isFinite(fileSize) ? fileSize : null, recorded_at: recordedAt }; +} + +// ─── Service functions ──────────────────────────────────────────────────────── + +/** + * Verifies that the given projectUnitId exists in the database. + * Returns true if found, false otherwise. + * + * TODO: The userId parameter is currently unused in the lookup, but will be + * utilized once assignment validation checks are uncommented and enforced. + */ +export async function verifyProjectUnitExists( + projectUnitId: number, + userId: number +): Promise { + try { + const result = await db + .select({ id: schema.project_units.id }) + .from(schema.project_units) + .where(eq(schema.project_units.id, projectUnitId)) + .limit(1); + + return result.length > 0; + } catch (error) { + logger.error('Failed to query project unit', { error, projectUnitId, userId }); + throw error; + } +} + +/** + * Orchestrates the full audio sync workflow: + * 1. Validates the project unit exists. + * 2. Builds the R2 key from the relative path. + * 3. Uploads the audio buffer to Cloudflare R2. + * 4. Upserts the recording record in PostgreSQL. + * + * Returns the transformed R2 key on success. + * Throws on any unrecoverable error so the route can map it to an HTTP response. + */ +export async function syncRecording(input: SyncRecordingInput): Promise { + const { projectUnitId, bibleTextId, relativePath, file, fileSizeRaw, recordedAtRaw, userId } = + input; + + // 1. Verify project unit + const exists = await verifyProjectUnitExists(projectUnitId, userId); + if (!exists) { + throw Object.assign(new Error('Project unit not found.'), { code: 'NOT_FOUND' }); + } + + // 2. Build R2 key + const transformedRelativePath = buildR2Key(relativePath, projectUnitId); + if (!transformedRelativePath) { + throw Object.assign(new Error('Invalid relative_path format.'), { code: 'INVALID_PATH' }); + } + + // ── COMMENTED OUT — Assignment validation (pending model clarification) ───── + // + // Confirms the Bearer-token user is the assigned translator for this chapter. + // Uncomment once the assignment model is finalised. + // + // const isAssigned = await repo.validateAssignment(projectUnitId, bibleTextId, userId); + // if (!isAssigned) { + // logger.warn('Unauthorized upload attempt: user not assigned to chapter', { + // userId, projectUnitId, bibleTextId, + // }); + // throw Object.assign( + // new Error('You are not assigned to this translation task.'), + // { code: 'FORBIDDEN' }, + // ); + // } + + // 3. Upload to Cloudflare R2 + const audioBuffer = Buffer.from(await file.arrayBuffer()); + const contentType = file.type || 'audio/mp4'; + await uploadToR2(audioBuffer, transformedRelativePath, contentType); + + // 4. Upsert in PostgreSQL + const metadata = buildMetadata(fileSizeRaw, recordedAtRaw); + + const upsertData: UpsertRecordingData = { + projectUnitId, + bibleTextId, + relativePath: transformedRelativePath, + recordedByUserId: userId, + metadata, + }; + + try { + await repo.upsertRecording(upsertData); + } catch (error) { + logger.error('DB upsert failed after successful R2 upload', { + transformedRelativePath, + userId, + error: error instanceof Error ? error.message : String(error), + }); + throw Object.assign(new Error('Recording uploaded but failed to register in database.'), { + code: 'DB_ERROR', + }); + } + + logger.info('Recording synced successfully', { + transformedRelativePath, + userId, + projectUnitId, + bibleTextId, + sizeBytes: audioBuffer.length, + }); + + return { transformedRelativePath }; +} diff --git a/src/domains/recordings/recordings.types.ts b/src/domains/recordings/recordings.types.ts new file mode 100644 index 00000000..c581c04e --- /dev/null +++ b/src/domains/recordings/recordings.types.ts @@ -0,0 +1,30 @@ +import { z } from '@hono/zod-openapi'; + +// ─── Request / Domain types ─────────────────────────────────────────────────── + +export interface RecordingMetadata { + size: number | null; + recorded_at: string; +} + +export interface UpsertRecordingData { + projectUnitId: number; + bibleTextId: number; + relativePath: string; + recordedByUserId: number; + metadata?: RecordingMetadata; +} + +// ─── Response schemas ───────────────────────────────────────────────────────── + +export const syncSuccessSchema = z.object({ + success: z.literal(true), + message: z.string(), + data: z.object({ + relative_path: z.string(), + }), +}); + +// ─── Inferred types ─────────────────────────────────────────────────────────── + +export type SyncSuccessResponse = z.infer; diff --git a/src/env.ts b/src/env.ts index ca05521c..eebdfd98 100644 --- a/src/env.ts +++ b/src/env.ts @@ -41,6 +41,12 @@ const EnvSchema = z.object({ // '/api/v1' via env with no code change. Leading slash optional; trailing // slashes are trimmed when the request URL is assembled. FLUENT_AI_API_PREFIX: z.string().default(''), + + // ── Cloudflare R2 audio storage ─────────────────────────────────────────── + R2_ACCOUNT_ID: z.string().min(1), + R2_ACCESS_KEY_ID: z.string().min(1), + R2_SECRET_ACCESS_KEY: z.string().min(1), + R2_BUCKET_NAME: z.string().min(1), }); export type env = z.infer; diff --git a/src/lib/r2-upload.ts b/src/lib/r2-upload.ts new file mode 100644 index 00000000..d3ac3163 --- /dev/null +++ b/src/lib/r2-upload.ts @@ -0,0 +1,206 @@ +/** + * r2-upload.ts + * + * Server-side Cloudflare R2 upload using AWS Signature V4. + * Uses Node's native `node:crypto` — no external packages or hand-rolled crypto. + * + * Credentials are read from the validated env schema (never hardcoded). + */ + +import { Buffer } from 'node:buffer'; +import { createHash, createHmac } from 'node:crypto'; + +import env from '@/env'; +import { logger } from '@/lib/logger'; + +// ─── Crypto helpers ─────────────────────────────────────────────────────────── + +function sha256Hex(data: string | Buffer): string { + return createHash('sha256').update(data).digest('hex'); +} + +function hmacSha256(key: Buffer | string, data: string): Buffer { + const keyBuf = typeof key === 'string' ? Buffer.from(key, 'utf8') : key; + return createHmac('sha256', keyBuf).update(data, 'utf8').digest(); +} + +function toHex(buf: Buffer): string { + return buf.toString('hex'); +} + +// ─── Date helpers ───────────────────────────────────────────────────────────── + +function pad2(n: number): string { + return String(n).padStart(2, '0'); +} + +function getAmzTimestamps(now: Date): { dateStamp: string; amzDate: string } { + const dateStamp = + `${now.getUTCFullYear()}` + `${pad2(now.getUTCMonth() + 1)}` + `${pad2(now.getUTCDate())}`; + + const amzDate = + `${dateStamp}T` + + `${pad2(now.getUTCHours())}` + + `${pad2(now.getUTCMinutes())}` + + `${pad2(now.getUTCSeconds())}Z`; + + return { dateStamp, amzDate }; +} + +// ─── SigV4 builder ──────────────────────────────────────────────────────────── + +function buildAuthorizationHeader(params: { + method: string; + host: string; + urlPath: string; + contentType: string; + payloadHash: string; + amzDate: string; + dateStamp: string; + accessKeyId: string; + secretAccessKey: string; +}): string { + const { + method, + host, + urlPath, + contentType, + payloadHash, + amzDate, + dateStamp, + accessKeyId, + secretAccessKey, + } = params; + + const region = 'auto'; + const service = 's3'; + const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date'; + + // Canonical headers must be sorted alphabetically by header name + const canonicalHeaders = + `content-type:${contentType}\n` + + `host:${host}\n` + + `x-amz-content-sha256:${payloadHash}\n` + + `x-amz-date:${amzDate}\n`; + + const canonicalRequest = [ + method, + urlPath, + '', // query string (empty for direct PUT) + canonicalHeaders, + signedHeaders, + payloadHash, + ].join('\n'); + + const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`; + + const stringToSign = [ + 'AWS4-HMAC-SHA256', + amzDate, + credentialScope, + sha256Hex(canonicalRequest), + ].join('\n'); + + // Derive signing key + const kDate = hmacSha256(`AWS4${secretAccessKey}`, dateStamp); + const kRegion = hmacSha256(kDate, region); + const kService = hmacSha256(kRegion, service); + const kSigning = hmacSha256(kService, 'aws4_request'); + const signature = toHex(hmacSha256(kSigning, stringToSign)); + + return ( + `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, ` + + `SignedHeaders=${signedHeaders}, Signature=${signature}` + ); +} + +// ─── Public API ─────────────────────────────────────────────────────────────── + +/** + * Uploads a binary buffer to Cloudflare R2 using AWS Signature V4. + * + * @param buffer The raw audio file content. + * @param r2Key Object key in the bucket (used verbatim — must be the + * `relative_path` sent by the mobile client). + * @param contentType MIME type, e.g. 'audio/mp4'. Defaults to 'audio/mp4'. + * + * @throws Error with the R2 error body if the upload is rejected. + */ +export async function uploadToR2( + buffer: Buffer, + r2Key: string, + contentType: string = 'audio/mp4' +): Promise { + const accountId = env.R2_ACCOUNT_ID; + const accessKeyId = env.R2_ACCESS_KEY_ID; + const secretAccessKey = env.R2_SECRET_ACCESS_KEY; + const bucket = env.R2_BUCKET_NAME; + + const host = `${accountId}.r2.cloudflarestorage.com`; + + // Encode each path segment individually to preserve slashes as separators + const encodedKey = r2Key.split('/').map(encodeURIComponent).join('/'); + + const urlPath = `/${bucket}/${encodedKey}`; + const url = `https://${host}${urlPath}`; + + const { dateStamp, amzDate } = getAmzTimestamps(new Date()); + const payloadHash = sha256Hex(buffer); + + const authorization = buildAuthorizationHeader({ + method: 'PUT', + host, + urlPath, + contentType, + payloadHash, + amzDate, + dateStamp, + accessKeyId, + secretAccessKey, + }); + + logger.info('Uploading to R2', { + r2Key, + contentType, + sizeBytes: buffer.length, + bucket, + }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 15000); + + let response: Response; + try { + response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': contentType, + 'x-amz-date': amzDate, + 'x-amz-content-sha256': payloadHash, + Authorization: authorization, + }, + body: buffer, + signal: controller.signal, + }); + } catch (networkError) { + logger.error('R2 network request failed', { r2Key, error: networkError }); + throw new Error( + `R2 network error: ${networkError instanceof Error ? networkError.message : String(networkError)}` + ); + } finally { + clearTimeout(timeoutId); + } + + if (!response.ok) { + const errorBody = await response.text().catch(() => '(could not read response body)'); + logger.error('R2 rejected upload', { + r2Key, + status: response.status, + statusText: response.statusText, + errorBody, + }); + throw new Error(`R2 upload failed [HTTP ${response.status}]: ${errorBody}`); + } + + logger.info('R2 upload successful', { r2Key, sizeBytes: buffer.length }); +}