diff --git a/src/db/migrations/0028_add_audio_availability.sql b/src/db/migrations/0028_add_audio_availability.sql new file mode 100644 index 00000000..1f1785c4 --- /dev/null +++ b/src/db/migrations/0028_add_audio_availability.sql @@ -0,0 +1,2 @@ +ALTER TABLE "bible_books" ADD COLUMN "has_audio" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "bibles" ADD COLUMN "has_audio" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/meta/0028_snapshot.json b/src/db/migrations/meta/0028_snapshot.json new file mode 100644 index 00000000..c3cd4141 --- /dev/null +++ b/src/db/migrations/meta/0028_snapshot.json @@ -0,0 +1,3436 @@ +{ + "id": "af942093-a73e-4844-902e-66d138a8dda9", + "prevId": "1a5633c5-a85c-4a7c-ac6f-db75b70d0a18", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.active_chapter_editors": { + "name": "active_chapter_editors", + "schema": "", + "columns": { + "chapter_assignment_id": { + "name": "chapter_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_heartbeat": { + "name": "last_heartbeat", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_active_editors_chapter": { + "name": "idx_active_editors_chapter", + "columns": [ + { + "expression": "chapter_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk": { + "name": "active_chapter_editors_chapter_assignment_id_chapter_assignments_id_fk", + "tableFrom": "active_chapter_editors", + "tableTo": "chapter_assignments", + "columnsFrom": ["chapter_assignment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "active_chapter_editors_user_id_users_id_fk": { + "name": "active_chapter_editors_user_id_users_id_fk", + "tableFrom": "active_chapter_editors", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "active_chapter_editors_chapter_assignment_id_user_id_pk": { + "name": "active_chapter_editors_chapter_assignment_id_user_id_pk", + "columns": ["chapter_assignment_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_suggestion_usage_log": { + "name": "ai_suggestion_usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "was_used": { + "name": "was_used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_ai_usage_user": { + "name": "idx_ai_usage_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_ai_usage_project_unit": { + "name": "idx_ai_usage_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_usage_user_text": { + "name": "uq_ai_usage_user_text", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestion_usage_log_user_id_users_id_fk": { + "name": "ai_suggestion_usage_log_user_id_users_id_fk", + "tableFrom": "ai_suggestion_usage_log", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk": { + "name": "ai_suggestion_usage_log_bible_text_id_bible_texts_id_fk", + "tableFrom": "ai_suggestion_usage_log", + "tableTo": "bible_texts", + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestion_usage_log_project_unit_id_project_units_id_fk": { + "name": "ai_suggestion_usage_log_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestion_usage_log", + "tableTo": "project_units", + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_suggestions": { + "name": "ai_suggestions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bible_text_id": { + "name": "bible_text_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_unit_id": { + "name": "project_unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "suggested_text": { + "name": "suggested_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model_info": { + "name": "model_info", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_ai_suggestions_bible_text": { + "name": "idx_ai_suggestions_bible_text", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_ai_suggestions_per_text_unit": { + "name": "uq_ai_suggestions_per_text_unit", + "columns": [ + { + "expression": "bible_text_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_suggestions_bible_text_id_bible_texts_id_fk": { + "name": "ai_suggestions_bible_text_id_bible_texts_id_fk", + "tableFrom": "ai_suggestions", + "tableTo": "bible_texts", + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ai_suggestions_project_unit_id_project_units_id_fk": { + "name": "ai_suggestions_project_unit_id_project_units_id_fk", + "tableFrom": "ai_suggestions", + "tableTo": "project_units", + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_audit_log": { + "name": "auth_audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_audit_log_user": { + "name": "idx_audit_log_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_event": { + "name": "idx_audit_log_event", + "columns": [ + { + "expression": "event", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_audit_log_created": { + "name": "idx_audit_log_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_audit_log_user_id_auth_user_id_fk": { + "name": "auth_audit_log_user_id_auth_user_id_fk", + "tableFrom": "auth_audit_log", + "tableTo": "auth_user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(36)", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(36)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_mobile": { + "name": "is_mobile", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_org_id": { + "name": "active_org_id", + "type": "integer", + "primaryKey": false, + "notNull": 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" + }, + "auth_session_active_org_id_organizations_id_fk": { + "name": "auth_session_active_org_id_organizations_id_fk", + "tableFrom": "auth_session", + "tableTo": "organizations", + "columnsFrom": ["active_org_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "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 + }, + "has_audio": { + "name": "has_audio", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_bible_books_bible_book": { + "name": "idx_bible_books_bible_book", + "columns": [ + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": true, + "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 + }, + "provider": { + "name": "provider", + "type": "bible_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'dbl'" + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "has_audio": { + "name": "has_audio", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_bibles_provider_external_id": { + "name": "idx_bibles_provider_external_id", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"bibles\".\"external_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": { + "books_code_unique": { + "name": "books_code_unique", + "nullsNotDistinct": false, + "columns": ["code"] + } + }, + "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'" + }, + "has_claim_conflict": { + "name": "has_claim_conflict", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "claim_conflict_user_id": { + "name": "claim_conflict_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_ai_enabled": { + "name": "is_ai_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "submitted_time": { + "name": "submitted_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "uq_chapter_assignment_per_chapter": { + "name": "uq_chapter_assignment_per_chapter", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_assigned_user": { + "name": "idx_chapter_assignments_assigned_user", + "columns": [ + { + "expression": "assigned_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_peer_checker_status": { + "name": "idx_chapter_assignments_peer_checker_status", + "columns": [ + { + "expression": "peer_checker_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chapter_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chapter_assignments_project_unit": { + "name": "idx_chapter_assignments_project_unit", + "columns": [ + { + "expression": "project_unit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chapter_assignments_project_unit_id_project_units_id_fk": { + "name": "chapter_assignments_project_unit_id_project_units_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "project_units", + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "chapter_assignments_bible_id_bibles_id_fk": { + "name": "chapter_assignments_bible_id_bibles_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "bibles", + "columnsFrom": ["bible_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_book_id_books_id_fk": { + "name": "chapter_assignments_book_id_books_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "books", + "columnsFrom": ["book_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_assigned_user_id_users_id_fk": { + "name": "chapter_assignments_assigned_user_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": ["assigned_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_peer_checker_id_users_id_fk": { + "name": "chapter_assignments_peer_checker_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": ["peer_checker_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "chapter_assignments_claim_conflict_user_id_users_id_fk": { + "name": "chapter_assignments_claim_conflict_user_id_users_id_fk", + "tableFrom": "chapter_assignments", + "tableTo": "users", + "columnsFrom": ["claim_conflict_user_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(255)", + "primaryKey": false, + "notNull": true + }, + "lang_name_localized": { + "name": "lang_name_localized", + "type": "varchar(255)", + "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": { + "languages_lang_code_iso_639_3_unique": { + "name": "languages_lang_code_iso_639_3_unique", + "nullsNotDistinct": false, + "columns": ["lang_code_iso_639_3"] + } + }, + "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.pericope_sets": { + "name": "pericope_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(500)", + "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": { + "pericope_sets_name_unique": { + "name": "pericope_sets_name_unique", + "nullsNotDistinct": false, + "columns": ["name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pericope_verses": { + "name": "pericope_verses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pericope_set_id": { + "name": "pericope_set_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 + }, + "section": { + "name": "section", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pericope_number": { + "name": "pericope_number", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "pericope_title": { + "name": "pericope_title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_pericope_verses_set_book_chapter_verse": { + "name": "idx_pericope_verses_set_book_chapter_verse", + "columns": [ + { + "expression": "pericope_set_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": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_pericope_verses_set_book_pericope": { + "name": "idx_pericope_verses_set_book_pericope", + "columns": [ + { + "expression": "pericope_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "book_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pericope_number", + "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": { + "pericope_verses_pericope_set_id_pericope_sets_id_fk": { + "name": "pericope_verses_pericope_set_id_pericope_sets_id_fk", + "tableFrom": "pericope_verses", + "tableTo": "pericope_sets", + "columnsFrom": ["pericope_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pericope_verses_book_id_books_id_fk": { + "name": "pericope_verses_book_id_books_id_fk", + "tableFrom": "pericope_verses", + "tableTo": "books", + "columnsFrom": ["book_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "running_header": { + "name": "running_header", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "book_title": { + "name": "book_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "toc_long_name": { + "name": "toc_long_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "toc_short_name": { + "name": "toc_short_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "toc_abbreviation": { + "name": "toc_abbreviation", + "type": "varchar", + "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": { + "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.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" + }, + "pericope_set_id": { + "name": "pericope_set_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_activity_at": { + "name": "last_activity_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "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" + }, + "projects_pericope_set_id_pericope_sets_id_fk": { + "name": "projects_pericope_set_id_pericope_sets_id_fk", + "tableFrom": "projects", + "tableTo": "pericope_sets", + "columnsFrom": ["pericope_set_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "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.storage_objects": { + "name": "storage_objects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "uq_storage_object_bucket_key": { + "name": "uq_storage_object_bucket_key", + "columns": [ + { + "expression": "bucket", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_storage_objects_unreclaimed": { + "name": "idx_storage_objects_unreclaimed", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "markers": { + "name": "markers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "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.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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": { + "uq_user_role_grant": { + "name": "uq_user_role_grant", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"org_id\", -1)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "COALESCE(\"project_id\", -1)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "role_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_user": { + "name": "idx_user_roles_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_org": { + "name": "idx_user_roles_org", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_user_roles_project": { + "name": "idx_user_roles_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_org_id_organizations_id_fk": { + "name": "user_roles_org_id_organizations_id_fk", + "tableFrom": "user_roles", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_project_id_projects_id_fk": { + "name": "user_roles_project_id_projects_id_fk", + "tableFrom": "user_roles", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": ["role_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_roles_created_by_users_id_fk": { + "name": "user_roles_created_by_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "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": {}, + "foreignKeys": { + "user_settings_user_id_users_id_fk": { + "name": "user_settings_user_id_users_id_fk", + "tableFrom": "user_settings", + "tableTo": "users", + "columnsFrom": ["user_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 + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "last_active_org_id": { + "name": "last_active_org_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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_last_active_org_id_organizations_id_fk": { + "name": "users_last_active_org_id_organizations_id_fk", + "tableFrom": "users", + "tableTo": "organizations", + "columnsFrom": ["last_active_org_id"], + "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 + }, + "public.verse_audio_recordings": { + "name": "verse_audio_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 + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_object_id": { + "name": "storage_object_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "version_token": { + "name": "version_token", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "conflict_status": { + "name": "conflict_status", + "type": "verse_audio_conflict_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'clean'" + }, + "active_take_id": { + "name": "active_take_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_verse_audio_per_bible_text": { + "name": "uq_verse_audio_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": { + "verse_audio_recordings_project_unit_id_project_units_id_fk": { + "name": "verse_audio_recordings_project_unit_id_project_units_id_fk", + "tableFrom": "verse_audio_recordings", + "tableTo": "project_units", + "columnsFrom": ["project_unit_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "verse_audio_recordings_bible_text_id_bible_texts_id_fk": { + "name": "verse_audio_recordings_bible_text_id_bible_texts_id_fk", + "tableFrom": "verse_audio_recordings", + "tableTo": "bible_texts", + "columnsFrom": ["bible_text_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "verse_audio_recordings_uploaded_by_users_id_fk": { + "name": "verse_audio_recordings_uploaded_by_users_id_fk", + "tableFrom": "verse_audio_recordings", + "tableTo": "users", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "verse_audio_recordings_storage_object_id_storage_objects_id_fk": { + "name": "verse_audio_recordings_storage_object_id_storage_objects_id_fk", + "tableFrom": "verse_audio_recordings", + "tableTo": "storage_objects", + "columnsFrom": ["storage_object_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "verse_audio_recordings_active_take_id_verse_audio_takes_id_fk": { + "name": "verse_audio_recordings_active_take_id_verse_audio_takes_id_fk", + "tableFrom": "verse_audio_recordings", + "tableTo": "verse_audio_takes", + "columnsFrom": ["active_take_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verse_audio_takes": { + "name": "verse_audio_takes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "recording_id": { + "name": "recording_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "storage_object_id": { + "name": "storage_object_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_verse_audio_takes_recording": { + "name": "idx_verse_audio_takes_recording", + "columns": [ + { + "expression": "recording_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_verse_audio_take_content_hash": { + "name": "uq_verse_audio_take_content_hash", + "columns": [ + { + "expression": "recording_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "verse_audio_takes_recording_id_verse_audio_recordings_id_fk": { + "name": "verse_audio_takes_recording_id_verse_audio_recordings_id_fk", + "tableFrom": "verse_audio_takes", + "tableTo": "verse_audio_recordings", + "columnsFrom": ["recording_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "verse_audio_takes_uploaded_by_users_id_fk": { + "name": "verse_audio_takes_uploaded_by_users_id_fk", + "tableFrom": "verse_audio_takes", + "tableTo": "users", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "verse_audio_takes_storage_object_id_storage_objects_id_fk": { + "name": "verse_audio_takes_storage_object_id_storage_objects_id_fk", + "tableFrom": "verse_audio_takes", + "tableTo": "storage_objects", + "columnsFrom": ["storage_object_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.assignment_role": { + "name": "assignment_role", + "schema": "public", + "values": ["drafter", "peer_checker"] + }, + "public.bible_provider": { + "name": "bible_provider", + "schema": "public", + "values": ["dbl"] + }, + "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"] + }, + "public.verse_audio_conflict_status": { + "name": "verse_audio_conflict_status", + "schema": "public", + "values": ["clean", "conflict"] + } + }, + "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 ad176167..278e1393 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -197,6 +197,13 @@ "when": 1787886800280, "tag": "0027_add_verse_audio_takes_and_conflict_status", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1788971960061, + "tag": "0028_add_audio_availability", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index 6ce13b01..0aa20e67 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -226,6 +226,7 @@ export const bibles = pgTable( abbreviation: varchar('abbreviation', { length: 50 }).notNull().unique(), provider: bibleProviderEnum('provider').notNull().default('dbl'), externalId: varchar('external_id', { length: 255 }), + hasAudio: boolean('has_audio').notNull().default(false), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') .defaultNow() @@ -298,6 +299,7 @@ export const bible_books = pgTable( bookId: integer('book_id') .notNull() .references(() => books.id), + hasAudio: boolean('has_audio').notNull().default(false), createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at') .defaultNow() diff --git a/src/db/scripts/sync-dbl-catalogue.ts b/src/db/scripts/sync-dbl-catalogue.ts index 5fcfbfba..9c010eb5 100644 --- a/src/db/scripts/sync-dbl-catalogue.ts +++ b/src/db/scripts/sync-dbl-catalogue.ts @@ -1,12 +1,12 @@ import { syncBiblesFromDbl } from '@/domains/bibles/sync/dbl-bible-sync'; -import { syncBooksFromDbl } from '@/domains/books/sync/dbl-book-sync'; +import { syncAudioAvailability, syncBooksFromDbl } from '@/domains/books/sync/dbl-book-sync'; import { syncLanguagesFromDbl } from '@/domains/languages/sync/dbl-language-sync'; import { logger } from '@/lib/logger'; async function main() { logger.info('Starting manual DBL catalogue sync...'); - logger.info('Step 1/3: Syncing languages from DBL...'); + logger.info('Step 1/4: Syncing languages from DBL...'); const langResult = await syncLanguagesFromDbl(); if (!langResult.ok) { logger.error('Language sync failed', { error: langResult.error }); @@ -14,7 +14,7 @@ async function main() { } logger.info('Languages sync completed', { data: langResult.data }); - logger.info('Step 2/3: Syncing bibles from DBL...'); + logger.info('Step 2/4: Syncing bibles from DBL...'); const biblesResult = await syncBiblesFromDbl(); if (!biblesResult.ok) { logger.error('Bibles sync failed', { error: biblesResult.error }); @@ -22,7 +22,7 @@ async function main() { } logger.info('Bibles sync completed', { data: biblesResult.data }); - logger.info('Step 3/3: Syncing books from DBL...'); + logger.info('Step 3/4: Syncing books from DBL...'); const booksResult = await syncBooksFromDbl(); if (!booksResult.ok) { logger.error('Books sync failed', { error: booksResult.error }); @@ -30,6 +30,14 @@ async function main() { } logger.info('Books sync completed', { data: booksResult.data }); + logger.info('Step 4/4: Syncing audio availability from DBL...'); + const audioResult = await syncAudioAvailability(); + if (!audioResult.ok) { + logger.error('Audio availability sync failed', { error: audioResult.error }); + process.exit(1); + } + logger.info('Audio availability sync completed', { data: audioResult.data }); + logger.info('DBL catalogue sync completed successfully!'); process.exit(0); } diff --git a/src/domains/bible-books/bible-books.repository.ts b/src/domains/bible-books/bible-books.repository.ts index 0ed36479..f6e93e66 100644 --- a/src/domains/bible-books/bible-books.repository.ts +++ b/src/domains/bible-books/bible-books.repository.ts @@ -13,6 +13,7 @@ import type { BibleBook, BibleBookWithDetails, CreateBibleBookInput } from './bi const bibleBookSelect = { bibleId: bible_books.bibleId, bookId: bible_books.bookId, + hasAudio: bible_books.hasAudio, createdAt: bible_books.createdAt, updatedAt: bible_books.updatedAt, book: { diff --git a/src/domains/bible-books/bible-books.service.ts b/src/domains/bible-books/bible-books.service.ts index 05624c4d..fae2eef0 100644 --- a/src/domains/bible-books/bible-books.service.ts +++ b/src/domains/bible-books/bible-books.service.ts @@ -14,6 +14,7 @@ function toDetailResponse(record: BibleBookWithDetails): BibleBookDetailResponse return { bibleId: record.bibleId, bookId: record.bookId, + hasAudio: record.hasAudio, createdAt: record.createdAt ? record.createdAt.toISOString() : null, updatedAt: record.updatedAt ? record.updatedAt.toISOString() : null, book: { @@ -32,6 +33,7 @@ function toResponse(record: BibleBook): BibleBookResponse { return { bibleId: record.bibleId, bookId: record.bookId, + hasAudio: record.hasAudio, createdAt: record.createdAt ? record.createdAt.toISOString() : null, updatedAt: record.updatedAt ? record.updatedAt.toISOString() : null, }; diff --git a/src/domains/bible-books/bible-books.types.ts b/src/domains/bible-books/bible-books.types.ts index 05fcd465..53cd0427 100644 --- a/src/domains/bible-books/bible-books.types.ts +++ b/src/domains/bible-books/bible-books.types.ts @@ -12,6 +12,7 @@ export const createBibleBookSchema = z.object({ export const bibleBookResponseSchema = z.object({ bibleId: z.number().int(), bookId: z.number().int(), + hasAudio: z.boolean(), createdAt: z.string().nullable(), updatedAt: z.string().nullable(), }); @@ -19,6 +20,7 @@ export const bibleBookResponseSchema = z.object({ export const bibleBookDetailResponseSchema = z.object({ bibleId: z.number().int(), bookId: z.number().int(), + hasAudio: z.boolean(), createdAt: z.string().nullable(), updatedAt: z.string().nullable(), book: z.object({ diff --git a/src/domains/bibles/bibles.repository.ts b/src/domains/bibles/bibles.repository.ts index 07d90c40..e5aeabf1 100644 --- a/src/domains/bibles/bibles.repository.ts +++ b/src/domains/bibles/bibles.repository.ts @@ -86,6 +86,7 @@ export async function searchSourceBibles(query: string): Promise(); @@ -120,6 +127,7 @@ export async function searchSourceBibles(query: string): Promise { expect.objectContaining({ languageId: 100, externalId: 'b2', abbreviation: 'B2A' }), ]); }); + + it('correctly maps hasAudio to true if audioBibles exist, false otherwise', async () => { + mockUpsertFromDbl.mockResolvedValue(ok({ inserted: 2, updated: 0 })); + const client = fakeClient([ + bible({ id: 'b1', langId: 'eng', audioBibles: [{ id: 'ab1' }] }), + bible({ id: 'b2', langId: 'eng', audioBibles: [] }), + bible({ id: 'b3', langId: 'eng' }), // audioBibles undefined + ]); + + const result = await syncBiblesFromDbl(client); + + expect(result.ok).toBe(true); + expect(mockUpsertFromDbl).toHaveBeenCalledWith([ + expect.objectContaining({ externalId: 'b1', hasAudio: true }), + expect.objectContaining({ externalId: 'b2', hasAudio: false }), + expect.objectContaining({ externalId: 'b3', hasAudio: false }), + ]); + }); }); diff --git a/src/domains/bibles/sync/dbl-bible-sync.ts b/src/domains/bibles/sync/dbl-bible-sync.ts index 37e33f45..007fe1a6 100644 --- a/src/domains/bibles/sync/dbl-bible-sync.ts +++ b/src/domains/bibles/sync/dbl-bible-sync.ts @@ -84,6 +84,7 @@ export async function syncBiblesFromDbl( // If this Bible was already synced, reuse its stored abbreviation and name // to avoid regenerating collision suffixes on every re-sync. const existing = existingByExternalId.get(bible.id); + const hasAudio = !!(bible.audioBibles && bible.audioBibles.length > 0); if (existing) { rows.push({ languageId, @@ -91,6 +92,7 @@ export async function syncBiblesFromDbl( abbreviation: existing.abbreviation, provider: 'dbl', externalId: bible.id, + hasAudio, }); continue; } @@ -129,6 +131,7 @@ export async function syncBiblesFromDbl( abbreviation: abbrev, provider: 'dbl', externalId: bible.id, + hasAudio, }); } diff --git a/src/domains/books/books.repository.ts b/src/domains/books/books.repository.ts index 73535b5e..6feb2304 100644 --- a/src/domains/books/books.repository.ts +++ b/src/domains/books/books.repository.ts @@ -1,4 +1,4 @@ -import { eq, inArray } from 'drizzle-orm'; +import { and, eq, inArray, notInArray } from 'drizzle-orm'; import type { Result } from '@/lib/types'; @@ -190,3 +190,76 @@ export async function upsertFromDbl( return err(ErrorCode.INTERNAL_ERROR); } } + +/** + * Updates `has_audio` on `bible_books` rows for a given Bible. + * + * Sets `has_audio = true` for books whose code is in `audioBookCodes`, + * and `has_audio = false` for all other books of that Bible. + */ +export async function updateAudioAvailability( + bibleId: number, + audioBookCodes: string[] +): Promise> { + try { + let updated = 0; + + await db.transaction(async (tx) => { + // Resolve book codes to IDs + const audioBookRows = + audioBookCodes.length > 0 + ? await tx + .select({ id: books.id }) + .from(books) + .where( + inArray( + books.code, + audioBookCodes.map((c) => c.toUpperCase()) + ) + ) + : []; + const audioBookIds = audioBookRows.map((b) => b.id); + + // Turn audio ON for books that should have it but currently don't + if (audioBookIds.length > 0) { + const turnedOn = await tx + .update(bible_books) + .set({ hasAudio: true }) + .where( + and( + eq(bible_books.bibleId, bibleId), + eq(bible_books.hasAudio, false), + inArray(bible_books.bookId, audioBookIds) + ) + ) + .returning({ bookId: bible_books.bookId }); + updated += turnedOn.length; + } + + // Turn audio OFF for books that shouldn't have it but currently do + const turnedOff = await tx + .update(bible_books) + .set({ hasAudio: false }) + .where( + audioBookIds.length > 0 + ? and( + eq(bible_books.bibleId, bibleId), + eq(bible_books.hasAudio, true), + notInArray(bible_books.bookId, audioBookIds) + ) + : and(eq(bible_books.bibleId, bibleId), eq(bible_books.hasAudio, true)) + ) + .returning({ bookId: bible_books.bookId }); + updated += turnedOff.length; + }); + + return ok({ updated }); + } catch (error) { + logger.error({ + cause: error, + message: 'Failed to update audio availability', + context: { bibleId, audioBookCodeCount: audioBookCodes.length }, + }); + return err(ErrorCode.INTERNAL_ERROR); + } +} diff --git a/src/domains/books/sync/dbl-book-sync.test.ts b/src/domains/books/sync/dbl-book-sync.test.ts index 15c50954..f816746f 100644 --- a/src/domains/books/sync/dbl-book-sync.test.ts +++ b/src/domains/books/sync/dbl-book-sync.test.ts @@ -6,7 +6,8 @@ import type { DblBook } from '@/lib/services/dbl/dbl.types'; import { ErrorCode, ok } from '@/lib/types'; -import { syncBooksFromDbl } from './dbl-book-sync'; +import * as booksRepo from '../books.repository'; +import { syncAudioAvailability, syncBooksFromDbl } from './dbl-book-sync'; const { mockUpsertFromDbl, mockGetAllBibles } = vi.hoisted(() => ({ mockUpsertFromDbl: vi.fn(), @@ -15,6 +16,7 @@ const { mockUpsertFromDbl, mockGetAllBibles } = vi.hoisted(() => ({ vi.mock('../books.repository', () => ({ upsertFromDbl: mockUpsertFromDbl, + updateAudioAvailability: vi.fn(), })); vi.mock('@/domains/bibles/bibles.repository', () => ({ @@ -27,6 +29,8 @@ function fakeClient(responses: Record): DblClient { if (responses[bibleId]) return Promise.resolve(ok(responses[bibleId])); return Promise.resolve({ ok: false, error: { code: ErrorCode.INTERNAL_ERROR } }); }), + getBible: vi.fn(), + getAudioBibleBooks: vi.fn(), } as unknown as DblClient; } @@ -73,3 +77,73 @@ describe('syncBooksFromDbl', () => { ]); }); }); + +describe('syncAudioAvailability', () => { + beforeEach(() => { + vi.mocked(booksRepo.updateAudioAvailability).mockResolvedValue(ok({ updated: 1 })); + }); + + it('clears audio for bibles without audio (fast-path)', async () => { + mockGetAllBibles.mockResolvedValue( + ok([{ id: 1, externalId: 'b1', provider: 'dbl', hasAudio: false }] as Bible[]) + ); + const client = fakeClient({}); + const result = await syncAudioAvailability(client); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.totalBiblesProcessed).toBe(1); + } + expect(client.getBible).not.toHaveBeenCalled(); + expect(booksRepo.updateAudioAvailability).toHaveBeenCalledWith(1, []); + }); + + it('fetches audio bibles and updates availability for bibles with audio', async () => { + mockGetAllBibles.mockResolvedValue( + ok([{ id: 1, externalId: 'b1', provider: 'dbl', hasAudio: true }] as Bible[]) + ); + const client = fakeClient({}); + vi.mocked(client.getBible).mockResolvedValue( + ok({ audioBibles: [{ id: 'ab1' }, { id: 'ab2' }] } as any) + ); + vi.mocked(client.getAudioBibleBooks).mockImplementation(async (audioId) => { + if (audioId === 'ab1') return ok([{ id: 'GEN' }] as any); + if (audioId === 'ab2') return ok([{ id: 'EXO' }] as any); + return ok([]); + }); + + const result = await syncAudioAvailability(client); + + expect(result.ok).toBe(true); + expect(client.getBible).toHaveBeenCalledWith('b1'); + expect(client.getAudioBibleBooks).toHaveBeenCalledWith('ab1'); + expect(client.getAudioBibleBooks).toHaveBeenCalledWith('ab2'); + + // Set doesn't guarantee order, so we check the set of elements + const updateCall = vi.mocked(booksRepo.updateAudioAvailability).mock.calls[0]; + expect(updateCall[0]).toBe(1); + expect(updateCall[1]).toContain('GEN'); + expect(updateCall[1]).toContain('EXO'); + expect(updateCall[1]).toHaveLength(2); + }); + it('does NOT persist partial results when one audio bible book fetch fails', async () => { + mockGetAllBibles.mockResolvedValue( + ok([{ id: 1, externalId: 'b1', provider: 'dbl', hasAudio: true }] as Bible[]) + ); + const client = fakeClient({}); + vi.mocked(client.getBible).mockResolvedValue( + ok({ audioBibles: [{ id: 'ab1' }, { id: 'ab2' }] } as any) + ); + vi.mocked(client.getAudioBibleBooks).mockImplementation(async (audioId) => { + if (audioId === 'ab1') return ok([{ id: 'GEN' }] as any); + return { ok: false, error: { code: ErrorCode.DBL_SERVICE_UNAVAILABLE, message: 'timeout' } }; + }); + + const result = await syncAudioAvailability(client); + + // Since it's the ONLY bible in the test and it failed, the overall sync returns an error + expect(result.ok).toBe(false); + // Partial data must NOT be persisted + expect(booksRepo.updateAudioAvailability).not.toHaveBeenCalled(); + }); +}); diff --git a/src/domains/books/sync/dbl-book-sync.ts b/src/domains/books/sync/dbl-book-sync.ts index 5995f398..b2e4ac8a 100644 --- a/src/domains/books/sync/dbl-book-sync.ts +++ b/src/domains/books/sync/dbl-book-sync.ts @@ -13,6 +13,8 @@ import * as booksRepository from '../books.repository'; export interface DblBookSyncSummary { totalBiblesProcessed: number; totalBooksLinked: number; + errorCount: number; + partialFailure: boolean; } /** @@ -72,11 +74,161 @@ export async function syncBooksFromDbl( return err(ErrorCode.INTERNAL_ERROR); } + if (errorCount > 0) { + logger.warn('Completed book sync with partial failures', { + errorCount, + totalBibles: dbBibles.length, + }); + } + return { ok: true, data: { totalBiblesProcessed: dbBibles.length - errorCount, totalBooksLinked, + errorCount, + partialFailure: errorCount > 0, + }, + }; +} + +// ─── Audio availability sync ──────────────────────────────────────────────── + +export interface AudioAvailabilitySyncSummary { + totalBiblesProcessed: number; + totalBooksUpdated: number; + errorCount: number; + partialFailure: boolean; +} + +/** + * Syncs book-level audio availability for all DBL Bibles. + * + * For each Bible with `has_audio = true`, queries `GET /audio-bibles/{id}/books` + * to determine which books have audio, then updates the `has_audio` flag on + * the `bible_books` junction table. + * + * Full-sync: ALL DBL Bibles are processed. Bibles with `has_audio = false` + * are fast-pathed to clear any stale `has_audio` flags on their books (handles + * the case where a publisher removes an Audio Bible from DBL). Bibles with + * `has_audio = true` fetch their audio book list from DBL and update accordingly. + * + * If any `getAudioBibleBooks` request fails for a given Bible, the entire + * update for that Bible is skipped to avoid persisting partial data. + * + * Note: `syncBooksFromDbl` should be called BEFORE this to ensure the + * `bible_books` junction table is fully populated. + */ +export async function syncAudioAvailability( + client: DblClient = dblClient +): Promise> { + // 1. Fetch all DBL Bibles that have audio + const biblesResult = await biblesRepository.getAll(); + if (!biblesResult.ok) return biblesResult; + + const dblBibles = biblesResult.data.filter((b) => b.externalId && b.provider === 'dbl'); + + if (dblBibles.length === 0) { + return { + ok: true, + data: { totalBiblesProcessed: 0, totalBooksUpdated: 0, errorCount: 0, partialFailure: false }, + }; + } + + let totalBooksUpdated = 0; + let errorCount = 0; + + for (const bible of dblBibles) { + if (!bible.externalId) continue; + + try { + if (!bible.hasAudio) { + // Fast-path: clear audio availability for all books if the Bible has no audio. + // This handles cases where DBL removed the last audio Bible. + const updateResult = await booksRepository.updateAudioAvailability(bible.id, []); + if (!updateResult.ok) { + errorCount++; + } else { + totalBooksUpdated += updateResult.data.updated; + } + continue; + } + // 2. Fetch the full Bible metadata from DBL to get audioBible IDs + const dblBibleResult = await client.getBible(bible.externalId); + if (!dblBibleResult.ok) { + logger.error(`Failed to fetch DBL Bible metadata for audio sync: ${bible.externalId}`, { + error: dblBibleResult.error, + }); + errorCount++; + continue; + } + + const dblAudioBibles = dblBibleResult.data.audioBibles; + if (!dblAudioBibles || dblAudioBibles.length === 0) { + continue; + } + + // 3. Collect audio book codes from all associated Audio Bibles + const audioBookCodes = new Set(); + let audioBookFetchFailed = false; + for (const audioBible of dblAudioBibles) { + const audioBooksResult = await client.getAudioBibleBooks(audioBible.id); + if (!audioBooksResult.ok) { + logger.warn( + `Failed to fetch audio books for audioBible ${audioBible.id} (text bible ${bible.externalId})`, + { error: audioBooksResult.error } + ); + audioBookFetchFailed = true; + continue; + } + for (const book of audioBooksResult.data) { + audioBookCodes.add(book.id); + } + } + + if (audioBookFetchFailed) { + // Do not persist a partial Audio Bible result. + errorCount++; + continue; + } + + // 4. Update bible_books.has_audio for this Bible + const updateResult = await booksRepository.updateAudioAvailability(bible.id, [ + ...audioBookCodes, + ]); + if (!updateResult.ok) { + errorCount++; + continue; + } + + totalBooksUpdated += updateResult.data.updated; + } catch (error) { + errorCount++; + logger.error(`Unexpected error syncing audio availability for bible ${bible.externalId}`, { + error, + }); + } + } + + if (errorCount > 0 && errorCount === dblBibles.length) { + logger.error('Failed to sync audio availability for all bibles', { errorCount }); + return err(ErrorCode.INTERNAL_ERROR); + } + + if (errorCount > 0) { + logger.warn('Completed audio availability sync with partial failures', { + errorCount, + totalBibles: dblBibles.length, + }); + } + + return { + ok: true, + data: { + totalBiblesProcessed: dblBibles.length - errorCount, + totalBooksUpdated, + errorCount, + partialFailure: errorCount > 0, }, }; } diff --git a/src/domains/languages/sync/dbl-language-sync.test.ts b/src/domains/languages/sync/dbl-language-sync.test.ts index 70cd79b5..ee261f53 100644 --- a/src/domains/languages/sync/dbl-language-sync.test.ts +++ b/src/domains/languages/sync/dbl-language-sync.test.ts @@ -52,6 +52,7 @@ function fakeClient(bibles: DblBibleSummary[]): DblClient { getVerse: vi.fn(), getPassage: vi.fn(), getAudioChapter: vi.fn(), + getAudioBibleBooks: vi.fn(), }; } @@ -79,6 +80,7 @@ describe('syncLanguagesFromDbl', () => { getVerse: vi.fn(), getPassage: vi.fn(), getAudioChapter: vi.fn(), + getAudioBibleBooks: vi.fn(), }; const result = await syncLanguagesFromDbl(client); diff --git a/src/domains/source-audio/source-audio.service.test.ts b/src/domains/source-audio/source-audio.service.test.ts index 63fc7266..d5b61d41 100644 --- a/src/domains/source-audio/source-audio.service.test.ts +++ b/src/domains/source-audio/source-audio.service.test.ts @@ -39,6 +39,7 @@ const fluentBible: Bible = { languageId: 1, provider: 'dbl', externalId: null, + hasAudio: false, createdAt: new Date(), updatedAt: new Date(), }; diff --git a/src/lib/services/dbl/dbl.client.ts b/src/lib/services/dbl/dbl.client.ts index 4b922ef3..cc525ef2 100644 --- a/src/lib/services/dbl/dbl.client.ts +++ b/src/lib/services/dbl/dbl.client.ts @@ -298,6 +298,12 @@ export interface DblClient { chapterId: string, options?: DblRequestOptions ) => Promise>; + /** GET /audio-bibles/{audioBibleId}/books — lists which books have audio available. */ + getAudioBibleBooks: ( + audioBibleId: string, + params?: DblListBooksParams, + options?: DblRequestOptions + ) => Promise>; } // ─── Factory ─────────────────────────────────────────────────────────────── @@ -417,6 +423,19 @@ export function createDblClient(config: DblClientConfig): DblClient { options ); }, + + getAudioBibleBooks(audioBibleId, params, options) { + return dblRequest( + config, + `audio-bibles/${encodeURIComponent(audioBibleId)}/books`, + { + 'include-chapters': params?.includeChapters, + 'include-chapters-and-sections': params?.includeChaptersAndSections, + }, + dblBookSchema.array(), + options + ); + }, }; } diff --git a/src/workers/dbl-sync.worker.test.ts b/src/workers/dbl-sync.worker.test.ts index d4223f81..3f9114c0 100644 --- a/src/workers/dbl-sync.worker.test.ts +++ b/src/workers/dbl-sync.worker.test.ts @@ -17,6 +17,7 @@ vi.mock('@/domains/bibles/sync/dbl-bible-sync', () => ({ vi.mock('@/domains/books/sync/dbl-book-sync', () => ({ syncBooksFromDbl: vi.fn(), + syncAudioAvailability: vi.fn(), })); describe('dblSyncWorker', () => { @@ -39,11 +40,13 @@ describe('dblSyncWorker', () => { vi.mocked(languageSyncModule.syncLanguagesFromDbl).mockResolvedValueOnce(ok({} as any)); vi.mocked(bibleSyncModule.syncBiblesFromDbl).mockResolvedValueOnce(ok({} as any)); vi.mocked(bookSyncModule.syncBooksFromDbl).mockResolvedValueOnce(ok({} as any)); + vi.mocked(bookSyncModule.syncAudioAvailability).mockResolvedValueOnce(ok({} as any)); await handler([{ id: 'job-1' }]); expect(languageSyncModule.syncLanguagesFromDbl).toHaveBeenCalledTimes(1); expect(bibleSyncModule.syncBiblesFromDbl).toHaveBeenCalledTimes(1); expect(bookSyncModule.syncBooksFromDbl).toHaveBeenCalledTimes(1); + expect(bookSyncModule.syncAudioAvailability).toHaveBeenCalledTimes(1); vi.mocked(languageSyncModule.syncLanguagesFromDbl).mockResolvedValueOnce({ ok: false, diff --git a/src/workers/dbl-sync.worker.ts b/src/workers/dbl-sync.worker.ts index 45781bc4..0b691b41 100644 --- a/src/workers/dbl-sync.worker.ts +++ b/src/workers/dbl-sync.worker.ts @@ -1,7 +1,7 @@ import type { PgBoss } from 'pg-boss'; import { syncBiblesFromDbl } from '@/domains/bibles/sync/dbl-bible-sync'; -import { syncBooksFromDbl } from '@/domains/books/sync/dbl-book-sync'; +import { syncAudioAvailability, syncBooksFromDbl } from '@/domains/books/sync/dbl-book-sync'; import { syncLanguagesFromDbl } from '@/domains/languages/sync/dbl-language-sync'; import { logger } from '../lib/logger'; @@ -41,14 +41,19 @@ export async function registerDblSyncWorker(boss: PgBoss) { const biblesResult = await syncBiblesFromDbl(); if (!biblesResult.ok) throw new Error(`Bibles sync failed: ${biblesResult.error.message}`); - logger.info('Starting step 3/3: Syncing books from DBL...'); + logger.info('Starting step 3/4: Syncing books from DBL...'); const booksResult = await syncBooksFromDbl(); if (!booksResult.ok) throw new Error(`Books sync failed: ${booksResult.error.message}`); + logger.info('Starting step 4/4: Syncing audio availability from DBL...'); + const audioResult = await syncAudioAvailability(); + if (!audioResult.ok) throw new Error(`Audio sync failed: ${audioResult.error.message}`); + logger.info('DBL sync job completed successfully', { languages: langResult.data, bibles: biblesResult.data, books: booksResult.data, + audio: audioResult.data, }); } catch (error) { // Re-throw so pg-boss marks this job as failed and applies its