statement-breakpoint
+CREATE TABLE "book_chapter_versions" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "book_id" integer NOT NULL,
+ "chapters" jsonb NOT NULL,
+ "source" varchar(20) NOT NULL,
+ "chapter_count" integer NOT NULL,
+ "created_by" integer,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "chapter_lookup_queue" (
+ "claim_token" uuid DEFAULT gen_random_uuid() NOT NULL,
+ "book_id" integer PRIMARY KEY NOT NULL,
+ "user_id" integer,
+ "status" varchar(20) DEFAULT 'queued' NOT NULL,
+ "reason" varchar(40),
+ "result" jsonb,
+ "attempt_count" integer DEFAULT 0 NOT NULL,
+ "last_error" text,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "finished_at" timestamp with time zone,
+ "available_at" timestamp with time zone DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "audiobook_manifest_revisions" (
+ "book_id" integer NOT NULL,
+ "revision" varchar(64) NOT NULL,
+ "positions_key" varchar(64) NOT NULL,
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT "audiobook_manifest_revisions_book_id_revision_pk" PRIMARY KEY("book_id","revision")
+);
+--> statement-breakpoint
+ALTER TABLE "book_files" ADD COLUMN "chapter_write_provenance" jsonb;--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD COLUMN "chapter_count" integer GENERATED ALWAYS AS (CASE WHEN jsonb_typeof(chapters) = 'array' THEN jsonb_array_length(chapters) ELSE 0 END) STORED;--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD COLUMN "numbered_chapter_count" integer GENERATED ALWAYS AS (CASE WHEN jsonb_typeof(chapters) = 'array' THEN jsonb_array_length(jsonb_path_query_array(chapters, '$[*] ? (@.title like_regex "^[[:space:]]*((track|disc|cd|part)[[:space:]]*)?[#._-]*[[:space:]]*[0-9]+([[:space:]]*(of|/)[[:space:]]*[0-9]+)?[[:space:]]*$" flag "i")')) ELSE 0 END) STORED;--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD COLUMN "manifest_legacy_recorded" boolean DEFAULT false NOT NULL;--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD COLUMN "chapters_source" varchar(20);--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD COLUMN "chapters_updated_at" timestamp with time zone;--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD COLUMN "chapters_updated_by" integer;--> statement-breakpoint
+ALTER TABLE "book_chapter_jobs" ADD CONSTRAINT "book_chapter_jobs_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "book_chapter_jobs" ADD CONSTRAINT "book_chapter_jobs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "book_chapter_versions" ADD CONSTRAINT "book_chapter_versions_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "book_chapter_versions" ADD CONSTRAINT "book_chapter_versions_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "chapter_lookup_queue" ADD CONSTRAINT "chapter_lookup_queue_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "chapter_lookup_queue" ADD CONSTRAINT "chapter_lookup_queue_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "audiobook_manifest_revisions" ADD CONSTRAINT "audiobook_manifest_revisions_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "bcj_status_created_idx" ON "book_chapter_jobs" USING btree ("status","created_at");--> statement-breakpoint
+CREATE UNIQUE INDEX "bcj_request_idx" ON "book_chapter_jobs" USING btree ("user_id","book_id","kind","base_revision");--> statement-breakpoint
+CREATE INDEX "bcv_book_created_idx" ON "book_chapter_versions" USING btree ("book_id","created_at");--> statement-breakpoint
+CREATE INDEX "bcv_created_by_idx" ON "book_chapter_versions" USING btree ("created_by") WHERE "book_chapter_versions"."created_by" is not null;--> statement-breakpoint
+CREATE INDEX "clq_status_available_idx" ON "chapter_lookup_queue" USING btree ("status","available_at");--> statement-breakpoint
+CREATE INDEX "clq_user_status_idx" ON "chapter_lookup_queue" USING btree ("user_id","status");--> statement-breakpoint
+CREATE INDEX "amr_book_created_idx" ON "audiobook_manifest_revisions" USING btree ("book_id","created_at");--> statement-breakpoint
+ALTER TABLE "book_metadata" ADD CONSTRAINT "book_metadata_chapters_updated_by_users_id_fk" FOREIGN KEY ("chapters_updated_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "bm_chapter_count_idx" ON "book_metadata" USING btree ("chapter_count","book_id");--> statement-breakpoint
+CREATE INDEX "bm_numbered_chapters_idx" ON "book_metadata" USING btree ("book_id") WHERE numbered_chapter_count >= chapter_count * 0.8 AND chapter_count >= 2;--> statement-breakpoint
+CREATE INDEX "bm_chapters_updated_by_idx" ON "book_metadata" USING btree ("chapters_updated_by") WHERE "book_metadata"."chapters_updated_by" is not null;--> statement-breakpoint
+-- Chapter lists stored before their source was recorded. A provider lookup stored a durationMs on
+-- every chapter, which a list read from the audio files never carries.
+UPDATE "book_metadata"
+SET "chapters_source" = CASE WHEN jsonb_path_exists("chapters", '$[*].durationMs') THEN 'provider' ELSE 'embedded' END
+WHERE "chapters_source" IS NULL AND jsonb_typeof("chapters") = 'array' AND jsonb_array_length("chapters") > 0;
+--> statement-breakpoint
+-- A book locked with "lock all" stays fully locked now that chapters can be locked too.
+UPDATE "book_metadata"
+SET "locked_fields" = array_append("locked_fields", 'chapters')
+WHERE "locked_fields" @> ARRAY['title', 'subtitle', 'authors', 'description', 'publisher', 'publishedYear', 'language', 'pageCount', 'seriesName', 'seriesIndex', 'isbn13', 'isbn10', 'genres', 'tags', 'rating', 'communityRating', 'narrators', 'durationSeconds', 'abridged', 'googleBooksId', 'goodreadsId', 'amazonId', 'hardcoverId', 'hardcoverEditionId', 'openLibraryId', 'itunesId', 'audibleId', 'librofmId', 'koboId', 'comicvineId', 'ranobedbId', 'lubimyczytacId', 'aladinId', 'comicIssueNumber', 'comicVolumeName', 'comicStoryArcs', 'comicPencillers', 'comicInkers', 'comicColorists', 'comicLetterers', 'comicCoverArtists', 'comicCharacters', 'comicTeams', 'comicLocations', 'cover', 'audioCover']::text[]
+ AND NOT ('chapters' = ANY("locked_fields"));
diff --git a/server/src/db/migrations/meta/0127_snapshot.json b/server/src/db/migrations/meta/0127_snapshot.json
new file mode 100644
index 000000000..25b9ac9e2
--- /dev/null
+++ b/server/src/db/migrations/meta/0127_snapshot.json
@@ -0,0 +1,25470 @@
+{
+ "id": "8bc90ecc-a24c-4196-9768-88efed96af7a",
+ "prevId": "dba0c5d9-6be1-4de6-94b8-771e99184590",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_username": {
+ "name": "actor_username",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource": {
+ "name": "resource",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_label": {
+ "name": "resource_label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip": {
+ "name": "ip",
+ "type": "varchar(45)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta": {
+ "name": "meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_audit_user_id": {
+ "name": "idx_audit_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_actor_username": {
+ "name": "idx_audit_actor_username",
+ "columns": [
+ {
+ "expression": "actor_username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_actor_username_trgm": {
+ "name": "idx_audit_actor_username_trgm",
+ "columns": [
+ {
+ "expression": "actor_username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_audit_resource": {
+ "name": "idx_audit_resource",
+ "columns": [
+ {
+ "expression": "resource",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_resource_id": {
+ "name": "idx_audit_resource_id",
+ "columns": [
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_resource_trgm": {
+ "name": "idx_audit_resource_trgm",
+ "columns": [
+ {
+ "expression": "resource",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_audit_action": {
+ "name": "idx_audit_action",
+ "columns": [
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_action_trgm": {
+ "name": "idx_audit_action_trgm",
+ "columns": [
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_audit_description_trgm": {
+ "name": "idx_audit_description_trgm",
+ "columns": [
+ {
+ "expression": "description",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_audit_resource_label_trgm": {
+ "name": "idx_audit_resource_label_trgm",
+ "columns": [
+ {
+ "expression": "resource_label",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_audit_meta_text_trgm": {
+ "name": "idx_audit_meta_text_trgm",
+ "columns": [
+ {
+ "expression": "(\"meta\"::text) gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "idx_audit_ip": {
+ "name": "idx_audit_ip",
+ "columns": [
+ {
+ "expression": "ip",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_created_at": {
+ "name": "idx_audit_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_reading_insights_access": {
+ "name": "idx_audit_reading_insights_access",
+ "columns": [
+ {
+ "expression": "resource",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_log_user_id_users_id_fk": {
+ "name": "audit_log_user_id_users_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.app_settings": {
+ "name": "app_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "app_settings_key_unique": {
+ "name": "app_settings_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.auth_sessions": {
+ "name": "auth_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_version": {
+ "name": "token_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "authentication_method": {
+ "name": "authentication_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_kind": {
+ "name": "client_kind",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ },
+ "device_label": {
+ "name": "device_label",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "auth_sessions_user_id_idx": {
+ "name": "auth_sessions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auth_sessions_expires_at_idx": {
+ "name": "auth_sessions_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "auth_sessions_user_id_users_id_fk": {
+ "name": "auth_sessions_user_id_users_id_fk",
+ "tableFrom": "auth_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "auth_sessions_client_kind_chk": {
+ "name": "auth_sessions_client_kind_chk",
+ "value": "\"auth_sessions\".\"client_kind\" in ('web', 'native')"
+ },
+ "auth_sessions_authentication_method_chk": {
+ "name": "auth_sessions_authentication_method_chk",
+ "value": "\"auth_sessions\".\"authentication_method\" in ('password', 'oidc', 'magic_link', 'setup', 'legacy')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.magic_access_tokens": {
+ "name": "magic_access_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "raw_token": {
+ "name": "raw_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "use_count": {
+ "name": "use_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "magic_access_tokens_user_id_idx": {
+ "name": "magic_access_tokens_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "magic_access_tokens_user_id_users_id_fk": {
+ "name": "magic_access_tokens_user_id_users_id_fk",
+ "tableFrom": "magic_access_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "magic_access_tokens_created_by_users_id_fk": {
+ "name": "magic_access_tokens_created_by_users_id_fk",
+ "tableFrom": "magic_access_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "magic_access_tokens_token_hash_unique": {
+ "name": "magic_access_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "magic_access_tokens_use_count_nonneg_chk": {
+ "name": "magic_access_tokens_use_count_nonneg_chk",
+ "value": "\"magic_access_tokens\".\"use_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.password_reset_tokens": {
+ "name": "password_reset_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "password_reset_tokens_user_id_idx": {
+ "name": "password_reset_tokens_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "password_reset_tokens_expires_at_idx": {
+ "name": "password_reset_tokens_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "password_reset_tokens_user_id_users_id_fk": {
+ "name": "password_reset_tokens_user_id_users_id_fk",
+ "tableFrom": "password_reset_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "password_reset_tokens_token_hash_unique": {
+ "name": "password_reset_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "password_reset_tokens_expires_after_created_chk": {
+ "name": "password_reset_tokens_expires_after_created_chk",
+ "value": "\"password_reset_tokens\".\"expires_at\" > \"password_reset_tokens\".\"created_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.refresh_tokens": {
+ "name": "refresh_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "authentication_method": {
+ "name": "authentication_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'legacy'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rotated_at": {
+ "name": "rotated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replaced_by_token_hash": {
+ "name": "replaced_by_token_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "refresh_tokens_user_id_idx": {
+ "name": "refresh_tokens_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "refresh_tokens_session_id_idx": {
+ "name": "refresh_tokens_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "refresh_tokens_expires_at_idx": {
+ "name": "refresh_tokens_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "refresh_tokens_user_id_users_id_fk": {
+ "name": "refresh_tokens_user_id_users_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "refresh_tokens_session_id_auth_sessions_id_fk": {
+ "name": "refresh_tokens_session_id_auth_sessions_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "auth_sessions",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "refresh_tokens_token_hash_unique": {
+ "name": "refresh_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "refresh_tokens_expires_after_created_chk": {
+ "name": "refresh_tokens_expires_after_created_chk",
+ "value": "\"refresh_tokens\".\"expires_at\" > \"refresh_tokens\".\"created_at\""
+ },
+ "refresh_tokens_authentication_method_chk": {
+ "name": "refresh_tokens_authentication_method_chk",
+ "value": "\"refresh_tokens\".\"authentication_method\" in ('password', 'oidc', 'magic_link', 'setup', 'legacy')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_content_filter_genres": {
+ "name": "user_content_filter_genres",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filter_type": {
+ "name": "filter_type",
+ "type": "content_filter_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "genre_id": {
+ "name": "genre_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "user_content_filter_genres_genre_id_idx": {
+ "name": "user_content_filter_genres_genre_id_idx",
+ "columns": [
+ {
+ "expression": "genre_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_content_filter_genres_user_id_users_id_fk": {
+ "name": "user_content_filter_genres_user_id_users_id_fk",
+ "tableFrom": "user_content_filter_genres",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_content_filter_genres_genre_id_genres_id_fk": {
+ "name": "user_content_filter_genres_genre_id_genres_id_fk",
+ "tableFrom": "user_content_filter_genres",
+ "tableTo": "genres",
+ "columnsFrom": ["genre_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_content_filter_genres_user_id_filter_type_genre_id_pk": {
+ "name": "user_content_filter_genres_user_id_filter_type_genre_id_pk",
+ "columns": ["user_id", "filter_type", "genre_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_content_filter_tags": {
+ "name": "user_content_filter_tags",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filter_type": {
+ "name": "filter_type",
+ "type": "content_filter_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "user_content_filter_tags_tag_id_idx": {
+ "name": "user_content_filter_tags_tag_id_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_content_filter_tags_user_id_users_id_fk": {
+ "name": "user_content_filter_tags_user_id_users_id_fk",
+ "tableFrom": "user_content_filter_tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_content_filter_tags_tag_id_tags_id_fk": {
+ "name": "user_content_filter_tags_tag_id_tags_id_fk",
+ "tableFrom": "user_content_filter_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_content_filter_tags_user_id_filter_type_tag_id_pk": {
+ "name": "user_content_filter_tags_user_id_filter_type_tag_id_pk",
+ "columns": ["user_id", "filter_type", "tag_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_library_access": {
+ "name": "user_library_access",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_level": {
+ "name": "access_level",
+ "type": "library_access_level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'viewer'"
+ }
+ },
+ "indexes": {
+ "user_library_access_library_user_idx": {
+ "name": "user_library_access_library_user_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_library_access_user_id_users_id_fk": {
+ "name": "user_library_access_user_id_users_id_fk",
+ "tableFrom": "user_library_access",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_library_access_library_id_libraries_id_fk": {
+ "name": "user_library_access_library_id_libraries_id_fk",
+ "tableFrom": "user_library_access",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_library_access_user_id_library_id_pk": {
+ "name": "user_library_access_user_id_library_id_pk",
+ "columns": ["user_id", "library_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_permissions": {
+ "name": "user_permissions",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_name": {
+ "name": "permission_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_permissions_user_id_users_id_fk": {
+ "name": "user_permissions_user_id_users_id_fk",
+ "tableFrom": "user_permissions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_permissions_user_id_permission_name_pk": {
+ "name": "user_permissions_user_id_permission_name_pk",
+ "columns": ["user_id", "permission_name"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_superuser": {
+ "name": "is_superuser",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_default_password": {
+ "name": "is_default_password",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "token_version": {
+ "name": "token_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "failed_login_attempts": {
+ "name": "failed_login_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "locked_until": {
+ "name": "locked_until",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "oidc_subject": {
+ "name": "oidc_subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oidc_issuer": {
+ "name": "oidc_issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_source": {
+ "name": "avatar_source",
+ "type": "user_avatar_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "avatar_version": {
+ "name": "avatar_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "provisioning_method": {
+ "name": "provisioning_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'local'"
+ },
+ "last_login_at": {
+ "name": "last_login_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_authenticated_at": {
+ "name": "last_authenticated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "see_own_requested_books": {
+ "name": "see_own_requested_books",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "reading_insights_sharing_level": {
+ "name": "reading_insights_sharing_level",
+ "type": "reading_insights_sharing_level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'private'"
+ },
+ "reading_insights_consented_at": {
+ "name": "reading_insights_consented_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "users_username_lower_uidx": {
+ "name": "users_username_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"username\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_lower_uidx": {
+ "name": "users_email_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"users\".\"email\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_oidc_subject_issuer_uidx": {
+ "name": "users_oidc_subject_issuer_uidx",
+ "columns": [
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_issuer",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"users\".\"oidc_subject\" is not null and \"users\".\"oidc_issuer\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_last_authenticated_at_idx": {
+ "name": "users_last_authenticated_at_idx",
+ "columns": [
+ {
+ "expression": "last_authenticated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "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": {
+ "users_provisioning_method_chk": {
+ "name": "users_provisioning_method_chk",
+ "value": "\"users\".\"provisioning_method\" in ('local', 'manual', 'oidc', 'shared')"
+ },
+ "users_token_version_nonnegative_chk": {
+ "name": "users_token_version_nonnegative_chk",
+ "value": "\"users\".\"token_version\" >= 0"
+ },
+ "users_failed_login_attempts_nonnegative_chk": {
+ "name": "users_failed_login_attempts_nonnegative_chk",
+ "value": "\"users\".\"failed_login_attempts\" >= 0"
+ },
+ "users_avatar_version_nonnegative_chk": {
+ "name": "users_avatar_version_nonnegative_chk",
+ "value": "\"users\".\"avatar_version\" >= 0"
+ },
+ "users_reading_insights_consent_chk": {
+ "name": "users_reading_insights_consent_chk",
+ "value": "(\"users\".\"reading_insights_sharing_level\" = 'private' and \"users\".\"reading_insights_consented_at\" is null) or (\"users\".\"reading_insights_sharing_level\" <> 'private' and \"users\".\"reading_insights_consented_at\" is not null)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.comic_metadata": {
+ "name": "comic_metadata",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issue_number": {
+ "name": "issue_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "volume_name": {
+ "name": "volume_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pencillers": {
+ "name": "pencillers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inkers": {
+ "name": "inkers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "colorists": {
+ "name": "colorists",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "letterers": {
+ "name": "letterers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_artists": {
+ "name": "cover_artists",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "characters": {
+ "name": "characters",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "teams": {
+ "name": "teams",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locations": {
+ "name": "locations",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "story_arcs": {
+ "name": "story_arcs",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "comic_metadata_book_id_books_id_fk": {
+ "name": "comic_metadata_book_id_books_id_fk",
+ "tableFrom": "comic_metadata",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "comic_metadata_book_id_unique": {
+ "name": "comic_metadata_book_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_custom_metadata_values": {
+ "name": "book_custom_metadata_values",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_id": {
+ "name": "field_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value_text": {
+ "name": "value_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value_number": {
+ "name": "value_number",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value_date": {
+ "name": "value_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "value_boolean": {
+ "name": "value_boolean",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_custom_metadata_values_field_idx": {
+ "name": "book_custom_metadata_values_field_idx",
+ "columns": [
+ {
+ "expression": "field_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_custom_metadata_values_book_id_books_id_fk": {
+ "name": "book_custom_metadata_values_book_id_books_id_fk",
+ "tableFrom": "book_custom_metadata_values",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_custom_metadata_values_field_id_custom_metadata_fields_id_fk": {
+ "name": "book_custom_metadata_values_field_id_custom_metadata_fields_id_fk",
+ "tableFrom": "book_custom_metadata_values",
+ "tableTo": "custom_metadata_fields",
+ "columnsFrom": ["field_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_custom_metadata_values_book_id_field_id_pk": {
+ "name": "book_custom_metadata_values_book_id_field_id_pk",
+ "columns": ["book_id", "field_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_custom_metadata_values_single_value_chk": {
+ "name": "book_custom_metadata_values_single_value_chk",
+ "value": "(\n (\"book_custom_metadata_values\".\"value_text\" is not null)::int +\n (\"book_custom_metadata_values\".\"value_number\" is not null)::int +\n (\"book_custom_metadata_values\".\"value_date\" is not null)::int +\n (\"book_custom_metadata_values\".\"value_boolean\" is not null)::int\n ) <= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.custom_metadata_fields": {
+ "name": "custom_metadata_fields",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_metadata_fields_key_uidx": {
+ "name": "custom_metadata_fields_key_uidx",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_metadata_fields_active_order_idx": {
+ "name": "custom_metadata_fields_active_order_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "label",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "custom_metadata_fields_type_chk": {
+ "name": "custom_metadata_fields_type_chk",
+ "value": "\"custom_metadata_fields\".\"type\" in ('text', 'url', 'number', 'date', 'boolean')"
+ },
+ "custom_metadata_fields_key_format_chk": {
+ "name": "custom_metadata_fields_key_format_chk",
+ "value": "\"custom_metadata_fields\".\"key\" ~ '^[a-z0-9][a-z0-9_]{0,99}$'"
+ },
+ "custom_metadata_fields_label_not_blank_chk": {
+ "name": "custom_metadata_fields_label_not_blank_chk",
+ "value": "length(btrim(\"custom_metadata_fields\".\"label\")) > 0"
+ },
+ "custom_metadata_fields_display_order_nonnegative_chk": {
+ "name": "custom_metadata_fields_display_order_nonnegative_chk",
+ "value": "\"custom_metadata_fields\".\"display_order\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.custom_metadata_library_fields": {
+ "name": "custom_metadata_library_fields",
+ "schema": "",
+ "columns": {
+ "field_id": {
+ "name": "field_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_metadata_library_fields_field_idx": {
+ "name": "custom_metadata_library_fields_field_idx",
+ "columns": [
+ {
+ "expression": "field_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_metadata_library_fields_library_order_idx": {
+ "name": "custom_metadata_library_fields_library_order_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "field_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_metadata_library_fields_field_id_custom_metadata_fields_id_fk": {
+ "name": "custom_metadata_library_fields_field_id_custom_metadata_fields_id_fk",
+ "tableFrom": "custom_metadata_library_fields",
+ "tableTo": "custom_metadata_fields",
+ "columnsFrom": ["field_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_metadata_library_fields_library_id_libraries_id_fk": {
+ "name": "custom_metadata_library_fields_library_id_libraries_id_fk",
+ "tableFrom": "custom_metadata_library_fields",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "custom_metadata_library_fields_library_id_field_id_pk": {
+ "name": "custom_metadata_library_fields_library_id_field_id_pk",
+ "columns": ["library_id", "field_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "custom_metadata_library_fields_display_order_nonnegative_chk": {
+ "name": "custom_metadata_library_fields_display_order_nonnegative_chk",
+ "value": "\"custom_metadata_library_fields\".\"display_order\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.custom_icons": {
+ "name": "custom_icons",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stored_file_name": {
+ "name": "stored_file_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "custom_icons_slug_uidx": {
+ "name": "custom_icons_slug_uidx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "custom_icons_slug_chk": {
+ "name": "custom_icons_slug_chk",
+ "value": "\"custom_icons\".\"slug\" ~ '^[a-z0-9][a-z0-9-]*[a-z0-9]$' or \"custom_icons\".\"slug\" ~ '^[a-z0-9]$'"
+ },
+ "custom_icons_file_size_positive_chk": {
+ "name": "custom_icons_file_size_positive_chk",
+ "value": "\"custom_icons\".\"file_size\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_plan_artifacts": {
+ "name": "migration_plan_artifacts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_id": {
+ "name": "profile_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_snapshot_hash": {
+ "name": "source_snapshot_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_hash": {
+ "name": "profile_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_hash": {
+ "name": "plan_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_data": {
+ "name": "source_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_plan_artifacts_plan_hash_uidx": {
+ "name": "migration_plan_artifacts_plan_hash_uidx",
+ "columns": [
+ {
+ "expression": "plan_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_plan_artifacts_source_id_idx": {
+ "name": "migration_plan_artifacts_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_plan_artifacts_profile_id_idx": {
+ "name": "migration_plan_artifacts_profile_id_idx",
+ "columns": [
+ {
+ "expression": "profile_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_plan_artifacts_source_id_migration_sources_id_fk": {
+ "name": "migration_plan_artifacts_source_id_migration_sources_id_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "migration_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_plan_artifacts_profile_id_migration_profiles_id_fk": {
+ "name": "migration_plan_artifacts_profile_id_migration_profiles_id_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_plan_artifacts_created_by_user_id_users_id_fk": {
+ "name": "migration_plan_artifacts_created_by_user_id_users_id_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "migration_plan_artifacts_profile_source_fk": {
+ "name": "migration_plan_artifacts_profile_source_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id", "source_id"],
+ "columnsTo": ["id", "source_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "migration_plan_artifacts_id_source_profile_unique": {
+ "name": "migration_plan_artifacts_id_source_profile_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "source_id", "profile_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.migration_profiles": {
+ "name": "migration_profiles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "user_mappings": {
+ "name": "user_mappings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path_mappings": {
+ "name": "path_mappings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "scope": {
+ "name": "scope",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_profiles_source_name_version_uidx": {
+ "name": "migration_profiles_source_name_version_uidx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_profiles_source_id_idx": {
+ "name": "migration_profiles_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_profiles_source_id_migration_sources_id_fk": {
+ "name": "migration_profiles_source_id_migration_sources_id_fk",
+ "tableFrom": "migration_profiles",
+ "tableTo": "migration_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_profiles_created_by_user_id_users_id_fk": {
+ "name": "migration_profiles_created_by_user_id_users_id_fk",
+ "tableFrom": "migration_profiles",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "migration_profiles_id_source_id_unique": {
+ "name": "migration_profiles_id_source_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "source_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "migration_profiles_version_positive_chk": {
+ "name": "migration_profiles_version_positive_chk",
+ "value": "\"migration_profiles\".\"version\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_run_metrics": {
+ "name": "migration_run_metrics",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage": {
+ "name": "stage",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processed": {
+ "name": "processed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "imported": {
+ "name": "imported",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped": {
+ "name": "skipped",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "unresolved": {
+ "name": "unresolved",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed": {
+ "name": "failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_run_metrics_run_stage_entity_uidx": {
+ "name": "migration_run_metrics_run_stage_entity_uidx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stage",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_run_metrics_run_id_idx": {
+ "name": "migration_run_metrics_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_run_metrics_run_id_migration_runs_id_fk": {
+ "name": "migration_run_metrics_run_id_migration_runs_id_fk",
+ "tableFrom": "migration_run_metrics",
+ "tableTo": "migration_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "migration_run_metrics_processed_nonnegative_chk": {
+ "name": "migration_run_metrics_processed_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"processed\" >= 0"
+ },
+ "migration_run_metrics_imported_nonnegative_chk": {
+ "name": "migration_run_metrics_imported_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"imported\" >= 0"
+ },
+ "migration_run_metrics_skipped_nonnegative_chk": {
+ "name": "migration_run_metrics_skipped_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"skipped\" >= 0"
+ },
+ "migration_run_metrics_unresolved_nonnegative_chk": {
+ "name": "migration_run_metrics_unresolved_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"unresolved\" >= 0"
+ },
+ "migration_run_metrics_failed_nonnegative_chk": {
+ "name": "migration_run_metrics_failed_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"failed\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_runs": {
+ "name": "migration_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_id": {
+ "name": "profile_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_artifact_id": {
+ "name": "plan_artifact_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_key": {
+ "name": "target_key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bookorbit'"
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "current_stage": {
+ "name": "current_stage",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "triggered_by_user_id": {
+ "name": "triggered_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_runs_source_target_state_idx": {
+ "name": "migration_runs_source_target_state_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_runs_state_idx": {
+ "name": "migration_runs_state_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_runs_source_id_migration_sources_id_fk": {
+ "name": "migration_runs_source_id_migration_sources_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_runs_profile_id_migration_profiles_id_fk": {
+ "name": "migration_runs_profile_id_migration_profiles_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_runs_plan_artifact_id_migration_plan_artifacts_id_fk": {
+ "name": "migration_runs_plan_artifact_id_migration_plan_artifacts_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_plan_artifacts",
+ "columnsFrom": ["plan_artifact_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "migration_runs_triggered_by_user_id_users_id_fk": {
+ "name": "migration_runs_triggered_by_user_id_users_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "users",
+ "columnsFrom": ["triggered_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "migration_runs_profile_source_fk": {
+ "name": "migration_runs_profile_source_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id", "source_id"],
+ "columnsTo": ["id", "source_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "migration_runs_plan_artifact_source_profile_fk": {
+ "name": "migration_runs_plan_artifact_source_profile_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_plan_artifacts",
+ "columnsFrom": ["plan_artifact_id", "source_id", "profile_id"],
+ "columnsTo": ["id", "source_id", "profile_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "migration_runs_state_chk": {
+ "name": "migration_runs_state_chk",
+ "value": "\"state\" IN ('draft', 'preflight_failed', 'dry_run_ready', 'running', 'failed', 'completed')"
+ },
+ "migration_runs_started_before_ended_chk": {
+ "name": "migration_runs_started_before_ended_chk",
+ "value": "\"migration_runs\".\"ended_at\" is null or \"migration_runs\".\"started_at\" is null or \"migration_runs\".\"ended_at\" >= \"migration_runs\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_sources": {
+ "name": "migration_sources",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_config": {
+ "name": "connection_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "capabilities": {
+ "name": "capabilities",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_validated_at": {
+ "name": "last_validated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_sources_type_name_uidx": {
+ "name": "migration_sources_type_name_uidx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_sources_created_by_user_id_idx": {
+ "name": "migration_sources_created_by_user_id_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_sources_created_by_user_id_users_id_fk": {
+ "name": "migration_sources_created_by_user_id_users_id_fk",
+ "tableFrom": "migration_sources",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.libraries": {
+ "name": "libraries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'books'"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cover_aspect_ratio": {
+ "name": "cover_aspect_ratio",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'2/3'"
+ },
+ "watch": {
+ "name": "watch",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "watch_local_folders": {
+ "name": "watch_local_folders",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_scan_cron_expression": {
+ "name": "auto_scan_cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_precedence": {
+ "name": "metadata_precedence",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"folderStructure\",\"embedded\",\"nfoFile\",\"opfFile\",\"sidecar\"]'::jsonb"
+ },
+ "regex_metadata": {
+ "name": "regex_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format_priority": {
+ "name": "format_priority",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"epub\",\"pdf\",\"cbz\",\"cbr\",\"cb7\",\"mobi\",\"azw3\",\"azw\",\"fb2\",\"m4b\",\"mp3\",\"m4a\",\"opus\",\"ogg\",\"flac\"]'::jsonb"
+ },
+ "allowed_formats": {
+ "name": "allowed_formats",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "organization_mode": {
+ "name": "organization_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'book_per_folder'"
+ },
+ "added_at_source": {
+ "name": "added_at_source",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'imported'"
+ },
+ "exclude_patterns": {
+ "name": "exclude_patterns",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "reading_threshold": {
+ "name": "reading_threshold",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0.25
+ },
+ "mark_as_finished_percent_complete": {
+ "name": "mark_as_finished_percent_complete",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 98
+ },
+ "file_write_enabled": {
+ "name": "file_write_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_write_cover": {
+ "name": "file_write_write_cover",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_write_epub_enabled": {
+ "name": "file_write_epub_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_write_epub_max_file_size_mb": {
+ "name": "file_write_epub_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "file_write_fb2_enabled": {
+ "name": "file_write_fb2_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_fb2_max_file_size_mb": {
+ "name": "file_write_fb2_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "file_write_pdf_enabled": {
+ "name": "file_write_pdf_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_write_pdf_max_file_size_mb": {
+ "name": "file_write_pdf_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "file_write_cbx_enabled": {
+ "name": "file_write_cbx_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_cbx_max_file_size_mb": {
+ "name": "file_write_cbx_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 500
+ },
+ "file_write_kindle_enabled": {
+ "name": "file_write_kindle_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_kindle_max_file_size_mb": {
+ "name": "file_write_kindle_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "file_write_audio_enabled": {
+ "name": "file_write_audio_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_audio_max_file_size_mb": {
+ "name": "file_write_audio_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 500
+ },
+ "file_write_all_files": {
+ "name": "file_write_all_files",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_read_along_enabled": {
+ "name": "file_write_read_along_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_read_along_max_file_size_mb": {
+ "name": "file_write_read_along_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1000
+ },
+ "file_rename_enabled": {
+ "name": "file_rename_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_naming_pattern": {
+ "name": "file_naming_pattern",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_fetch_preferences": {
+ "name": "metadata_fetch_preferences",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_config": {
+ "name": "book_metadata_fetch_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_last_run_at": {
+ "name": "book_metadata_fetch_last_run_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_last_queued_count": {
+ "name": "book_metadata_fetch_last_queued_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scan_mode": {
+ "name": "scan_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'auto'"
+ },
+ "poll_interval_seconds": {
+ "name": "poll_interval_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 300
+ },
+ "koreader_hash_revision": {
+ "name": "koreader_hash_revision",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "libraries_name_lower_uidx": {
+ "name": "libraries_name_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "libraries_display_order_nonnegative_chk": {
+ "name": "libraries_display_order_nonnegative_chk",
+ "value": "\"libraries\".\"display_order\" >= 0"
+ },
+ "libraries_type_chk": {
+ "name": "libraries_type_chk",
+ "value": "\"libraries\".\"type\" in ('books', 'podcasts')"
+ },
+ "libraries_organization_mode_chk": {
+ "name": "libraries_organization_mode_chk",
+ "value": "\"libraries\".\"organization_mode\" in ('book_per_folder', 'book_per_file')"
+ },
+ "libraries_added_at_source_chk": {
+ "name": "libraries_added_at_source_chk",
+ "value": "\"libraries\".\"added_at_source\" in ('imported', 'file_modified', 'file_created')"
+ },
+ "libraries_reading_threshold_range_chk": {
+ "name": "libraries_reading_threshold_range_chk",
+ "value": "\"libraries\".\"reading_threshold\" >= 0 and \"libraries\".\"reading_threshold\" <= 100"
+ },
+ "libraries_mark_finished_percent_range_chk": {
+ "name": "libraries_mark_finished_percent_range_chk",
+ "value": "\"libraries\".\"mark_as_finished_percent_complete\" >= 0 and \"libraries\".\"mark_as_finished_percent_complete\" <= 100"
+ },
+ "libraries_scan_mode_chk": {
+ "name": "libraries_scan_mode_chk",
+ "value": "\"libraries\".\"scan_mode\" in ('auto', 'manual')"
+ },
+ "libraries_poll_interval_nonnegative_chk": {
+ "name": "libraries_poll_interval_nonnegative_chk",
+ "value": "\"libraries\".\"poll_interval_seconds\" is null or \"libraries\".\"poll_interval_seconds\" >= 0"
+ },
+ "libraries_file_write_epub_max_size_chk": {
+ "name": "libraries_file_write_epub_max_size_chk",
+ "value": "\"libraries\".\"file_write_epub_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_pdf_max_size_chk": {
+ "name": "libraries_file_write_pdf_max_size_chk",
+ "value": "\"libraries\".\"file_write_pdf_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_cbx_max_size_chk": {
+ "name": "libraries_file_write_cbx_max_size_chk",
+ "value": "\"libraries\".\"file_write_cbx_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_kindle_max_size_chk": {
+ "name": "libraries_file_write_kindle_max_size_chk",
+ "value": "\"libraries\".\"file_write_kindle_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_audio_max_size_chk": {
+ "name": "libraries_file_write_audio_max_size_chk",
+ "value": "\"libraries\".\"file_write_audio_max_file_size_mb\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.library_folders": {
+ "name": "library_folders",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scan_state_version": {
+ "name": "scan_state_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'downloads'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "library_folders_library_id_idx": {
+ "name": "library_folders_library_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "library_folders_library_path_uidx": {
+ "name": "library_folders_library_path_uidx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "library_folders_library_id_libraries_id_fk": {
+ "name": "library_folders_library_id_libraries_id_fk",
+ "tableFrom": "library_folders",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "library_folders_id_library_id_unique": {
+ "name": "library_folders_id_library_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "library_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "library_folders_scan_state_version_nonnegative_chk": {
+ "name": "library_folders_scan_state_version_nonnegative_chk",
+ "value": "\"library_folders\".\"scan_state_version\" >= 0"
+ },
+ "library_folders_role_chk": {
+ "name": "library_folders_role_chk",
+ "value": "\"library_folders\".\"role\" in ('downloads', 'local')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_files": {
+ "name": "book_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "public_id": {
+ "name": "public_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_folder_id": {
+ "name": "library_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "absolute_path": {
+ "name": "absolute_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rel_path": {
+ "name": "rel_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ino": {
+ "name": "ino",
+ "type": "numeric(20, 0)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mtime": {
+ "name": "mtime",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'content'"
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_write_provenance": {
+ "name": "chapter_write_provenance",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_fixed_layout": {
+ "name": "is_fixed_layout",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_overlay_available": {
+ "name": "media_overlay_available",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "media_overlay_duration_seconds": {
+ "name": "media_overlay_duration_seconds",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_overlay_checked_at": {
+ "name": "media_overlay_checked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_files_public_id_uidx": {
+ "name": "book_files_public_id_uidx",
+ "columns": [
+ {
+ "expression": "public_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_absolute_path_uidx": {
+ "name": "book_files_absolute_path_uidx",
+ "columns": [
+ {
+ "expression": "absolute_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_book_id_idx": {
+ "name": "book_files_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_change_timestamp_idx": {
+ "name": "book_files_change_timestamp_idx",
+ "columns": [
+ {
+ "expression": "greatest(\"created_at\", \"updated_at\") desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_library_folder_id_idx": {
+ "name": "book_files_library_folder_id_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_file_hash_idx": {
+ "name": "book_files_file_hash_idx",
+ "columns": [
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_ino_idx": {
+ "name": "book_files_ino_idx",
+ "columns": [
+ {
+ "expression": "ino",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_format_idx": {
+ "name": "book_files_format_idx",
+ "columns": [
+ {
+ "expression": "format",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_media_overlay_available_idx": {
+ "name": "book_files_media_overlay_available_idx",
+ "columns": [
+ {
+ "expression": "media_overlay_available",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_files\".\"media_overlay_available\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_library_folder_file_hash_idx": {
+ "name": "book_files_library_folder_file_hash_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_library_folder_ino_idx": {
+ "name": "book_files_library_folder_ino_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "ino",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_files_book_id_books_id_fk": {
+ "name": "book_files_book_id_books_id_fk",
+ "tableFrom": "book_files",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_files_library_folder_id_library_folders_id_fk": {
+ "name": "book_files_library_folder_id_library_folders_id_fk",
+ "tableFrom": "book_files",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_files_book_folder_consistency_fk": {
+ "name": "book_files_book_folder_consistency_fk",
+ "tableFrom": "book_files",
+ "tableTo": "books",
+ "columnsFrom": ["book_id", "library_folder_id"],
+ "columnsTo": ["id", "library_folder_id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_files_role_chk": {
+ "name": "book_files_role_chk",
+ "value": "\"book_files\".\"role\" in ('content', 'cover', 'metadata', 'supplement')"
+ },
+ "book_files_size_bytes_nonnegative_chk": {
+ "name": "book_files_size_bytes_nonnegative_chk",
+ "value": "\"book_files\".\"size_bytes\" is null or \"book_files\".\"size_bytes\" >= 0"
+ },
+ "book_files_duration_seconds_nonnegative_chk": {
+ "name": "book_files_duration_seconds_nonnegative_chk",
+ "value": "\"book_files\".\"duration_seconds\" is null or \"book_files\".\"duration_seconds\" >= 0"
+ },
+ "book_files_media_overlay_duration_seconds_nonnegative_chk": {
+ "name": "book_files_media_overlay_duration_seconds_nonnegative_chk",
+ "value": "\"book_files\".\"media_overlay_duration_seconds\" is null or \"book_files\".\"media_overlay_duration_seconds\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.books": {
+ "name": "books",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_folder_id": {
+ "name": "library_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "primary_file_id": {
+ "name": "primary_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "primary_author_sort_name": {
+ "name": "primary_author_sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_path": {
+ "name": "folder_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'present'"
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "books_library_id_folder_path_idx": {
+ "name": "books_library_id_folder_path_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "folder_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_primary_file_id_idx": {
+ "name": "books_primary_file_id_idx",
+ "columns": [
+ {
+ "expression": "primary_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_status_id_idx": {
+ "name": "books_library_status_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_added_at_idx": {
+ "name": "books_library_added_at_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"added_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_visible_added_id_idx": {
+ "name": "books_library_visible_added_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"added_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_author_sort_id_idx": {
+ "name": "books_library_author_sort_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"primary_author_sort_name\" asc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_author_sort_desc_id_idx": {
+ "name": "books_library_author_sort_desc_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"primary_author_sort_name\" desc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "books_library_id_libraries_id_fk": {
+ "name": "books_library_id_libraries_id_fk",
+ "tableFrom": "books",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "books_library_folder_id_library_folders_id_fk": {
+ "name": "books_library_folder_id_library_folders_id_fk",
+ "tableFrom": "books",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "books_primary_file_id_book_files_id_fk": {
+ "name": "books_primary_file_id_book_files_id_fk",
+ "tableFrom": "books",
+ "tableTo": "book_files",
+ "columnsFrom": ["primary_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "books_library_folder_library_fk": {
+ "name": "books_library_folder_library_fk",
+ "tableFrom": "books",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id", "library_id"],
+ "columnsTo": ["id", "library_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "books_id_library_folder_id_unique": {
+ "name": "books_id_library_folder_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "library_folder_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "books_status_chk": {
+ "name": "books_status_chk",
+ "value": "\"books\".\"status\" in ('present', 'missing', 'processing')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_covers": {
+ "name": "book_covers",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "medium": {
+ "name": "medium",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(9)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(12)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "dormant_since": {
+ "name": "dormant_since",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "book_covers_book_id_books_id_fk": {
+ "name": "book_covers_book_id_books_id_fk",
+ "tableFrom": "book_covers",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_covers_book_id_medium_pk": {
+ "name": "book_covers_book_id_medium_pk",
+ "columns": ["book_id", "medium"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_covers_medium_chk": {
+ "name": "book_covers_medium_chk",
+ "value": "\"book_covers\".\"medium\" in ('ebook', 'audio')"
+ },
+ "book_covers_source_chk": {
+ "name": "book_covers_source_chk",
+ "value": "\"book_covers\".\"source\" in ('extracted', 'custom')"
+ },
+ "book_covers_origin_chk": {
+ "name": "book_covers_origin_chk",
+ "value": "\"book_covers\".\"origin\" in ('embedded', 'folder_image', 'opf', 'provider', 'dock', 'upload', 'legacy')"
+ },
+ "book_covers_width_positive_chk": {
+ "name": "book_covers_width_positive_chk",
+ "value": "\"book_covers\".\"width\" is null or \"book_covers\".\"width\" > 0"
+ },
+ "book_covers_height_positive_chk": {
+ "name": "book_covers_height_positive_chk",
+ "value": "\"book_covers\".\"height\" is null or \"book_covers\".\"height\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_duplicate_dismissals": {
+ "name": "book_duplicate_dismissals",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id_a": {
+ "name": "book_id_a",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id_b": {
+ "name": "book_id_b",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_duplicate_dismissals_user_created_idx": {
+ "name": "book_duplicate_dismissals_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"created_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_duplicate_dismissals_user_id_users_id_fk": {
+ "name": "book_duplicate_dismissals_user_id_users_id_fk",
+ "tableFrom": "book_duplicate_dismissals",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_dismissals_book_id_a_books_id_fk": {
+ "name": "book_duplicate_dismissals_book_id_a_books_id_fk",
+ "tableFrom": "book_duplicate_dismissals",
+ "tableTo": "books",
+ "columnsFrom": ["book_id_a"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_dismissals_book_id_b_books_id_fk": {
+ "name": "book_duplicate_dismissals_book_id_b_books_id_fk",
+ "tableFrom": "book_duplicate_dismissals",
+ "tableTo": "books",
+ "columnsFrom": ["book_id_b"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_duplicate_dismissals_user_id_book_id_a_book_id_b_pk": {
+ "name": "book_duplicate_dismissals_user_id_book_id_a_book_id_b_pk",
+ "columns": ["user_id", "book_id_a", "book_id_b"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_duplicate_dismissals_order_chk": {
+ "name": "book_duplicate_dismissals_order_chk",
+ "value": "\"book_duplicate_dismissals\".\"book_id_a\" < \"book_duplicate_dismissals\".\"book_id_b\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_duplicate_group_members": {
+ "name": "book_duplicate_group_members",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scan_id": {
+ "name": "scan_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_duplicate_group_members_group_idx": {
+ "name": "book_duplicate_group_members_group_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_duplicate_group_members_group_id_book_duplicate_groups_id_fk": {
+ "name": "book_duplicate_group_members_group_id_book_duplicate_groups_id_fk",
+ "tableFrom": "book_duplicate_group_members",
+ "tableTo": "book_duplicate_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_group_members_scan_id_book_duplicate_scans_id_fk": {
+ "name": "book_duplicate_group_members_scan_id_book_duplicate_scans_id_fk",
+ "tableFrom": "book_duplicate_group_members",
+ "tableTo": "book_duplicate_scans",
+ "columnsFrom": ["scan_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_group_members_book_id_books_id_fk": {
+ "name": "book_duplicate_group_members_book_id_books_id_fk",
+ "tableFrom": "book_duplicate_group_members",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_duplicate_group_members_scan_id_book_id_pk": {
+ "name": "book_duplicate_group_members_scan_id_book_id_pk",
+ "columns": ["scan_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_duplicate_groups": {
+ "name": "book_duplicate_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "scan_id": {
+ "name": "scan_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "root_book_id": {
+ "name": "root_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reasons": {
+ "name": "reasons",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "max_title_similarity": {
+ "name": "max_title_similarity",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_count": {
+ "name": "member_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "confidence": {
+ "name": "confidence",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "member_bytes_total": {
+ "name": "member_bytes_total",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "member_bytes_max": {
+ "name": "member_bytes_max",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "book_duplicate_groups_scan_root_uidx": {
+ "name": "book_duplicate_groups_scan_root_uidx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "root_book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_groups_scan_id_idx": {
+ "name": "book_duplicate_groups_scan_id_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_groups_scan_member_count_idx": {
+ "name": "book_duplicate_groups_scan_member_count_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "member_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_groups_scan_reclaimable_idx": {
+ "name": "book_duplicate_groups_scan_reclaimable_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "(\"member_bytes_total\" - \"member_bytes_max\") desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_groups_scan_confidence_idx": {
+ "name": "book_duplicate_groups_scan_confidence_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "confidence",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_duplicate_groups_scan_id_book_duplicate_scans_id_fk": {
+ "name": "book_duplicate_groups_scan_id_book_duplicate_scans_id_fk",
+ "tableFrom": "book_duplicate_groups",
+ "tableTo": "book_duplicate_scans",
+ "columnsFrom": ["scan_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_groups_root_book_id_books_id_fk": {
+ "name": "book_duplicate_groups_root_book_id_books_id_fk",
+ "tableFrom": "book_duplicate_groups",
+ "tableTo": "books",
+ "columnsFrom": ["root_book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_duplicate_groups_confidence_chk": {
+ "name": "book_duplicate_groups_confidence_chk",
+ "value": "\"book_duplicate_groups\".\"confidence\" between 1 and 4"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_duplicate_pairs": {
+ "name": "book_duplicate_pairs",
+ "schema": "",
+ "columns": {
+ "scan_id": {
+ "name": "scan_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_id_a": {
+ "name": "book_id_a",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id_b": {
+ "name": "book_id_b",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reasons": {
+ "name": "reasons",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title_similarity": {
+ "name": "title_similarity",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "book_duplicate_pairs_group_idx": {
+ "name": "book_duplicate_pairs_group_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_pairs_scan_a_idx": {
+ "name": "book_duplicate_pairs_scan_a_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_pairs_scan_b_idx": {
+ "name": "book_duplicate_pairs_scan_b_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_duplicate_pairs_scan_id_book_duplicate_scans_id_fk": {
+ "name": "book_duplicate_pairs_scan_id_book_duplicate_scans_id_fk",
+ "tableFrom": "book_duplicate_pairs",
+ "tableTo": "book_duplicate_scans",
+ "columnsFrom": ["scan_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_pairs_group_id_book_duplicate_groups_id_fk": {
+ "name": "book_duplicate_pairs_group_id_book_duplicate_groups_id_fk",
+ "tableFrom": "book_duplicate_pairs",
+ "tableTo": "book_duplicate_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_pairs_book_id_a_books_id_fk": {
+ "name": "book_duplicate_pairs_book_id_a_books_id_fk",
+ "tableFrom": "book_duplicate_pairs",
+ "tableTo": "books",
+ "columnsFrom": ["book_id_a"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_pairs_book_id_b_books_id_fk": {
+ "name": "book_duplicate_pairs_book_id_b_books_id_fk",
+ "tableFrom": "book_duplicate_pairs",
+ "tableTo": "books",
+ "columnsFrom": ["book_id_b"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_duplicate_pairs_scan_id_book_id_a_book_id_b_pk": {
+ "name": "book_duplicate_pairs_scan_id_book_id_a_book_id_b_pk",
+ "columns": ["scan_id", "book_id_a", "book_id_b"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_duplicate_pairs_order_chk": {
+ "name": "book_duplicate_pairs_order_chk",
+ "value": "\"book_duplicate_pairs\".\"book_id_a\" < \"book_duplicate_pairs\".\"book_id_b\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_duplicate_scan_keys": {
+ "name": "book_duplicate_scan_keys",
+ "schema": "",
+ "columns": {
+ "scan_id": {
+ "name": "scan_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_duplicate_scan_keys_lookup_idx": {
+ "name": "book_duplicate_scan_keys_lookup_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "value",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_duplicate_scan_keys_scan_id_book_duplicate_scans_id_fk": {
+ "name": "book_duplicate_scan_keys_scan_id_book_duplicate_scans_id_fk",
+ "tableFrom": "book_duplicate_scan_keys",
+ "tableTo": "book_duplicate_scans",
+ "columnsFrom": ["scan_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_duplicate_scan_keys_book_id_books_id_fk": {
+ "name": "book_duplicate_scan_keys_book_id_books_id_fk",
+ "tableFrom": "book_duplicate_scan_keys",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_duplicate_scan_keys_scan_id_book_id_kind_value_pk": {
+ "name": "book_duplicate_scan_keys_scan_id_book_id_kind_value_pk",
+ "columns": ["scan_id", "book_id", "kind", "value"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_duplicate_scan_keys_kind_chk": {
+ "name": "book_duplicate_scan_keys_kind_chk",
+ "value": "\"book_duplicate_scan_keys\".\"kind\" in ('file_hash', 'isbn', 'exact_metadata')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_duplicate_scans": {
+ "name": "book_duplicate_scans",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_ids": {
+ "name": "library_ids",
+ "type": "integer[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "requested_library_id": {
+ "name": "requested_library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "similarity_percent": {
+ "name": "similarity_percent",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "processed_books": {
+ "name": "processed_books",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_books": {
+ "name": "total_books",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_groups": {
+ "name": "total_groups",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_extra_copies": {
+ "name": "total_extra_copies",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_reclaimable_bytes": {
+ "name": "total_reclaimable_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "book_duplicate_scans_user_created_idx": {
+ "name": "book_duplicate_scans_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"created_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_duplicate_scans_status_idx": {
+ "name": "book_duplicate_scans_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_duplicate_scans_user_id_users_id_fk": {
+ "name": "book_duplicate_scans_user_id_users_id_fk",
+ "tableFrom": "book_duplicate_scans",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_duplicate_scans_similarity_chk": {
+ "name": "book_duplicate_scans_similarity_chk",
+ "value": "\"book_duplicate_scans\".\"similarity_percent\" between 70 and 100"
+ },
+ "book_duplicate_scans_status_chk": {
+ "name": "book_duplicate_scans_status_chk",
+ "value": "\"book_duplicate_scans\".\"status\" in ('queued', 'running', 'completed', 'failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_move_jobs": {
+ "name": "book_move_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "started_by": {
+ "name": "started_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_library_id": {
+ "name": "target_library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_folder_id": {
+ "name": "target_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_library_ids": {
+ "name": "source_library_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "total_books": {
+ "name": "total_books",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "succeeded": {
+ "name": "succeeded",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "merged": {
+ "name": "merged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed": {
+ "name": "failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped": {
+ "name": "skipped",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "book_move_jobs_status_idx": {
+ "name": "book_move_jobs_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_move_jobs_started_at_idx": {
+ "name": "book_move_jobs_started_at_idx",
+ "columns": [
+ {
+ "expression": "\"started_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_move_jobs_started_by_users_id_fk": {
+ "name": "book_move_jobs_started_by_users_id_fk",
+ "tableFrom": "book_move_jobs",
+ "tableTo": "users",
+ "columnsFrom": ["started_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_move_jobs_target_library_id_libraries_id_fk": {
+ "name": "book_move_jobs_target_library_id_libraries_id_fk",
+ "tableFrom": "book_move_jobs",
+ "tableTo": "libraries",
+ "columnsFrom": ["target_library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_move_jobs_target_folder_id_library_folders_id_fk": {
+ "name": "book_move_jobs_target_folder_id_library_folders_id_fk",
+ "tableFrom": "book_move_jobs",
+ "tableTo": "library_folders",
+ "columnsFrom": ["target_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_move_jobs_status_chk": {
+ "name": "book_move_jobs_status_chk",
+ "value": "\"book_move_jobs\".\"status\" in ('running', 'completed', 'failed', 'interrupted')"
+ },
+ "book_move_jobs_counts_nonnegative_chk": {
+ "name": "book_move_jobs_counts_nonnegative_chk",
+ "value": "\"book_move_jobs\".\"total_books\" >= 0 and \"book_move_jobs\".\"succeeded\" >= 0 and \"book_move_jobs\".\"merged\" >= 0 and \"book_move_jobs\".\"failed\" >= 0 and \"book_move_jobs\".\"skipped\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.collection_books": {
+ "name": "collection_books",
+ "schema": "",
+ "columns": {
+ "collection_id": {
+ "name": "collection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "position": {
+ "name": "position",
+ "type": "bigserial",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "collection_books_book_id_idx": {
+ "name": "collection_books_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "collection_books_collection_position_idx": {
+ "name": "collection_books_collection_position_idx",
+ "columns": [
+ {
+ "expression": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collection_books_collection_id_collections_id_fk": {
+ "name": "collection_books_collection_id_collections_id_fk",
+ "tableFrom": "collection_books",
+ "tableTo": "collections",
+ "columnsFrom": ["collection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "collection_books_book_id_books_id_fk": {
+ "name": "collection_books_book_id_books_id_fk",
+ "tableFrom": "collection_books",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "collection_books_collection_id_book_id_pk": {
+ "name": "collection_books_collection_id_book_id_pk",
+ "columns": ["collection_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collection_podcasts": {
+ "name": "collection_podcasts",
+ "schema": "",
+ "columns": {
+ "collection_id": {
+ "name": "collection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "podcast_id": {
+ "name": "podcast_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collection_podcasts_podcast_id_idx": {
+ "name": "collection_podcasts_podcast_id_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collection_podcasts_collection_id_collections_id_fk": {
+ "name": "collection_podcasts_collection_id_collections_id_fk",
+ "tableFrom": "collection_podcasts",
+ "tableTo": "collections",
+ "columnsFrom": ["collection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "collection_podcasts_podcast_id_podcasts_id_fk": {
+ "name": "collection_podcasts_podcast_id_podcasts_id_fk",
+ "tableFrom": "collection_podcasts",
+ "tableTo": "podcasts",
+ "columnsFrom": ["podcast_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "collection_podcasts_collection_id_podcast_id_pk": {
+ "name": "collection_podcasts_collection_id_podcast_id_pk",
+ "columns": ["collection_id", "podcast_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collections": {
+ "name": "collections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_type": {
+ "name": "media_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'books'"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sync_to_kobo": {
+ "name": "sync_to_kobo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collections_user_media_type_name_uidx": {
+ "name": "collections_user_media_type_name_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "media_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "collections_user_display_name_idx": {
+ "name": "collections_user_display_name_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "collections_public_display_name_idx": {
+ "name": "collections_public_display_name_idx",
+ "columns": [
+ {
+ "expression": "is_public",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collections_user_id_users_id_fk": {
+ "name": "collections_user_id_users_id_fk",
+ "tableFrom": "collections",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "collections_media_type_chk": {
+ "name": "collections_media_type_chk",
+ "value": "\"collections\".\"media_type\" in ('books', 'podcasts')"
+ },
+ "collections_kobo_books_only_chk": {
+ "name": "collections_kobo_books_only_chk",
+ "value": "\"collections\".\"sync_to_kobo\" = false or \"collections\".\"media_type\" = 'books'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.smart_scope_kobo_subscriptions": {
+ "name": "smart_scope_kobo_subscriptions",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "smart_scope_id": {
+ "name": "smart_scope_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "smart_scope_kobo_subscriptions_smart_scope_id_idx": {
+ "name": "smart_scope_kobo_subscriptions_smart_scope_id_idx",
+ "columns": [
+ {
+ "expression": "smart_scope_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "smart_scope_kobo_subscriptions_user_id_users_id_fk": {
+ "name": "smart_scope_kobo_subscriptions_user_id_users_id_fk",
+ "tableFrom": "smart_scope_kobo_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "smart_scope_kobo_subscriptions_smart_scope_id_smart_scopes_id_fk": {
+ "name": "smart_scope_kobo_subscriptions_smart_scope_id_smart_scopes_id_fk",
+ "tableFrom": "smart_scope_kobo_subscriptions",
+ "tableTo": "smart_scopes",
+ "columnsFrom": ["smart_scope_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "smart_scope_kobo_subscriptions_user_id_smart_scope_id_pk": {
+ "name": "smart_scope_kobo_subscriptions_user_id_smart_scope_id_pk",
+ "columns": ["user_id", "smart_scope_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.smart_scopes": {
+ "name": "smart_scopes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_type": {
+ "name": "media_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'books'"
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filter": {
+ "name": "filter",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_sort": {
+ "name": "default_sort",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sync_to_kobo": {
+ "name": "sync_to_kobo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "smart_scopes_user_media_type_idx": {
+ "name": "smart_scopes_user_media_type_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "media_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "smart_scopes_user_id_users_id_fk": {
+ "name": "smart_scopes_user_id_users_id_fk",
+ "tableFrom": "smart_scopes",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "smart_scopes_library_id_libraries_id_fk": {
+ "name": "smart_scopes_library_id_libraries_id_fk",
+ "tableFrom": "smart_scopes",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "smart_scopes_user_id_media_type_name_unique": {
+ "name": "smart_scopes_user_id_media_type_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "media_type", "name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "smart_scopes_media_type_chk": {
+ "name": "smart_scopes_media_type_chk",
+ "value": "\"smart_scopes\".\"media_type\" in ('books', 'podcasts')"
+ },
+ "smart_scopes_library_scope_chk": {
+ "name": "smart_scopes_library_scope_chk",
+ "value": "(\"smart_scopes\".\"media_type\" = 'podcasts') = (\"smart_scopes\".\"library_id\" is not null)"
+ },
+ "smart_scopes_kobo_books_only_chk": {
+ "name": "smart_scopes_kobo_books_only_chk",
+ "value": "\"smart_scopes\".\"sync_to_kobo\" = false or \"smart_scopes\".\"media_type\" = 'books'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_series": {
+ "name": "book_series",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_name": {
+ "name": "normalized_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expected_book_count": {
+ "name": "expected_book_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expected_book_count_source": {
+ "name": "expected_book_count_source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expected_book_count_updated_at": {
+ "name": "expected_book_count_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_series_normalized_name_uidx": {
+ "name": "book_series_normalized_name_uidx",
+ "columns": [
+ {
+ "expression": "normalized_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_name_trgm_idx": {
+ "name": "book_series_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "book_series_name_unaccent_trgm_idx": {
+ "name": "book_series_name_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"name\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "book_series_name_lower_idx": {
+ "name": "book_series_name_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_series_expected_book_count_range_chk": {
+ "name": "book_series_expected_book_count_range_chk",
+ "value": "\"book_series\".\"expected_book_count\" IS NULL OR (\"book_series\".\"expected_book_count\" >= 1 AND \"book_series\".\"expected_book_count\" <= 10000)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_series_memberships": {
+ "name": "book_series_memberships",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "series_id": {
+ "name": "series_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "series_index": {
+ "name": "series_index",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_series_memberships_book_display_uidx": {
+ "name": "book_series_memberships_book_display_uidx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_memberships_series_index_book_idx": {
+ "name": "book_series_memberships_series_index_book_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "(CASE WHEN \"series_index\" IS NULL THEN NULL ELSE ARRAY[split_part(\"series_index\", '.', 1)::numeric, CASE WHEN strpos(\"series_index\", '.') = 0 THEN -1::numeric ELSE split_part(\"series_index\", '.', 2)::numeric END] END)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"series_index\" COLLATE \"C\"",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_memberships_book_display_idx": {
+ "name": "book_series_memberships_book_display_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_series_memberships_book_id_books_id_fk": {
+ "name": "book_series_memberships_book_id_books_id_fk",
+ "tableFrom": "book_series_memberships",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_series_memberships_series_id_book_series_id_fk": {
+ "name": "book_series_memberships_series_id_book_series_id_fk",
+ "tableFrom": "book_series_memberships",
+ "tableTo": "book_series",
+ "columnsFrom": ["series_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_series_memberships_book_id_series_id_pk": {
+ "name": "book_series_memberships_book_id_series_id_pk",
+ "columns": ["book_id", "series_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_series_memberships_display_order_nonnegative_chk": {
+ "name": "book_series_memberships_display_order_nonnegative_chk",
+ "value": "\"book_series_memberships\".\"display_order\" >= 0"
+ },
+ "book_series_memberships_series_index_format_chk": {
+ "name": "book_series_memberships_series_index_format_chk",
+ "value": "\"book_series_memberships\".\"series_index\" is null or \"book_series_memberships\".\"series_index\" ~ '^[0-9]+([.][0-9]+)?$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.author_enrichment_queue": {
+ "name": "author_enrichment_queue",
+ "schema": "",
+ "columns": {
+ "author_id": {
+ "name": "author_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_http_status": {
+ "name": "last_http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ignore_cache": {
+ "name": "ignore_cache",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "author_enrichment_queue_status_next_attempt_idx": {
+ "name": "author_enrichment_queue_status_next_attempt_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "author_enrichment_queue_next_attempt_idx": {
+ "name": "author_enrichment_queue_next_attempt_idx",
+ "columns": [
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "author_enrichment_queue_single_processing_idx": {
+ "name": "author_enrichment_queue_single_processing_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"author_enrichment_queue\".\"status\" = 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "author_enrichment_queue_author_id_authors_id_fk": {
+ "name": "author_enrichment_queue_author_id_authors_id_fk",
+ "tableFrom": "author_enrichment_queue",
+ "tableTo": "authors",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "author_enrichment_queue_status_chk": {
+ "name": "author_enrichment_queue_status_chk",
+ "value": "\"author_enrichment_queue\".\"status\" in ('queued', 'processing', 'rate_limited', 'failed', 'done')"
+ },
+ "author_enrichment_queue_reason_chk": {
+ "name": "author_enrichment_queue_reason_chk",
+ "value": "\"author_enrichment_queue\".\"reason\" in ('unknown', 'metadata_replace', 'manual_backfill', 'manual_backfill_all', 'author_rename', 'author_merge_target')"
+ },
+ "author_enrichment_queue_attempt_count_nonnegative_chk": {
+ "name": "author_enrichment_queue_attempt_count_nonnegative_chk",
+ "value": "\"author_enrichment_queue\".\"attempt_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.authors": {
+ "name": "authors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_name": {
+ "name": "sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "has_photo": {
+ "name": "has_photo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "birth_date": {
+ "name": "birth_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "birth_year": {
+ "name": "birth_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "death_date": {
+ "name": "death_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "death_year": {
+ "name": "death_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website": {
+ "name": "website",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "genres": {
+ "name": "genres",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "influences": {
+ "name": "influences",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_provider": {
+ "name": "metadata_provider",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_provider_id": {
+ "name": "metadata_provider_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_provider_pinned": {
+ "name": "metadata_provider_pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "last_enriched_at": {
+ "name": "last_enriched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "authors_name_trgm_idx": {
+ "name": "authors_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "authors_name_unaccent_trgm_idx": {
+ "name": "authors_name_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"name\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "authors_name_unique": {
+ "name": "authors_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_authors": {
+ "name": "book_authors",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "book_authors_author_id_idx": {
+ "name": "book_authors_author_id_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_authors_book_display_idx": {
+ "name": "book_authors_book_display_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_authors_book_display_author_idx": {
+ "name": "book_authors_book_display_author_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_authors_book_id_books_id_fk": {
+ "name": "book_authors_book_id_books_id_fk",
+ "tableFrom": "book_authors",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_authors_author_id_authors_id_fk": {
+ "name": "book_authors_author_id_authors_id_fk",
+ "tableFrom": "book_authors",
+ "tableTo": "authors",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_authors_book_id_author_id_pk": {
+ "name": "book_authors_book_id_author_id_pk",
+ "columns": ["book_id", "author_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_chapter_jobs": {
+ "name": "book_chapter_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "base_revision": {
+ "name": "base_revision",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(12)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "files_written": {
+ "name": "files_written",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "files_total": {
+ "name": "files_total",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bcj_status_created_idx": {
+ "name": "bcj_status_created_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bcj_request_idx": {
+ "name": "bcj_request_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "base_revision",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_chapter_jobs_book_id_books_id_fk": {
+ "name": "book_chapter_jobs_book_id_books_id_fk",
+ "tableFrom": "book_chapter_jobs",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_chapter_jobs_user_id_users_id_fk": {
+ "name": "book_chapter_jobs_user_id_users_id_fk",
+ "tableFrom": "book_chapter_jobs",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_chapter_versions": {
+ "name": "book_chapter_versions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chapters": {
+ "name": "chapters",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chapter_count": {
+ "name": "chapter_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bcv_book_created_idx": {
+ "name": "bcv_book_created_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bcv_created_by_idx": {
+ "name": "bcv_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_chapter_versions\".\"created_by\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_chapter_versions_book_id_books_id_fk": {
+ "name": "book_chapter_versions_book_id_books_id_fk",
+ "tableFrom": "book_chapter_versions",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_chapter_versions_created_by_users_id_fk": {
+ "name": "book_chapter_versions_created_by_users_id_fk",
+ "tableFrom": "book_chapter_versions",
+ "tableTo": "users",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_community_ratings": {
+ "name": "book_community_ratings",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rating": {
+ "name": "rating",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rating_count": {
+ "name": "rating_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_community_ratings_provider_idx": {
+ "name": "book_community_ratings_provider_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_community_ratings_rating_book_idx": {
+ "name": "book_community_ratings_rating_book_idx",
+ "columns": [
+ {
+ "expression": "rating",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_community_ratings_provider_rating_book_idx": {
+ "name": "book_community_ratings_provider_rating_book_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "rating",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_community_ratings_count_book_idx": {
+ "name": "book_community_ratings_count_book_idx",
+ "columns": [
+ {
+ "expression": "rating_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_community_ratings_provider_count_book_idx": {
+ "name": "book_community_ratings_provider_count_book_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "rating_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_community_ratings_book_id_books_id_fk": {
+ "name": "book_community_ratings_book_id_books_id_fk",
+ "tableFrom": "book_community_ratings",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_community_ratings_book_id_provider_pk": {
+ "name": "book_community_ratings_book_id_provider_pk",
+ "columns": ["book_id", "provider"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_community_ratings_rating_range_chk": {
+ "name": "book_community_ratings_rating_range_chk",
+ "value": "\"book_community_ratings\".\"rating\" >= 0 and \"book_community_ratings\".\"rating\" <= 5"
+ },
+ "book_community_ratings_count_nonnegative_chk": {
+ "name": "book_community_ratings_count_nonnegative_chk",
+ "value": "\"book_community_ratings\".\"rating_count\" is null or \"book_community_ratings\".\"rating_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_genres": {
+ "name": "book_genres",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "genre_id": {
+ "name": "genre_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_genres_genre_id_idx": {
+ "name": "book_genres_genre_id_idx",
+ "columns": [
+ {
+ "expression": "genre_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_genres_book_id_books_id_fk": {
+ "name": "book_genres_book_id_books_id_fk",
+ "tableFrom": "book_genres",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_genres_genre_id_genres_id_fk": {
+ "name": "book_genres_genre_id_genres_id_fk",
+ "tableFrom": "book_genres",
+ "tableTo": "genres",
+ "columnsFrom": ["genre_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_genres_book_id_genre_id_pk": {
+ "name": "book_genres_book_id_genre_id_pk",
+ "columns": ["book_id", "genre_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_metadata": {
+ "name": "book_metadata",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subtitle": {
+ "name": "subtitle",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isbn10": {
+ "name": "isbn10",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isbn13": {
+ "name": "isbn13",
+ "type": "varchar(13)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "publisher": {
+ "name": "publisher",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_date": {
+ "name": "published_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_year": {
+ "name": "published_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "page_count": {
+ "name": "page_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_id": {
+ "name": "series_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_name": {
+ "name": "series_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_index": {
+ "name": "series_index",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rating": {
+ "name": "rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_source": {
+ "name": "cover_source",
+ "type": "varchar(9)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_books_id": {
+ "name": "google_books_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goodreads_id": {
+ "name": "goodreads_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "amazon_id": {
+ "name": "amazon_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_id": {
+ "name": "hardcover_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_edition_id": {
+ "name": "hardcover_edition_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "open_library_id": {
+ "name": "open_library_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "itunes_id": {
+ "name": "itunes_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_id": {
+ "name": "kobo_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_score": {
+ "name": "metadata_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_metadata_fetch_at": {
+ "name": "last_metadata_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(256)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_written_at": {
+ "name": "last_written_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "abridged": {
+ "name": "abridged",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "audible_id": {
+ "name": "audible_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "librofm_id": {
+ "name": "librofm_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comicvine_id": {
+ "name": "comicvine_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ranobedb_id": {
+ "name": "ranobedb_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lubimyczytac_id": {
+ "name": "lubimyczytac_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aladin_id": {
+ "name": "aladin_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapters": {
+ "name": "chapters",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_count": {
+ "name": "chapter_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE WHEN jsonb_typeof(chapters) = 'array' THEN jsonb_array_length(chapters) ELSE 0 END",
+ "type": "stored"
+ }
+ },
+ "numbered_chapter_count": {
+ "name": "numbered_chapter_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "generated": {
+ "as": "CASE WHEN jsonb_typeof(chapters) = 'array' THEN jsonb_array_length(jsonb_path_query_array(chapters, '$[*] ? (@.title like_regex \"^[[:space:]]*((track|disc|cd|part)[[:space:]]*)?[#._-]*[[:space:]]*[0-9]+([[:space:]]*(of|/)[[:space:]]*[0-9]+)?[[:space:]]*$\" flag \"i\")')) ELSE 0 END",
+ "type": "stored"
+ }
+ },
+ "manifest_legacy_recorded": {
+ "name": "manifest_legacy_recorded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "chapters_source": {
+ "name": "chapters_source",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapters_updated_at": {
+ "name": "chapters_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapters_updated_by": {
+ "name": "chapters_updated_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_fields": {
+ "name": "locked_fields",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "cover_updated_at": {
+ "name": "cover_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bm_chapter_count_idx": {
+ "name": "bm_chapter_count_idx",
+ "columns": [
+ {
+ "expression": "chapter_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_numbered_chapters_idx": {
+ "name": "bm_numbered_chapters_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "numbered_chapter_count >= chapter_count * 0.8 AND chapter_count >= 2",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_title_trgm_idx": {
+ "name": "bm_title_trgm_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_title_unaccent_trgm_idx": {
+ "name": "bm_title_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"title\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_title_lower_idx": {
+ "name": "bm_title_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(\"title\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_title_book_id_idx": {
+ "name": "bm_title_book_id_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_subtitle_unaccent_trgm_idx": {
+ "name": "bm_subtitle_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"subtitle\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_series_trgm_idx": {
+ "name": "bm_series_trgm_idx",
+ "columns": [
+ {
+ "expression": "series_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_series_unaccent_trgm_idx": {
+ "name": "bm_series_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"series_name\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_series_id_idx": {
+ "name": "bm_series_id_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_chapters_updated_by_idx": {
+ "name": "bm_chapters_updated_by_idx",
+ "columns": [
+ {
+ "expression": "chapters_updated_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_metadata\".\"chapters_updated_by\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_id_index_book_id_idx": {
+ "name": "bm_series_id_index_book_id_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "(CASE WHEN \"series_index\" IS NULL THEN NULL ELSE ARRAY[split_part(\"series_index\", '.', 1)::numeric, CASE WHEN strpos(\"series_index\", '.') = 0 THEN -1::numeric ELSE split_part(\"series_index\", '.', 2)::numeric END] END)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"series_index\" COLLATE \"C\"",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_index_sort_idx": {
+ "name": "bm_series_index_sort_idx",
+ "columns": [
+ {
+ "expression": "(CASE WHEN \"series_index\" IS NULL THEN NULL ELSE ARRAY[split_part(\"series_index\", '.', 1)::numeric, CASE WHEN strpos(\"series_index\", '.') = 0 THEN -1::numeric ELSE split_part(\"series_index\", '.', 2)::numeric END] END)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"series_index\" COLLATE \"C\"",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_name_lower_btrim_idx": {
+ "name": "bm_series_name_lower_btrim_idx",
+ "columns": [
+ {
+ "expression": "lower(btrim(\"series_name\"))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_publisher_trgm_idx": {
+ "name": "bm_publisher_trgm_idx",
+ "columns": [
+ {
+ "expression": "publisher",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_publisher_unaccent_trgm_idx": {
+ "name": "bm_publisher_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"publisher\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_language_idx": {
+ "name": "bm_language_idx",
+ "columns": [
+ {
+ "expression": "language",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_language_trgm_idx": {
+ "name": "bm_language_trgm_idx",
+ "columns": [
+ {
+ "expression": "language",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_language_unaccent_trgm_idx": {
+ "name": "bm_language_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"language\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_published_date_idx": {
+ "name": "bm_published_date_idx",
+ "columns": [
+ {
+ "expression": "published_date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_published_year_idx": {
+ "name": "bm_published_year_idx",
+ "columns": [
+ {
+ "expression": "published_year",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_page_count_idx": {
+ "name": "bm_page_count_idx",
+ "columns": [
+ {
+ "expression": "page_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_published_date_sort_idx": {
+ "name": "bm_published_date_sort_idx",
+ "columns": [
+ {
+ "expression": "coalesce(\"published_date\", make_date(\"published_year\", 1, 1))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_name_index_idx": {
+ "name": "bm_series_name_index_idx",
+ "columns": [
+ {
+ "expression": "series_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "(CASE WHEN \"series_index\" IS NULL THEN NULL ELSE ARRAY[split_part(\"series_index\", '.', 1)::numeric, CASE WHEN strpos(\"series_index\", '.') = 0 THEN -1::numeric ELSE split_part(\"series_index\", '.', 2)::numeric END] END)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"series_index\" COLLATE \"C\"",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_isbn10_idx": {
+ "name": "bm_isbn10_idx",
+ "columns": [
+ {
+ "expression": "isbn10",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_isbn13_idx": {
+ "name": "bm_isbn13_idx",
+ "columns": [
+ {
+ "expression": "isbn13",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_embedding_hnsw_cosine_idx": {
+ "name": "bm_embedding_hnsw_cosine_idx",
+ "columns": [
+ {
+ "expression": "\"embedding\" vector_cosine_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_metadata_book_id_books_id_fk": {
+ "name": "book_metadata_book_id_books_id_fk",
+ "tableFrom": "book_metadata",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_metadata_series_id_book_series_id_fk": {
+ "name": "book_metadata_series_id_book_series_id_fk",
+ "tableFrom": "book_metadata",
+ "tableTo": "book_series",
+ "columnsFrom": ["series_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_metadata_chapters_updated_by_users_id_fk": {
+ "name": "book_metadata_chapters_updated_by_users_id_fk",
+ "tableFrom": "book_metadata",
+ "tableTo": "users",
+ "columnsFrom": ["chapters_updated_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_metadata_rating_range_chk": {
+ "name": "book_metadata_rating_range_chk",
+ "value": "\"book_metadata\".\"rating\" is null or (\"book_metadata\".\"rating\" >= 1 and \"book_metadata\".\"rating\" <= 10)"
+ },
+ "book_metadata_published_date_range_chk": {
+ "name": "book_metadata_published_date_range_chk",
+ "value": "\"book_metadata\".\"published_date\" is null or (extract(year from \"book_metadata\".\"published_date\") >= 1000 and extract(year from \"book_metadata\".\"published_date\") <= 2200)"
+ },
+ "book_metadata_published_year_range_chk": {
+ "name": "book_metadata_published_year_range_chk",
+ "value": "\"book_metadata\".\"published_year\" is null or (\"book_metadata\".\"published_year\" >= 1000 and \"book_metadata\".\"published_year\" <= 2200)"
+ },
+ "book_metadata_page_count_nonnegative_chk": {
+ "name": "book_metadata_page_count_nonnegative_chk",
+ "value": "\"book_metadata\".\"page_count\" is null or \"book_metadata\".\"page_count\" >= 0"
+ },
+ "book_metadata_series_index_format_chk": {
+ "name": "book_metadata_series_index_format_chk",
+ "value": "\"book_metadata\".\"series_index\" is null or \"book_metadata\".\"series_index\" ~ '^[0-9]+([.][0-9]+)?$'"
+ },
+ "book_metadata_duration_seconds_nonnegative_chk": {
+ "name": "book_metadata_duration_seconds_nonnegative_chk",
+ "value": "\"book_metadata\".\"duration_seconds\" is null or \"book_metadata\".\"duration_seconds\" >= 0"
+ },
+ "book_metadata_cover_source_chk": {
+ "name": "book_metadata_cover_source_chk",
+ "value": "\"book_metadata\".\"cover_source\" is null or \"book_metadata\".\"cover_source\" in ('extracted', 'custom')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_metadata_fetch_queue": {
+ "name": "book_metadata_fetch_queue",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'manual_trigger'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_http_status": {
+ "name": "last_http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ignore_cache": {
+ "name": "ignore_cache",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bmfq_status_idx": {
+ "name": "bmfq_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bmfq_created_at_idx": {
+ "name": "bmfq_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bmfq_status_created_book_idx": {
+ "name": "bmfq_status_created_book_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_metadata_fetch_queue_book_id_books_id_fk": {
+ "name": "book_metadata_fetch_queue_book_id_books_id_fk",
+ "tableFrom": "book_metadata_fetch_queue",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_metadata_fetch_queue_status_chk": {
+ "name": "book_metadata_fetch_queue_status_chk",
+ "value": "\"book_metadata_fetch_queue\".\"status\" in ('queued', 'processing', 'failed')"
+ },
+ "book_metadata_fetch_queue_reason_chk": {
+ "name": "book_metadata_fetch_queue_reason_chk",
+ "value": "\"book_metadata_fetch_queue\".\"reason\" in ('event_import', 'manual_trigger', 'manual_retry')"
+ },
+ "book_metadata_fetch_queue_attempt_count_nonnegative_chk": {
+ "name": "book_metadata_fetch_queue_attempt_count_nonnegative_chk",
+ "value": "\"book_metadata_fetch_queue\".\"attempt_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_tags": {
+ "name": "book_tags",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_tags_tag_id_idx": {
+ "name": "book_tags_tag_id_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_tags_book_id_books_id_fk": {
+ "name": "book_tags_book_id_books_id_fk",
+ "tableFrom": "book_tags",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_tags_tag_id_tags_id_fk": {
+ "name": "book_tags_tag_id_tags_id_fk",
+ "tableFrom": "book_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_tags_book_id_tag_id_pk": {
+ "name": "book_tags_book_id_tag_id_pk",
+ "columns": ["book_id", "tag_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chapter_lookup_queue": {
+ "name": "chapter_lookup_queue",
+ "schema": "",
+ "columns": {
+ "claim_token": {
+ "name": "claim_token",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "available_at": {
+ "name": "available_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "clq_status_available_idx": {
+ "name": "clq_status_available_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "available_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "clq_user_status_idx": {
+ "name": "clq_user_status_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chapter_lookup_queue_book_id_books_id_fk": {
+ "name": "chapter_lookup_queue_book_id_books_id_fk",
+ "tableFrom": "chapter_lookup_queue",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chapter_lookup_queue_user_id_users_id_fk": {
+ "name": "chapter_lookup_queue_user_id_users_id_fk",
+ "tableFrom": "chapter_lookup_queue",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.genres": {
+ "name": "genres",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "genres_name_trgm_idx": {
+ "name": "genres_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "genres_name_unaccent_trgm_idx": {
+ "name": "genres_name_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"name\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "genres_name_unique": {
+ "name": "genres_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "tags_name_trgm_idx": {
+ "name": "tags_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "tags_name_unaccent_trgm_idx": {
+ "name": "tags_name_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"name\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "tags_name_unique": {
+ "name": "tags_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_narrators": {
+ "name": "book_narrators",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "narrator_id": {
+ "name": "narrator_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "book_narrators_narrator_id_idx": {
+ "name": "book_narrators_narrator_id_idx",
+ "columns": [
+ {
+ "expression": "narrator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_narrators_book_id_books_id_fk": {
+ "name": "book_narrators_book_id_books_id_fk",
+ "tableFrom": "book_narrators",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_narrators_narrator_id_narrators_id_fk": {
+ "name": "book_narrators_narrator_id_narrators_id_fk",
+ "tableFrom": "book_narrators",
+ "tableTo": "narrators",
+ "columnsFrom": ["narrator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_narrators_book_id_narrator_id_pk": {
+ "name": "book_narrators_book_id_narrator_id_pk",
+ "columns": ["book_id", "narrator_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.narrators": {
+ "name": "narrators",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_name": {
+ "name": "sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "narrators_name_trgm_idx": {
+ "name": "narrators_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "narrators_name_unaccent_trgm_idx": {
+ "name": "narrators_name_unaccent_trgm_idx",
+ "columns": [
+ {
+ "expression": "public.bookorbit_unaccent(\"name\") gin_trgm_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "narrators_name_unique": {
+ "name": "narrators_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.library_dir_scan_state": {
+ "name": "library_dir_scan_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_folder_id": {
+ "name": "library_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dir_path": {
+ "name": "dir_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_mtime_ms": {
+ "name": "last_seen_mtime_ms",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "library_dir_scan_state_folder_dir_uidx": {
+ "name": "library_dir_scan_state_folder_dir_uidx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dir_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "library_dir_scan_state_folder_idx": {
+ "name": "library_dir_scan_state_folder_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "library_dir_scan_state_library_folder_id_library_folders_id_fk": {
+ "name": "library_dir_scan_state_library_folder_id_library_folders_id_fk",
+ "tableFrom": "library_dir_scan_state",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scan_jobs": {
+ "name": "scan_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "triggered_by": {
+ "name": "triggered_by",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "added_count": {
+ "name": "added_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_count": {
+ "name": "updated_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "missing_count": {
+ "name": "missing_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "scan_jobs_library_status_idx": {
+ "name": "scan_jobs_library_status_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scan_jobs_library_started_at_idx": {
+ "name": "scan_jobs_library_started_at_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scan_jobs_library_id_libraries_id_fk": {
+ "name": "scan_jobs_library_id_libraries_id_fk",
+ "tableFrom": "scan_jobs",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "scan_jobs_status_chk": {
+ "name": "scan_jobs_status_chk",
+ "value": "\"scan_jobs\".\"status\" in ('running', 'completed', 'failed')"
+ },
+ "scan_jobs_triggered_by_chk": {
+ "name": "scan_jobs_triggered_by_chk",
+ "value": "\"scan_jobs\".\"triggered_by\" in ('manual', 'watcher', 'schedule')"
+ },
+ "scan_jobs_added_count_nonnegative_chk": {
+ "name": "scan_jobs_added_count_nonnegative_chk",
+ "value": "\"scan_jobs\".\"added_count\" >= 0"
+ },
+ "scan_jobs_updated_count_nonnegative_chk": {
+ "name": "scan_jobs_updated_count_nonnegative_chk",
+ "value": "\"scan_jobs\".\"updated_count\" >= 0"
+ },
+ "scan_jobs_missing_count_nonnegative_chk": {
+ "name": "scan_jobs_missing_count_nonnegative_chk",
+ "value": "\"scan_jobs\".\"missing_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.annotation_positions": {
+ "name": "annotation_positions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "annotation_id": {
+ "name": "annotation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(12)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pos0": {
+ "name": "pos0",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pos1": {
+ "name": "pos1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'exact'"
+ },
+ "converter_version": {
+ "name": "converter_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "extras": {
+ "name": "extras",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "annotation_positions_annotation_format_uidx": {
+ "name": "annotation_positions_annotation_format_uidx",
+ "columns": [
+ {
+ "expression": "annotation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "format",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotation_positions_user_idx": {
+ "name": "annotation_positions_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotation_positions_book_file_id_idx": {
+ "name": "annotation_positions_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotation_positions_format_status_idx": {
+ "name": "annotation_positions_format_status_idx",
+ "columns": [
+ {
+ "expression": "format",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "annotation_positions_annotation_id_annotations_id_fk": {
+ "name": "annotation_positions_annotation_id_annotations_id_fk",
+ "tableFrom": "annotation_positions",
+ "tableTo": "annotations",
+ "columnsFrom": ["annotation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "annotation_positions_user_id_users_id_fk": {
+ "name": "annotation_positions_user_id_users_id_fk",
+ "tableFrom": "annotation_positions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "annotation_positions_book_file_id_book_files_id_fk": {
+ "name": "annotation_positions_book_file_id_book_files_id_fk",
+ "tableFrom": "annotation_positions",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "annotation_positions_format_chk": {
+ "name": "annotation_positions_format_chk",
+ "value": "\"annotation_positions\".\"format\" in ('cfi', 'xpointer', 'pdf', 'kobo_span')"
+ },
+ "annotation_positions_status_chk": {
+ "name": "annotation_positions_status_chk",
+ "value": "\"annotation_positions\".\"status\" in ('exact', 'repaired', 'failed', 'pending')"
+ },
+ "annotation_positions_pos0_chk": {
+ "name": "annotation_positions_pos0_chk",
+ "value": "\"annotation_positions\".\"status\" in ('failed', 'pending') or \"annotation_positions\".\"pos0\" is not null"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.annotation_sync_state": {
+ "name": "annotation_sync_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "annotation_id": {
+ "name": "annotation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_key": {
+ "name": "external_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_created_at": {
+ "name": "external_created_at",
+ "type": "varchar(19)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_applied_version": {
+ "name": "last_applied_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "device_content": {
+ "name": "device_content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "served_clock": {
+ "name": "served_clock",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "delete_acked_at": {
+ "name": "delete_acked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "first_synced_at": {
+ "name": "first_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "annotation_sync_state_annotation_source_device_uidx": {
+ "name": "annotation_sync_state_annotation_source_device_uidx",
+ "columns": [
+ {
+ "expression": "annotation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotation_sync_state_user_source_device_key_idx": {
+ "name": "annotation_sync_state_user_source_device_key_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotation_sync_state_user_key_idx": {
+ "name": "annotation_sync_state_user_key_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotation_sync_state_annotation_id_idx": {
+ "name": "annotation_sync_state_annotation_id_idx",
+ "columns": [
+ {
+ "expression": "annotation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "annotation_sync_state_annotation_id_annotations_id_fk": {
+ "name": "annotation_sync_state_annotation_id_annotations_id_fk",
+ "tableFrom": "annotation_sync_state",
+ "tableTo": "annotations",
+ "columnsFrom": ["annotation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "annotation_sync_state_user_id_users_id_fk": {
+ "name": "annotation_sync_state_user_id_users_id_fk",
+ "tableFrom": "annotation_sync_state",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "annotation_sync_state_source_chk": {
+ "name": "annotation_sync_state_source_chk",
+ "value": "\"annotation_sync_state\".\"source\" in ('koreader', 'kobo')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.annotations": {
+ "name": "annotations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'yellow'"
+ },
+ "style": {
+ "name": "style",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'highlight'"
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_title": {
+ "name": "chapter_title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "device_created_at": {
+ "name": "device_created_at",
+ "type": "varchar(19)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "device_updated_at": {
+ "name": "device_updated_at",
+ "type": "varchar(19)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_created_at": {
+ "name": "source_created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "starred_at": {
+ "name": "starred_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "annotations_user_id_idx": {
+ "name": "annotations_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_user_id_id_idx": {
+ "name": "annotations_user_id_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_user_book_idx": {
+ "name": "annotations_user_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_book_id_idx": {
+ "name": "annotations_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_user_book_active_idx": {
+ "name": "annotations_user_book_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"annotations\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_user_created_active_idx": {
+ "name": "annotations_user_created_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"source_created_at\", \"created_at\") desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"annotations\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_user_book_created_active_idx": {
+ "name": "annotations_user_book_created_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"source_created_at\", \"created_at\") desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"annotations\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "annotations_user_id_users_id_fk": {
+ "name": "annotations_user_id_users_id_fk",
+ "tableFrom": "annotations",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "annotations_book_id_books_id_fk": {
+ "name": "annotations_book_id_books_id_fk",
+ "tableFrom": "annotations",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "annotations_style_chk": {
+ "name": "annotations_style_chk",
+ "value": "\"annotations\".\"style\" in ('highlight', 'underline', 'strikethrough', 'squiggly', 'invert')"
+ },
+ "annotations_origin_chk": {
+ "name": "annotations_origin_chk",
+ "value": "\"annotations\".\"origin\" in ('web', 'koreader', 'kobo')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.audiobook_manifest_revisions": {
+ "name": "audiobook_manifest_revisions",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "positions_key": {
+ "name": "positions_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "amr_book_created_idx": {
+ "name": "amr_book_created_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audiobook_manifest_revisions_book_id_books_id_fk": {
+ "name": "audiobook_manifest_revisions_book_id_books_id_fk",
+ "tableFrom": "audiobook_manifest_revisions",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "audiobook_manifest_revisions_book_id_revision_pk": {
+ "name": "audiobook_manifest_revisions_book_id_revision_pk",
+ "columns": ["book_id", "revision"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audiobook_progress": {
+ "name": "audiobook_progress",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "current_file_id": {
+ "name": "current_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "captured_at": {
+ "name": "captured_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "operation_id": {
+ "name": "operation_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manifest_revision": {
+ "name": "manifest_revision",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "abp_user_id_idx": {
+ "name": "abp_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audiobook_progress_book_id_idx": {
+ "name": "audiobook_progress_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audiobook_progress_current_file_id_idx": {
+ "name": "audiobook_progress_current_file_id_idx",
+ "columns": [
+ {
+ "expression": "current_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audiobook_progress_user_id_users_id_fk": {
+ "name": "audiobook_progress_user_id_users_id_fk",
+ "tableFrom": "audiobook_progress",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "audiobook_progress_book_id_books_id_fk": {
+ "name": "audiobook_progress_book_id_books_id_fk",
+ "tableFrom": "audiobook_progress",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "audiobook_progress_current_file_id_book_files_id_fk": {
+ "name": "audiobook_progress_current_file_id_book_files_id_fk",
+ "tableFrom": "audiobook_progress",
+ "tableTo": "book_files",
+ "columnsFrom": ["current_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "audiobook_progress_user_id_book_id_pk": {
+ "name": "audiobook_progress_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "audiobook_progress_percentage_range_chk": {
+ "name": "audiobook_progress_percentage_range_chk",
+ "value": "\"audiobook_progress\".\"percentage\" >= 0 and \"audiobook_progress\".\"percentage\" <= 100"
+ },
+ "audiobook_progress_position_seconds_nonnegative_chk": {
+ "name": "audiobook_progress_position_seconds_nonnegative_chk",
+ "value": "\"audiobook_progress\".\"position_seconds\" >= 0"
+ },
+ "audiobook_progress_revision_positive_chk": {
+ "name": "audiobook_progress_revision_positive_chk",
+ "value": "\"audiobook_progress\".\"revision\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_journal_entries": {
+ "name": "book_journal_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quote": {
+ "name": "quote",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_title": {
+ "name": "chapter_title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position_percent": {
+ "name": "position_percent",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "book_journal_entries_user_client_id_uidx": {
+ "name": "book_journal_entries_user_client_id_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_journal_entries_user_book_created_active_idx": {
+ "name": "book_journal_entries_user_book_created_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_journal_entries\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_journal_entries_book_id_idx": {
+ "name": "book_journal_entries_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_journal_entries_user_id_users_id_fk": {
+ "name": "book_journal_entries_user_id_users_id_fk",
+ "tableFrom": "book_journal_entries",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_journal_entries_book_id_books_id_fk": {
+ "name": "book_journal_entries_book_id_books_id_fk",
+ "tableFrom": "book_journal_entries",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_journal_entries_body_length_chk": {
+ "name": "book_journal_entries_body_length_chk",
+ "value": "char_length(\"book_journal_entries\".\"body\") between 1 and 10000"
+ },
+ "book_journal_entries_quote_length_chk": {
+ "name": "book_journal_entries_quote_length_chk",
+ "value": "\"book_journal_entries\".\"quote\" is null or char_length(\"book_journal_entries\".\"quote\") <= 5000"
+ },
+ "book_journal_entries_position_percent_range_chk": {
+ "name": "book_journal_entries_position_percent_range_chk",
+ "value": "\"book_journal_entries\".\"position_percent\" is null or (\"book_journal_entries\".\"position_percent\" >= 0 and \"book_journal_entries\".\"position_percent\" <= 100)"
+ },
+ "book_journal_entries_position_seconds_nonnegative_chk": {
+ "name": "book_journal_entries_position_seconds_nonnegative_chk",
+ "value": "\"book_journal_entries\".\"position_seconds\" is null or \"book_journal_entries\".\"position_seconds\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.bookmarks": {
+ "name": "bookmarks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_id": {
+ "name": "chapter_id",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ },
+ "device_pos": {
+ "name": "device_pos",
+ "type": "varchar(4000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pageno": {
+ "name": "pageno",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "bookmarks_user_book_client_id_uidx": {
+ "name": "bookmarks_user_book_client_id_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_user_book_idx": {
+ "name": "bookmarks_user_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_book_id_idx": {
+ "name": "bookmarks_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_deleted_at_idx": {
+ "name": "bookmarks_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"bookmarks\".\"deleted_at\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_user_book_cfi_uidx": {
+ "name": "bookmarks_user_book_cfi_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cfi",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"bookmarks\".\"cfi\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_user_book_pos_uidx": {
+ "name": "bookmarks_user_book_pos_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position_seconds",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"bookmarks\".\"position_seconds\" is not null and \"bookmarks\".\"cfi\" is null and \"bookmarks\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "bookmarks_user_id_users_id_fk": {
+ "name": "bookmarks_user_id_users_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "bookmarks_book_id_books_id_fk": {
+ "name": "bookmarks_book_id_books_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "bookmarks_book_file_id_book_files_id_fk": {
+ "name": "bookmarks_book_file_id_book_files_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "bookmarks_origin_chk": {
+ "name": "bookmarks_origin_chk",
+ "value": "\"bookmarks\".\"origin\" in ('web', 'koreader', 'kobo')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reader_default_preferences": {
+ "name": "reader_default_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "format_group": {
+ "name": "format_group",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "rdp_user_format_idx": {
+ "name": "rdp_user_format_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "format_group",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reader_default_preferences_user_id_users_id_fk": {
+ "name": "reader_default_preferences_user_id_users_id_fk",
+ "tableFrom": "reader_default_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reader_default_preferences_format_group_chk": {
+ "name": "reader_default_preferences_format_group_chk",
+ "value": "\"reader_default_preferences\".\"format_group\" in ('epub', 'pdf', 'cbx', 'audio')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reader_preferences": {
+ "name": "reader_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "rp_user_file_idx": {
+ "name": "rp_user_file_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reader_preferences_book_file_id_idx": {
+ "name": "reader_preferences_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reader_preferences_user_id_users_id_fk": {
+ "name": "reader_preferences_user_id_users_id_fk",
+ "tableFrom": "reader_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reader_preferences_book_file_id_book_files_id_fk": {
+ "name": "reader_preferences_book_file_id_book_files_id_fk",
+ "tableFrom": "reader_preferences",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reading_attempts": {
+ "name": "reading_attempts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_on": {
+ "name": "started_on",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_on": {
+ "name": "ended_on",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "outcome": {
+ "name": "outcome",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_provider": {
+ "name": "external_provider",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reading_attempts_user_book_idx": {
+ "name": "reading_attempts_user_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_attempts_book_id_idx": {
+ "name": "reading_attempts_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_attempts_user_outcome_ended_idx": {
+ "name": "reading_attempts_user_outcome_ended_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "outcome",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "ended_on",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_attempts_one_active_uidx": {
+ "name": "reading_attempts_one_active_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"reading_attempts\".\"outcome\" is null and \"reading_attempts\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_attempts_external_uidx": {
+ "name": "reading_attempts_external_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"reading_attempts\".\"external_provider\" is not null and \"reading_attempts\".\"external_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reading_attempts_user_id_users_id_fk": {
+ "name": "reading_attempts_user_id_users_id_fk",
+ "tableFrom": "reading_attempts",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_attempts_book_id_books_id_fk": {
+ "name": "reading_attempts_book_id_books_id_fk",
+ "tableFrom": "reading_attempts",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reading_attempts_outcome_chk": {
+ "name": "reading_attempts_outcome_chk",
+ "value": "\"reading_attempts\".\"outcome\" is null or \"reading_attempts\".\"outcome\" in ('completed', 'skimmed', 'abandoned')"
+ },
+ "reading_attempts_origin_chk": {
+ "name": "reading_attempts_origin_chk",
+ "value": "\"reading_attempts\".\"origin\" in ('manual', 'bookorbit', 'kobo', 'koreader', 'hardcover', 'migration')"
+ },
+ "reading_attempts_end_after_start_chk": {
+ "name": "reading_attempts_end_after_start_chk",
+ "value": "\"reading_attempts\".\"ended_on\" is null or \"reading_attempts\".\"started_on\" is null or \"reading_attempts\".\"ended_on\" >= \"reading_attempts\".\"started_on\""
+ },
+ "reading_attempts_closed_has_outcome_chk": {
+ "name": "reading_attempts_closed_has_outcome_chk",
+ "value": "\"reading_attempts\".\"ended_on\" is null or \"reading_attempts\".\"outcome\" is not null"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reading_progress": {
+ "name": "reading_progress",
+ "schema": "",
+ "columns": {
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "page_number": {
+ "name": "page_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_overlay_fragment": {
+ "name": "media_overlay_fragment",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "media_overlay_section_index": {
+ "name": "media_overlay_section_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_source": {
+ "name": "kobo_location_source",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_type": {
+ "name": "kobo_location_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_value": {
+ "name": "kobo_location_value",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_content_source_progress_percent": {
+ "name": "kobo_content_source_progress_percent",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "koreader_progress": {
+ "name": "koreader_progress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "narration_percentage": {
+ "name": "narration_percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "narration_updated_at": {
+ "name": "narration_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_updated_at": {
+ "name": "text_updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reading_progress_user_id_idx": {
+ "name": "reading_progress_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_progress_user_last_read_at_idx": {
+ "name": "reading_progress_user_last_read_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_read_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reading_progress_book_file_id_book_files_id_fk": {
+ "name": "reading_progress_book_file_id_book_files_id_fk",
+ "tableFrom": "reading_progress",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_progress_user_id_users_id_fk": {
+ "name": "reading_progress_user_id_users_id_fk",
+ "tableFrom": "reading_progress",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "reading_progress_book_file_id_user_id_pk": {
+ "name": "reading_progress_book_file_id_user_id_pk",
+ "columns": ["book_file_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reading_progress_percentage_range_chk": {
+ "name": "reading_progress_percentage_range_chk",
+ "value": "\"reading_progress\".\"percentage\" >= 0 and \"reading_progress\".\"percentage\" <= 100"
+ },
+ "reading_progress_page_number_nonnegative_chk": {
+ "name": "reading_progress_page_number_nonnegative_chk",
+ "value": "\"reading_progress\".\"page_number\" is null or \"reading_progress\".\"page_number\" >= 0"
+ },
+ "reading_progress_position_seconds_nonnegative_chk": {
+ "name": "reading_progress_position_seconds_nonnegative_chk",
+ "value": "\"reading_progress\".\"position_seconds\" is null or \"reading_progress\".\"position_seconds\" >= 0"
+ },
+ "reading_progress_media_overlay_section_index_nonnegative_chk": {
+ "name": "reading_progress_media_overlay_section_index_nonnegative_chk",
+ "value": "\"reading_progress\".\"media_overlay_section_index\" is null or \"reading_progress\".\"media_overlay_section_index\" >= 0"
+ },
+ "reading_progress_kobo_content_source_progress_range_chk": {
+ "name": "reading_progress_kobo_content_source_progress_range_chk",
+ "value": "\"reading_progress\".\"kobo_content_source_progress_percent\" is null or (\"reading_progress\".\"kobo_content_source_progress_percent\" >= 0 and \"reading_progress\".\"kobo_content_source_progress_percent\" <= 100)"
+ },
+ "reading_progress_narration_percentage_range_chk": {
+ "name": "reading_progress_narration_percentage_range_chk",
+ "value": "\"reading_progress\".\"narration_percentage\" is null or (\"reading_progress\".\"narration_percentage\" >= 0 and \"reading_progress\".\"narration_percentage\" <= 100)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reading_session_sync_cursors": {
+ "name": "reading_session_sync_cursors",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_device_key": {
+ "name": "source_device_key",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "counter": {
+ "name": "counter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_modified": {
+ "name": "last_modified",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "rssc_book_id_idx": {
+ "name": "rssc_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reading_session_sync_cursors_user_id_users_id_fk": {
+ "name": "reading_session_sync_cursors_user_id_users_id_fk",
+ "tableFrom": "reading_session_sync_cursors",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_session_sync_cursors_book_id_books_id_fk": {
+ "name": "reading_session_sync_cursors_book_id_books_id_fk",
+ "tableFrom": "reading_session_sync_cursors",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "reading_session_sync_cursors_user_id_book_id_source_source_device_key_pk": {
+ "name": "reading_session_sync_cursors_user_id_book_id_source_source_device_key_pk",
+ "columns": ["user_id", "book_id", "source", "source_device_key"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "rssc_counter_nonnegative_chk": {
+ "name": "rssc_counter_nonnegative_chk",
+ "value": "\"reading_session_sync_cursors\".\"counter\" >= 0"
+ },
+ "rssc_generation_nonnegative_chk": {
+ "name": "rssc_generation_nonnegative_chk",
+ "value": "\"reading_session_sync_cursors\".\"generation\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reading_sessions": {
+ "name": "reading_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "attempt_id": {
+ "name": "attempt_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_device_key": {
+ "name": "source_device_key",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_type": {
+ "name": "session_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'read'"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "progress_delta": {
+ "name": "progress_delta",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_progress": {
+ "name": "end_progress",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_interval_started_at": {
+ "name": "sync_interval_started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "rs_user_session_id_uidx": {
+ "name": "rs_user_session_id_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_user_started_at_idx": {
+ "name": "rs_user_started_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_book_file_started_at_idx": {
+ "name": "rs_book_file_started_at_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_user_book_file_idx": {
+ "name": "rs_user_book_file_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_user_book_started_at_idx": {
+ "name": "rs_user_book_started_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_user_book_source_device_started_idx": {
+ "name": "rs_user_book_source_device_started_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_device_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_sessions_book_id_idx": {
+ "name": "reading_sessions_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_attempt_started_at_idx": {
+ "name": "rs_attempt_started_at_idx",
+ "columns": [
+ {
+ "expression": "attempt_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reading_sessions_user_id_users_id_fk": {
+ "name": "reading_sessions_user_id_users_id_fk",
+ "tableFrom": "reading_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_sessions_book_file_id_book_files_id_fk": {
+ "name": "reading_sessions_book_file_id_book_files_id_fk",
+ "tableFrom": "reading_sessions",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_sessions_book_id_books_id_fk": {
+ "name": "reading_sessions_book_id_books_id_fk",
+ "tableFrom": "reading_sessions",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_sessions_attempt_id_reading_attempts_id_fk": {
+ "name": "reading_sessions_attempt_id_reading_attempts_id_fk",
+ "tableFrom": "reading_sessions",
+ "tableTo": "reading_attempts",
+ "columnsFrom": ["attempt_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reading_sessions_source_chk": {
+ "name": "reading_sessions_source_chk",
+ "value": "\"reading_sessions\".\"source\" in ('web', 'ios', 'watchos', 'android', 'koreader', 'manual', 'kobo')"
+ },
+ "reading_sessions_duration_seconds_nonnegative_chk": {
+ "name": "reading_sessions_duration_seconds_nonnegative_chk",
+ "value": "\"reading_sessions\".\"duration_seconds\" >= 0"
+ },
+ "reading_sessions_end_progress_range_chk": {
+ "name": "reading_sessions_end_progress_range_chk",
+ "value": "\"reading_sessions\".\"end_progress\" is null or (\"reading_sessions\".\"end_progress\" >= 0 and \"reading_sessions\".\"end_progress\" <= 100)"
+ },
+ "reading_sessions_ended_after_started_chk": {
+ "name": "reading_sessions_ended_after_started_chk",
+ "value": "\"reading_sessions\".\"ended_at\" >= \"reading_sessions\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_book_notes": {
+ "name": "user_book_notes",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ubn_user_id_idx": {
+ "name": "ubn_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubn_book_id_idx": {
+ "name": "ubn_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubn_book_user_idx": {
+ "name": "ubn_book_user_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_book_notes_user_id_users_id_fk": {
+ "name": "user_book_notes_user_id_users_id_fk",
+ "tableFrom": "user_book_notes",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_book_notes_book_id_books_id_fk": {
+ "name": "user_book_notes_book_id_books_id_fk",
+ "tableFrom": "user_book_notes",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_book_notes_user_id_book_id_pk": {
+ "name": "user_book_notes_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_book_notes_note_length_chk": {
+ "name": "user_book_notes_note_length_chk",
+ "value": "\"user_book_notes\".\"note\" is null or char_length(\"user_book_notes\".\"note\") <= 10000"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_book_ratings": {
+ "name": "user_book_ratings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rating": {
+ "name": "rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ubr_user_id_idx": {
+ "name": "ubr_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubr_book_id_idx": {
+ "name": "ubr_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubr_book_user_rating_idx": {
+ "name": "ubr_book_user_rating_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_book_ratings_user_id_users_id_fk": {
+ "name": "user_book_ratings_user_id_users_id_fk",
+ "tableFrom": "user_book_ratings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_book_ratings_book_id_books_id_fk": {
+ "name": "user_book_ratings_book_id_books_id_fk",
+ "tableFrom": "user_book_ratings",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_book_ratings_user_id_book_id_pk": {
+ "name": "user_book_ratings_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_book_ratings_rating_range_chk": {
+ "name": "user_book_ratings_rating_range_chk",
+ "value": "\"user_book_ratings\".\"rating\" is null or (\"user_book_ratings\".\"rating\" >= 1 and \"user_book_ratings\".\"rating\" <= 5)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_book_read_aloud_sync_settings": {
+ "name": "user_book_read_aloud_sync_settings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mode": {
+ "name": "mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'auto'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ubrass_user_id_idx": {
+ "name": "ubrass_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_book_read_aloud_sync_settings_user_id_users_id_fk": {
+ "name": "user_book_read_aloud_sync_settings_user_id_users_id_fk",
+ "tableFrom": "user_book_read_aloud_sync_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_book_read_aloud_sync_settings_book_id_books_id_fk": {
+ "name": "user_book_read_aloud_sync_settings_book_id_books_id_fk",
+ "tableFrom": "user_book_read_aloud_sync_settings",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_book_read_aloud_sync_settings_user_id_book_id_pk": {
+ "name": "user_book_read_aloud_sync_settings_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_book_read_aloud_sync_settings_mode_chk": {
+ "name": "user_book_read_aloud_sync_settings_mode_chk",
+ "value": "\"user_book_read_aloud_sync_settings\".\"mode\" in ('auto', 'disabled')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_book_status": {
+ "name": "user_book_status",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unread'"
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'auto'"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ubs_user_id_idx": {
+ "name": "ubs_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubs_user_status_idx": {
+ "name": "ubs_user_status_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubs_book_user_idx": {
+ "name": "ubs_book_user_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_book_status_user_id_users_id_fk": {
+ "name": "user_book_status_user_id_users_id_fk",
+ "tableFrom": "user_book_status",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_book_status_book_id_books_id_fk": {
+ "name": "user_book_status_book_id_books_id_fk",
+ "tableFrom": "user_book_status",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_book_status_user_id_book_id_pk": {
+ "name": "user_book_status_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_book_status_status_chk": {
+ "name": "user_book_status_status_chk",
+ "value": "\"user_book_status\".\"status\" in ('unread', 'want_to_read', 'reading', 'on_hold', 'rereading', 'read', 'skimmed', 'abandoned')"
+ },
+ "user_book_status_source_chk": {
+ "name": "user_book_status_source_chk",
+ "value": "\"user_book_status\".\"source\" in ('auto', 'manual')"
+ },
+ "user_book_status_finished_after_started_chk": {
+ "name": "user_book_status_finished_after_started_chk",
+ "value": "\"user_book_status\".\"finished_at\" is null or \"user_book_status\".\"started_at\" is null or \"user_book_status\".\"finished_at\" >= \"user_book_status\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_reading_daily_stats": {
+ "name": "user_reading_daily_stats",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_seconds": {
+ "name": "reading_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "progress_delta": {
+ "name": "progress_delta",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "sessions_count": {
+ "name": "sessions_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "urds_user_day_idx": {
+ "name": "urds_user_day_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_reading_daily_stats_library_id_idx": {
+ "name": "user_reading_daily_stats_library_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_reading_daily_stats_user_id_users_id_fk": {
+ "name": "user_reading_daily_stats_user_id_users_id_fk",
+ "tableFrom": "user_reading_daily_stats",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_reading_daily_stats_library_id_libraries_id_fk": {
+ "name": "user_reading_daily_stats_library_id_libraries_id_fk",
+ "tableFrom": "user_reading_daily_stats",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_reading_daily_stats_user_id_library_id_day_pk": {
+ "name": "user_reading_daily_stats_user_id_library_id_day_pk",
+ "columns": ["user_id", "library_id", "day"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_reading_daily_stats_reading_seconds_nonnegative_chk": {
+ "name": "user_reading_daily_stats_reading_seconds_nonnegative_chk",
+ "value": "\"user_reading_daily_stats\".\"reading_seconds\" >= 0"
+ },
+ "user_reading_daily_stats_sessions_count_nonnegative_chk": {
+ "name": "user_reading_daily_stats_sessions_count_nonnegative_chk",
+ "value": "\"user_reading_daily_stats\".\"sessions_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.oidc_group_mappings": {
+ "name": "oidc_group_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_group_claim": {
+ "name": "oidc_group_claim",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_name": {
+ "name": "permission_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oidc_group_mappings_provider_claim_uidx": {
+ "name": "oidc_group_mappings_provider_claim_uidx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_group_claim",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_group_mappings_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_group_mappings_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_group_mappings",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_identities": {
+ "name": "oidc_identities",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_subject": {
+ "name": "oidc_subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_issuer": {
+ "name": "oidc_issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "linked_at": {
+ "name": "linked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oidc_identities_provider_subject_uidx": {
+ "name": "oidc_identities_provider_subject_uidx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_identities_user_provider_uidx": {
+ "name": "oidc_identities_user_provider_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_identities_issuer_subject_uidx": {
+ "name": "oidc_identities_issuer_subject_uidx",
+ "columns": [
+ {
+ "expression": "oidc_issuer",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_identities_user_id_idx": {
+ "name": "oidc_identities_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_identities_user_id_users_id_fk": {
+ "name": "oidc_identities_user_id_users_id_fk",
+ "tableFrom": "oidc_identities",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_identities_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_identities_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_identities",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_permission_grants": {
+ "name": "oidc_permission_grants",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_name": {
+ "name": "permission_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oidc_permission_grants_user_id_users_id_fk": {
+ "name": "oidc_permission_grants_user_id_users_id_fk",
+ "tableFrom": "oidc_permission_grants",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_permission_grants_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_permission_grants_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_permission_grants",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "oidc_permission_grants_user_id_provider_id_permission_name_pk": {
+ "name": "oidc_permission_grants_user_id_provider_id_permission_name_pk",
+ "columns": ["user_id", "provider_id", "permission_name"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_providers": {
+ "name": "oidc_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "issuer_uri": {
+ "name": "issuer_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'openid profile email'"
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claim_mapping": {
+ "name": "claim_mapping",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"username\":\"preferred_username\",\"name\":\"name\",\"email\":\"email\",\"groups\":\"groups\"}'::jsonb"
+ },
+ "auto_provision": {
+ "name": "auto_provision",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"enabled\":false,\"allowLocalLinking\":false,\"defaultPermissionNames\":[]}'::jsonb"
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oidc_providers_enabled_order_idx": {
+ "name": "oidc_providers_enabled_order_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oidc_providers_slug_unique": {
+ "name": "oidc_providers_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slug"]
+ },
+ "oidc_providers_issuer_uri_unique": {
+ "name": "oidc_providers_issuer_uri_unique",
+ "nullsNotDistinct": false,
+ "columns": ["issuer_uri"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_sessions": {
+ "name": "oidc_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oidc_subject": {
+ "name": "oidc_subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_issuer": {
+ "name": "oidc_issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_session_id": {
+ "name": "oidc_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token_hint": {
+ "name": "id_token_hint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idp_refresh_token": {
+ "name": "idp_refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked": {
+ "name": "revoked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oidc_sessions_user_active_idx": {
+ "name": "oidc_sessions_user_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"oidc_sessions\".\"revoked\" = false",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_subject_issuer_idx": {
+ "name": "oidc_sessions_subject_issuer_idx",
+ "columns": [
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_issuer",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_sid_idx": {
+ "name": "oidc_sessions_sid_idx",
+ "columns": [
+ {
+ "expression": "oidc_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_session_id_idx": {
+ "name": "oidc_sessions_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_expires_at_idx": {
+ "name": "oidc_sessions_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_provider_id_idx": {
+ "name": "oidc_sessions_provider_id_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_sessions_user_id_users_id_fk": {
+ "name": "oidc_sessions_user_id_users_id_fk",
+ "tableFrom": "oidc_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_sessions_session_id_auth_sessions_id_fk": {
+ "name": "oidc_sessions_session_id_auth_sessions_id_fk",
+ "tableFrom": "oidc_sessions",
+ "tableTo": "auth_sessions",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_sessions_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_sessions_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_sessions",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_states": {
+ "name": "oidc_states",
+ "schema": "",
+ "columns": {
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "meta": {
+ "name": "meta",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "oidc_states_expires_at_idx": {
+ "name": "oidc_states_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_states_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_states_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_states",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_used_jtis": {
+ "name": "oidc_used_jtis",
+ "schema": "",
+ "columns": {
+ "jti": {
+ "name": "jti",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oidc_used_jtis_expires_at_idx": {
+ "name": "oidc_used_jtis_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.opds_users": {
+ "name": "opds_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "read_aloud_delivery": {
+ "name": "read_aloud_delivery",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "opds_sort_order",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'recent'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "opds_users_username_lower_uidx": {
+ "name": "opds_users_username_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"username\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "opds_users_user_id_users_id_fk": {
+ "name": "opds_users_user_id_users_id_fk",
+ "tableFrom": "opds_users",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "opds_users_username_unique": {
+ "name": "opds_users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": ["username"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_annotation_serves": {
+ "name": "kobo_annotation_serves",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "etag": {
+ "name": "etag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "served_at": {
+ "name": "served_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_annotation_serves_device_book_uidx": {
+ "name": "kobo_annotation_serves_device_book_uidx",
+ "columns": [
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_annotation_serves_user_id_users_id_fk": {
+ "name": "kobo_annotation_serves_user_id_users_id_fk",
+ "tableFrom": "kobo_annotation_serves",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_annotation_serves_book_id_books_id_fk": {
+ "name": "kobo_annotation_serves_book_id_books_id_fk",
+ "tableFrom": "kobo_annotation_serves",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_annotation_serves_owner_fk": {
+ "name": "kobo_annotation_serves_owner_fk",
+ "tableFrom": "kobo_annotation_serves",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_book_entitlements": {
+ "name": "kobo_book_entitlements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entitlement_id": {
+ "name": "entitlement_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cover_image_id": {
+ "name": "cover_image_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "needs_legacy_numeric_removal": {
+ "name": "needs_legacy_numeric_removal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_book_entitlements_user_id_idx": {
+ "name": "kobo_book_entitlements_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_book_entitlements_book_id_idx": {
+ "name": "kobo_book_entitlements_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_book_entitlements_user_id_users_id_fk": {
+ "name": "kobo_book_entitlements_user_id_users_id_fk",
+ "tableFrom": "kobo_book_entitlements",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_book_entitlements_book_id_books_id_fk": {
+ "name": "kobo_book_entitlements_book_id_books_id_fk",
+ "tableFrom": "kobo_book_entitlements",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_book_entitlements_user_book_unique": {
+ "name": "kobo_book_entitlements_user_book_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ },
+ "kobo_book_entitlements_user_entitlement_unique": {
+ "name": "kobo_book_entitlements_user_entitlement_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "entitlement_id"]
+ },
+ "kobo_book_entitlements_user_cover_unique": {
+ "name": "kobo_book_entitlements_user_cover_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "cover_image_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_bookmark_links": {
+ "name": "kobo_bookmark_links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "bookmark_id": {
+ "name": "bookmark_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_key": {
+ "name": "external_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "location": {
+ "name": "location",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applied_at": {
+ "name": "applied_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_bookmark_links_bookmark_device_uidx": {
+ "name": "kobo_bookmark_links_bookmark_device_uidx",
+ "columns": [
+ {
+ "expression": "bookmark_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_bookmark_links_device_key_uidx": {
+ "name": "kobo_bookmark_links_device_key_uidx",
+ "columns": [
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_bookmark_links_user_device_book_idx": {
+ "name": "kobo_bookmark_links_user_device_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_bookmark_links_bookmark_id_bookmarks_id_fk": {
+ "name": "kobo_bookmark_links_bookmark_id_bookmarks_id_fk",
+ "tableFrom": "kobo_bookmark_links",
+ "tableTo": "bookmarks",
+ "columnsFrom": ["bookmark_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_bookmark_links_user_id_users_id_fk": {
+ "name": "kobo_bookmark_links_user_id_users_id_fk",
+ "tableFrom": "kobo_bookmark_links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_bookmark_links_book_id_books_id_fk": {
+ "name": "kobo_bookmark_links_book_id_books_id_fk",
+ "tableFrom": "kobo_bookmark_links",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_bookmark_links_owner_fk": {
+ "name": "kobo_bookmark_links_owner_fk",
+ "tableFrom": "kobo_bookmark_links",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_device_deliveries": {
+ "name": "kobo_device_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_file_id": {
+ "name": "source_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "options": {
+ "name": "options",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'offered'"
+ },
+ "previous_id": {
+ "name": "previous_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current": {
+ "name": "current",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_device_deliveries_current_idx": {
+ "name": "kobo_device_deliveries_current_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"kobo_device_deliveries\".\"current\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_device_deliveries_device_book_idx": {
+ "name": "kobo_device_deliveries_device_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_device_deliveries_user_id_users_id_fk": {
+ "name": "kobo_device_deliveries_user_id_users_id_fk",
+ "tableFrom": "kobo_device_deliveries",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_device_deliveries_book_id_books_id_fk": {
+ "name": "kobo_device_deliveries_book_id_books_id_fk",
+ "tableFrom": "kobo_device_deliveries",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_device_deliveries_source_file_id_book_files_id_fk": {
+ "name": "kobo_device_deliveries_source_file_id_book_files_id_fk",
+ "tableFrom": "kobo_device_deliveries",
+ "tableTo": "book_files",
+ "columnsFrom": ["source_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "kobo_device_deliveries_owner_fk": {
+ "name": "kobo_device_deliveries_owner_fk",
+ "tableFrom": "kobo_device_deliveries",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_device_reading_states": {
+ "name": "kobo_device_reading_states",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entitlement_id": {
+ "name": "entitlement_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "delivery_id": {
+ "name": "delivery_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at_kobo": {
+ "name": "created_at_kobo",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified_kobo": {
+ "name": "last_modified_kobo",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority_timestamp": {
+ "name": "priority_timestamp",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_bookmark": {
+ "name": "current_bookmark",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "statistics": {
+ "name": "statistics",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_info": {
+ "name": "status_info",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_device_reading_states_user_id_users_id_fk": {
+ "name": "kobo_device_reading_states_user_id_users_id_fk",
+ "tableFrom": "kobo_device_reading_states",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_device_reading_states_book_id_books_id_fk": {
+ "name": "kobo_device_reading_states_book_id_books_id_fk",
+ "tableFrom": "kobo_device_reading_states",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_device_reading_states_delivery_id_kobo_device_deliveries_id_fk": {
+ "name": "kobo_device_reading_states_delivery_id_kobo_device_deliveries_id_fk",
+ "tableFrom": "kobo_device_reading_states",
+ "tableTo": "kobo_device_deliveries",
+ "columnsFrom": ["delivery_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "kobo_device_reading_states_owner_fk": {
+ "name": "kobo_device_reading_states_owner_fk",
+ "tableFrom": "kobo_device_reading_states",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "kobo_device_reading_states_user_id_device_id_book_id_pk": {
+ "name": "kobo_device_reading_states_user_id_device_id_book_id_pk",
+ "columns": ["user_id", "device_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_devices": {
+ "name": "kobo_devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_device_id": {
+ "name": "client_device_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_devices_user_id_idx": {
+ "name": "kobo_devices_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_devices_user_id_users_id_fk": {
+ "name": "kobo_devices_user_id_users_id_fk",
+ "tableFrom": "kobo_devices",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_devices_token_unique": {
+ "name": "kobo_devices_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ },
+ "kobo_devices_client_device_id_unique": {
+ "name": "kobo_devices_client_device_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["client_device_id"]
+ },
+ "kobo_devices_id_user_id_unique": {
+ "name": "kobo_devices_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_known_positions": {
+ "name": "kobo_known_positions",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "location_source": {
+ "name": "location_source",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "location_value": {
+ "name": "location_value",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_set_at": {
+ "name": "position_set_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_known_positions_user_id_users_id_fk": {
+ "name": "kobo_known_positions_user_id_users_id_fk",
+ "tableFrom": "kobo_known_positions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_known_positions_book_id_books_id_fk": {
+ "name": "kobo_known_positions_book_id_books_id_fk",
+ "tableFrom": "kobo_known_positions",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_known_positions_owner_fk": {
+ "name": "kobo_known_positions_owner_fk",
+ "tableFrom": "kobo_known_positions",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "kobo_known_positions_user_id_device_id_book_id_pk": {
+ "name": "kobo_known_positions_user_id_device_id_book_id_pk",
+ "columns": ["user_id", "device_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_library_snapshots": {
+ "name": "kobo_library_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "legacy_device_cutoff_at": {
+ "name": "legacy_device_cutoff_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_library_snapshots_user_id_users_id_fk": {
+ "name": "kobo_library_snapshots_user_id_users_id_fk",
+ "tableFrom": "kobo_library_snapshots",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_library_snapshots_user_id_unique": {
+ "name": "kobo_library_snapshots_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_snapshot_books": {
+ "name": "kobo_snapshot_books",
+ "schema": "",
+ "columns": {
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "synced": {
+ "name": "synced",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "pending_delete": {
+ "name": "pending_delete",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_new": {
+ "name": "is_new",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "removed_by_device": {
+ "name": "removed_by_device",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_hash": {
+ "name": "delivery_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_hash": {
+ "name": "metadata_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kobo_snapshot_books_book_id_idx": {
+ "name": "kobo_snapshot_books_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_snapshot_books_snapshot_synced_book_idx": {
+ "name": "kobo_snapshot_books_snapshot_synced_book_idx",
+ "columns": [
+ {
+ "expression": "snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "synced",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_snapshot_books_snapshot_id_kobo_library_snapshots_id_fk": {
+ "name": "kobo_snapshot_books_snapshot_id_kobo_library_snapshots_id_fk",
+ "tableFrom": "kobo_snapshot_books",
+ "tableTo": "kobo_library_snapshots",
+ "columnsFrom": ["snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_snapshot_books_book_id_books_id_fk": {
+ "name": "kobo_snapshot_books_book_id_books_id_fk",
+ "tableFrom": "kobo_snapshot_books",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "kobo_snapshot_books_snapshot_id_book_id_pk": {
+ "name": "kobo_snapshot_books_snapshot_id_book_id_pk",
+ "columns": ["snapshot_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_device_library_snapshots": {
+ "name": "kobo_device_library_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_device_library_snapshots_user_id_idx": {
+ "name": "kobo_device_library_snapshots_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_device_library_snapshots_user_id_users_id_fk": {
+ "name": "kobo_device_library_snapshots_user_id_users_id_fk",
+ "tableFrom": "kobo_device_library_snapshots",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_device_library_snapshots_device_owner_fk": {
+ "name": "kobo_device_library_snapshots_device_owner_fk",
+ "tableFrom": "kobo_device_library_snapshots",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_device_library_snapshots_device_id_unique": {
+ "name": "kobo_device_library_snapshots_device_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["device_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_reading_states": {
+ "name": "kobo_reading_states",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entitlement_id": {
+ "name": "entitlement_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at_kobo": {
+ "name": "created_at_kobo",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified_kobo": {
+ "name": "last_modified_kobo",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority_timestamp": {
+ "name": "priority_timestamp",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_bookmark": {
+ "name": "current_bookmark",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "statistics": {
+ "name": "statistics",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_info": {
+ "name": "status_info",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_reading_states_book_id_idx": {
+ "name": "kobo_reading_states_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_reading_states_user_id_users_id_fk": {
+ "name": "kobo_reading_states_user_id_users_id_fk",
+ "tableFrom": "kobo_reading_states",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_reading_states_book_id_books_id_fk": {
+ "name": "kobo_reading_states_book_id_books_id_fk",
+ "tableFrom": "kobo_reading_states",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_reading_states_user_id_book_id_unique": {
+ "name": "kobo_reading_states_user_id_book_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_device_snapshot_books": {
+ "name": "kobo_device_snapshot_books",
+ "schema": "",
+ "columns": {
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "delivery_retry_at": {
+ "name": "delivery_retry_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_failure_code": {
+ "name": "delivery_failure_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "synced": {
+ "name": "synced",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "pending_delete": {
+ "name": "pending_delete",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_new": {
+ "name": "is_new",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "removed_by_device": {
+ "name": "removed_by_device",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "needs_legacy_numeric_removal": {
+ "name": "needs_legacy_numeric_removal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_hash": {
+ "name": "delivery_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_hash": {
+ "name": "metadata_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image_id": {
+ "name": "cover_image_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kobo_device_snapshot_books_book_id_idx": {
+ "name": "kobo_device_snapshot_books_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_device_snapshot_books_snapshot_synced_book_idx": {
+ "name": "kobo_device_snapshot_books_snapshot_synced_book_idx",
+ "columns": [
+ {
+ "expression": "snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "synced",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_device_snapshot_books_snapshot_id_kobo_device_library_snapshots_id_fk": {
+ "name": "kobo_device_snapshot_books_snapshot_id_kobo_device_library_snapshots_id_fk",
+ "tableFrom": "kobo_device_snapshot_books",
+ "tableTo": "kobo_device_library_snapshots",
+ "columnsFrom": ["snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_device_snapshot_books_book_id_books_id_fk": {
+ "name": "kobo_device_snapshot_books_book_id_books_id_fk",
+ "tableFrom": "kobo_device_snapshot_books",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "kobo_device_snapshot_books_snapshot_id_book_id_pk": {
+ "name": "kobo_device_snapshot_books_snapshot_id_book_id_pk",
+ "columns": ["snapshot_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_sync_history": {
+ "name": "kobo_sync_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "event": {
+ "name": "event",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "counts": {
+ "name": "counts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error_class": {
+ "name": "error_class",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_sync_history_user_created_idx": {
+ "name": "kobo_sync_history_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_sync_history_device_created_idx": {
+ "name": "kobo_sync_history_device_created_idx",
+ "columns": [
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_sync_history_user_id_users_id_fk": {
+ "name": "kobo_sync_history_user_id_users_id_fk",
+ "tableFrom": "kobo_sync_history",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_sync_history_device_id_kobo_devices_id_fk": {
+ "name": "kobo_sync_history_device_id_kobo_devices_id_fk",
+ "tableFrom": "kobo_sync_history",
+ "tableTo": "kobo_devices",
+ "columnsFrom": ["device_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kobo_sync_history_event_chk": {
+ "name": "kobo_sync_history_event_chk",
+ "value": "\"kobo_sync_history\".\"event\" in ('library_sync', 'book_download', 'progress_update', 'annotations_pull', 'annotations_push')"
+ },
+ "kobo_sync_history_status_chk": {
+ "name": "kobo_sync_history_status_chk",
+ "value": "\"kobo_sync_history\".\"status\" in ('success', 'failed')"
+ },
+ "kobo_sync_history_duration_nonnegative_chk": {
+ "name": "kobo_sync_history_duration_nonnegative_chk",
+ "value": "\"kobo_sync_history\".\"duration_ms\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.kobo_sync_settings": {
+ "name": "kobo_sync_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_threshold": {
+ "name": "reading_threshold",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "finished_threshold": {
+ "name": "finished_threshold",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 99
+ },
+ "convert_to_kepub": {
+ "name": "convert_to_kepub",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "force_enable_hyphenation": {
+ "name": "force_enable_hyphenation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "kepub_conversion_limit_mb": {
+ "name": "kepub_conversion_limit_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "two_way_progress_sync": {
+ "name": "two_way_progress_sync",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sync_bookorbit_annotations_to_kobo": {
+ "name": "sync_bookorbit_annotations_to_kobo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "read_aloud_delivery": {
+ "name": "read_aloud_delivery",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "store_sync": {
+ "name": "store_sync",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "full_size_covers": {
+ "name": "full_size_covers",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_sync_settings_user_id_users_id_fk": {
+ "name": "kobo_sync_settings_user_id_users_id_fk",
+ "tableFrom": "kobo_sync_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_sync_settings_user_id_unique": {
+ "name": "kobo_sync_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "kobo_sync_settings_reading_threshold_range_chk": {
+ "name": "kobo_sync_settings_reading_threshold_range_chk",
+ "value": "\"kobo_sync_settings\".\"reading_threshold\" >= 0 and \"kobo_sync_settings\".\"reading_threshold\" <= 100"
+ },
+ "kobo_sync_settings_finished_threshold_range_chk": {
+ "name": "kobo_sync_settings_finished_threshold_range_chk",
+ "value": "\"kobo_sync_settings\".\"finished_threshold\" >= 0 and \"kobo_sync_settings\".\"finished_threshold\" <= 100"
+ },
+ "kobo_sync_settings_conversion_limit_nonnegative_chk": {
+ "name": "kobo_sync_settings_conversion_limit_nonnegative_chk",
+ "value": "\"kobo_sync_settings\".\"kepub_conversion_limit_mb\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_dock_files": {
+ "name": "book_dock_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "absolute_path": {
+ "name": "absolute_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit_directory": {
+ "name": "unit_directory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "embedded_metadata": {
+ "name": "embedded_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "selected_metadata": {
+ "name": "selected_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fetched_metadata": {
+ "name": "fetched_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_path": {
+ "name": "cover_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_library_id": {
+ "name": "target_library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_folder_id": {
+ "name": "target_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confidence": {
+ "name": "confidence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fetched_metadata_sources": {
+ "name": "fetched_metadata_sources",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_edited_at": {
+ "name": "metadata_edited_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_finalize_suppressed": {
+ "name": "auto_finalize_suppressed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_dock_files_status_idx": {
+ "name": "book_dock_files_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_dock_files_target_library_id_idx": {
+ "name": "book_dock_files_target_library_id_idx",
+ "columns": [
+ {
+ "expression": "target_library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_dock_files_uploaded_by_idx": {
+ "name": "book_dock_files_uploaded_by_idx",
+ "columns": [
+ {
+ "expression": "uploaded_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_dock_files_target_library_id_libraries_id_fk": {
+ "name": "book_dock_files_target_library_id_libraries_id_fk",
+ "tableFrom": "book_dock_files",
+ "tableTo": "libraries",
+ "columnsFrom": ["target_library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_dock_files_target_folder_id_library_folders_id_fk": {
+ "name": "book_dock_files_target_folder_id_library_folders_id_fk",
+ "tableFrom": "book_dock_files",
+ "tableTo": "library_folders",
+ "columnsFrom": ["target_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_dock_files_uploaded_by_users_id_fk": {
+ "name": "book_dock_files_uploaded_by_users_id_fk",
+ "tableFrom": "book_dock_files",
+ "tableTo": "users",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "book_dock_files_absolute_path_unique": {
+ "name": "book_dock_files_absolute_path_unique",
+ "nullsNotDistinct": false,
+ "columns": ["absolute_path"]
+ },
+ "book_dock_files_unit_directory_unique": {
+ "name": "book_dock_files_unit_directory_unique",
+ "nullsNotDistinct": false,
+ "columns": ["unit_directory"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "book_dock_files_status_chk": {
+ "name": "book_dock_files_status_chk",
+ "value": "\"book_dock_files\".\"status\" in ('pending', 'extracting', 'fetching', 'ready', 'error')"
+ },
+ "book_dock_files_confidence_range_chk": {
+ "name": "book_dock_files_confidence_range_chk",
+ "value": "\"book_dock_files\".\"confidence\" is null or (\"book_dock_files\".\"confidence\" >= 0 and \"book_dock_files\".\"confidence\" <= 100)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_dock_unit_files": {
+ "name": "book_dock_unit_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "dock_file_id": {
+ "name": "dock_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "absolute_path": {
+ "name": "absolute_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'content'"
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_dock_unit_files_dock_file_id_sort_order_idx": {
+ "name": "book_dock_unit_files_dock_file_id_sort_order_idx",
+ "columns": [
+ {
+ "expression": "dock_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_dock_unit_files_dock_file_id_book_dock_files_id_fk": {
+ "name": "book_dock_unit_files_dock_file_id_book_dock_files_id_fk",
+ "tableFrom": "book_dock_unit_files",
+ "tableTo": "book_dock_files",
+ "columnsFrom": ["dock_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "book_dock_unit_files_absolute_path_unique": {
+ "name": "book_dock_unit_files_absolute_path_unique",
+ "nullsNotDistinct": false,
+ "columns": ["absolute_path"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "book_dock_unit_files_role_chk": {
+ "name": "book_dock_unit_files_role_chk",
+ "value": "\"book_dock_unit_files\".\"role\" in ('content', 'cover', 'metadata', 'supplement')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_request_dedupe_aliases": {
+ "name": "book_request_dedupe_aliases",
+ "schema": "",
+ "columns": {
+ "request_id": {
+ "name": "request_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dedupe_key": {
+ "name": "dedupe_key",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_request_dedupe_aliases_key_idx": {
+ "name": "book_request_dedupe_aliases_key_idx",
+ "columns": [
+ {
+ "expression": "dedupe_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_request_dedupe_aliases_request_id_book_requests_id_fk": {
+ "name": "book_request_dedupe_aliases_request_id_book_requests_id_fk",
+ "tableFrom": "book_request_dedupe_aliases",
+ "tableTo": "book_requests",
+ "columnsFrom": ["request_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_request_dedupe_aliases_request_id_dedupe_key_pk": {
+ "name": "book_request_dedupe_aliases_request_id_dedupe_key_pk",
+ "columns": ["request_id", "dedupe_key"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_request_dismissals": {
+ "name": "book_request_dismissals",
+ "schema": "",
+ "columns": {
+ "request_id": {
+ "name": "request_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_request_dismissals_user_id_idx": {
+ "name": "book_request_dismissals_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_request_dismissals_request_id_book_requests_id_fk": {
+ "name": "book_request_dismissals_request_id_book_requests_id_fk",
+ "tableFrom": "book_request_dismissals",
+ "tableTo": "book_requests",
+ "columnsFrom": ["request_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_request_dismissals_user_id_users_id_fk": {
+ "name": "book_request_dismissals_user_id_users_id_fk",
+ "tableFrom": "book_request_dismissals",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_request_dismissals_request_id_user_id_pk": {
+ "name": "book_request_dismissals_request_id_user_id_pk",
+ "columns": ["request_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_request_subscribers": {
+ "name": "book_request_subscribers",
+ "schema": "",
+ "columns": {
+ "request_id": {
+ "name": "request_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_request_subscribers_user_id_idx": {
+ "name": "book_request_subscribers_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_request_subscribers_request_id_book_requests_id_fk": {
+ "name": "book_request_subscribers_request_id_book_requests_id_fk",
+ "tableFrom": "book_request_subscribers",
+ "tableTo": "book_requests",
+ "columnsFrom": ["request_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_request_subscribers_user_id_users_id_fk": {
+ "name": "book_request_subscribers_user_id_users_id_fk",
+ "tableFrom": "book_request_subscribers",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_request_subscribers_request_id_user_id_pk": {
+ "name": "book_request_subscribers_request_id_user_id_pk",
+ "columns": ["request_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_requests": {
+ "name": "book_requests",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_kind": {
+ "name": "media_kind",
+ "type": "book_request_media_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subtitle": {
+ "name": "subtitle",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authors": {
+ "name": "authors",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "series_name": {
+ "name": "series_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_index": {
+ "name": "series_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isbn10": {
+ "name": "isbn10",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isbn13": {
+ "name": "isbn13",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_year": {
+ "name": "published_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_url": {
+ "name": "cover_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_key": {
+ "name": "provider_key",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_sources": {
+ "name": "metadata_sources",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "preferred_formats": {
+ "name": "preferred_formats",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_library_id": {
+ "name": "target_library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_folder_id": {
+ "name": "target_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decided_by_user_id": {
+ "name": "decided_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decided_at": {
+ "name": "decided_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decision_note": {
+ "name": "decision_note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "matched_book_id": {
+ "name": "matched_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_dock_file_id": {
+ "name": "book_dock_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_reason": {
+ "name": "status_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_code": {
+ "name": "failure_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_meta": {
+ "name": "failure_meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "self_serve": {
+ "name": "self_serve",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "fulfiller_user_id": {
+ "name": "fulfiller_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "companion_request_id": {
+ "name": "companion_request_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_book_id": {
+ "name": "target_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dedupe_key": {
+ "name": "dedupe_key",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_requests_user_status_idx": {
+ "name": "book_requests_user_status_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_status_created_at_idx": {
+ "name": "book_requests_status_created_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_target_library_id_idx": {
+ "name": "book_requests_target_library_id_idx",
+ "columns": [
+ {
+ "expression": "target_library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_created_at_idx": {
+ "name": "book_requests_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_title_idx": {
+ "name": "book_requests_title_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_matched_book_user_idx": {
+ "name": "book_requests_matched_book_user_idx",
+ "columns": [
+ {
+ "expression": "matched_book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_requests\".\"matched_book_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_companion_request_id_idx": {
+ "name": "book_requests_companion_request_id_idx",
+ "columns": [
+ {
+ "expression": "companion_request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_requests\".\"companion_request_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_target_book_id_idx": {
+ "name": "book_requests_target_book_id_idx",
+ "columns": [
+ {
+ "expression": "target_book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_requests\".\"target_book_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_self_serve_idx": {
+ "name": "book_requests_self_serve_idx",
+ "columns": [
+ {
+ "expression": "self_serve",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"book_requests\".\"self_serve\"",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_requests_active_dedupe_key_uidx": {
+ "name": "book_requests_active_dedupe_key_uidx",
+ "columns": [
+ {
+ "expression": "dedupe_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"book_requests\".\"status\" in ('pending', 'approved', 'searching', 'grabbed', 'downloading', 'importing', 'needs_review')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_requests_user_id_users_id_fk": {
+ "name": "book_requests_user_id_users_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_requests_target_library_id_libraries_id_fk": {
+ "name": "book_requests_target_library_id_libraries_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "libraries",
+ "columnsFrom": ["target_library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_target_folder_id_library_folders_id_fk": {
+ "name": "book_requests_target_folder_id_library_folders_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "library_folders",
+ "columnsFrom": ["target_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_created_by_user_id_users_id_fk": {
+ "name": "book_requests_created_by_user_id_users_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_decided_by_user_id_users_id_fk": {
+ "name": "book_requests_decided_by_user_id_users_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "users",
+ "columnsFrom": ["decided_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_matched_book_id_books_id_fk": {
+ "name": "book_requests_matched_book_id_books_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "books",
+ "columnsFrom": ["matched_book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_book_dock_file_id_book_dock_files_id_fk": {
+ "name": "book_requests_book_dock_file_id_book_dock_files_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "book_dock_files",
+ "columnsFrom": ["book_dock_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_fulfiller_user_id_users_id_fk": {
+ "name": "book_requests_fulfiller_user_id_users_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "users",
+ "columnsFrom": ["fulfiller_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_companion_request_id_book_requests_id_fk": {
+ "name": "book_requests_companion_request_id_book_requests_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "book_requests",
+ "columnsFrom": ["companion_request_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_requests_target_book_id_books_id_fk": {
+ "name": "book_requests_target_book_id_books_id_fk",
+ "tableFrom": "book_requests",
+ "tableTo": "books",
+ "columnsFrom": ["target_book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_requests_status_chk": {
+ "name": "book_requests_status_chk",
+ "value": "\"book_requests\".\"status\" in ('pending', 'approved', 'rejected', 'cancelled', 'searching', 'grabbed', 'downloading', 'importing', 'needs_review', 'available', 'failed')"
+ },
+ "book_requests_series_index_nonnegative_chk": {
+ "name": "book_requests_series_index_nonnegative_chk",
+ "value": "\"book_requests\".\"series_index\" is null or \"book_requests\".\"series_index\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_request_downloads": {
+ "name": "book_request_downloads",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "download_client_id": {
+ "name": "download_client_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "indexer_id": {
+ "name": "indexer_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "automated": {
+ "name": "automated",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "release_title": {
+ "name": "release_title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "release_guid": {
+ "name": "release_guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "release_size_bytes": {
+ "name": "release_size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "release_seeders": {
+ "name": "release_seeders",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "release_format": {
+ "name": "release_format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "freeleech": {
+ "name": "freeleech",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "release_info_url": {
+ "name": "release_info_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hold_for_review": {
+ "name": "hold_for_review",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "client_key": {
+ "name": "client_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "progress_percent": {
+ "name": "progress_percent",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "downloaded_bytes": {
+ "name": "downloaded_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "total_bytes": {
+ "name": "total_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "direct_url": {
+ "name": "direct_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "direct_file_name": {
+ "name": "direct_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "direct_etag": {
+ "name": "direct_etag",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "direct_last_modified": {
+ "name": "direct_last_modified",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_path": {
+ "name": "content_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "local_path": {
+ "name": "local_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_dock_file_id": {
+ "name": "book_dock_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "release_units": {
+ "name": "release_units",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grabbed_at": {
+ "name": "grabbed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_progress_at": {
+ "name": "last_progress_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "imported_at": {
+ "name": "imported_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_request_downloads_status_idx": {
+ "name": "book_request_downloads_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_request_downloads_request_latest_idx": {
+ "name": "book_request_downloads_request_latest_idx",
+ "columns": [
+ {
+ "expression": "request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_request_downloads_book_dock_file_id_idx": {
+ "name": "book_request_downloads_book_dock_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_dock_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_request_downloads_active_hash_uidx": {
+ "name": "book_request_downloads_active_hash_uidx",
+ "columns": [
+ {
+ "expression": "coalesce(\"download_client_id\", 0)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"book_request_downloads\".\"status\" in ('queued', 'downloading', 'completed', 'importing', 'needs_review')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_request_downloads_request_id_book_requests_id_fk": {
+ "name": "book_request_downloads_request_id_book_requests_id_fk",
+ "tableFrom": "book_request_downloads",
+ "tableTo": "book_requests",
+ "columnsFrom": ["request_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_request_downloads_download_client_id_download_clients_id_fk": {
+ "name": "book_request_downloads_download_client_id_download_clients_id_fk",
+ "tableFrom": "book_request_downloads",
+ "tableTo": "download_clients",
+ "columnsFrom": ["download_client_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_request_downloads_indexer_id_request_indexers_id_fk": {
+ "name": "book_request_downloads_indexer_id_request_indexers_id_fk",
+ "tableFrom": "book_request_downloads",
+ "tableTo": "request_indexers",
+ "columnsFrom": ["indexer_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_request_downloads_book_dock_file_id_book_dock_files_id_fk": {
+ "name": "book_request_downloads_book_dock_file_id_book_dock_files_id_fk",
+ "tableFrom": "book_request_downloads",
+ "tableTo": "book_dock_files",
+ "columnsFrom": ["book_dock_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_request_downloads_source_chk": {
+ "name": "book_request_downloads_source_chk",
+ "value": "\"book_request_downloads\".\"source\" in ('magnet', 'torrent_file', 'direct_url', 'nzb_file')"
+ },
+ "book_request_downloads_status_chk": {
+ "name": "book_request_downloads_status_chk",
+ "value": "\"book_request_downloads\".\"status\" in ('queued', 'downloading', 'completed', 'importing', 'needs_review', 'imported', 'failed')"
+ },
+ "book_request_downloads_progress_range_chk": {
+ "name": "book_request_downloads_progress_range_chk",
+ "value": "\"book_request_downloads\".\"progress_percent\" >= 0 and \"book_request_downloads\".\"progress_percent\" <= 100"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.download_client_path_mappings": {
+ "name": "download_client_path_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "download_client_id": {
+ "name": "download_client_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remote_path": {
+ "name": "remote_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "local_path": {
+ "name": "local_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "download_client_path_mappings_client_remote_uidx": {
+ "name": "download_client_path_mappings_client_remote_uidx",
+ "columns": [
+ {
+ "expression": "download_client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "remote_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "download_client_path_mappings_download_client_id_download_clients_id_fk": {
+ "name": "download_client_path_mappings_download_client_id_download_clients_id_fk",
+ "tableFrom": "download_client_path_mappings",
+ "tableTo": "download_clients",
+ "columnsFrom": ["download_client_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.download_clients": {
+ "name": "download_clients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "adapter_type": {
+ "name": "adapter_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credentials_enc": {
+ "name": "credentials_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bookorbit'"
+ },
+ "use_hardlinks": {
+ "name": "use_hardlinks",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "allow_private_address": {
+ "name": "allow_private_address",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_tested_at": {
+ "name": "last_tested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_ok": {
+ "name": "last_test_ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_message": {
+ "name": "last_error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "download_clients_name_lower_uidx": {
+ "name": "download_clients_name_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "download_clients_enabled_priority_idx": {
+ "name": "download_clients_enabled_priority_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "priority",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "download_clients_adapter_type_chk": {
+ "name": "download_clients_adapter_type_chk",
+ "value": "\"download_clients\".\"adapter_type\" in ('qbittorrent', 'transmission', 'deluge', 'nzbget', 'sabnzbd')"
+ },
+ "download_clients_priority_range_chk": {
+ "name": "download_clients_priority_range_chk",
+ "value": "\"download_clients\".\"priority\" >= 1 and \"download_clients\".\"priority\" <= 100"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.request_indexers": {
+ "name": "request_indexers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "adapter_type": {
+ "name": "adapter_type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "manager_id": {
+ "name": "manager_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_external_id": {
+ "name": "manager_external_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_available": {
+ "name": "manager_available",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "manager_last_seen_at": {
+ "name": "manager_last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "manager_metadata": {
+ "name": "manager_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credentials_enc": {
+ "name": "credentials_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allow_private_address": {
+ "name": "allow_private_address",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "apply_tracker_seed_goals": {
+ "name": "apply_tracker_seed_goals",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "seed_ratio_goal": {
+ "name": "seed_ratio_goal",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seed_time_minutes": {
+ "name": "seed_time_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled_media_kinds": {
+ "name": "disabled_media_kinds",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "isbn_search_disabled": {
+ "name": "isbn_search_disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "network_profile": {
+ "name": "network_profile",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_tested_at": {
+ "name": "last_tested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_ok": {
+ "name": "last_test_ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_message": {
+ "name": "last_error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_search_at": {
+ "name": "last_search_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_search_ok": {
+ "name": "last_search_ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_search_error": {
+ "name": "last_search_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "search_failure_streak": {
+ "name": "search_failure_streak",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "request_indexers_name_lower_uidx": {
+ "name": "request_indexers_name_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "request_indexers_manager_external_uidx": {
+ "name": "request_indexers_manager_external_uidx",
+ "columns": [
+ {
+ "expression": "manager_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "manager_external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "request_indexers_enabled_idx": {
+ "name": "request_indexers_enabled_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "request_indexers_manager_idx": {
+ "name": "request_indexers_manager_idx",
+ "columns": [
+ {
+ "expression": "manager_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "request_indexers_manager_id_request_indexer_managers_id_fk": {
+ "name": "request_indexers_manager_id_request_indexer_managers_id_fk",
+ "tableFrom": "request_indexers",
+ "tableTo": "request_indexer_managers",
+ "columnsFrom": ["manager_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "request_indexers_adapter_type_chk": {
+ "name": "request_indexers_adapter_type_chk",
+ "value": "\"request_indexers\".\"adapter_type\" ~ '^[a-z0-9][a-z0-9-]{0,29}$'"
+ },
+ "request_indexers_manager_fields_chk": {
+ "name": "request_indexers_manager_fields_chk",
+ "value": "(\"request_indexers\".\"manager_id\" is null and \"request_indexers\".\"manager_external_id\" is null and \"request_indexers\".\"manager_metadata\" is null) or (\"request_indexers\".\"manager_id\" is not null and \"request_indexers\".\"manager_external_id\" is not null and \"request_indexers\".\"manager_metadata\" is not null and \"request_indexers\".\"credentials_enc\" is null and \"request_indexers\".\"adapter_type\" in ('torznab', 'newznab'))"
+ },
+ "request_indexers_seed_ratio_goal_chk": {
+ "name": "request_indexers_seed_ratio_goal_chk",
+ "value": "\"request_indexers\".\"seed_ratio_goal\" is null or (\"request_indexers\".\"seed_ratio_goal\" > 0 and \"request_indexers\".\"seed_ratio_goal\" < 'Infinity'::double precision)"
+ },
+ "request_indexers_seed_time_minutes_chk": {
+ "name": "request_indexers_seed_time_minutes_chk",
+ "value": "\"request_indexers\".\"seed_time_minutes\" is null or \"request_indexers\".\"seed_time_minutes\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.request_indexer_managers": {
+ "name": "request_indexer_managers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credentials_enc": {
+ "name": "credentials_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "allow_private_address": {
+ "name": "allow_private_address",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sync_new_indexers": {
+ "name": "sync_new_indexers",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "per_indexer_timeout_seconds": {
+ "name": "per_indexer_timeout_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 20
+ },
+ "overall_search_budget_seconds": {
+ "name": "overall_search_budget_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 60
+ },
+ "auto_expand_categories": {
+ "name": "auto_expand_categories",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "inherit_seed_limits": {
+ "name": "inherit_seed_limits",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "network_profile": {
+ "name": "network_profile",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_tested_at": {
+ "name": "last_tested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_ok": {
+ "name": "last_test_ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error_message": {
+ "name": "last_error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_ok": {
+ "name": "last_sync_ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_error": {
+ "name": "last_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "request_indexer_managers_name_lower_uidx": {
+ "name": "request_indexer_managers_name_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "request_indexer_managers_type_chk": {
+ "name": "request_indexer_managers_type_chk",
+ "value": "\"request_indexer_managers\".\"type\" in ('prowlarr')"
+ },
+ "request_indexer_managers_timeout_chk": {
+ "name": "request_indexer_managers_timeout_chk",
+ "value": "\"request_indexer_managers\".\"per_indexer_timeout_seconds\" between 5 and 180"
+ },
+ "request_indexer_managers_budget_chk": {
+ "name": "request_indexer_managers_budget_chk",
+ "value": "\"request_indexer_managers\".\"overall_search_budget_seconds\" between 5 and 300"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.file_write_log": {
+ "name": "file_write_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fields_written": {
+ "name": "fields_written",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "triggered_by": {
+ "name": "triggered_by",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "written_at": {
+ "name": "written_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "fwl_book_id_written_at_idx": {
+ "name": "fwl_book_id_written_at_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "written_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "file_write_log_book_file_id_idx": {
+ "name": "file_write_log_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "file_write_log_book_id_books_id_fk": {
+ "name": "file_write_log_book_id_books_id_fk",
+ "tableFrom": "file_write_log",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "file_write_log_book_file_id_book_files_id_fk": {
+ "name": "file_write_log_book_file_id_book_files_id_fk",
+ "tableFrom": "file_write_log",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "file_write_log_user_id_users_id_fk": {
+ "name": "file_write_log_user_id_users_id_fk",
+ "tableFrom": "file_write_log",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "file_write_log_status_chk": {
+ "name": "file_write_log_status_chk",
+ "value": "\"file_write_log\".\"status\" in ('success', 'skipped', 'failed')"
+ },
+ "file_write_log_triggered_by_chk": {
+ "name": "file_write_log_triggered_by_chk",
+ "value": "\"file_write_log\".\"triggered_by\" in ('auto', 'sync')"
+ },
+ "file_write_log_duration_ms_nonnegative_chk": {
+ "name": "file_write_log_duration_ms_nonnegative_chk",
+ "value": "\"file_write_log\".\"duration_ms\" is null or \"file_write_log\".\"duration_ms\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.email_templates": {
+ "name": "email_templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body_text": {
+ "name": "body_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_system": {
+ "name": "is_system",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_templates_one_default_per_user_uidx": {
+ "name": "email_templates_one_default_per_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_templates\".\"is_default\" = true and \"email_templates\".\"user_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_templates_system_name_unique": {
+ "name": "email_templates_system_name_unique",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_templates\".\"user_id\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_templates_user_id_users_id_fk": {
+ "name": "email_templates_user_id_users_id_fk",
+ "tableFrom": "email_templates",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_templates_user_id_name_unique": {
+ "name": "email_templates_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_providers": {
+ "name": "email_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "host": {
+ "name": "host",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "port": {
+ "name": "port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password_enc": {
+ "name": "password_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_address": {
+ "name": "from_address",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth": {
+ "name": "auth",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "ssl": {
+ "name": "ssl",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "start_tls": {
+ "name": "start_tls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_shared": {
+ "name": "is_shared",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_system_provider": {
+ "name": "is_system_provider",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tls_reject_unauthorized": {
+ "name": "tls_reject_unauthorized",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_tested_at": {
+ "name": "last_tested_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_ok": {
+ "name": "last_test_ok",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_test_error": {
+ "name": "last_test_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_providers_one_default_per_user_uidx": {
+ "name": "email_providers_one_default_per_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_providers\".\"is_default\" = true and \"email_providers\".\"user_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_providers_one_system_provider_uidx": {
+ "name": "email_providers_one_system_provider_uidx",
+ "columns": [
+ {
+ "expression": "is_system_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_providers\".\"is_system_provider\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_providers_user_id_users_id_fk": {
+ "name": "email_providers_user_id_users_id_fk",
+ "tableFrom": "email_providers",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_providers_user_id_name_unique": {
+ "name": "email_providers_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ },
+ "email_providers_id_user_id_unique": {
+ "name": "email_providers_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "email_providers_port_range_chk": {
+ "name": "email_providers_port_range_chk",
+ "value": "\"email_providers\".\"port\" >= 1 and \"email_providers\".\"port\" <= 65535"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.email_recipient_group_members": {
+ "name": "email_recipient_group_members",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipient_id": {
+ "name": "recipient_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_recipient_group_members_user_id_users_id_fk": {
+ "name": "email_recipient_group_members_user_id_users_id_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_group_id_email_recipient_groups_id_fk": {
+ "name": "email_recipient_group_members_group_id_email_recipient_groups_id_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipient_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_recipient_id_email_recipients_id_fk": {
+ "name": "email_recipient_group_members_recipient_id_email_recipients_id_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["recipient_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_group_user_fk": {
+ "name": "email_recipient_group_members_group_user_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipient_groups",
+ "columnsFrom": ["group_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_recipient_user_fk": {
+ "name": "email_recipient_group_members_recipient_user_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["recipient_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "email_recipient_group_members_group_id_recipient_id_pk": {
+ "name": "email_recipient_group_members_group_id_recipient_id_pk",
+ "columns": ["group_id", "recipient_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_recipient_groups": {
+ "name": "email_recipient_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_template_id": {
+ "name": "default_template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_recipient_groups_user_id_users_id_fk": {
+ "name": "email_recipient_groups_user_id_users_id_fk",
+ "tableFrom": "email_recipient_groups",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_groups_default_template_id_email_templates_id_fk": {
+ "name": "email_recipient_groups_default_template_id_email_templates_id_fk",
+ "tableFrom": "email_recipient_groups",
+ "tableTo": "email_templates",
+ "columnsFrom": ["default_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_recipient_groups_user_id_name_unique": {
+ "name": "email_recipient_groups_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ },
+ "email_recipient_groups_id_user_id_unique": {
+ "name": "email_recipient_groups_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_recipients": {
+ "name": "email_recipients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "device_type": {
+ "name": "device_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "read_aloud_delivery": {
+ "name": "read_aloud_delivery",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preferred_format": {
+ "name": "preferred_format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_template_id": {
+ "name": "default_template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_recipients_user_email_lower_uidx": {
+ "name": "email_recipients_user_email_lower_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_recipients_one_default_per_user_uidx": {
+ "name": "email_recipients_one_default_per_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_recipients\".\"is_default\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_recipients_user_id_users_id_fk": {
+ "name": "email_recipients_user_id_users_id_fk",
+ "tableFrom": "email_recipients",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipients_default_template_id_email_templates_id_fk": {
+ "name": "email_recipients_default_template_id_email_templates_id_fk",
+ "tableFrom": "email_recipients",
+ "tableTo": "email_templates",
+ "columnsFrom": ["default_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_recipients_user_id_email_unique": {
+ "name": "email_recipients_user_id_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "email"]
+ },
+ "email_recipients_id_user_id_unique": {
+ "name": "email_recipients_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "email_recipients_device_type_chk": {
+ "name": "email_recipients_device_type_chk",
+ "value": "\"email_recipients\".\"device_type\" is null or \"email_recipients\".\"device_type\" in ('kindle', 'kobo', 'other')"
+ },
+ "email_recipients_preferred_format_chk": {
+ "name": "email_recipients_preferred_format_chk",
+ "value": "\"email_recipients\".\"preferred_format\" is null or \"email_recipients\".\"preferred_format\" in ('epub', 'pdf', 'mobi', 'azw3', 'cbz', 'cbr', 'djvu')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.email_preferences": {
+ "name": "email_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_provider_id": {
+ "name": "default_provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_recipient_id": {
+ "name": "default_recipient_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_template_id": {
+ "name": "default_template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_preferences_user_id_users_id_fk": {
+ "name": "email_preferences_user_id_users_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_provider_id_email_providers_id_fk": {
+ "name": "email_preferences_default_provider_id_email_providers_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_providers",
+ "columnsFrom": ["default_provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_recipient_id_email_recipients_id_fk": {
+ "name": "email_preferences_default_recipient_id_email_recipients_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["default_recipient_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_template_id_email_templates_id_fk": {
+ "name": "email_preferences_default_template_id_email_templates_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_templates",
+ "columnsFrom": ["default_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_recipient_owner_fk": {
+ "name": "email_preferences_default_recipient_owner_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["default_recipient_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_preferences_user_id_unique": {
+ "name": "email_preferences_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_send_log": {
+ "name": "email_send_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "to_email": {
+ "name": "to_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "to_name": {
+ "name": "to_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_policy": {
+ "name": "delivery_policy",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_revision": {
+ "name": "source_revision",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "artifact_id": {
+ "name": "artifact_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_metadata": {
+ "name": "delivery_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actual_variant": {
+ "name": "actual_variant",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "device_type": {
+ "name": "device_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recipient_id": {
+ "name": "recipient_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failure_code": {
+ "name": "failure_code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dispatch_phase": {
+ "name": "dispatch_phase",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_retry_at": {
+ "name": "next_retry_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_send_log_user_created_at_idx": {
+ "name": "email_send_log_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_log_book_id_idx": {
+ "name": "email_send_log_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_log_book_file_id_idx": {
+ "name": "email_send_log_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_log_created_at_idx": {
+ "name": "email_send_log_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_log_status_next_retry_idx": {
+ "name": "email_send_log_status_next_retry_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_retry_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_send_log_user_id_users_id_fk": {
+ "name": "email_send_log_user_id_users_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_send_log_book_id_books_id_fk": {
+ "name": "email_send_log_book_id_books_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_send_log_book_file_id_book_files_id_fk": {
+ "name": "email_send_log_book_file_id_book_files_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_send_log_provider_id_email_providers_id_fk": {
+ "name": "email_send_log_provider_id_email_providers_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "email_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_send_log_template_id_email_templates_id_fk": {
+ "name": "email_send_log_template_id_email_templates_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "email_templates",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "email_send_log_status_chk": {
+ "name": "email_send_log_status_chk",
+ "value": "\"email_send_log\".\"status\" in ('pending', 'sent', 'failed')"
+ },
+ "email_send_log_attempt_count_nonnegative_chk": {
+ "name": "email_send_log_attempt_count_nonnegative_chk",
+ "value": "\"email_send_log\".\"attempt_count\" >= 0"
+ },
+ "email_send_log_sent_after_created_chk": {
+ "name": "email_send_log_sent_after_created_chk",
+ "value": "\"email_send_log\".\"sent_at\" is null or \"email_send_log\".\"sent_at\" >= \"email_send_log\".\"created_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.notifications": {
+ "name": "notifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_url": {
+ "name": "action_url",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta": {
+ "name": "meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "read": {
+ "name": "read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "group_key": {
+ "name": "group_key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "count": {
+ "name": "count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "notifications_user_id_idx": {
+ "name": "notifications_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_user_unread_idx": {
+ "name": "notifications_user_unread_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_created_at_idx": {
+ "name": "notifications_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_user_updated_at_idx": {
+ "name": "notifications_user_updated_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_updated_at_idx": {
+ "name": "notifications_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_user_unread_group_key_idx": {
+ "name": "notifications_user_unread_group_key_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"notifications\".\"read\" = false and \"notifications\".\"group_key\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notifications_user_id_users_id_fk": {
+ "name": "notifications_user_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.dismissed_duplicate_pairs": {
+ "name": "dismissed_duplicate_pairs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_a": {
+ "name": "entity_id_a",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_b": {
+ "name": "entity_id_b",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_by": {
+ "name": "dismissed_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "dismissed_duplicate_pairs_entity_a_idx": {
+ "name": "dismissed_duplicate_pairs_entity_a_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "dismissed_duplicate_pairs_entity_b_idx": {
+ "name": "dismissed_duplicate_pairs_entity_b_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "dismissed_duplicate_pairs_dismissed_by_users_id_fk": {
+ "name": "dismissed_duplicate_pairs_dismissed_by_users_id_fk",
+ "tableFrom": "dismissed_duplicate_pairs",
+ "tableTo": "users",
+ "columnsFrom": ["dismissed_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "dismissed_duplicate_pairs_unique": {
+ "name": "dismissed_duplicate_pairs_unique",
+ "nullsNotDistinct": false,
+ "columns": ["entity_type", "entity_id_a", "entity_id_b"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.dismissed_inline_duplicate_pairs": {
+ "name": "dismissed_inline_duplicate_pairs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value_a": {
+ "name": "value_a",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value_b": {
+ "name": "value_b",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_by": {
+ "name": "dismissed_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "dismissed_inline_pairs_entity_a_idx": {
+ "name": "dismissed_inline_pairs_entity_a_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "value_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "dismissed_inline_pairs_entity_b_idx": {
+ "name": "dismissed_inline_pairs_entity_b_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "value_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "dismissed_inline_duplicate_pairs_dismissed_by_users_id_fk": {
+ "name": "dismissed_inline_duplicate_pairs_dismissed_by_users_id_fk",
+ "tableFrom": "dismissed_inline_duplicate_pairs",
+ "tableTo": "users",
+ "columnsFrom": ["dismissed_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "dismissed_inline_duplicate_pairs_unique": {
+ "name": "dismissed_inline_duplicate_pairs_unique",
+ "nullsNotDistinct": false,
+ "columns": ["entity_type", "value_a", "value_b"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.entity_duplicate_candidates": {
+ "name": "entity_duplicate_candidates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_a": {
+ "name": "entity_id_a",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_b": {
+ "name": "entity_id_b",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sim_score": {
+ "name": "sim_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "computed_at": {
+ "name": "computed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "entity_dup_candidates_type_sim_idx": {
+ "name": "entity_dup_candidates_type_sim_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sim_score",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entity_dup_candidates_entity_a_idx": {
+ "name": "entity_dup_candidates_entity_a_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entity_dup_candidates_entity_b_idx": {
+ "name": "entity_dup_candidates_entity_b_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "entity_duplicate_candidates_unique": {
+ "name": "entity_duplicate_candidates_unique",
+ "nullsNotDistinct": false,
+ "columns": ["entity_type", "entity_id_a", "entity_id_b"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.entity_duplicate_scan_status": {
+ "name": "entity_duplicate_scan_status",
+ "schema": "",
+ "columns": {
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "is_computing": {
+ "name": "is_computing",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "computed_at": {
+ "name": "computed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_pairs": {
+ "name": "total_pairs",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "threshold": {
+ "name": "threshold",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_count": {
+ "name": "processed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_count": {
+ "name": "total_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.server_fonts": {
+ "name": "server_fonts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "family_name": {
+ "name": "family_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stored_file_name": {
+ "name": "stored_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weight": {
+ "name": "weight",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 400
+ },
+ "style": {
+ "name": "style",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'normal'"
+ },
+ "weight_min": {
+ "name": "weight_min",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "weight_max": {
+ "name": "weight_max",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "instances": {
+ "name": "instances",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "sf_hash_uidx": {
+ "name": "sf_hash_uidx",
+ "columns": [
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sf_family_weight_style_uidx": {
+ "name": "sf_family_weight_style_uidx",
+ "columns": [
+ {
+ "expression": "family_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "weight",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "style",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "server_fonts_uploaded_by_users_id_fk": {
+ "name": "server_fonts_uploaded_by_users_id_fk",
+ "tableFrom": "server_fonts",
+ "tableTo": "users",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "server_fonts_format_chk": {
+ "name": "server_fonts_format_chk",
+ "value": "\"server_fonts\".\"format\" in ('ttf', 'otf', 'woff', 'woff2')"
+ },
+ "server_fonts_weight_chk": {
+ "name": "server_fonts_weight_chk",
+ "value": "\"server_fonts\".\"weight\" >= 1 and \"server_fonts\".\"weight\" <= 1000"
+ },
+ "server_fonts_style_chk": {
+ "name": "server_fonts_style_chk",
+ "value": "\"server_fonts\".\"style\" in ('normal', 'italic')"
+ },
+ "server_fonts_weight_range_chk": {
+ "name": "server_fonts_weight_range_chk",
+ "value": "(\"server_fonts\".\"weight_min\" is null) = (\"server_fonts\".\"weight_max\" is null) and (\"server_fonts\".\"weight_min\" is null or (\"server_fonts\".\"weight_min\" >= 1 and \"server_fonts\".\"weight_max\" <= 1000 and \"server_fonts\".\"weight_min\" < \"server_fonts\".\"weight_max\"))"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_fonts": {
+ "name": "user_fonts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "family_name": {
+ "name": "family_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stored_file_name": {
+ "name": "stored_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weight": {
+ "name": "weight",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 400
+ },
+ "style": {
+ "name": "style",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'normal'"
+ },
+ "weight_min": {
+ "name": "weight_min",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "weight_max": {
+ "name": "weight_max",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "instances": {
+ "name": "instances",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uf_user_hash_uidx": {
+ "name": "uf_user_hash_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uf_user_family_weight_style_uidx": {
+ "name": "uf_user_family_weight_style_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "family_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "weight",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "style",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_fonts_user_id_users_id_fk": {
+ "name": "user_fonts_user_id_users_id_fk",
+ "tableFrom": "user_fonts",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_fonts_format_chk": {
+ "name": "user_fonts_format_chk",
+ "value": "\"user_fonts\".\"format\" in ('ttf', 'otf', 'woff', 'woff2')"
+ },
+ "user_fonts_weight_chk": {
+ "name": "user_fonts_weight_chk",
+ "value": "\"user_fonts\".\"weight\" >= 1 and \"user_fonts\".\"weight\" <= 1000"
+ },
+ "user_fonts_style_chk": {
+ "name": "user_fonts_style_chk",
+ "value": "\"user_fonts\".\"style\" in ('normal', 'italic')"
+ },
+ "user_fonts_weight_range_chk": {
+ "name": "user_fonts_weight_range_chk",
+ "value": "(\"user_fonts\".\"weight_min\" is null) = (\"user_fonts\".\"weight_max\" is null) and (\"user_fonts\".\"weight_min\" is null or (\"user_fonts\".\"weight_min\" >= 1 and \"user_fonts\".\"weight_max\" <= 1000 and \"user_fonts\".\"weight_min\" < \"user_fonts\".\"weight_max\"))"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_file_chapters": {
+ "name": "book_file_chapters",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chapter_index": {
+ "name": "chapter_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "href": {
+ "name": "href",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spine_index": {
+ "name": "spine_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "book_file_chapters_book_file_id_chapter_index_uidx": {
+ "name": "book_file_chapters_book_file_id_chapter_index_uidx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chapter_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_file_chapters_book_file_id_idx": {
+ "name": "book_file_chapters_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_file_chapters_book_file_id_book_files_id_fk": {
+ "name": "book_file_chapters_book_file_id_book_files_id_fk",
+ "tableFrom": "book_file_chapters",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_file_hash_history": {
+ "name": "book_file_hash_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_file_hash_history_book_file_id_file_hash_idx": {
+ "name": "book_file_hash_history_book_file_id_file_hash_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_file_hash_history_file_hash_idx": {
+ "name": "book_file_hash_history_file_hash_idx",
+ "columns": [
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_file_hash_history_book_file_id_book_files_id_fk": {
+ "name": "book_file_hash_history_book_file_id_book_files_id_fk",
+ "tableFrom": "book_file_hash_history",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_file_hash_history_reason_chk": {
+ "name": "book_file_hash_history_reason_chk",
+ "value": "\"book_file_hash_history\".\"reason\" in ('file_write', 'external_change', 'rescan', 'kobo_download', 'read_aloud_replace')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_book_hash_links": {
+ "name": "koreader_book_hash_links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "koreader_title": {
+ "name": "koreader_title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "koreader_authors": {
+ "name": "koreader_authors",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "koreader_last_open": {
+ "name": "koreader_last_open",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_book_hash_links_user_hash_uidx": {
+ "name": "koreader_book_hash_links_user_hash_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_book_hash_links_user_updated_idx": {
+ "name": "koreader_book_hash_links_user_updated_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"updated_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_book_hash_links_book_file_id_idx": {
+ "name": "koreader_book_hash_links_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_book_hash_links_user_id_users_id_fk": {
+ "name": "koreader_book_hash_links_user_id_users_id_fk",
+ "tableFrom": "koreader_book_hash_links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_book_hash_links_book_file_id_book_files_id_fk": {
+ "name": "koreader_book_hash_links_book_file_id_book_files_id_fk",
+ "tableFrom": "koreader_book_hash_links",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_book_hash_links_hash_chk": {
+ "name": "koreader_book_hash_links_hash_chk",
+ "value": "\"koreader_book_hash_links\".\"hash\" ~ '^[0-9a-f]{32}$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_bookmark_links": {
+ "name": "koreader_bookmark_links",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "bookmark_id": {
+ "name": "bookmark_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "koreader_key": {
+ "name": "koreader_key",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_datetime": {
+ "name": "device_datetime",
+ "type": "varchar(19)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applied_at": {
+ "name": "applied_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_bookmark_links_bookmark_device_uidx": {
+ "name": "koreader_bookmark_links_bookmark_device_uidx",
+ "columns": [
+ {
+ "expression": "bookmark_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_bookmark_links_user_device_key_idx": {
+ "name": "koreader_bookmark_links_user_device_key_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "koreader_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_bookmark_links_user_device_idx": {
+ "name": "koreader_bookmark_links_user_device_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_bookmark_links_bookmark_id_idx": {
+ "name": "koreader_bookmark_links_bookmark_id_idx",
+ "columns": [
+ {
+ "expression": "bookmark_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_bookmark_links_bookmark_id_bookmarks_id_fk": {
+ "name": "koreader_bookmark_links_bookmark_id_bookmarks_id_fk",
+ "tableFrom": "koreader_bookmark_links",
+ "tableTo": "bookmarks",
+ "columnsFrom": ["bookmark_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_bookmark_links_user_id_users_id_fk": {
+ "name": "koreader_bookmark_links_user_id_users_id_fk",
+ "tableFrom": "koreader_bookmark_links",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_device_delivery_settings": {
+ "name": "koreader_device_delivery_settings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "read_aloud_delivery": {
+ "name": "read_aloud_delivery",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "koreader_device_delivery_settings_user_id_users_id_fk": {
+ "name": "koreader_device_delivery_settings_user_id_users_id_fk",
+ "tableFrom": "koreader_device_delivery_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_device_delivery_settings_credential_id_koreader_users_id_fk": {
+ "name": "koreader_device_delivery_settings_credential_id_koreader_users_id_fk",
+ "tableFrom": "koreader_device_delivery_settings",
+ "tableTo": "koreader_users",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_device_delivery_settings_user_id_device_id_pk": {
+ "name": "koreader_device_delivery_settings_user_id_device_id_pk",
+ "columns": ["user_id", "device_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_device_delivery_chk": {
+ "name": "koreader_device_delivery_chk",
+ "value": "\"koreader_device_delivery_settings\".\"read_aloud_delivery\" in ('original', 'audioless_epub')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_device_progress": {
+ "name": "koreader_device_progress",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device": {
+ "name": "device",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'KOReader'"
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "progress": {
+ "name": "progress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_index": {
+ "name": "chapter_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_timestamp": {
+ "name": "sync_timestamp",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "orphaned": {
+ "name": "orphaned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "orphaned_hash": {
+ "name": "orphaned_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_device_progress_book_user_device_uidx": {
+ "name": "koreader_device_progress_book_user_device_uidx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"koreader_device_progress\".\"orphaned\" = false",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_device_progress_orphaned_hash_idx": {
+ "name": "koreader_device_progress_orphaned_hash_idx",
+ "columns": [
+ {
+ "expression": "orphaned_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"koreader_device_progress\".\"orphaned\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_device_progress_user_updated_at_idx": {
+ "name": "koreader_device_progress_user_updated_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_device_progress_book_file_id_idx": {
+ "name": "koreader_device_progress_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"koreader_device_progress\".\"book_file_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_device_progress_book_file_id_book_files_id_fk": {
+ "name": "koreader_device_progress_book_file_id_book_files_id_fk",
+ "tableFrom": "koreader_device_progress",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "koreader_device_progress_user_id_users_id_fk": {
+ "name": "koreader_device_progress_user_id_users_id_fk",
+ "tableFrom": "koreader_device_progress",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_device_progress_percentage_range_chk": {
+ "name": "koreader_device_progress_percentage_range_chk",
+ "value": "\"koreader_device_progress\".\"percentage\" is null or (\"koreader_device_progress\".\"percentage\" >= 0 and \"koreader_device_progress\".\"percentage\" <= 1)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_device_retirements": {
+ "name": "koreader_device_retirements",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "retired_at": {
+ "name": "retired_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_device_retirements_user_id_idx": {
+ "name": "koreader_device_retirements_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_device_retirements_user_id_users_id_fk": {
+ "name": "koreader_device_retirements_user_id_users_id_fk",
+ "tableFrom": "koreader_device_retirements",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_device_retirements_user_id_device_id_pk": {
+ "name": "koreader_device_retirements_user_id_device_id_pk",
+ "columns": ["user_id", "device_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_device_settings": {
+ "name": "koreader_device_settings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_naming_pattern": {
+ "name": "file_naming_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "series_file_naming_pattern": {
+ "name": "series_file_naming_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "standalone_file_naming_pattern": {
+ "name": "standalone_file_naming_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_device_settings_user_id_idx": {
+ "name": "koreader_device_settings_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_device_settings_user_id_users_id_fk": {
+ "name": "koreader_device_settings_user_id_users_id_fk",
+ "tableFrom": "koreader_device_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_device_settings_user_id_device_id_pk": {
+ "name": "koreader_device_settings_user_id_device_id_pk",
+ "columns": ["user_id", "device_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_device_sweeps": {
+ "name": "koreader_device_sweeps",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_model": {
+ "name": "device_model",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'KOReader'"
+ },
+ "plugin_version": {
+ "name": "plugin_version",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sweep_at": {
+ "name": "last_sweep_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_sweep_books_matched": {
+ "name": "last_sweep_books_matched",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_sweep_page_stats": {
+ "name": "last_sweep_page_stats",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_sweep_annotations": {
+ "name": "last_sweep_annotations",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kds_user_id_idx": {
+ "name": "kds_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_device_sweeps_user_id_users_id_fk": {
+ "name": "koreader_device_sweeps_user_id_users_id_fk",
+ "tableFrom": "koreader_device_sweeps",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_device_sweeps_user_id_device_id_pk": {
+ "name": "koreader_device_sweeps_user_id_device_id_pk",
+ "columns": ["user_id", "device_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_page_stats": {
+ "name": "koreader_page_stats",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "page": {
+ "name": "page",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_time": {
+ "name": "start_time",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "total_pages": {
+ "name": "total_pages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kps_user_file_device_page_start_uidx": {
+ "name": "kps_user_file_device_page_start_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "page",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "start_time",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kps_user_file_device_start_idx": {
+ "name": "kps_user_file_device_start_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "start_time",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kps_user_id_idx": {
+ "name": "kps_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_page_stats_book_file_id_idx": {
+ "name": "koreader_page_stats_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_page_stats_user_id_users_id_fk": {
+ "name": "koreader_page_stats_user_id_users_id_fk",
+ "tableFrom": "koreader_page_stats",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_page_stats_book_file_id_book_files_id_fk": {
+ "name": "koreader_page_stats_book_file_id_book_files_id_fk",
+ "tableFrom": "koreader_page_stats",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_page_stats_duration_nonnegative_chk": {
+ "name": "koreader_page_stats_duration_nonnegative_chk",
+ "value": "\"koreader_page_stats\".\"duration_seconds\" >= 0"
+ },
+ "koreader_page_stats_page_nonnegative_chk": {
+ "name": "koreader_page_stats_page_nonnegative_chk",
+ "value": "\"koreader_page_stats\".\"page\" >= 0"
+ },
+ "koreader_page_stats_total_pages_positive_chk": {
+ "name": "koreader_page_stats_total_pages_positive_chk",
+ "value": "\"koreader_page_stats\".\"total_pages\" > 0"
+ },
+ "koreader_page_stats_start_time_positive_chk": {
+ "name": "koreader_page_stats_start_time_positive_chk",
+ "value": "\"koreader_page_stats\".\"start_time\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_progress_reset_devices": {
+ "name": "koreader_progress_reset_devices",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "converged_at": {
+ "name": "converged_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "koreader_progress_reset_devices_reset_fk": {
+ "name": "koreader_progress_reset_devices_reset_fk",
+ "tableFrom": "koreader_progress_reset_devices",
+ "tableTo": "koreader_progress_resets",
+ "columnsFrom": ["user_id", "book_file_id"],
+ "columnsTo": ["user_id", "book_file_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_progress_reset_devices_user_id_book_file_id_device_id_pk": {
+ "name": "koreader_progress_reset_devices_user_id_book_file_id_device_id_pk",
+ "columns": ["user_id", "book_file_id", "device_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_progress_resets": {
+ "name": "koreader_progress_resets",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reset_at": {
+ "name": "reset_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_progress_resets_book_file_id_idx": {
+ "name": "koreader_progress_resets_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_progress_resets_user_id_users_id_fk": {
+ "name": "koreader_progress_resets_user_id_users_id_fk",
+ "tableFrom": "koreader_progress_resets",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_progress_resets_book_file_id_book_files_id_fk": {
+ "name": "koreader_progress_resets_book_file_id_book_files_id_fk",
+ "tableFrom": "koreader_progress_resets",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_progress_resets_user_id_book_file_id_pk": {
+ "name": "koreader_progress_resets_user_id_book_file_id_pk",
+ "columns": ["user_id", "book_file_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_session_tombstones": {
+ "name": "koreader_session_tombstones",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_key": {
+ "name": "device_key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "start_time": {
+ "name": "start_time",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_time": {
+ "name": "end_time",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kst_user_file_device_start_idx": {
+ "name": "kst_user_file_device_start_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "start_time",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_session_tombstones_book_file_id_idx": {
+ "name": "koreader_session_tombstones_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_session_tombstones_user_id_users_id_fk": {
+ "name": "koreader_session_tombstones_user_id_users_id_fk",
+ "tableFrom": "koreader_session_tombstones",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_session_tombstones_book_file_id_book_files_id_fk": {
+ "name": "koreader_session_tombstones_book_file_id_book_files_id_fk",
+ "tableFrom": "koreader_session_tombstones",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_session_tombstones_range_chk": {
+ "name": "koreader_session_tombstones_range_chk",
+ "value": "\"koreader_session_tombstones\".\"end_time\" > \"koreader_session_tombstones\".\"start_time\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_unmatched_book_devices": {
+ "name": "koreader_unmatched_book_devices",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_unmatched_book_devices_user_device_idx": {
+ "name": "koreader_unmatched_book_devices_user_device_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_unmatched_book_devices_user_id_users_id_fk": {
+ "name": "koreader_unmatched_book_devices_user_id_users_id_fk",
+ "tableFrom": "koreader_unmatched_book_devices",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "koreader_unmatched_book_devices_user_hash_fk": {
+ "name": "koreader_unmatched_book_devices_user_hash_fk",
+ "tableFrom": "koreader_unmatched_book_devices",
+ "tableTo": "koreader_unmatched_books",
+ "columnsFrom": ["user_id", "hash"],
+ "columnsTo": ["user_id", "hash"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_unmatched_book_devices_user_id_hash_device_id_pk": {
+ "name": "koreader_unmatched_book_devices_user_id_hash_device_id_pk",
+ "columns": ["user_id", "hash", "device_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_unmatched_book_devices_hash_chk": {
+ "name": "koreader_unmatched_book_devices_hash_chk",
+ "value": "\"koreader_unmatched_book_devices\".\"hash\" ~ '^[0-9a-f]{32}$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_unmatched_books": {
+ "name": "koreader_unmatched_books",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hash": {
+ "name": "hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authors": {
+ "name": "authors",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_open": {
+ "name": "last_open",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'statistics'"
+ },
+ "metadata_ambiguous": {
+ "name": "metadata_ambiguous",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "manual_link_requested": {
+ "name": "manual_link_requested",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "first_seen_at": {
+ "name": "first_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_unmatched_books_user_seen_idx": {
+ "name": "koreader_unmatched_books_user_seen_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"last_seen_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_unmatched_books_user_id_users_id_fk": {
+ "name": "koreader_unmatched_books_user_id_users_id_fk",
+ "tableFrom": "koreader_unmatched_books",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "koreader_unmatched_books_user_id_hash_pk": {
+ "name": "koreader_unmatched_books_user_id_hash_pk",
+ "columns": ["user_id", "hash"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_unmatched_books_hash_chk": {
+ "name": "koreader_unmatched_books_hash_chk",
+ "value": "\"koreader_unmatched_books\".\"hash\" ~ '^[0-9a-f]{32}$'"
+ },
+ "koreader_unmatched_books_source_chk": {
+ "name": "koreader_unmatched_books_source_chk",
+ "value": "\"koreader_unmatched_books\".\"source\" in ('current_file', 'file', 'statistics')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_user_settings": {
+ "name": "koreader_user_settings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "default_file_naming_pattern": {
+ "name": "default_file_naming_pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "koreader_user_settings_user_id_users_id_fk": {
+ "name": "koreader_user_settings_user_id_users_id_fk",
+ "tableFrom": "koreader_user_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.koreader_users": {
+ "name": "koreader_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_md5": {
+ "name": "password_md5",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_enabled": {
+ "name": "sync_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "read_aloud_delivery": {
+ "name": "read_aloud_delivery",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hide_audio_only_books": {
+ "name": "hide_audio_only_books",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_users_user_id_uidx": {
+ "name": "koreader_users_user_id_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_users_username_uidx": {
+ "name": "koreader_users_username_uidx",
+ "columns": [
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_users_user_id_users_id_fk": {
+ "name": "koreader_users_user_id_users_id_fk",
+ "tableFrom": "koreader_users",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_users_delivery_chk": {
+ "name": "koreader_users_delivery_chk",
+ "value": "\"koreader_users\".\"read_aloud_delivery\" in ('original', 'audioless_epub')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.achievements": {
+ "name": "achievements",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "group_key": {
+ "name": "group_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tier": {
+ "name": "tier",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon_name": {
+ "name": "icon_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rarity": {
+ "name": "rarity",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "threshold": {
+ "name": "threshold",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hidden": {
+ "name": "hidden",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "achievements_category_sort_idx": {
+ "name": "achievements_category_sort_idx",
+ "columns": [
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "achievements_group_key_idx": {
+ "name": "achievements_group_key_idx",
+ "columns": [
+ {
+ "expression": "group_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "achievements_category_chk": {
+ "name": "achievements_category_chk",
+ "value": "\"achievements\".\"category\" in ('reading', 'library', 'exploration', 'dedication', 'devices')"
+ },
+ "achievements_rarity_chk": {
+ "name": "achievements_rarity_chk",
+ "value": "\"achievements\".\"rarity\" in ('common', 'rare', 'epic', 'legendary')"
+ },
+ "achievements_tier_chk": {
+ "name": "achievements_tier_chk",
+ "value": "\"achievements\".\"tier\" is null or (\"achievements\".\"tier\" >= 1 and \"achievements\".\"tier\" <= 4)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_achievements": {
+ "name": "user_achievements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "achievement_key": {
+ "name": "achievement_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "awarded_at": {
+ "name": "awarded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "context_json": {
+ "name": "context_json",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "celebration_claim_id": {
+ "name": "celebration_claim_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "celebration_claimed_at": {
+ "name": "celebration_claimed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "celebrated_at": {
+ "name": "celebrated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_achievements_user_key_uidx": {
+ "name": "user_achievements_user_key_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "achievement_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_achievements_user_id_idx": {
+ "name": "user_achievements_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_achievements_user_awarded_at_idx": {
+ "name": "user_achievements_user_awarded_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "awarded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_achievements_pending_celebration_idx": {
+ "name": "user_achievements_pending_celebration_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "celebrated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "awarded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_achievements_celebration_claim_uidx": {
+ "name": "user_achievements_celebration_claim_uidx",
+ "columns": [
+ {
+ "expression": "celebration_claim_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_achievements_user_id_users_id_fk": {
+ "name": "user_achievements_user_id_users_id_fk",
+ "tableFrom": "user_achievements",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_achievements_achievement_key_achievements_key_fk": {
+ "name": "user_achievements_achievement_key_achievements_key_fk",
+ "tableFrom": "user_achievements",
+ "tableTo": "achievements",
+ "columnsFrom": ["achievement_key"],
+ "columnsTo": ["key"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.hardcover_book_state": {
+ "name": "hardcover_book_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hardcover_book_id": {
+ "name": "hardcover_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_edition_id": {
+ "name": "hardcover_edition_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_user_book_id": {
+ "name": "hardcover_user_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_read_id": {
+ "name": "hardcover_read_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_method": {
+ "name": "match_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_basis": {
+ "name": "match_basis",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_error": {
+ "name": "match_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_status": {
+ "name": "last_synced_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_progress": {
+ "name": "last_synced_progress",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_rating": {
+ "name": "last_synced_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_started_at": {
+ "name": "last_synced_started_at",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_finished_at": {
+ "name": "last_synced_finished_at",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_error": {
+ "name": "sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_override": {
+ "name": "sync_override",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_excluded": {
+ "name": "sync_excluded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "hardcover_book_state_book_id_idx": {
+ "name": "hardcover_book_state_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "hardcover_book_state_user_id_users_id_fk": {
+ "name": "hardcover_book_state_user_id_users_id_fk",
+ "tableFrom": "hardcover_book_state",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "hardcover_book_state_book_id_books_id_fk": {
+ "name": "hardcover_book_state_book_id_books_id_fk",
+ "tableFrom": "hardcover_book_state",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "hardcover_book_state_user_book_uidx": {
+ "name": "hardcover_book_state_user_book_uidx",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.hardcover_user_settings": {
+ "name": "hardcover_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "api_token": {
+ "name": "api_token",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "book_sync_mode": {
+ "name": "book_sync_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all_eligible'"
+ },
+ "auto_sync_on_status_change": {
+ "name": "auto_sync_on_status_change",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_sync_on_progress_update": {
+ "name": "auto_sync_on_progress_update",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_sync_on_rating_change": {
+ "name": "auto_sync_on_rating_change",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "privacy_setting_id": {
+ "name": "privacy_setting_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "hardcover_user_settings_user_id_users_id_fk": {
+ "name": "hardcover_user_settings_user_id_users_id_fk",
+ "tableFrom": "hardcover_user_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "hardcover_user_settings_user_id_unique": {
+ "name": "hardcover_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.readwise_user_settings": {
+ "name": "readwise_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "api_token": {
+ "name": "api_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_synced_annotation_id": {
+ "name": "last_synced_annotation_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "disabled_reason": {
+ "name": "disabled_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "readwise_user_settings_user_id_users_id_fk": {
+ "name": "readwise_user_settings_user_id_users_id_fk",
+ "tableFrom": "readwise_user_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "readwise_user_settings_user_id_unique": {
+ "name": "readwise_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.read_aloud_job_events": {
+ "name": "read_aloud_job_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "at": {
+ "name": "at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "level": {
+ "name": "level",
+ "type": "varchar(5)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage": {
+ "name": "stage",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(60)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "read_aloud_job_events_job_idx": {
+ "name": "read_aloud_job_events_job_idx",
+ "columns": [
+ {
+ "expression": "job_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "read_aloud_job_events_job_id_read_aloud_jobs_id_fk": {
+ "name": "read_aloud_job_events_job_id_read_aloud_jobs_id_fk",
+ "tableFrom": "read_aloud_job_events",
+ "tableTo": "read_aloud_jobs",
+ "columnsFrom": ["job_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "read_aloud_job_events_level_chk": {
+ "name": "read_aloud_job_events_level_chk",
+ "value": "\"read_aloud_job_events\".\"level\" in ('info', 'warn', 'error')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.read_aloud_job_reports": {
+ "name": "read_aloud_job_reports",
+ "schema": "",
+ "columns": {
+ "job_id": {
+ "name": "job_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "chapters": {
+ "name": "chapters",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "read_aloud_job_reports_job_id_read_aloud_jobs_id_fk": {
+ "name": "read_aloud_job_reports_job_id_read_aloud_jobs_id_fk",
+ "tableFrom": "read_aloud_job_reports",
+ "tableTo": "read_aloud_jobs",
+ "columnsFrom": ["job_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.read_aloud_jobs": {
+ "name": "read_aloud_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "requested_by_user_id": {
+ "name": "requested_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output_file_id": {
+ "name": "output_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replace_file_id": {
+ "name": "replace_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "stage": {
+ "name": "stage",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "progress": {
+ "name": "progress",
+ "type": "smallint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "stage_current": {
+ "name": "stage_current",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stage_total": {
+ "name": "stage_total",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stage_unit": {
+ "name": "stage_unit",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "input": {
+ "name": "input",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "decisions": {
+ "name": "decisions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"chapters\":{},\"audio\":[]}'::jsonb"
+ },
+ "resume_from": {
+ "name": "resume_from",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "report": {
+ "name": "report",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "warnings": {
+ "name": "warnings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "coverage": {
+ "name": "coverage",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "device": {
+ "name": "device",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lease_until": {
+ "name": "lease_until",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_requested": {
+ "name": "cancel_requested",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "publish_approved": {
+ "name": "publish_approved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "worker_event_seq": {
+ "name": "worker_event_seq",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reminded_at": {
+ "name": "reminded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "work_expires_at": {
+ "name": "work_expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "read_aloud_jobs_active_book_uidx": {
+ "name": "read_aloud_jobs_active_book_uidx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"read_aloud_jobs\".\"status\" in ('queued', 'processing', 'awaiting_review')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "read_aloud_jobs_due_idx": {
+ "name": "read_aloud_jobs_due_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "read_aloud_jobs_book_updated_idx": {
+ "name": "read_aloud_jobs_book_updated_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"updated_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "read_aloud_jobs_user_updated_idx": {
+ "name": "read_aloud_jobs_user_updated_idx",
+ "columns": [
+ {
+ "expression": "requested_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"updated_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "read_aloud_jobs_output_file_idx": {
+ "name": "read_aloud_jobs_output_file_idx",
+ "columns": [
+ {
+ "expression": "output_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "read_aloud_jobs_replace_file_idx": {
+ "name": "read_aloud_jobs_replace_file_idx",
+ "columns": [
+ {
+ "expression": "replace_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "read_aloud_jobs_book_id_books_id_fk": {
+ "name": "read_aloud_jobs_book_id_books_id_fk",
+ "tableFrom": "read_aloud_jobs",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "read_aloud_jobs_requested_by_user_id_users_id_fk": {
+ "name": "read_aloud_jobs_requested_by_user_id_users_id_fk",
+ "tableFrom": "read_aloud_jobs",
+ "tableTo": "users",
+ "columnsFrom": ["requested_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "read_aloud_jobs_output_file_id_book_files_id_fk": {
+ "name": "read_aloud_jobs_output_file_id_book_files_id_fk",
+ "tableFrom": "read_aloud_jobs",
+ "tableTo": "book_files",
+ "columnsFrom": ["output_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "read_aloud_jobs_replace_file_id_book_files_id_fk": {
+ "name": "read_aloud_jobs_replace_file_id_book_files_id_fk",
+ "tableFrom": "read_aloud_jobs",
+ "tableTo": "book_files",
+ "columnsFrom": ["replace_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "read_aloud_jobs_status_chk": {
+ "name": "read_aloud_jobs_status_chk",
+ "value": "\"read_aloud_jobs\".\"status\" in ('queued', 'processing', 'awaiting_review', 'completed', 'completed_with_warnings', 'failed', 'cancelled')"
+ },
+ "read_aloud_jobs_stage_chk": {
+ "name": "read_aloud_jobs_stage_chk",
+ "value": "\"read_aloud_jobs\".\"stage\" in ('queued', 'preparing_audio', 'downloading_model', 'generating_emissions', 'marking_up_ebook', 'aligning', 'validating', 'awaiting_review', 'publishing', 'completed')"
+ },
+ "read_aloud_jobs_stage_unit_chk": {
+ "name": "read_aloud_jobs_stage_unit_chk",
+ "value": "\"read_aloud_jobs\".\"stage_unit\" is null or \"read_aloud_jobs\".\"stage_unit\" in ('file', 'track', 'chapter')"
+ },
+ "read_aloud_jobs_progress_chk": {
+ "name": "read_aloud_jobs_progress_chk",
+ "value": "\"read_aloud_jobs\".\"progress\" between 0 and 100"
+ },
+ "read_aloud_jobs_attempt_nonnegative_chk": {
+ "name": "read_aloud_jobs_attempt_nonnegative_chk",
+ "value": "\"read_aloud_jobs\".\"attempt_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.storygraph_book_state": {
+ "name": "storygraph_book_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storygraph_book_id": {
+ "name": "storygraph_book_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_method": {
+ "name": "match_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_error": {
+ "name": "match_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_status": {
+ "name": "last_synced_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_progress": {
+ "name": "last_synced_progress",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_error": {
+ "name": "sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_override": {
+ "name": "sync_override",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "storygraph_book_state_book_id_idx": {
+ "name": "storygraph_book_state_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "storygraph_book_state_user_sync_override_idx": {
+ "name": "storygraph_book_state_user_sync_override_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sync_override",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "storygraph_book_state_user_sync_error_idx": {
+ "name": "storygraph_book_state_user_sync_error_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"storygraph_book_state\".\"sync_error\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "storygraph_book_state_user_id_users_id_fk": {
+ "name": "storygraph_book_state_user_id_users_id_fk",
+ "tableFrom": "storygraph_book_state",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "storygraph_book_state_book_id_books_id_fk": {
+ "name": "storygraph_book_state_book_id_books_id_fk",
+ "tableFrom": "storygraph_book_state",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "storygraph_book_state_user_book_uidx": {
+ "name": "storygraph_book_state_user_book_uidx",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.storygraph_user_settings": {
+ "name": "storygraph_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_cookie": {
+ "name": "session_cookie",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "remember_token": {
+ "name": "remember_token",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "book_sync_mode": {
+ "name": "book_sync_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all_eligible'"
+ },
+ "auto_sync_on_status_change": {
+ "name": "auto_sync_on_status_change",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_sync_on_progress_update": {
+ "name": "auto_sync_on_progress_update",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "storygraph_user_settings_user_id_users_id_fk": {
+ "name": "storygraph_user_settings_user_id_users_id_fk",
+ "tableFrom": "storygraph_user_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "storygraph_user_settings_user_id_unique": {
+ "name": "storygraph_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.shared_reading_insight_view_sessions": {
+ "name": "shared_reading_insight_view_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "viewer_user_id": {
+ "name": "viewer_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_user_id": {
+ "name": "subject_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sharing_level": {
+ "name": "sharing_level",
+ "type": "reading_insights_sharing_level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "shared_insight_view_subject_created_idx": {
+ "name": "shared_insight_view_subject_created_idx",
+ "columns": [
+ {
+ "expression": "subject_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "shared_insight_view_expires_idx": {
+ "name": "shared_insight_view_expires_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "shared_reading_insight_view_sessions_viewer_user_id_users_id_fk": {
+ "name": "shared_reading_insight_view_sessions_viewer_user_id_users_id_fk",
+ "tableFrom": "shared_reading_insight_view_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["viewer_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "shared_reading_insight_view_sessions_subject_user_id_users_id_fk": {
+ "name": "shared_reading_insight_view_sessions_subject_user_id_users_id_fk",
+ "tableFrom": "shared_reading_insight_view_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["subject_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_preferences": {
+ "name": "user_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "up_user_category_idx": {
+ "name": "up_user_category_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_preferences_user_id_users_id_fk": {
+ "name": "user_preferences_user_id_users_id_fk",
+ "tableFrom": "user_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tts_book_preferences": {
+ "name": "tts_book_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "voice_id": {
+ "name": "voice_id",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "speed": {
+ "name": "speed",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tts_book_prefs_user_book_uidx": {
+ "name": "tts_book_prefs_user_book_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tts_book_preferences_user_id_users_id_fk": {
+ "name": "tts_book_preferences_user_id_users_id_fk",
+ "tableFrom": "tts_book_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tts_book_preferences_book_id_books_id_fk": {
+ "name": "tts_book_preferences_book_id_books_id_fk",
+ "tableFrom": "tts_book_preferences",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tts_book_preferences_provider_id_tts_providers_id_fk": {
+ "name": "tts_book_preferences_provider_id_tts_providers_id_fk",
+ "tableFrom": "tts_book_preferences",
+ "tableTo": "tts_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tts_providers": {
+ "name": "tts_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "base_url": {
+ "name": "base_url",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "api_key": {
+ "name": "api_key",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_model": {
+ "name": "default_model",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "static_voices": {
+ "name": "static_voices",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supports_voice_discovery": {
+ "name": "supports_voice_discovery",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tts_reading_position": {
+ "name": "tts_reading_position",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chapter_index": {
+ "name": "chapter_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tts_reading_pos_user_file_uidx": {
+ "name": "tts_reading_pos_user_file_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tts_reading_position_user_id_users_id_fk": {
+ "name": "tts_reading_position_user_id_users_id_fk",
+ "tableFrom": "tts_reading_position",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tts_reading_position_book_file_id_book_files_id_fk": {
+ "name": "tts_reading_position_book_file_id_book_files_id_fk",
+ "tableFrom": "tts_reading_position",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tts_user_preferences": {
+ "name": "tts_user_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "voice_id": {
+ "name": "voice_id",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "speed": {
+ "name": "speed",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tts_user_prefs_user_uidx": {
+ "name": "tts_user_prefs_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tts_user_preferences_user_id_users_id_fk": {
+ "name": "tts_user_preferences_user_id_users_id_fk",
+ "tableFrom": "tts_user_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tts_user_preferences_provider_id_tts_providers_id_fk": {
+ "name": "tts_user_preferences_provider_id_tts_providers_id_fk",
+ "tableFrom": "tts_user_preferences",
+ "tableTo": "tts_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.upload_sessions": {
+ "name": "upload_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_kind": {
+ "name": "target_kind",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_library_id": {
+ "name": "target_library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_folder_id": {
+ "name": "target_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_book_id": {
+ "name": "target_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "received_bytes": {
+ "name": "received_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "staging_path": {
+ "name": "staging_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'receiving'"
+ },
+ "expected_sha256": {
+ "name": "expected_sha256",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "varchar(80)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result_book_id": {
+ "name": "result_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result_book_dock_file_id": {
+ "name": "result_book_dock_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "upload_sessions_user_idempotency_uidx": {
+ "name": "upload_sessions_user_idempotency_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "idempotency_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "upload_sessions_user_created_idx": {
+ "name": "upload_sessions_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"created_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "upload_sessions_status_expires_idx": {
+ "name": "upload_sessions_status_expires_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "upload_sessions_user_id_users_id_fk": {
+ "name": "upload_sessions_user_id_users_id_fk",
+ "tableFrom": "upload_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "upload_sessions_target_library_id_libraries_id_fk": {
+ "name": "upload_sessions_target_library_id_libraries_id_fk",
+ "tableFrom": "upload_sessions",
+ "tableTo": "libraries",
+ "columnsFrom": ["target_library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "upload_sessions_target_folder_id_library_folders_id_fk": {
+ "name": "upload_sessions_target_folder_id_library_folders_id_fk",
+ "tableFrom": "upload_sessions",
+ "tableTo": "library_folders",
+ "columnsFrom": ["target_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "upload_sessions_target_book_id_books_id_fk": {
+ "name": "upload_sessions_target_book_id_books_id_fk",
+ "tableFrom": "upload_sessions",
+ "tableTo": "books",
+ "columnsFrom": ["target_book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "upload_sessions_result_book_id_books_id_fk": {
+ "name": "upload_sessions_result_book_id_books_id_fk",
+ "tableFrom": "upload_sessions",
+ "tableTo": "books",
+ "columnsFrom": ["result_book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "upload_sessions_staging_path_unique": {
+ "name": "upload_sessions_staging_path_unique",
+ "nullsNotDistinct": false,
+ "columns": ["staging_path"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "upload_sessions_target_kind_chk": {
+ "name": "upload_sessions_target_kind_chk",
+ "value": "\"upload_sessions\".\"target_kind\" in ('library', 'existing_book', 'book_dock')"
+ },
+ "upload_sessions_status_chk": {
+ "name": "upload_sessions_status_chk",
+ "value": "\"upload_sessions\".\"status\" in ('receiving', 'processing', 'completed', 'failed', 'cancelled', 'expired')"
+ },
+ "upload_sessions_size_positive_chk": {
+ "name": "upload_sessions_size_positive_chk",
+ "value": "\"upload_sessions\".\"size_bytes\" > 0"
+ },
+ "upload_sessions_received_range_chk": {
+ "name": "upload_sessions_received_range_chk",
+ "value": "\"upload_sessions\".\"received_bytes\" >= 0 and \"upload_sessions\".\"received_bytes\" <= \"upload_sessions\".\"size_bytes\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcast_bookmarks": {
+ "name": "podcast_bookmarks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "episode_id": {
+ "name": "episode_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcast_bookmarks_user_episode_idx": {
+ "name": "podcast_bookmarks_user_episode_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "episode_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_bookmarks_user_episode_position_idx": {
+ "name": "podcast_bookmarks_user_episode_position_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "episode_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position_seconds",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcast_bookmarks_user_id_users_id_fk": {
+ "name": "podcast_bookmarks_user_id_users_id_fk",
+ "tableFrom": "podcast_bookmarks",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "podcast_bookmarks_episode_id_podcast_episodes_id_fk": {
+ "name": "podcast_bookmarks_episode_id_podcast_episodes_id_fk",
+ "tableFrom": "podcast_bookmarks",
+ "tableTo": "podcast_episodes",
+ "columnsFrom": ["episode_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcast_bookmarks_position_nonnegative_chk": {
+ "name": "podcast_bookmarks_position_nonnegative_chk",
+ "value": "\"podcast_bookmarks\".\"position_seconds\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcast_episode_media": {
+ "name": "podcast_episode_media",
+ "schema": "",
+ "columns": {
+ "episode_id": {
+ "name": "episode_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'remote'"
+ },
+ "local_path": {
+ "name": "local_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checksum": {
+ "name": "checksum",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "downloaded_at": {
+ "name": "downloaded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_remote_validated_at": {
+ "name": "last_remote_validated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcast_episode_media_status_idx": {
+ "name": "podcast_episode_media_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episode_media_downloaded_idx": {
+ "name": "podcast_episode_media_downloaded_idx",
+ "columns": [
+ {
+ "expression": "downloaded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcast_episode_media_episode_id_podcast_episodes_id_fk": {
+ "name": "podcast_episode_media_episode_id_podcast_episodes_id_fk",
+ "tableFrom": "podcast_episode_media",
+ "tableTo": "podcast_episodes",
+ "columnsFrom": ["episode_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcast_episode_media_status_chk": {
+ "name": "podcast_episode_media_status_chk",
+ "value": "\"podcast_episode_media\".\"status\" in ('remote', 'queued', 'downloading', 'local', 'failed', 'unavailable')"
+ },
+ "podcast_episode_media_size_nonnegative_chk": {
+ "name": "podcast_episode_media_size_nonnegative_chk",
+ "value": "\"podcast_episode_media\".\"size_bytes\" is null or \"podcast_episode_media\".\"size_bytes\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcast_episodes": {
+ "name": "podcast_episodes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "podcast_id": {
+ "name": "podcast_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identity_hash": {
+ "name": "identity_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'feed'"
+ },
+ "guid": {
+ "name": "guid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subtitle": {
+ "name": "subtitle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "season": {
+ "name": "season",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "episode": {
+ "name": "episode",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "episode_type": {
+ "name": "episode_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "explicit": {
+ "name": "explicit",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enclosure_url_encrypted": {
+ "name": "enclosure_url_encrypted",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enclosure_url_hash": {
+ "name": "enclosure_url_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enclosure_type": {
+ "name": "enclosure_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enclosure_size_bytes": {
+ "name": "enclosure_size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapters": {
+ "name": "chapters",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "transcripts": {
+ "name": "transcripts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "locked_fields": {
+ "name": "locked_fields",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "in_feed": {
+ "name": "in_feed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "unavailable_at": {
+ "name": "unavailable_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcast_episodes_podcast_identity_uidx": {
+ "name": "podcast_episodes_podcast_identity_uidx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "identity_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episodes_podcast_id_idx": {
+ "name": "podcast_episodes_podcast_id_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episodes_podcast_created_idx": {
+ "name": "podcast_episodes_podcast_created_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episodes_podcast_published_id_idx": {
+ "name": "podcast_episodes_podcast_published_id_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"published_at\" desc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"id\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episodes_title_trgm_idx": {
+ "name": "podcast_episodes_title_trgm_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "podcast_episodes_duration_idx": {
+ "name": "podcast_episodes_duration_idx",
+ "columns": [
+ {
+ "expression": "duration_seconds",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episodes_enclosure_hash_idx": {
+ "name": "podcast_episodes_enclosure_hash_idx",
+ "columns": [
+ {
+ "expression": "enclosure_url_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_episodes_in_feed_idx": {
+ "name": "podcast_episodes_in_feed_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "in_feed",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcast_episodes_podcast_id_podcasts_id_fk": {
+ "name": "podcast_episodes_podcast_id_podcasts_id_fk",
+ "tableFrom": "podcast_episodes",
+ "tableTo": "podcasts",
+ "columnsFrom": ["podcast_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcast_episodes_origin_chk": {
+ "name": "podcast_episodes_origin_chk",
+ "value": "\"podcast_episodes\".\"origin\" in ('feed', 'local')"
+ },
+ "podcast_episodes_feed_origin_requires_enclosure_chk": {
+ "name": "podcast_episodes_feed_origin_requires_enclosure_chk",
+ "value": "\"podcast_episodes\".\"origin\" = 'local' or \"podcast_episodes\".\"enclosure_url_hash\" is not null"
+ },
+ "podcast_episodes_duration_nonnegative_chk": {
+ "name": "podcast_episodes_duration_nonnegative_chk",
+ "value": "\"podcast_episodes\".\"duration_seconds\" is null or \"podcast_episodes\".\"duration_seconds\" >= 0"
+ },
+ "podcast_episodes_size_nonnegative_chk": {
+ "name": "podcast_episodes_size_nonnegative_chk",
+ "value": "\"podcast_episodes\".\"enclosure_size_bytes\" is null or \"podcast_episodes\".\"enclosure_size_bytes\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcast_feed_aliases": {
+ "name": "podcast_feed_aliases",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "podcast_id": {
+ "name": "podcast_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url_hash": {
+ "name": "url_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url_encrypted": {
+ "name": "url_encrypted",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcast_feed_aliases_podcast_hash_uidx": {
+ "name": "podcast_feed_aliases_podcast_hash_uidx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "url_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_feed_aliases_hash_idx": {
+ "name": "podcast_feed_aliases_hash_idx",
+ "columns": [
+ {
+ "expression": "url_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcast_feed_aliases_podcast_id_podcasts_id_fk": {
+ "name": "podcast_feed_aliases_podcast_id_podcasts_id_fk",
+ "tableFrom": "podcast_feed_aliases",
+ "tableTo": "podcasts",
+ "columnsFrom": ["podcast_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.podcast_jobs": {
+ "name": "podcast_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dedupe_key": {
+ "name": "dedupe_key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "podcast_id": {
+ "name": "podcast_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "episode_id": {
+ "name": "episode_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_by_user_id": {
+ "name": "requested_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "download_batch_id": {
+ "name": "download_batch_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "progress_current": {
+ "name": "progress_current",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "progress_total": {
+ "name": "progress_total",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lease_until": {
+ "name": "lease_until",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancel_requested": {
+ "name": "cancel_requested",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcast_jobs_active_dedupe_uidx": {
+ "name": "podcast_jobs_active_dedupe_uidx",
+ "columns": [
+ {
+ "expression": "dedupe_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"podcast_jobs\".\"status\" in ('queued', 'processing')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_jobs_due_idx": {
+ "name": "podcast_jobs_due_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_jobs_library_status_idx": {
+ "name": "podcast_jobs_library_status_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_jobs_status_updated_idx": {
+ "name": "podcast_jobs_status_updated_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_jobs_download_batch_idx": {
+ "name": "podcast_jobs_download_batch_idx",
+ "columns": [
+ {
+ "expression": "requested_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "download_batch_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"podcast_jobs\".\"type\" = 'download' and \"podcast_jobs\".\"download_batch_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_jobs_active_podcast_type_idx": {
+ "name": "podcast_jobs_active_podcast_type_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "requested_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"podcast_jobs\".\"status\" in ('queued', 'processing') and \"podcast_jobs\".\"podcast_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_jobs_active_episode_type_idx": {
+ "name": "podcast_jobs_active_episode_type_idx",
+ "columns": [
+ {
+ "expression": "episode_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"podcast_jobs\".\"status\" in ('queued', 'processing') and \"podcast_jobs\".\"episode_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcast_jobs_library_id_libraries_id_fk": {
+ "name": "podcast_jobs_library_id_libraries_id_fk",
+ "tableFrom": "podcast_jobs",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "podcast_jobs_podcast_id_podcasts_id_fk": {
+ "name": "podcast_jobs_podcast_id_podcasts_id_fk",
+ "tableFrom": "podcast_jobs",
+ "tableTo": "podcasts",
+ "columnsFrom": ["podcast_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "podcast_jobs_episode_id_podcast_episodes_id_fk": {
+ "name": "podcast_jobs_episode_id_podcast_episodes_id_fk",
+ "tableFrom": "podcast_jobs",
+ "tableTo": "podcast_episodes",
+ "columnsFrom": ["episode_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "podcast_jobs_requested_by_user_id_users_id_fk": {
+ "name": "podcast_jobs_requested_by_user_id_users_id_fk",
+ "tableFrom": "podcast_jobs",
+ "tableTo": "users",
+ "columnsFrom": ["requested_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcast_jobs_type_chk": {
+ "name": "podcast_jobs_type_chk",
+ "value": "\"podcast_jobs\".\"type\" in ('refresh', 'reparse', 'download', 'retention', 'purge', 'opml_import', 'import_scan', 'local_import', 'merge', 'digest', 'file_cleanup')"
+ },
+ "podcast_jobs_status_chk": {
+ "name": "podcast_jobs_status_chk",
+ "value": "\"podcast_jobs\".\"status\" in ('queued', 'processing', 'completed', 'failed', 'cancelled')"
+ },
+ "podcast_jobs_attempt_nonnegative_chk": {
+ "name": "podcast_jobs_attempt_nonnegative_chk",
+ "value": "\"podcast_jobs\".\"attempt_count\" >= 0"
+ },
+ "podcast_jobs_progress_nonnegative_chk": {
+ "name": "podcast_jobs_progress_nonnegative_chk",
+ "value": "\"podcast_jobs\".\"progress_current\" >= 0 and (\"podcast_jobs\".\"progress_total\" is null or \"podcast_jobs\".\"progress_total\" >= 0)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcast_library_settings": {
+ "name": "podcast_library_settings",
+ "schema": "",
+ "columns": {
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "storage_quota_bytes": {
+ "name": "storage_quota_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "107374182400"
+ },
+ "minimum_free_space_bytes": {
+ "name": "minimum_free_space_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "5368709120"
+ },
+ "default_refresh_interval_minutes": {
+ "name": "default_refresh_interval_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 60
+ },
+ "completion_remaining_seconds": {
+ "name": "completion_remaining_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 60
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "podcast_library_settings_library_id_libraries_id_fk": {
+ "name": "podcast_library_settings_library_id_libraries_id_fk",
+ "tableFrom": "podcast_library_settings",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcast_library_storage_quota_nonnegative_chk": {
+ "name": "podcast_library_storage_quota_nonnegative_chk",
+ "value": "\"podcast_library_settings\".\"storage_quota_bytes\" >= 0"
+ },
+ "podcast_library_free_space_nonnegative_chk": {
+ "name": "podcast_library_free_space_nonnegative_chk",
+ "value": "\"podcast_library_settings\".\"minimum_free_space_bytes\" >= 0"
+ },
+ "podcast_library_refresh_positive_chk": {
+ "name": "podcast_library_refresh_positive_chk",
+ "value": "\"podcast_library_settings\".\"default_refresh_interval_minutes\" > 0"
+ },
+ "podcast_library_completion_nonnegative_chk": {
+ "name": "podcast_library_completion_nonnegative_chk",
+ "value": "\"podcast_library_settings\".\"completion_remaining_seconds\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcast_listening_sessions": {
+ "name": "podcast_listening_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "episode_id": {
+ "name": "episode_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "end_position_seconds": {
+ "name": "end_position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcast_sessions_user_session_uidx": {
+ "name": "podcast_sessions_user_session_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_sessions_user_started_idx": {
+ "name": "podcast_sessions_user_started_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcast_sessions_episode_started_idx": {
+ "name": "podcast_sessions_episode_started_idx",
+ "columns": [
+ {
+ "expression": "episode_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcast_listening_sessions_user_id_users_id_fk": {
+ "name": "podcast_listening_sessions_user_id_users_id_fk",
+ "tableFrom": "podcast_listening_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "podcast_listening_sessions_episode_id_podcast_episodes_id_fk": {
+ "name": "podcast_listening_sessions_episode_id_podcast_episodes_id_fk",
+ "tableFrom": "podcast_listening_sessions",
+ "tableTo": "podcast_episodes",
+ "columnsFrom": ["episode_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcast_sessions_duration_nonnegative_chk": {
+ "name": "podcast_sessions_duration_nonnegative_chk",
+ "value": "\"podcast_listening_sessions\".\"duration_seconds\" >= 0"
+ },
+ "podcast_sessions_position_nonnegative_chk": {
+ "name": "podcast_sessions_position_nonnegative_chk",
+ "value": "\"podcast_listening_sessions\".\"end_position_seconds\" >= 0"
+ },
+ "podcast_sessions_time_order_chk": {
+ "name": "podcast_sessions_time_order_chk",
+ "value": "\"podcast_listening_sessions\".\"ended_at\" >= \"podcast_listening_sessions\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.podcasts": {
+ "name": "podcasts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author": {
+ "name": "author",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'feed'"
+ },
+ "feed_url_encrypted": {
+ "name": "feed_url_encrypted",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "feed_url_hash": {
+ "name": "feed_url_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "local_folder_path": {
+ "name": "local_folder_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image_url_encrypted": {
+ "name": "image_url_encrypted",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "podcast_type": {
+ "name": "podcast_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "explicit": {
+ "name": "explicit",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "acquisition_policy": {
+ "name": "acquisition_policy",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'remote_only'"
+ },
+ "auto_download_limit": {
+ "name": "auto_download_limit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auto_download_window_days": {
+ "name": "auto_download_window_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "download_cleanup": {
+ "name": "download_cleanup",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'keep'"
+ },
+ "download_cleanup_delay_hours": {
+ "name": "download_cleanup_delay_hours",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 24
+ },
+ "refresh_interval_minutes": {
+ "name": "refresh_interval_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 60
+ },
+ "next_refresh_at": {
+ "name": "next_refresh_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_refresh_at": {
+ "name": "last_refresh_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_refresh_success_at": {
+ "name": "last_refresh_success_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consecutive_failures": {
+ "name": "consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_http_status": {
+ "name": "last_http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "etag": {
+ "name": "etag",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified": {
+ "name": "last_modified",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_fields": {
+ "name": "locked_fields",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "custom_artwork_at": {
+ "name": "custom_artwork_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "feed_snapshot_at": {
+ "name": "feed_snapshot_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "missing_at": {
+ "name": "missing_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "podcasts_library_feed_hash_uidx": {
+ "name": "podcasts_library_feed_hash_uidx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "feed_url_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"podcasts\".\"feed_url_hash\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcasts_library_local_folder_uidx": {
+ "name": "podcasts_library_local_folder_uidx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "local_folder_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"podcasts\".\"local_folder_path\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcasts_library_archived_title_idx": {
+ "name": "podcasts_library_archived_title_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcasts_title_trgm_idx": {
+ "name": "podcasts_title_trgm_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "podcasts_author_trgm_idx": {
+ "name": "podcasts_author_trgm_idx",
+ "columns": [
+ {
+ "expression": "author",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "podcasts_due_refresh_idx": {
+ "name": "podcasts_due_refresh_idx",
+ "columns": [
+ {
+ "expression": "next_refresh_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"podcasts\".\"archived_at\" is null and \"podcasts\".\"origin\" = 'feed'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "podcasts_library_cleanup_idx": {
+ "name": "podcasts_library_cleanup_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"podcasts\".\"download_cleanup\" <> 'keep'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "podcasts_library_id_libraries_id_fk": {
+ "name": "podcasts_library_id_libraries_id_fk",
+ "tableFrom": "podcasts",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "podcasts_origin_chk": {
+ "name": "podcasts_origin_chk",
+ "value": "\"podcasts\".\"origin\" in ('feed', 'local')"
+ },
+ "podcasts_feed_origin_requires_feed_chk": {
+ "name": "podcasts_feed_origin_requires_feed_chk",
+ "value": "\"podcasts\".\"origin\" = 'local' or \"podcasts\".\"feed_url_hash\" is not null"
+ },
+ "podcasts_acquisition_policy_chk": {
+ "name": "podcasts_acquisition_policy_chk",
+ "value": "\"podcasts\".\"acquisition_policy\" in ('remote_only', 'manual', 'newest', 'window')"
+ },
+ "podcasts_download_cleanup_chk": {
+ "name": "podcasts_download_cleanup_chk",
+ "value": "\"podcasts\".\"download_cleanup\" in ('keep', 'after_finished')"
+ },
+ "podcasts_download_cleanup_delay_chk": {
+ "name": "podcasts_download_cleanup_delay_chk",
+ "value": "\"podcasts\".\"download_cleanup_delay_hours\" >= 0"
+ },
+ "podcasts_refresh_interval_positive_chk": {
+ "name": "podcasts_refresh_interval_positive_chk",
+ "value": "\"podcasts\".\"refresh_interval_minutes\" > 0"
+ },
+ "podcasts_failures_nonnegative_chk": {
+ "name": "podcasts_failures_nonnegative_chk",
+ "value": "\"podcasts\".\"consecutive_failures\" >= 0"
+ },
+ "podcasts_auto_download_limit_chk": {
+ "name": "podcasts_auto_download_limit_chk",
+ "value": "\"podcasts\".\"auto_download_limit\" is null or \"podcasts\".\"auto_download_limit\" > 0"
+ },
+ "podcasts_auto_download_window_chk": {
+ "name": "podcasts_auto_download_window_chk",
+ "value": "\"podcasts\".\"auto_download_window_days\" is null or \"podcasts\".\"auto_download_window_days\" > 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_podcast_episode_state": {
+ "name": "user_podcast_episode_state",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "episode_id": {
+ "name": "episode_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "progress_percent": {
+ "name": "progress_percent",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "finished": {
+ "name": "finished",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pinned": {
+ "name": "pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "last_listened_at": {
+ "name": "last_listened_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_podcast_episode_state_user_finished_idx": {
+ "name": "user_podcast_episode_state_user_finished_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "finished",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"updated_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_podcast_episode_state_episode_active_idx": {
+ "name": "user_podcast_episode_state_episode_active_idx",
+ "columns": [
+ {
+ "expression": "episode_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "finished",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position_seconds",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_podcast_episode_state_user_id_users_id_fk": {
+ "name": "user_podcast_episode_state_user_id_users_id_fk",
+ "tableFrom": "user_podcast_episode_state",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_podcast_episode_state_episode_id_podcast_episodes_id_fk": {
+ "name": "user_podcast_episode_state_episode_id_podcast_episodes_id_fk",
+ "tableFrom": "user_podcast_episode_state",
+ "tableTo": "podcast_episodes",
+ "columnsFrom": ["episode_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_podcast_episode_state_user_id_episode_id_pk": {
+ "name": "user_podcast_episode_state_user_id_episode_id_pk",
+ "columns": ["user_id", "episode_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_podcast_episode_state_position_chk": {
+ "name": "user_podcast_episode_state_position_chk",
+ "value": "\"user_podcast_episode_state\".\"position_seconds\" >= 0"
+ },
+ "user_podcast_episode_state_progress_chk": {
+ "name": "user_podcast_episode_state_progress_chk",
+ "value": "\"user_podcast_episode_state\".\"progress_percent\" >= 0 and \"user_podcast_episode_state\".\"progress_percent\" <= 100"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_podcast_follows": {
+ "name": "user_podcast_follows",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "podcast_id": {
+ "name": "podcast_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "notification_mode": {
+ "name": "notification_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'off'"
+ },
+ "last_notified_at": {
+ "name": "last_notified_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_podcast_follows_podcast_idx": {
+ "name": "user_podcast_follows_podcast_idx",
+ "columns": [
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_podcast_follows_due_digest_idx": {
+ "name": "user_podcast_follows_due_digest_idx",
+ "columns": [
+ {
+ "expression": "notification_mode",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_notified_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "podcast_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"user_podcast_follows\".\"notification_mode\" in ('daily', 'weekly')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_podcast_follows_user_id_users_id_fk": {
+ "name": "user_podcast_follows_user_id_users_id_fk",
+ "tableFrom": "user_podcast_follows",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_podcast_follows_podcast_id_podcasts_id_fk": {
+ "name": "user_podcast_follows_podcast_id_podcasts_id_fk",
+ "tableFrom": "user_podcast_follows",
+ "tableTo": "podcasts",
+ "columnsFrom": ["podcast_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_podcast_follows_user_id_podcast_id_pk": {
+ "name": "user_podcast_follows_user_id_podcast_id_pk",
+ "columns": ["user_id", "podcast_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_podcast_follows_notification_chk": {
+ "name": "user_podcast_follows_notification_chk",
+ "value": "\"user_podcast_follows\".\"notification_mode\" in ('off', 'immediate', 'daily', 'weekly')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_podcast_queue": {
+ "name": "user_podcast_queue",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "episode_id": {
+ "name": "episode_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_podcast_queue_user_position_uidx": {
+ "name": "user_podcast_queue_user_position_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_podcast_queue_user_id_users_id_fk": {
+ "name": "user_podcast_queue_user_id_users_id_fk",
+ "tableFrom": "user_podcast_queue",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_podcast_queue_episode_id_podcast_episodes_id_fk": {
+ "name": "user_podcast_queue_episode_id_podcast_episodes_id_fk",
+ "tableFrom": "user_podcast_queue",
+ "tableTo": "podcast_episodes",
+ "columnsFrom": ["episode_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_podcast_queue_user_id_episode_id_pk": {
+ "name": "user_podcast_queue_user_id_episode_id_pk",
+ "columns": ["user_id", "episode_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_podcast_queue_position_nonnegative_chk": {
+ "name": "user_podcast_queue_position_nonnegative_chk",
+ "value": "\"user_podcast_queue\".\"position\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.provider_metadata_cache": {
+ "name": "provider_metadata_cache",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key_hash": {
+ "name": "key_hash",
+ "type": "char(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "namespace": {
+ "name": "namespace",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "operation": {
+ "name": "operation",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema_version": {
+ "name": "schema_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_version": {
+ "name": "provider_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config_fingerprint": {
+ "name": "config_fingerprint",
+ "type": "char(16)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "max_candidates": {
+ "name": "max_candidates",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "covers_validated": {
+ "name": "covers_validated",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload_bytes": {
+ "name": "payload_bytes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fetched_at": {
+ "name": "fetched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_accessed_at": {
+ "name": "last_accessed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "upstream_ms": {
+ "name": "upstream_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "provider_metadata_cache_ns_provider_fetched_idx": {
+ "name": "provider_metadata_cache_ns_provider_fetched_idx",
+ "columns": [
+ {
+ "expression": "namespace",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "fetched_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "provider_metadata_cache_last_accessed_idx": {
+ "name": "provider_metadata_cache_last_accessed_idx",
+ "columns": [
+ {
+ "expression": "last_accessed_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "provider_metadata_cache_key_hash_unique": {
+ "name": "provider_metadata_cache_key_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "provider_metadata_cache_namespace_chk": {
+ "name": "provider_metadata_cache_namespace_chk",
+ "value": "\"provider_metadata_cache\".\"namespace\" in ('book', 'author')"
+ },
+ "provider_metadata_cache_operation_chk": {
+ "name": "provider_metadata_cache_operation_chk",
+ "value": "\"provider_metadata_cache\".\"operation\" in ('search', 'lookup')"
+ },
+ "provider_metadata_cache_status_chk": {
+ "name": "provider_metadata_cache_status_chk",
+ "value": "\"provider_metadata_cache\".\"status\" in ('results', 'empty', 'found', 'not_found')"
+ },
+ "provider_metadata_cache_payload_bytes_nonnegative_chk": {
+ "name": "provider_metadata_cache_payload_bytes_nonnegative_chk",
+ "value": "\"provider_metadata_cache\".\"payload_bytes\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.delivery_artifacts": {
+ "name": "delivery_artifacts",
+ "schema": "",
+ "columns": {
+ "selection_id": {
+ "name": "selection_id",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "delivery_artifacts_owner_file_idx": {
+ "name": "delivery_artifacts_owner_file_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "delivery_artifacts_user_id_users_id_fk": {
+ "name": "delivery_artifacts_user_id_users_id_fk",
+ "tableFrom": "delivery_artifacts",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "delivery_artifacts_book_file_id_book_files_id_fk": {
+ "name": "delivery_artifacts_book_file_id_book_files_id_fk",
+ "tableFrom": "delivery_artifacts",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.delivery_inspections": {
+ "name": "delivery_inspections",
+ "schema": "",
+ "columns": {
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_revision": {
+ "name": "source_revision",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "detector_version": {
+ "name": "detector_version",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(24)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "retry_at": {
+ "name": "retry_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checked_at": {
+ "name": "checked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "delivery_inspections_book_file_id_book_files_id_fk": {
+ "name": "delivery_inspections_book_file_id_book_files_id_fk",
+ "tableFrom": "delivery_inspections",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.server_identity": {
+ "name": "server_identity",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true,
+ "default": 1
+ },
+ "instance_id": {
+ "name": "instance_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "server_identity_singleton": {
+ "name": "server_identity_singleton",
+ "value": "\"server_identity\".\"id\" = 1"
+ }
+ },
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.content_filter_type": {
+ "name": "content_filter_type",
+ "schema": "public",
+ "values": ["include", "exclude"]
+ },
+ "public.library_access_level": {
+ "name": "library_access_level",
+ "schema": "public",
+ "values": ["viewer", "editor", "owner"]
+ },
+ "public.reading_insights_sharing_level": {
+ "name": "reading_insights_sharing_level",
+ "schema": "public",
+ "values": ["private", "summary", "detailed"]
+ },
+ "public.user_avatar_source": {
+ "name": "user_avatar_source",
+ "schema": "public",
+ "values": ["none", "external", "uploaded"]
+ },
+ "public.opds_sort_order": {
+ "name": "opds_sort_order",
+ "schema": "public",
+ "values": ["recent", "title_asc", "title_desc", "author_asc", "author_desc", "series_asc", "series_desc"]
+ },
+ "public.book_request_media_kind": {
+ "name": "book_request_media_kind",
+ "schema": "public",
+ "values": ["ebook", "audiobook", "comic"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/server/src/db/migrations/meta/_journal.json b/server/src/db/migrations/meta/_journal.json
index b3a997624..e921a39ae 100644
--- a/server/src/db/migrations/meta/_journal.json
+++ b/server/src/db/migrations/meta/_journal.json
@@ -890,6 +890,13 @@
"when": 1792562395825,
"tag": "0126_add_read_aloud_job_reports",
"breakpoints": true
+ },
+ {
+ "idx": 127,
+ "version": "7",
+ "when": 1792639172417,
+ "tag": "0127_add_audiobook_chapter_editor",
+ "breakpoints": true
}
]
}
diff --git a/server/src/db/preserve-audiobook-revision.ts b/server/src/db/preserve-audiobook-revision.ts
new file mode 100644
index 000000000..5645ed39e
--- /dev/null
+++ b/server/src/db/preserve-audiobook-revision.ts
@@ -0,0 +1,105 @@
+import { ConflictException } from '@nestjs/common';
+import type { AudiobookChapter } from '@bookorbit/types';
+import { and, eq, desc, notInArray } from 'drizzle-orm';
+import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
+import * as schema from './schema';
+import {
+ computeAudiobookManifestRevision,
+ computeAudiobookPositionsKey,
+ normalizeAudiobookChapters,
+ orderAudiobookTracks,
+ resolveStoredAudiobookChapters,
+} from '../common/utils/audiobook-chapters.utils';
+
+type Executor = Pick
, 'select' | 'insert' | 'update' | 'delete'>;
+
+/** Must be called in the transaction that writes chapters. The metadata row stays locked until commit. */
+export async function preserveOutgoingChapters(
+ executor: Executor,
+ bookId: number,
+ patch: Partial,
+ respectLocks: boolean,
+): Promise> {
+ if (!Object.prototype.hasOwnProperty.call(patch, 'chapters')) return patch;
+ await executor.insert(schema.bookMetadata).values({ bookId }).onConflictDoNothing();
+ const [state] = await executor
+ .select({
+ chapters: schema.bookMetadata.chapters,
+ source: schema.bookMetadata.chaptersSource,
+ locked: schema.bookMetadata.lockedFields,
+ recorded: schema.bookMetadata.manifestLegacyRecorded,
+ })
+ .from(schema.bookMetadata)
+ .where(eq(schema.bookMetadata.bookId, bookId))
+ .for('update');
+ const next = { ...patch };
+ if (state.locked.includes('chapters')) {
+ if (!respectLocks) throw new ConflictException('Chapters are protected');
+ delete next.chapters;
+ delete next.chaptersSource;
+ delete next.chaptersUpdatedAt;
+ delete next.chaptersUpdatedBy;
+ return next;
+ }
+ if (!state.recorded) {
+ const files = await executor
+ .select({
+ id: schema.bookFiles.id,
+ publicId: schema.bookFiles.publicId,
+ absolutePath: schema.bookFiles.absolutePath,
+ format: schema.bookFiles.format,
+ durationSeconds: schema.bookFiles.durationSeconds,
+ sortOrder: schema.bookFiles.sortOrder,
+ sizeBytes: schema.bookFiles.sizeBytes,
+ mtime: schema.bookFiles.mtime,
+ updatedAt: schema.bookFiles.updatedAt,
+ })
+ .from(schema.bookFiles)
+ .where(eq(schema.bookFiles.bookId, bookId));
+ const ordered = orderAudiobookTracks(files);
+ if (ordered.length)
+ await executor
+ .insert(schema.audiobookManifestRevisions)
+ .values({
+ bookId,
+ revision: computeAudiobookManifestRevision(
+ ordered,
+ resolveStoredAudiobookChapters(state.chapters as AudiobookChapter[] | null, files) ?? [],
+ ),
+ positionsKey: computeAudiobookPositionsKey(ordered),
+ })
+ .onConflictDoNothing();
+ }
+ next.manifestLegacyRecorded = true;
+ const [version] = await executor
+ .select({ id: schema.bookChapterVersions.id })
+ .from(schema.bookChapterVersions)
+ .where(eq(schema.bookChapterVersions.bookId, bookId))
+ .limit(1);
+ const versions: (typeof schema.bookChapterVersions.$inferInsert)[] = [];
+ const previous = state.chapters as AudiobookChapter[] | null;
+ if (!version && previous?.length)
+ versions.push({ bookId, chapters: previous, chapterCount: previous.length, source: state.source ?? 'embedded', createdBy: null });
+ const incoming = normalizeAudiobookChapters((next.chapters as AudiobookChapter[] | null) ?? []);
+ if (incoming.length)
+ versions.push({
+ bookId,
+ chapters: incoming,
+ chapterCount: incoming.length,
+ source: next.chaptersSource ?? 'manual',
+ createdBy: next.chaptersUpdatedBy ?? null,
+ });
+ if (versions.length) {
+ await executor.insert(schema.bookChapterVersions).values(versions);
+ const kept = executor
+ .select({ id: schema.bookChapterVersions.id })
+ .from(schema.bookChapterVersions)
+ .where(eq(schema.bookChapterVersions.bookId, bookId))
+ .orderBy(desc(schema.bookChapterVersions.createdAt), desc(schema.bookChapterVersions.id))
+ .limit(20);
+ await executor
+ .delete(schema.bookChapterVersions)
+ .where(and(eq(schema.bookChapterVersions.bookId, bookId), notInArray(schema.bookChapterVersions.id, kept)));
+ }
+ return next;
+}
diff --git a/server/src/db/schema/books.ts b/server/src/db/schema/books.ts
index 41889b0fb..9b3ac4e47 100644
--- a/server/src/db/schema/books.ts
+++ b/server/src/db/schema/books.ts
@@ -8,6 +8,7 @@ import {
foreignKey,
index,
integer,
+ jsonb,
numeric,
pgTable,
serial,
@@ -90,6 +91,13 @@ export const bookFiles = pgTable(
role: varchar('role', { length: 20 }).notNull().default('content'),
sortOrder: integer('sort_order'),
durationSeconds: integer('duration_seconds'),
+ chapterWriteProvenance: jsonb('chapter_write_provenance').$type<{
+ mtimeMs: number;
+ sizeBytes: number;
+ chapters: { title: string; startMs: number }[];
+ startsMs: number[];
+ continuation: boolean;
+ }>(),
// null means "not determined yet"; rows predating this column are backfilled lazily on Kobo sync.
isFixedLayout: boolean('is_fixed_layout'),
mediaOverlayAvailable: boolean('media_overlay_available').notNull().default(false),
diff --git a/server/src/db/schema/metadata.ts b/server/src/db/schema/metadata.ts
index c1e9161a8..96abdca48 100644
--- a/server/src/db/schema/metadata.ts
+++ b/server/src/db/schema/metadata.ts
@@ -16,6 +16,7 @@ import {
unique,
uniqueIndex,
varchar,
+ uuid,
} from 'drizzle-orm/pg-core';
const embedding256 = customType<{ data: number[]; driverData: string }>({
@@ -30,6 +31,9 @@ const embedding256 = customType<{ data: number[]; driverData: string }>({
},
});
+import type { AudiobookChapter, AudiobookChapterSource, ChapterLookupQueueStatus, ChapterLookupReviewSummary } from '@bookorbit/types';
+
+import { users } from './auth';
import { books } from './books';
import { bookSeries } from './series';
@@ -75,6 +79,18 @@ export const bookMetadata = pgTable(
lubimyczytacId: text('lubimyczytac_id'),
aladinId: varchar('aladin_id', { length: 20 }),
chapters: jsonb('chapters'),
+ chapterCount: integer('chapter_count').generatedAlwaysAs(
+ sql`CASE WHEN jsonb_typeof(chapters) = 'array' THEN jsonb_array_length(chapters) ELSE 0 END`,
+ ),
+ numberedChapterCount: integer('numbered_chapter_count').generatedAlwaysAs(
+ sql`CASE WHEN jsonb_typeof(chapters) = 'array' THEN jsonb_array_length(jsonb_path_query_array(chapters, '$[*] ? (@.title like_regex "^[[:space:]]*((track|disc|cd|part)[[:space:]]*)?[#._-]*[[:space:]]*[0-9]+([[:space:]]*(of|/)[[:space:]]*[0-9]+)?[[:space:]]*$" flag "i")')) ELSE 0 END`,
+ ),
+ manifestLegacyRecorded: boolean('manifest_legacy_recorded').notNull().default(false),
+ // Where `chapters` came from. An automated pass may clear or re-derive only a list that was
+ // itself read from the files; anything a provider, a migration or a person supplied stays.
+ chaptersSource: varchar('chapters_source', { length: 20 }).$type(),
+ chaptersUpdatedAt: timestamp('chapters_updated_at', { withTimezone: true }),
+ chaptersUpdatedBy: integer('chapters_updated_by').references(() => users.id, { onDelete: 'set null' }),
lockedFields: text('locked_fields')
.array()
.notNull()
@@ -88,6 +104,10 @@ export const bookMetadata = pgTable(
.$onUpdateFn(() => new Date()),
},
(t) => [
+ index('bm_chapter_count_idx').on(t.chapterCount, t.bookId),
+ index('bm_numbered_chapters_idx')
+ .on(t.bookId)
+ .where(sql`numbered_chapter_count >= chapter_count * 0.8 AND chapter_count >= 2`),
index('bm_title_trgm_idx').using('gin', t.title.op('gin_trgm_ops')),
index('bm_title_unaccent_trgm_idx').using('gin', sql`public.bookorbit_unaccent(${t.title}) gin_trgm_ops`),
index('bm_title_lower_idx').on(sql`lower(${t.title})`),
@@ -96,6 +116,9 @@ export const bookMetadata = pgTable(
index('bm_series_trgm_idx').using('gin', t.seriesName.op('gin_trgm_ops')),
index('bm_series_unaccent_trgm_idx').using('gin', sql`public.bookorbit_unaccent(${t.seriesName}) gin_trgm_ops`),
index('bm_series_id_idx').on(t.seriesId),
+ index('bm_chapters_updated_by_idx')
+ .on(t.chaptersUpdatedBy)
+ .where(sql`${t.chaptersUpdatedBy} is not null`),
index('bm_series_id_index_book_id_idx').on(
t.seriesId,
sql`(CASE WHEN ${t.seriesIndex} IS NULL THEN NULL ELSE ARRAY[split_part(${t.seriesIndex}, '.', 1)::numeric, CASE WHEN strpos(${t.seriesIndex}, '.') = 0 THEN -1::numeric ELSE split_part(${t.seriesIndex}, '.', 2)::numeric END] END)`,
@@ -248,6 +271,53 @@ export const authorEnrichmentQueue = pgTable(
],
);
+// The chapter lists the editor saved for a book, newest kept per book, so a bad save or a bad
+// lookup can be undone after the fact.
+export const bookChapterVersions = pgTable(
+ 'book_chapter_versions',
+ {
+ id: serial('id').primaryKey(),
+ bookId: integer('book_id')
+ .notNull()
+ .references(() => books.id, { onDelete: 'cascade' }),
+ chapters: jsonb('chapters').$type().notNull(),
+ source: varchar('source', { length: 20 }).$type().notNull(),
+ chapterCount: integer('chapter_count').notNull(),
+ createdBy: integer('created_by').references(() => users.id, { onDelete: 'set null' }),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ },
+ (t) => [
+ index('bcv_book_created_idx').on(t.bookId, t.createdAt),
+ index('bcv_created_by_idx')
+ .on(t.createdBy)
+ .where(sql`${t.createdBy} is not null`),
+ ],
+);
+
+// Books waiting for, or finished with, a bulk chapter lookup on Audible. One row per book: queuing a
+// book again replaces its last outcome. Rows a person still has to look at keep their result.
+export const chapterLookupQueue = pgTable(
+ 'chapter_lookup_queue',
+ {
+ claimToken: uuid('claim_token').notNull().defaultRandom(),
+ bookId: integer('book_id')
+ .primaryKey()
+ .references(() => books.id, { onDelete: 'cascade' }),
+ userId: integer('user_id').references(() => users.id, { onDelete: 'set null' }),
+ status: varchar('status', { length: 20 }).$type().notNull().default('queued'),
+ reason: varchar('reason', { length: 40 }),
+ result: jsonb('result').$type(),
+ attemptCount: integer('attempt_count').notNull().default(0),
+ lastError: text('last_error'),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
+ finishedAt: timestamp('finished_at', { withTimezone: true }),
+ /** A retry waits until this time, so a short outage does not use up every attempt at once. */
+ availableAt: timestamp('available_at', { withTimezone: true }).notNull().defaultNow(),
+ },
+ (t) => [index('clq_status_available_idx').on(t.status, t.availableAt), index('clq_user_status_idx').on(t.userId, t.status)],
+);
+
export const genres = pgTable(
'genres',
{
@@ -300,6 +370,7 @@ export const bookTags = pgTable(
export type BookMetadata = typeof bookMetadata.$inferSelect;
export type NewBookMetadata = typeof bookMetadata.$inferInsert;
+export type BookChapterVersionRow = typeof bookChapterVersions.$inferSelect;
export type BookCommunityRating = typeof bookCommunityRatings.$inferSelect;
export type NewBookCommunityRating = typeof bookCommunityRatings.$inferInsert;
@@ -347,3 +418,26 @@ export const bookMetadataFetchQueue = pgTable(
export type BookMetadataFetchQueue = typeof bookMetadataFetchQueue.$inferSelect;
export type NewBookMetadataFetchQueue = typeof bookMetadataFetchQueue.$inferInsert;
+
+export const bookChapterJobs = pgTable(
+ 'book_chapter_jobs',
+ {
+ id: uuid('id').primaryKey().defaultRandom(),
+ bookId: integer('book_id')
+ .notNull()
+ .references(() => books.id, { onDelete: 'cascade' }),
+ userId: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+ kind: varchar('kind', { length: 10 }).$type().notNull(),
+ baseRevision: varchar('base_revision', { length: 64 }).notNull(),
+ status: varchar('status', { length: 12 }).$type().notNull().default('queued'),
+ filesWritten: integer('files_written').notNull().default(0),
+ filesTotal: integer('files_total').notNull().default(0),
+ result: jsonb('result').$type(),
+ errorCode: varchar('error_code', { length: 100 }),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
+ },
+ (t) => [index('bcj_status_created_idx').on(t.status, t.createdAt), uniqueIndex('bcj_request_idx').on(t.userId, t.bookId, t.kind, t.baseRevision)],
+);
diff --git a/server/src/db/schema/reader.ts b/server/src/db/schema/reader.ts
index a7737362e..9bffc98bd 100644
--- a/server/src/db/schema/reader.ts
+++ b/server/src/db/schema/reader.ts
@@ -427,6 +427,22 @@ export const audiobookProgress = pgTable(
export type AudiobookProgress = typeof audiobookProgress.$inferSelect;
export type NewAudiobookProgress = typeof audiobookProgress.$inferInsert;
+// Manifest revisions a client may still hold, keyed to the track layout they describe. A playback
+// write naming an older revision is accepted when its positions key matches today's, because the
+// change (chapters, tags) did not move any audio. Released iOS never recovers from a 412.
+export const audiobookManifestRevisions = pgTable(
+ 'audiobook_manifest_revisions',
+ {
+ bookId: integer('book_id')
+ .notNull()
+ .references(() => books.id, { onDelete: 'cascade' }),
+ revision: varchar('revision', { length: 64 }).notNull(),
+ positionsKey: varchar('positions_key', { length: 64 }).notNull(),
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
+ },
+ (t) => [primaryKey({ columns: [t.bookId, t.revision] }), index('amr_book_created_idx').on(t.bookId, t.createdAt)],
+);
+
export type BookmarkOrigin = 'web' | 'koreader' | 'kobo';
export const userBookReadAloudSyncSettings = pgTable(
diff --git a/server/src/modules/audiobook-chapters/audiobook-chapters.module.ts b/server/src/modules/audiobook-chapters/audiobook-chapters.module.ts
new file mode 100644
index 000000000..54554d42d
--- /dev/null
+++ b/server/src/modules/audiobook-chapters/audiobook-chapters.module.ts
@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common';
+
+import { CommonModule } from '../../common/common.module';
+import { AudiobookChaptersRepository } from './audiobook-chapters.repository';
+import { AudiobookChaptersService } from './audiobook-chapters.service';
+
+@Module({
+ imports: [CommonModule],
+ providers: [AudiobookChaptersRepository, AudiobookChaptersService],
+ exports: [AudiobookChaptersService],
+})
+export class AudiobookChaptersModule {}
diff --git a/server/src/modules/audiobook-chapters/audiobook-chapters.repository.ts b/server/src/modules/audiobook-chapters/audiobook-chapters.repository.ts
new file mode 100644
index 000000000..aaf572040
--- /dev/null
+++ b/server/src/modules/audiobook-chapters/audiobook-chapters.repository.ts
@@ -0,0 +1,225 @@
+import { ConflictException, Inject, Injectable } from '@nestjs/common';
+import { BOOK_CHAPTERS_ERROR_CODES, type AudiobookChapter, type AudiobookChapterSource } from '@bookorbit/types';
+import { and, desc, eq, notInArray, sql } from 'drizzle-orm';
+import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
+
+import { DB } from '../../db';
+import {
+ computeChapterEditorRevision,
+ computeAudiobookManifestRevision,
+ computeAudiobookPositionsKey,
+ orderAudiobookTracks,
+ resolveStoredAudiobookChapters,
+} from '../../common/utils/audiobook-chapters.utils';
+import * as schema from '../../db/schema';
+import { audiobookManifestRevisions, bookChapterVersions, bookFiles, bookMetadata } from '../../db/schema';
+
+type Db = NodePgDatabase;
+
+const CHAPTER_VERSIONS_KEPT_PER_BOOK = 20;
+
+export interface StoredChapterState {
+ chapters: AudiobookChapter[] | null;
+ source: AudiobookChapterSource | null;
+}
+
+export interface ChapterVersionWrite {
+ chapters: AudiobookChapter[];
+ source: AudiobookChapterSource;
+ createdBy: number | null;
+}
+
+export interface ChapterMutationOptions {
+ expectedRevision?: string;
+ locked?: boolean;
+ requireUnlocked?: boolean;
+}
+
+export interface ChapterWrite {
+ chapters: AudiobookChapter[] | null;
+ chaptersSource: AudiobookChapterSource | null;
+ chaptersUpdatedAt: Date;
+ chaptersUpdatedBy: number | null;
+}
+
+@Injectable()
+export class AudiobookChaptersRepository {
+ constructor(@Inject(DB) private readonly db: Db) {}
+
+ findTrackRows(bookId: number) {
+ return this.db
+ .select({
+ id: bookFiles.id,
+ fileHash: bookFiles.fileHash,
+ chapterWriteProvenance: bookFiles.chapterWriteProvenance,
+ publicId: bookFiles.publicId,
+ absolutePath: bookFiles.absolutePath,
+ format: bookFiles.format,
+ sizeBytes: bookFiles.sizeBytes,
+ durationSeconds: bookFiles.durationSeconds,
+ sortOrder: bookFiles.sortOrder,
+ mtime: bookFiles.mtime,
+ updatedAt: bookFiles.updatedAt,
+ })
+ .from(bookFiles)
+ .where(eq(bookFiles.bookId, bookId));
+ }
+
+ async findChapterState(bookId: number): Promise {
+ const [row] = await this.db
+ .select({ chapters: bookMetadata.chapters, source: bookMetadata.chaptersSource })
+ .from(bookMetadata)
+ .where(eq(bookMetadata.bookId, bookId))
+ .limit(1);
+ if (!row) return null;
+ return { chapters: (row.chapters as AudiobookChapter[] | null) ?? null, source: row.source ?? null };
+ }
+
+ async writeChapters(bookId: number, values: ChapterWrite): Promise {
+ await this.db
+ .update(bookMetadata)
+ .set({ ...values, updatedAt: values.chaptersUpdatedAt })
+ .where(eq(bookMetadata.bookId, bookId));
+ }
+
+ async withWriteSnapshot(bookId: number, write: (markWritten: () => Promise) => Promise): Promise {
+ return this.db.transaction(async (tx) => {
+ await tx.insert(bookMetadata).values({ bookId }).onConflictDoNothing();
+ await tx.select({ bookId: bookMetadata.bookId }).from(bookMetadata).where(eq(bookMetadata.bookId, bookId)).for('update');
+ return write(async () => {
+ await tx.update(bookMetadata).set({ lastWrittenAt: new Date() }).where(eq(bookMetadata.bookId, bookId));
+ });
+ });
+ }
+
+ async hasChapterVersions(bookId: number): Promise {
+ const [row] = await this.db
+ .select({ id: bookChapterVersions.id })
+ .from(bookChapterVersions)
+ .where(eq(bookChapterVersions.bookId, bookId))
+ .limit(1);
+ return row !== undefined;
+ }
+
+ /** Writes the list and records each version in one transaction, then trims the oldest versions. */
+ async writeChaptersWithVersions(
+ bookId: number,
+ values: ChapterWrite,
+ versions: readonly ChapterVersionWrite[],
+ options: ChapterMutationOptions = {},
+ ): Promise {
+ await this.db.transaction(async (tx) => {
+ await tx.insert(bookMetadata).values({ bookId }).onConflictDoNothing();
+ const [state] = await tx
+ .select({
+ chapters: bookMetadata.chapters,
+ source: bookMetadata.chaptersSource,
+ updatedAt: bookMetadata.chaptersUpdatedAt,
+ lockedFields: bookMetadata.lockedFields,
+ legacyRecorded: bookMetadata.manifestLegacyRecorded,
+ })
+ .from(bookMetadata)
+ .where(eq(bookMetadata.bookId, bookId))
+ .for('update');
+ const tracks = await tx
+ .select({
+ id: bookFiles.id,
+ publicId: bookFiles.publicId,
+ absolutePath: bookFiles.absolutePath,
+ format: bookFiles.format,
+ durationSeconds: bookFiles.durationSeconds,
+ sortOrder: bookFiles.sortOrder,
+ fileHash: bookFiles.fileHash,
+ sizeBytes: bookFiles.sizeBytes,
+ mtime: bookFiles.mtime,
+ updatedAt: bookFiles.updatedAt,
+ })
+ .from(bookFiles)
+ .where(eq(bookFiles.bookId, bookId))
+ .for('share');
+ if (
+ options.expectedRevision !== undefined &&
+ computeChapterEditorRevision({ ...state, chapters: state.chapters as AudiobookChapter[] | null }, tracks) !== options.expectedRevision
+ ) {
+ throw new ConflictException({ message: 'Chapters changed since they were loaded', errorCode: BOOK_CHAPTERS_ERROR_CODES.stale });
+ }
+ if (options.requireUnlocked && state.lockedFields.includes('chapters')) {
+ throw new ConflictException({ message: 'Chapters are protected', errorCode: BOOK_CHAPTERS_ERROR_CODES.stale });
+ }
+ if (!state.legacyRecorded) {
+ const ordered = orderAudiobookTracks(tracks);
+ if (ordered.length > 0)
+ await tx
+ .insert(audiobookManifestRevisions)
+ .values({
+ bookId,
+ revision: computeAudiobookManifestRevision(
+ ordered,
+ resolveStoredAudiobookChapters(state.chapters as AudiobookChapter[] | null, tracks) ?? [],
+ ),
+ positionsKey: computeAudiobookPositionsKey(ordered),
+ })
+ .onConflictDoNothing();
+ }
+ const lockedFields =
+ options.locked === undefined
+ ? {}
+ : {
+ lockedFields: options.locked
+ ? sql`CASE WHEN 'chapters' = ANY(${bookMetadata.lockedFields}) THEN ${bookMetadata.lockedFields} ELSE array_append(${bookMetadata.lockedFields}, 'chapters') END`
+ : sql`array_remove(${bookMetadata.lockedFields}, 'chapters')`,
+ };
+ await tx
+ .update(bookMetadata)
+ .set({ ...values, ...lockedFields, manifestLegacyRecorded: true, updatedAt: values.chaptersUpdatedAt })
+ .where(eq(bookMetadata.bookId, bookId));
+
+ const [existingVersion] = await tx
+ .select({ id: bookChapterVersions.id })
+ .from(bookChapterVersions)
+ .where(eq(bookChapterVersions.bookId, bookId))
+ .limit(1);
+ const allVersions: ChapterVersionWrite[] = [...versions];
+ const previous = state.chapters as AudiobookChapter[] | null;
+ if (!existingVersion && previous?.length) allVersions.unshift({ chapters: previous, source: state.source ?? 'embedded', createdBy: null });
+ if (allVersions.length === 0) return;
+ await tx.insert(bookChapterVersions).values(allVersions.map((version) => ({ bookId, ...version, chapterCount: version.chapters.length })));
+ const kept = tx
+ .select({ id: bookChapterVersions.id })
+ .from(bookChapterVersions)
+ .where(eq(bookChapterVersions.bookId, bookId))
+ .orderBy(desc(bookChapterVersions.createdAt), desc(bookChapterVersions.id))
+ .limit(CHAPTER_VERSIONS_KEPT_PER_BOOK);
+ await tx.delete(bookChapterVersions).where(and(eq(bookChapterVersions.bookId, bookId), notInArray(bookChapterVersions.id, kept)));
+ });
+ }
+
+ async recordManifestRevision(bookId: number, revision: string, positionsKey: string): Promise {
+ const [known] = await this.db
+ .select({ recorded: bookMetadata.manifestLegacyRecorded })
+ .from(bookMetadata)
+ .where(eq(bookMetadata.bookId, bookId))
+ .limit(1);
+ if (known?.recorded) return;
+ await this.db.transaction(async (tx) => {
+ await tx.insert(bookMetadata).values({ bookId }).onConflictDoNothing();
+ const [state] = await tx
+ .select({ recorded: bookMetadata.manifestLegacyRecorded })
+ .from(bookMetadata)
+ .where(eq(bookMetadata.bookId, bookId))
+ .for('update');
+ if (state?.recorded) return;
+ await tx.insert(audiobookManifestRevisions).values({ bookId, revision, positionsKey }).onConflictDoNothing();
+ await tx.update(bookMetadata).set({ manifestLegacyRecorded: true }).where(eq(bookMetadata.bookId, bookId));
+ });
+ }
+
+ async findPositionsKey(bookId: number, revision: string): Promise {
+ const [row] = await this.db
+ .select({ positionsKey: audiobookManifestRevisions.positionsKey })
+ .from(audiobookManifestRevisions)
+ .where(and(eq(audiobookManifestRevisions.bookId, bookId), eq(audiobookManifestRevisions.revision, revision)))
+ .limit(1);
+ return row?.positionsKey ?? null;
+ }
+}
diff --git a/server/src/modules/audiobook-chapters/audiobook-chapters.service.test.ts b/server/src/modules/audiobook-chapters/audiobook-chapters.service.test.ts
new file mode 100644
index 000000000..5257acf79
--- /dev/null
+++ b/server/src/modules/audiobook-chapters/audiobook-chapters.service.test.ts
@@ -0,0 +1,142 @@
+import { ConflictException } from '@nestjs/common';
+import type { AudiobookChapter, AudiobookChapterSource } from '@bookorbit/types';
+import { FileLockService, bookOperationLockKey } from '../../common/services/file-lock.service';
+import { computeAudiobookPositionsKey } from '../../common/utils/audiobook-chapters.utils';
+import { AudiobookChaptersService } from './audiobook-chapters.service';
+
+const TRACKS = [
+ {
+ id: 1,
+ publicId: '11111111-1111-4111-8111-111111111111',
+ absolutePath: '/books/Part 1.mp3',
+ format: 'mp3',
+ sizeBytes: 1000,
+ durationSeconds: 10,
+ sortOrder: 1,
+ mtime: new Date('2026-01-01'),
+ updatedAt: new Date('2026-01-01'),
+ },
+];
+function setup(state: { chapters: AudiobookChapter[] | null; source: AudiobookChapterSource | null } | null = null) {
+ const repo = {
+ findTrackRows: vi.fn().mockResolvedValue(TRACKS),
+ findChapterState: vi.fn().mockResolvedValue(state),
+ writeChaptersWithVersions: vi.fn(),
+ recordManifestRevision: vi.fn(),
+ findPositionsKey: vi.fn().mockResolvedValue(null),
+ };
+ const locks = new FileLockService();
+ return { repo, locks, service: new AudiobookChaptersService(repo as never, locks) };
+}
+
+describe('Audiobook chapter mutation', () => {
+ it.each(['provider', 'migration', 'manual'])('keeps a %s list when the file has none', async (source) => {
+ const { service, repo } = setup({ chapters: [{ title: 'Curated', startMs: 0 }], source });
+ expect(await service.applyFileChapters(1, null)).toBe(false);
+ expect(repo.writeChaptersWithVersions).not.toHaveBeenCalled();
+ });
+ it('clears obsolete file chapters with an atomic unlocked requirement', async () => {
+ const { service, repo } = setup({ chapters: [{ title: 'Old', startMs: 0 }], source: 'embedded' });
+ expect(await service.applyFileChapters(1, [])).toBe(true);
+ expect(repo.writeChaptersWithVersions).toHaveBeenCalledWith(1, expect.objectContaining({ chapters: null, chaptersSource: null }), [], {
+ requireUnlocked: true,
+ });
+ });
+ it('does not rewrite an identical normalized list', async () => {
+ const { service, repo } = setup({ chapters: [{ title: 'Same', startMs: 0 }], source: 'embedded' });
+ expect(await service.applyFileChapters(1, [{ title: ' Same ', startMs: 0 }])).toBe(false);
+ expect(repo.writeChaptersWithVersions).not.toHaveBeenCalled();
+ });
+ it('passes revision and protection to the transaction with the cleaned edit and its version', async () => {
+ const { service, repo } = setup();
+ const options = { expectedRevision: 'revision', locked: true };
+ await service.saveEditedChapters(
+ 1,
+ [
+ { title: ' Second ', startMs: 5000 },
+ { title: '', startMs: 0 },
+ ],
+ 7,
+ options,
+ );
+ const chapters = [
+ { title: 'Chapter 1', startMs: 0 },
+ { title: 'Second', startMs: 5000 },
+ ];
+ expect(repo.writeChaptersWithVersions).toHaveBeenCalledWith(
+ 1,
+ expect.objectContaining({ chapters, chaptersSource: 'manual', chaptersUpdatedBy: 7 }),
+ [{ chapters, source: 'manual', createdBy: 7 }],
+ options,
+ );
+ });
+ it('credits lookup versions without declaring provider chapters a manual edit', async () => {
+ const { service, repo } = setup();
+ await service.saveLookedUpChapters(1, [{ title: 'Provider', startMs: 0 }], 7, { requireUnlocked: true });
+ expect(repo.writeChaptersWithVersions).toHaveBeenCalledWith(
+ 1,
+ expect.objectContaining({ chaptersSource: 'provider', chaptersUpdatedBy: null }),
+ [{ chapters: [{ title: 'Provider', startMs: 0 }], source: 'provider', createdBy: 7 }],
+ { requireUnlocked: true },
+ );
+ });
+ it('revert with no embedded chapters delegates preservation of the outgoing list to the transaction', async () => {
+ const { service, repo } = setup({ chapters: [{ title: 'Old', startMs: 0 }], source: 'manual' });
+ await service.restoreFileChapters(1, [], 7, { locked: false });
+ expect(repo.writeChaptersWithVersions).toHaveBeenCalledWith(1, expect.objectContaining({ chapters: null, chaptersSource: null }), [], {
+ locked: false,
+ });
+ });
+ it('waits for book file work before mutating chapters', async () => {
+ const { service, repo, locks } = setup();
+ let release!: () => void;
+ const write = locks.withLock(
+ bookOperationLockKey(1),
+ () =>
+ new Promise((resolve) => {
+ release = resolve;
+ }),
+ );
+ await vi.waitFor(() => expect(release).toBeTypeOf('function'));
+ const save = service.saveEditedChapters(1, [{ title: 'New', startMs: 0 }], 7);
+ await Promise.resolve();
+ expect(repo.writeChaptersWithVersions).not.toHaveBeenCalled();
+ release();
+ await Promise.all([write, save]);
+ expect(repo.writeChaptersWithVersions).toHaveBeenCalledTimes(1);
+ });
+ it('propagates an atomic stale-state refusal', async () => {
+ const { service, repo } = setup();
+ repo.writeChaptersWithVersions.mockRejectedValue(new ConflictException());
+ await expect(service.saveEditedChapters(1, [{ title: 'New', startMs: 0 }], 7)).rejects.toBeInstanceOf(ConflictException);
+ });
+});
+
+describe('Offline revision compatibility', () => {
+ it('keeps a revision compatible after more than 100 chapter edits without a registry lookup', async () => {
+ const { service, repo } = setup();
+ const held = await service.currentRevision(1, TRACKS);
+ for (let index = 0; index < 150; index += 1) {
+ repo.findChapterState.mockResolvedValue({ chapters: [{ title: String(index), startMs: 0 }], source: 'manual' });
+ await service.currentRevision(1, TRACKS);
+ }
+ expect(await service.isPositionCompatible(1, held!.revision, computeAudiobookPositionsKey(TRACKS))).toBe(true);
+ expect(repo.findPositionsKey).not.toHaveBeenCalled();
+ expect(await service.isPositionCompatible(1, held!.revision, computeAudiobookPositionsKey([{ ...TRACKS[0], durationSeconds: 20 }]))).toBe(false);
+ });
+ it('accepts recorded legacy revisions only for their original layout', async () => {
+ const { service, repo } = setup();
+ repo.findPositionsKey.mockResolvedValue('layout-a');
+ expect(await service.isPositionCompatible(1, 'legacy', 'layout-a')).toBe(true);
+ expect(await service.isPositionCompatible(1, 'legacy', 'layout-b')).toBe(false);
+ });
+ it('refuses required preservation if the database fails, so file work cannot destroy its legacy mapping', async () => {
+ const { service, repo } = setup();
+ repo.recordManifestRevision.mockRejectedValue(new Error('database unavailable'));
+ await expect(service.recordCurrentRevision(1, true)).rejects.toThrow('database unavailable');
+ });
+ it('has no revision without audio', async () => {
+ const { service } = setup();
+ expect(await service.currentRevision(1, [{ ...TRACKS[0], format: 'epub' }])).toBeNull();
+ });
+});
diff --git a/server/src/modules/audiobook-chapters/audiobook-chapters.service.ts b/server/src/modules/audiobook-chapters/audiobook-chapters.service.ts
new file mode 100644
index 000000000..b50224ebc
--- /dev/null
+++ b/server/src/modules/audiobook-chapters/audiobook-chapters.service.ts
@@ -0,0 +1,170 @@
+import { Injectable, Logger } from '@nestjs/common';
+import type { AudiobookChapter } from '@bookorbit/types';
+
+import {
+ chapterProvenance,
+ computeAudiobookManifestRevision,
+ computeLayoutManifestRevision,
+ computeAudiobookPositionsKey,
+ isFileDerivedChapterSource,
+ normalizeAudiobookChapters,
+ orderAudiobookTracks,
+ resolveStoredAudiobookChapters,
+ sameAudiobookChapters,
+} from '../../common/utils/audiobook-chapters.utils';
+import { FileLockService, bookOperationLockKey } from '../../common/services/file-lock.service';
+import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils';
+import { AudiobookChaptersRepository, type ChapterMutationOptions } from './audiobook-chapters.repository';
+
+type TrackRow = Omit>[number], 'fileHash' | 'chapterWriteProvenance'>;
+
+@Injectable()
+export class AudiobookChaptersService {
+ private readonly logger = new Logger(AudiobookChaptersService.name);
+
+ constructor(
+ private readonly repo: AudiobookChaptersRepository,
+ private readonly locks: FileLockService,
+ ) {}
+
+ /**
+ * The manifest revision clients are handed for this book right now, and the positions key it
+ * maps to. Recording the pair is what lets a write naming this revision through later, after a
+ * change that moved no audio.
+ */
+ async currentRevision(bookId: number, rows: readonly TrackRow[], required = false): Promise<{ revision: string; positionsKey: string } | null> {
+ const tracks = orderAudiobookTracks(rows);
+ if (tracks.length === 0) return null;
+ const state = await this.repo.findChapterState(bookId);
+ const chapters = resolveStoredAudiobookChapters(state?.chapters, rows) ?? [];
+ const revision = computeLayoutManifestRevision(tracks, chapters);
+ const positionsKey = computeAudiobookPositionsKey(tracks);
+ await this.recordManifestRevision(bookId, computeAudiobookManifestRevision(tracks, chapters), positionsKey, required);
+ return { revision, positionsKey };
+ }
+
+ withWriteSnapshot(bookId: number, write: (markWritten: () => Promise) => Promise): Promise {
+ return this.repo.withWriteSnapshot(bookId, write);
+ }
+
+ fileChapterHints(bookId: number) {
+ return this.repo.findTrackRows(bookId);
+ }
+
+ /** Whether a position written against `revision` still lands on the same audio today. */
+ async isPositionCompatible(bookId: number, revision: string, positionsKey: string): Promise {
+ if (/^[0-9a-f]{64}$/.test(revision) && revision.slice(0, 32) === positionsKey.slice(0, 32)) return true;
+ return (await this.repo.findPositionsKey(bookId, revision)) === positionsKey;
+ }
+
+ /**
+ * Records the revision clients hold right now, so a device that has not synced since keeps
+ * syncing after the chapter write that is about to replace it.
+ */
+ async recordCurrentRevision(bookId: number, required = false): Promise {
+ try {
+ await this.currentRevision(bookId, await this.repo.findTrackRows(bookId), required);
+ } catch (error) {
+ if (required) throw error;
+ this.logRecordFailure(bookId, error);
+ }
+ }
+
+ private async recordManifestRevision(bookId: number, revision: string, positionsKey: string, required = false): Promise {
+ try {
+ await this.repo.recordManifestRevision(bookId, revision, positionsKey);
+ } catch (error) {
+ if (required) throw error;
+ this.logRecordFailure(bookId, error);
+ return;
+ }
+ }
+
+ private logRecordFailure(bookId: number, error: unknown): void {
+ const errorClass = error instanceof Error ? error.name : 'Error';
+ const message = sanitizeLogValue(error instanceof Error ? error.message : String(error));
+ this.logger.warn(
+ `[audiobook_chapters.record_revision] [fail] bookId=${bookId} errorClass=${errorClass} error="${message}" - manifest revision not recorded`,
+ );
+ }
+
+ /**
+ * Stores chapters read from a book's audio files. An empty read clears only a list that itself
+ * came from the files: chapters a provider, a migration or a person supplied are never erased by
+ * a file that carries none. Callers have already dropped the write if chapters are locked.
+ */
+ async applyFileChapters(bookId: number, chapters: readonly AudiobookChapter[] | null): Promise {
+ return this.locks.withLock(bookOperationLockKey(bookId), async () => {
+ const state = await this.repo.findChapterState(bookId);
+ const current = state?.chapters?.length ? state.chapters : null;
+ const next = chapters && chapters.length > 0 ? normalizeAudiobookChapters(chapters) : null;
+
+ if (next === null && (current === null || !isFileDerivedChapterSource(state?.source))) return false;
+ if (next !== null && current !== null && sameAudiobookChapters(next, normalizeAudiobookChapters(current))) return false;
+
+ await this.recordCurrentRevision(bookId);
+ await this.repo.writeChaptersWithVersions(
+ bookId,
+ {
+ chapters: next,
+ chaptersSource: next ? 'embedded' : null,
+ chaptersUpdatedAt: new Date(),
+ chaptersUpdatedBy: null,
+ },
+ [],
+ { requireUnlocked: true },
+ );
+ return true;
+ });
+ }
+
+ /**
+ * Stores a list a person saved in the editor. The first save also keeps the list it replaces, so
+ * what the files or a provider supplied can always be restored from the version history.
+ */
+ async saveEditedChapters(bookId: number, chapters: readonly AudiobookChapter[], userId: number, options?: ChapterMutationOptions): Promise {
+ await this.locks.withLock(bookOperationLockKey(bookId), () => this.saveVersioned(bookId, chapters, 'manual', userId, options));
+ }
+
+ /** Stores a list a bulk lookup applied on someone's behalf, versioned like an edit so it can be undone. */
+ async saveLookedUpChapters(
+ bookId: number,
+ chapters: readonly AudiobookChapter[],
+ userId: number | null,
+ options?: ChapterMutationOptions,
+ ): Promise {
+ await this.locks.withLock(bookOperationLockKey(bookId), () => this.saveVersioned(bookId, chapters, 'provider', userId, options));
+ }
+
+ private async saveVersioned(
+ bookId: number,
+ chapters: readonly AudiobookChapter[],
+ source: 'manual' | 'provider',
+ userId: number | null,
+ options?: ChapterMutationOptions,
+ ): Promise {
+ const next = normalizeAudiobookChapters(chapters);
+ const versions: Parameters[2][number][] = [];
+ versions.push({ chapters: next, source, createdBy: userId });
+
+ await this.recordCurrentRevision(bookId);
+ await this.repo.writeChaptersWithVersions(bookId, { chapters: next, ...chapterProvenance(next, source, userId) }, versions, options);
+ }
+
+ /** The editor's "revert to automatic": whatever the files carry now, or nothing for the per-track list. */
+ async restoreFileChapters(
+ bookId: number,
+ chapters: readonly AudiobookChapter[] | null,
+ userId: number,
+ options?: ChapterMutationOptions,
+ ): Promise {
+ await this.locks.withLock(bookOperationLockKey(bookId), async () => {
+ const next = chapters && chapters.length > 0 ? normalizeAudiobookChapters(chapters) : null;
+ const versions: Parameters[2][number][] = [];
+ if (next) versions.push({ chapters: next, source: 'embedded', createdBy: userId });
+
+ await this.recordCurrentRevision(bookId);
+ await this.repo.writeChaptersWithVersions(bookId, { chapters: next, ...chapterProvenance(next, 'embedded') }, versions, options);
+ });
+ }
+}
diff --git a/server/src/modules/audiobook/audiobook.module.ts b/server/src/modules/audiobook/audiobook.module.ts
index ea35b68bc..01eea56bc 100644
--- a/server/src/modules/audiobook/audiobook.module.ts
+++ b/server/src/modules/audiobook/audiobook.module.ts
@@ -1,12 +1,13 @@
import { Module } from '@nestjs/common';
+import { AudiobookChaptersModule } from '../audiobook-chapters/audiobook-chapters.module';
import { BookModule } from '../book/book.module';
import { AudiobookController } from './audiobook.controller';
import { AudiobookRepository } from './audiobook.repository';
import { AudiobookService } from './audiobook.service';
@Module({
- imports: [BookModule],
+ imports: [AudiobookChaptersModule, BookModule],
controllers: [AudiobookController],
providers: [AudiobookRepository, AudiobookService],
})
diff --git a/server/src/modules/audiobook/audiobook.service.test.ts b/server/src/modules/audiobook/audiobook.service.test.ts
index 70272b8ca..29c1ccdd8 100644
--- a/server/src/modules/audiobook/audiobook.service.test.ts
+++ b/server/src/modules/audiobook/audiobook.service.test.ts
@@ -1,7 +1,11 @@
+import { FileLockService } from '../../common/services/file-lock.service';
+import { createHash } from 'node:crypto';
+
import { BadRequestException, ConflictException, PreconditionFailedException } from '@nestjs/common';
-import { AUDIOBOOK_MANIFEST_SCHEMA, AUDIOBOOK_MANIFEST_VERSION } from '@bookorbit/types';
+import { AUDIOBOOK_MANIFEST_SCHEMA, AUDIOBOOK_MANIFEST_VERSION, type AudiobookChapter } from '@bookorbit/types';
import type { RequestUser } from '../../common/types/request-user';
+import { AudiobookChaptersService } from '../audiobook-chapters/audiobook-chapters.service';
import { AudiobookService } from './audiobook.service';
const FIRST_PUBLIC_ID = '11111111-1111-4111-8111-111111111111';
@@ -50,6 +54,23 @@ function makeFixture() {
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
},
];
+ const stored: { chapters: AudiobookChapter[] | null } = {
+ chapters: [
+ { title: 'Opening', startMs: 0 },
+ { title: 'Second Part', startMs: 10_125 },
+ ],
+ };
+ const revisions = new Map();
+ const chaptersRepo = {
+ findTrackRows: vi.fn().mockImplementation(() => repo.findAudioFiles()),
+ findChapterState: vi.fn().mockImplementation(() => Promise.resolve({ chapters: stored.chapters, source: 'embedded' })),
+ writeChapters: vi.fn(),
+ recordManifestRevision: vi.fn().mockImplementation((_bookId: number, revision: string, positionsKey: string) => {
+ if (!revisions.has(revision)) revisions.set(revision, positionsKey);
+ return Promise.resolve();
+ }),
+ findPositionsKey: vi.fn().mockImplementation((_bookId: number, revision: string) => Promise.resolve(revisions.get(revision) ?? null)),
+ };
const repo = {
findAudioFiles: vi.fn().mockResolvedValue(files),
findAsset: vi.fn(),
@@ -75,10 +96,7 @@ function makeFixture() {
audioMetadata: {
durationSeconds: 30.125,
narrators: [{ name: 'Narrator' }],
- chapters: [
- { title: 'Opening', startMs: 0 },
- { title: 'Second Part', startMs: 10_125 },
- ],
+ chapters: stored.chapters,
},
}),
autoUpdateReadStatusForProgress: vi.fn().mockResolvedValue(undefined),
@@ -87,7 +105,20 @@ function makeFixture() {
return {
repo,
bookService,
- service: new AudiobookService(repo as never, bookService as never),
+ stored,
+ chaptersRepo,
+ service: new AudiobookService(repo as never, bookService as never, new AudiobookChaptersService(chaptersRepo as never, new FileLockService())),
+ };
+}
+
+function writeFor(manifest: { assets: { assetId: string }[] }, manifestRevision: string, operationId: string) {
+ return {
+ assetId: manifest.assets[0]!.assetId,
+ positionMs: 1_000,
+ capturedAt: '2026-02-01T00:00:00.000Z',
+ operationId,
+ baseRevision: 0,
+ manifestRevision,
};
}
@@ -123,6 +154,72 @@ describe('AudiobookService', () => {
expect(after.revision).toBe(before.revision);
});
+ it('preserves the released revision formula in the legacy registry while serving an opaque layout revision', async () => {
+ const { service, repo, stored, chaptersRepo } = makeFixture();
+ const files = [...(await repo.findAudioFiles())].sort((left, right) => left.sortOrder - right.sortOrder);
+ const released = createHash('sha256')
+ .update(
+ JSON.stringify({
+ files: files.map((file) => [file.publicId, file.sizeBytes, file.durationSeconds, file.mtime?.toISOString(), file.updatedAt.toISOString()]),
+ chapters: stored.chapters,
+ }),
+ )
+ .digest('hex');
+
+ const manifest = await service.getManifest(30, makeUser());
+
+ expect(manifest.revision).toMatch(/^[0-9a-f]{64}$/);
+ expect(chaptersRepo.recordManifestRevision).toHaveBeenCalledWith(30, released, expect.any(String));
+ });
+
+ it('accepts a playback write from before a chapter edit and hands back the current revision', async () => {
+ const { service, repo, stored, bookService } = makeFixture();
+ const before = await service.getManifest(30, makeUser());
+ stored.chapters = [
+ { title: 'Prologue', startMs: 0 },
+ { title: 'Chapter One', startMs: 4_000 },
+ ];
+ const detail = await bookService.getDetail();
+ bookService.getDetail.mockResolvedValue({ ...detail, audioMetadata: { ...detail.audioMetadata, chapters: stored.chapters } });
+ const after = await service.getManifest(30, makeUser());
+ repo.createPlaybackState.mockResolvedValue({ capturedAt: new Date('2026-02-01T00:00:00.000Z'), revision: 1 });
+
+ const saved = await service.putPlaybackState(30, writeFor(before, before.revision, 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'), makeUser());
+
+ expect(after.revision).not.toBe(before.revision);
+ expect(saved.manifestRevision).toBe(after.revision);
+ expect(repo.createPlaybackState).toHaveBeenCalledWith(7, 30, expect.objectContaining({ manifestRevision: after.revision }));
+ });
+
+ it('accepts a revision recorded just before a chapter write even if no client fetched it since', async () => {
+ const { service, repo, stored, chaptersRepo } = makeFixture();
+ await new AudiobookChaptersService(chaptersRepo as never, new FileLockService()).recordCurrentRevision(30);
+ const heldRevision = chaptersRepo.recordManifestRevision.mock.calls[0]![1] as string;
+ stored.chapters = [{ title: 'Only chapter', startMs: 0 }];
+ repo.createPlaybackState.mockResolvedValue({ capturedAt: new Date('2026-02-01T00:00:00.000Z'), revision: 1 });
+
+ const saved = await service.putPlaybackState(
+ 30,
+ writeFor({ assets: [{ assetId: `aud_${FIRST_PUBLIC_ID}` }] }, heldRevision, 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'),
+ makeUser(),
+ );
+
+ expect(saved.manifestRevision).not.toBe(heldRevision);
+ expect(saved.revision).toBe(1);
+ });
+
+ it('still rejects a revision whose track layout has since changed', async () => {
+ const { service, repo } = makeFixture();
+ const before = await service.getManifest(30, makeUser());
+ const files = await repo.findAudioFiles();
+ repo.findAudioFiles.mockResolvedValue(files.map((file) => (file.id === 12 ? { ...file, durationSeconds: 25 } : file)));
+
+ await expect(service.putPlaybackState(30, writeFor(before, before.revision, 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee'), makeUser())).rejects.toThrow(
+ PreconditionFailedException,
+ );
+ expect(repo.createPlaybackState).not.toHaveBeenCalled();
+ });
+
it('derives percentage and creates revision one from a matching manifest write', async () => {
const { service, repo, bookService } = makeFixture();
const manifest = await service.getManifest(30, makeUser());
@@ -328,6 +425,28 @@ describe('AudiobookService', () => {
await expect(service.createBookmark(30, { ...dto, positionMs: 31_126 }, makeUser())).rejects.toThrow(BadRequestException);
});
+ it('keeps a bookmark whose chapter id belongs to an earlier manifest, without the chapter', async () => {
+ const { service, repo } = makeFixture();
+ repo.createAudioBookmark.mockResolvedValue({
+ clientId: 'ffffffff-ffff-4fff-8fff-ffffffffffff',
+ bookId: 30,
+ positionSeconds: 3,
+ chapterId: null,
+ title: 'Mark',
+ note: null,
+ createdAt: new Date('2026-02-01T00:00:00.000Z'),
+ updatedAt: new Date('2026-02-01T00:00:00.000Z'),
+ });
+
+ await service.createBookmark(
+ 30,
+ { clientId: 'ffffffff-ffff-4fff-8fff-ffffffffffff', positionMs: 3_000, title: 'Mark', chapterId: 'ch_stale' },
+ makeUser(),
+ );
+
+ expect(repo.createAudioBookmark).toHaveBeenCalledWith(7, 30, expect.objectContaining({ chapterId: null }));
+ });
+
it('resolves asset content through the access-checked file path', async () => {
const { service, repo, bookService } = makeFixture();
repo.findAsset.mockResolvedValue({ id: 11, format: 'mp3' });
diff --git a/server/src/modules/audiobook/audiobook.service.ts b/server/src/modules/audiobook/audiobook.service.ts
index 5547f7693..f2c348299 100644
--- a/server/src/modules/audiobook/audiobook.service.ts
+++ b/server/src/modules/audiobook/audiobook.service.ts
@@ -14,8 +14,9 @@ import {
} from '@bookorbit/types';
import type { RequestUser } from '../../common/types/request-user';
-import { compareAudioTracks } from '../../common/utils/book-media.utils';
+import { orderAudiobookTracks } from '../../common/utils/audiobook-chapters.utils';
import type { BookmarkRow } from '../../db/schema';
+import { AudiobookChaptersService } from '../audiobook-chapters/audiobook-chapters.service';
import { BookService } from '../book/book.service';
import type { CreateAudiobookBookmarkDto } from './dto/create-audiobook-bookmark.dto';
import type { PutAudiobookPlaybackStateDto } from './dto/put-audiobook-playback-state.dto';
@@ -30,6 +31,7 @@ const DURATION_SLACK_MS = 1_000;
interface ManifestContext {
manifest: AudiobookManifest;
+ positionsKey: string;
files: AudioFileRow[];
libraryId: number;
hasCompleteDurations: boolean;
@@ -40,6 +42,7 @@ export class AudiobookService {
constructor(
private readonly repo: AudiobookRepository,
private readonly bookService: BookService,
+ private readonly audiobookChapters: AudiobookChaptersService,
) {}
async getManifest(bookId: number, user: RequestUser): Promise {
@@ -77,8 +80,14 @@ export class AudiobookService {
async putPlaybackState(bookId: number, dto: PutAudiobookPlaybackStateDto, user: RequestUser): Promise {
const context = await this.loadManifestContext(bookId, user);
- if (dto.manifestRevision !== context.manifest.revision) {
- throw new PreconditionFailedException('Audiobook manifest has changed');
+ let manifestRevision = dto.manifestRevision;
+ if (manifestRevision !== context.manifest.revision) {
+ // A chapter edit or a tag write changes the revision without moving any audio, and released
+ // iOS never recovers from a 412, so a revision with today's track layout is still accepted.
+ if (!(await this.audiobookChapters.isPositionCompatible(bookId, manifestRevision, context.positionsKey))) {
+ throw new PreconditionFailedException('Audiobook manifest has changed');
+ }
+ manifestRevision = context.manifest.revision;
}
const fileIndex = context.files.findIndex((file) => this.assetId(file.publicId) === dto.assetId);
if (fileIndex < 0) throw new BadRequestException('assetId does not belong to this audiobook');
@@ -114,7 +123,7 @@ export class AudiobookService {
percentage,
capturedAt: new Date(dto.capturedAt),
operationId: dto.operationId,
- manifestRevision: dto.manifestRevision,
+ manifestRevision,
};
const saved =
dto.baseRevision === 0
@@ -144,7 +153,7 @@ export class AudiobookService {
completed: percentage >= 100,
capturedAt: saved.capturedAt.toISOString(),
revision: saved.revision,
- manifestRevision: dto.manifestRevision,
+ manifestRevision,
};
}
@@ -165,13 +174,13 @@ export class AudiobookService {
if (totalDurationMs > 0 && dto.positionMs > totalDurationMs + DURATION_SLACK_MS) {
throw new BadRequestException('positionMs exceeds the audiobook duration');
}
- if (dto.chapterId && !context.manifest.chapters.some((chapter) => chapter.id === dto.chapterId)) {
- throw new BadRequestException('chapterId does not belong to this audiobook manifest');
- }
+ // Chapter ids change with every chapter edit, so a player still holding the previous manifest
+ // names one that no longer exists. The position is what matters; keep the bookmark without it.
+ const chapterId = dto.chapterId && context.manifest.chapters.some((chapter) => chapter.id === dto.chapterId) ? dto.chapterId : null;
const row = await this.repo.createAudioBookmark(user.id, bookId, {
clientId: dto.clientId,
positionSeconds: (totalDurationMs > 0 ? Math.min(dto.positionMs, totalDurationMs) : dto.positionMs) / 1000,
- chapterId: dto.chapterId ?? null,
+ chapterId,
title: dto.title,
note: dto.note ?? null,
});
@@ -199,17 +208,10 @@ export class AudiobookService {
private async loadManifestContext(bookId: number, user: RequestUser): Promise {
await this.bookService.verifyBookAccess(bookId, user);
const [detail, rows] = await Promise.all([this.bookService.getDetail(bookId, user), this.repo.findAudioFiles(bookId)]);
- const files = rows.filter((row) => row.format !== null && isAudioFormat(row.format)).sort(compareAudioTracks);
- if (files.length === 0) throw new NotFoundException('Book has no audiobook assets');
-
- const revision = createHash('sha256')
- .update(
- JSON.stringify({
- files: files.map((file) => [file.publicId, file.sizeBytes, file.durationSeconds, file.mtime?.toISOString(), file.updatedAt.toISOString()]),
- chapters: detail.audioMetadata?.chapters ?? [],
- }),
- )
- .digest('hex');
+ const files = orderAudiobookTracks(rows);
+ const current = await this.audiobookChapters.currentRevision(bookId, rows);
+ if (files.length === 0 || !current) throw new NotFoundException('Book has no audiobook assets');
+ const { revision, positionsKey } = current;
const assets = files.map((file, sequence) => ({
assetId: this.assetId(file.publicId),
fileId: file.id,
@@ -231,6 +233,7 @@ export class AudiobookService {
const chapters = this.buildChapters(bookId, revision, detail.audioMetadata?.chapters ?? [], assets, totalDurationMs);
return {
libraryId: detail.libraryId,
+ positionsKey,
files,
hasCompleteDurations,
manifest: {
diff --git a/server/src/modules/book-dock/book-dock-finalize.service.test.ts b/server/src/modules/book-dock/book-dock-finalize.service.test.ts
index b242827c7..a324791f5 100644
--- a/server/src/modules/book-dock/book-dock-finalize.service.test.ts
+++ b/server/src/modules/book-dock/book-dock-finalize.service.test.ts
@@ -1879,9 +1879,10 @@ describe('BookDockFinalizeService', () => {
expect(patch).not.toHaveProperty('durationSeconds');
expect(patch.chapters).toEqual([
{ title: 'Good', startMs: 1000 },
- { title: '', startMs: 2000 },
+ { title: 'Chapter 2', startMs: 2000 },
{ title: 'Stringy', startMs: 3001 },
]);
+ expect(patch).toMatchObject({ chaptersSource: 'embedded', chaptersUpdatedAt: expect.any(Date), chaptersUpdatedBy: null });
expect(metadataService.replaceNarrators).not.toHaveBeenCalled();
});
diff --git a/server/src/modules/book-dock/book-dock-finalize.service.ts b/server/src/modules/book-dock/book-dock-finalize.service.ts
index ff65b7e4b..f59460283 100644
--- a/server/src/modules/book-dock/book-dock-finalize.service.ts
+++ b/server/src/modules/book-dock/book-dock-finalize.service.ts
@@ -18,6 +18,7 @@ import { DatabaseError } from 'pg';
import type {
AudiobookChapter,
+ AudiobookChapterSource,
BookDockAutoFinalizeMetadataMode,
BookDockDiscardDuplicatesResult,
BookDockFinalizeFileResult,
@@ -45,6 +46,7 @@ import { BookReadService } from '../book/book-read.service';
import { NotificationService } from '../notification/notification.service';
import { SeriesIdentityService } from '../../common/services/series-identity.service';
import { SeriesMembershipService } from '../../common/services/series-membership.service';
+import { chapterProvenance, normalizeAudiobookChapters } from '../../common/utils/audiobook-chapters.utils';
import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils';
import { resolveExistingPathSpelling } from '../../common/utils/path-identity.utils';
import { resolveSingleFileBookPath } from '../../common/utils/book-path.utils';
@@ -1693,6 +1695,7 @@ function normalizeComicMetadata(value: BookDockMetadata['comicMetadata']): Comic
type AudioFinalizeFields = {
durationSeconds: number | null;
chapters: AudiobookChapter[] | null;
+ chaptersSource: AudiobookChapterSource;
narrators: string[];
abridged: boolean | null;
};
@@ -1708,15 +1711,18 @@ function resolveAudioFinalizeFields(
normalizedSelected?.durationSeconds !== undefined ? normalizedSelected.durationSeconds : normalizedEmbedded?.durationSeconds,
),
chapters: normalizeChapters(normalizedSelected?.chapters !== undefined ? normalizedSelected.chapters : normalizedEmbedded?.chapters),
+ chaptersSource: normalizedSelected?.chapters !== undefined ? 'provider' : 'embedded',
narrators: normalizeStringArray(normalizedSelected?.narrators !== undefined ? normalizedSelected.narrators : normalizedEmbedded?.narrators, 500),
abridged: normalizeAbridged(normalizedSelected?.abridged !== undefined ? normalizedSelected.abridged : normalizedEmbedded?.abridged),
};
}
-function buildAudioMetadataPatch(audio: AudioFinalizeFields): { durationSeconds?: number; chapters?: AudiobookChapter[]; abridged?: boolean } {
- const patch: { durationSeconds?: number; chapters?: AudiobookChapter[]; abridged?: boolean } = {};
+function buildAudioMetadataPatch(
+ audio: AudioFinalizeFields,
+): { durationSeconds?: number; chapters?: AudiobookChapter[]; abridged?: boolean } & Partial> {
+ const patch: { durationSeconds?: number; chapters?: AudiobookChapter[]; abridged?: boolean } & Partial> = {};
if (audio.durationSeconds !== null) patch.durationSeconds = audio.durationSeconds;
- if (audio.chapters !== null) patch.chapters = audio.chapters;
+ if (audio.chapters !== null) Object.assign(patch, { chapters: audio.chapters }, chapterProvenance(audio.chapters, audio.chaptersSource));
if (audio.abridged !== null) patch.abridged = audio.abridged;
return patch;
}
@@ -1747,7 +1753,7 @@ function normalizeChapters(value: unknown): AudiobookChapter[] | null {
const title = typeof candidate.title === 'string' ? candidate.title : '';
chapters.push({ title, startMs: Math.round(startMs) });
}
- return chapters.length > 0 ? chapters : null;
+ return chapters.length > 0 ? normalizeAudiobookChapters(chapters) : null;
}
function resolveFinalizeErrorMessage(error: unknown): string {
diff --git a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.test.ts b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.test.ts
index cd90272aa..d792500c8 100644
--- a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.test.ts
+++ b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.test.ts
@@ -350,6 +350,10 @@ describe('BookMetadataFetchOrchestratorService', () => {
audibleId: 'a1',
librofmId: '9781234567890',
koboId: 'kobo-1',
+ chapters: [{ title: 'Ch 1', startMs: 0 }],
+ chaptersSource: 'provider',
+ chaptersUpdatedAt: expect.any(Date),
+ chaptersUpdatedBy: null,
}),
);
expect(bookReadService.replaceCommunityRatings).toHaveBeenCalledWith(88, [
diff --git a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts
index 2f114476f..90d5b21f8 100644
--- a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts
+++ b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts
@@ -2,7 +2,9 @@ import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy, Optional }
import type { BookMetadataFetchReason } from '@bookorbit/types';
import { MetadataProviderKey, NotificationType, Permission, parseSeriesIndex } from '@bookorbit/types';
import { NotificationService } from '../notification/notification.service';
+import { chapterProvenance, normalizeAudiobookChapters } from '../../common/utils/audiobook-chapters.utils';
import { coverFetchInputs, resolveIsAudiobook, type CoverFetchState } from '../../common/utils/book-media.utils';
+import { AudiobookChaptersService } from '../audiobook-chapters/audiobook-chapters.service';
import { BookCoverStore } from '../book-cover-store/book-cover-store.service';
import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils';
import { normalizePublishedDate, publishedYearFromDateKey } from '../../common/utils/published-date.utils';
@@ -66,6 +68,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
private readonly notificationService: NotificationService,
private readonly coverStore: BookCoverStore,
@Optional() private readonly gateway?: BookMetadataFetchGateway,
+ @Optional() private readonly audiobookChapters?: AudiobookChaptersService,
) {}
async onApplicationBootstrap() {
@@ -424,7 +427,12 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
// overwrite an existing true value.
const abridged = this.asBoolean(filteredResolved.abridged);
if (abridged !== undefined) scalarFields.abridged = abridged;
- if (filteredResolved.chapters !== undefined) scalarFields.chapters = filteredResolved.chapters;
+ if (filteredResolved.chapters !== undefined) {
+ const chapters = filteredResolved.chapters.length > 0 ? normalizeAudiobookChapters(filteredResolved.chapters) : null;
+ scalarFields.chapters = chapters;
+ Object.assign(scalarFields, chapterProvenance(chapters, 'provider'));
+ await this.audiobookChapters?.recordCurrentRevision(bookId);
+ }
scalarFields.lastMetadataFetchAt = new Date();
scalarFields.updatedAt = new Date();
diff --git a/server/src/modules/book-metadata-fetch/book-metadata-fetch.module.ts b/server/src/modules/book-metadata-fetch/book-metadata-fetch.module.ts
index f6cd4a1ff..ba7f0dec5 100644
--- a/server/src/modules/book-metadata-fetch/book-metadata-fetch.module.ts
+++ b/server/src/modules/book-metadata-fetch/book-metadata-fetch.module.ts
@@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import type { StringValue } from 'ms';
+import { AudiobookChaptersModule } from '../audiobook-chapters/audiobook-chapters.module';
import { AuthModule } from '../auth/auth.module';
import { BookModule } from '../book/book.module';
import { BookCoverStoreModule } from '../book-cover-store/book-cover-store.module';
@@ -22,6 +23,7 @@ import { BookMetadataFetchSessionService } from './book-metadata-fetch-session.s
@Module({
imports: [
+ AudiobookChaptersModule,
forwardRef(() => BookModule),
BookCoverStoreModule,
BookMetadataLockModule,
diff --git a/server/src/modules/book-metadata-lock/book-metadata-lock.service.test.ts b/server/src/modules/book-metadata-lock/book-metadata-lock.service.test.ts
index a172e62eb..ccf6ca7a0 100644
--- a/server/src/modules/book-metadata-lock/book-metadata-lock.service.test.ts
+++ b/server/src/modules/book-metadata-lock/book-metadata-lock.service.test.ts
@@ -97,7 +97,7 @@ describe('BookMetadataLockService', () => {
await expect(service.isFieldLocked(1, 'title')).resolves.toBe(false);
});
- it('filterResolvedMetadata passes chapters through regardless of lock state', async () => {
+ it('filterResolvedMetadata passes chapters through while they are unlocked', async () => {
const { service } = makeService(['title', 'cover']);
const chapters = [{ title: 'Ch 1', startMs: 0 }];
@@ -107,6 +107,28 @@ describe('BookMetadataLockService', () => {
expect('title' in result.resolved).toBe(false);
});
+ it('keeps locked chapters away from provider refreshes and file scans', async () => {
+ const { service } = makeService(['chapters']);
+ const chapters = [{ title: 'Ch 1', startMs: 0 }];
+
+ const resolved = await service.filterResolvedMetadata(1, { title: 'Allowed', chapters }, {});
+ const automated = await service.filterAutomatedBookUpdate(1, { audioMetadata: { chapters, narrators: ['Reader'] } });
+
+ expect(resolved.resolved).toEqual({ title: 'Allowed' });
+ expect(resolved.skippedFields).toEqual(['chapters']);
+ expect(automated.dto).toEqual({ audioMetadata: { narrators: ['Reader'] } });
+ expect(automated.skippedFields).toEqual(['chapters']);
+ });
+
+ it('rejects a plain metadata update that targets locked chapters', async () => {
+ const { service } = makeService(['chapters']);
+
+ await expect(service.assertManualUpdateAllowed(1, { audioMetadata: { chapters: [{ title: 'Ch 1', startMs: 0 }] } })).rejects.toThrow(
+ ConflictException,
+ );
+ await expect(service.assertManualUpdateAllowed(1, { audioMetadata: { narrators: ['Reader'] } })).resolves.toBeUndefined();
+ });
+
it('filters resolved series memberships as a grouped series update', async () => {
const unlocked = makeService();
const memberships = [
diff --git a/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts b/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts
index eb173cdaa..962e80430 100644
--- a/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts
+++ b/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts
@@ -170,9 +170,7 @@ export class BookMetadataLockService {
this.copyResolvedCommunityRating(filteredResolved, resolved, lockedSet, skippedFields);
this.copyResolvedField(filteredResolved, resolved, 'hardcoverEditionId', 'hardcoverEditionId', lockedSet, skippedFields);
- if (resolved.chapters !== undefined) {
- filteredResolved.chapters = resolved.chapters;
- }
+ this.copyResolvedField(filteredResolved, resolved, 'chapters', 'chapters', lockedSet, skippedFields);
if (resolved.coverUrl !== undefined) {
if (lockedSet.has('cover')) skippedFields.add('cover');
@@ -305,8 +303,11 @@ export class BookMetadataLockService {
}
if ('chapters' in dto.audioMetadata) {
- filteredAudioMetadata.chapters = dto.audioMetadata.chapters;
- hasAudioMetadata = true;
+ if (lockedSet.has('chapters')) skippedFields.add('chapters');
+ else {
+ filteredAudioMetadata.chapters = dto.audioMetadata.chapters;
+ hasAudioMetadata = true;
+ }
}
if (hasAudioMetadata) {
@@ -449,6 +450,7 @@ export class BookMetadataLockService {
this.addFieldIfPresent(fields, dto.audioMetadata, 'narrators', 'narrators');
this.addFieldIfPresent(fields, dto.audioMetadata, 'durationSeconds', 'durationSeconds');
this.addFieldIfPresent(fields, dto.audioMetadata, 'abridged', 'abridged');
+ this.addFieldIfPresent(fields, dto.audioMetadata, 'chapters', 'chapters');
}
if (dto.comicMetadata) {
diff --git a/server/src/modules/book/book.module.ts b/server/src/modules/book/book.module.ts
index 74369ad9b..bc76f0bc0 100644
--- a/server/src/modules/book/book.module.ts
+++ b/server/src/modules/book/book.module.ts
@@ -3,6 +3,7 @@ import { SelfWriteRegistryModule } from '../../common/self-write-registry.module
import { AchievementModule } from '../achievement/achievement.module';
import { AppSettingsModule } from '../app-settings/app-settings.module';
+import { AudiobookChaptersModule } from '../audiobook-chapters/audiobook-chapters.module';
import { EmbeddingModule } from '../embedding/embedding.module';
import { FileWriteModule } from '../file-write/file-write.module';
import { LibraryModule } from '../library/library.module';
@@ -34,6 +35,7 @@ import { ReadingAttemptController } from './reading-attempt.controller';
@Module({
imports: [
forwardRef(() => LibraryModule),
+ AudiobookChaptersModule,
BookMetadataLockModule,
BookCoverStoreModule,
CustomMetadataModule,
diff --git a/server/src/modules/book/book.repository.ts b/server/src/modules/book/book.repository.ts
index 22a1418c6..0011484e5 100644
--- a/server/src/modules/book/book.repository.ts
+++ b/server/src/modules/book/book.repository.ts
@@ -28,6 +28,7 @@ import { SeriesIdentityService } from '../../common/services/series-identity.ser
import { SeriesMembershipService } from '../../common/services/series-membership.service';
import { BookQueryBuilder } from './book-query-builder.service';
import { letterJumpBucketExpr } from './jump-bucket-expr';
+import { preserveOutgoingChapters } from '../../db/preserve-audiobook-revision';
import { DB } from '../../db';
import * as schema from '../../db/schema';
import {
@@ -2362,9 +2363,17 @@ export class BookRepository {
fields: Partial,
executor: MetadataUpdateExecutor = this.db,
): Promise {
+ if (executor === this.db && Object.prototype.hasOwnProperty.call(fields, 'chapters')) {
+ return this.db.transaction((tx) => this.updateMetadataFields(bookId, fields, tx));
+ }
const shouldSyncSeries =
Object.prototype.hasOwnProperty.call(fields, 'seriesName') || Object.prototype.hasOwnProperty.call(fields, 'seriesIndex');
- const patch = (await this.seriesIdentity?.resolveMetadataPatch(fields, executor)) ?? fields;
+ const patch = await preserveOutgoingChapters(
+ executor,
+ bookId,
+ (await this.seriesIdentity?.resolveMetadataPatch(fields, executor)) ?? fields,
+ fields.chaptersSource !== 'manual',
+ );
await executor.update(bookMetadata).set(patch).where(eq(bookMetadata.bookId, bookId));
if (shouldSyncSeries) {
await this.seriesMemberships?.syncPrimaryFromMetadata(bookId, executor);
diff --git a/server/src/modules/book/book.service.test.ts b/server/src/modules/book/book.service.test.ts
index 13e970324..daba04328 100644
--- a/server/src/modules/book/book.service.test.ts
+++ b/server/src/modules/book/book.service.test.ts
@@ -1120,7 +1120,7 @@ describe('BookService', () => {
1,
{ title: 'Resolved', authors: ['A'], genres: ['G'], isbn10: '0306406152', isbn13: '9780306406157' },
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
expect(metadataService.downloadAndSaveCover).toHaveBeenCalledWith([{ url: 'https://img/c.jpg' }], 1, 'ebook');
expect(getDetailSpy).toHaveBeenCalledWith(1, user);
@@ -1172,6 +1172,25 @@ describe('BookService', () => {
expect(getDetailSpy).toHaveBeenCalledWith(1, user);
});
+ it('refreshMetadata hands the stored chapters to the pipeline so a fetch only fills a book without any', async () => {
+ const { service, bookRepo, pipeline, coverStore } = makeService();
+ const chapters = [{ title: 'Stored', startMs: 0 }];
+ bookRepo.findById.mockResolvedValue({
+ book: { books: { id: 1, libraryId: 7 }, book_metadata: { title: 'Old', chapters } },
+ authorRows: [],
+ genreRows: [],
+ narratorRows: [],
+ communityRatingRows: [],
+ fileRows: [{ format: 'm4b', role: 'content', mediaOverlayAvailable: false }],
+ });
+ pipeline.runWithSources.mockResolvedValue({ resolved: {}, sources: {}, providerIds: {} });
+ coverStore.fetchState.mockResolvedValue({ media: { hasEbook: false, hasAudio: true }, filled: { ebook: false, audio: true }, locked: [] });
+
+ await service.refreshMetadata(1, true, makeUser());
+
+ expect(pipeline.runWithSources).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ chapters }), 7, expect.any(Object));
+ });
+
it('refreshMetadata answers not found when the book disappears before its cover state is read', async () => {
const { service, bookRepo, pipeline, coverStore } = makeService();
bookRepo.findById.mockResolvedValue({
@@ -1441,7 +1460,7 @@ describe('BookService', () => {
openLibraryId: 'ol-id',
},
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
});
@@ -1482,7 +1501,7 @@ describe('BookService', () => {
hardcoverId: 'the-name-of-the-wind',
},
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
});
@@ -1570,7 +1589,7 @@ describe('BookService', () => {
communityRatings: [{ provider: MetadataProviderKey.HARDCOVER, rating: 4.25, ratingCount: 12345, updatedAt: null }],
},
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
});
@@ -1619,7 +1638,7 @@ describe('BookService', () => {
],
},
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
});
@@ -1668,7 +1687,7 @@ describe('BookService', () => {
},
},
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
});
@@ -1717,7 +1736,7 @@ describe('BookService', () => {
},
},
user,
- { postSaveMode: 'schedule' },
+ { postSaveMode: 'schedule', chaptersSource: 'provider' },
);
});
@@ -1755,7 +1774,7 @@ describe('BookService', () => {
await service.refreshMetadata(1, false, user);
- expect(updateSpy).toHaveBeenCalledWith(1, { authors: ['A'] }, user, { postSaveMode: 'schedule' });
+ expect(updateSpy).toHaveBeenCalledWith(1, { authors: ['A'] }, user, { postSaveMode: 'schedule', chaptersSource: 'provider' });
expect(metadataService.downloadAndSaveCover).not.toHaveBeenCalled();
});
@@ -1927,6 +1946,67 @@ describe('BookService', () => {
expect(seriesExpectedCount.setManual).toHaveBeenNthCalledWith(2, 'Hogwarts Library', null, expect.anything(), options);
});
+ it('updateMetadata cleans chapters instead of rejecting them and records who changed them', async () => {
+ const { service, bookRepo } = makeService();
+ const user = makeUser();
+ const audiobookChapters = { recordCurrentRevision: vi.fn().mockResolvedValue(undefined) };
+ Object.assign(service, { audiobookChapters });
+ vi.spyOn(service, 'verifyBookAccess').mockResolvedValue(undefined);
+ vi.spyOn(service, 'getDetail').mockResolvedValue({ id: 5 } as never);
+
+ await service.updateMetadata(
+ 5,
+ {
+ audioMetadata: {
+ chapters: [
+ { title: 'Two', startMs: 2_000 },
+ { title: '', startMs: 0 },
+ { title: 'Again', startMs: 2_000 },
+ ],
+ },
+ },
+ user,
+ );
+
+ expect(bookRepo.updateMetadataFields).toHaveBeenCalledWith(
+ 5,
+ expect.objectContaining({
+ chapters: [
+ { title: 'Chapter 1', startMs: 0 },
+ { title: 'Two', startMs: 2_000 },
+ ],
+ chaptersSource: 'manual',
+ chaptersUpdatedBy: user.id,
+ chaptersUpdatedAt: expect.any(Date),
+ }),
+ expect.anything(),
+ );
+ expect(audiobookChapters.recordCurrentRevision).toHaveBeenCalledWith(5);
+ });
+
+ it('updateMetadata stores provider chapters without naming a person, and clears the source with the list', async () => {
+ const { service, bookRepo } = makeService();
+ const user = makeUser();
+ vi.spyOn(service, 'verifyBookAccess').mockResolvedValue(undefined);
+ vi.spyOn(service, 'getDetail').mockResolvedValue({ id: 5 } as never);
+
+ await service.updateMetadata(5, { audioMetadata: { chapters: [{ title: 'One', startMs: 0 }] } }, user, { chaptersSource: 'provider' });
+ await service.updateMetadata(5, { audioMetadata: { chapters: [] } }, user);
+
+ expect(bookRepo.updateMetadataFields).toHaveBeenNthCalledWith(
+ 1,
+ 5,
+ expect.objectContaining({ chaptersSource: 'provider', chaptersUpdatedBy: null }),
+ expect.anything(),
+ );
+ expect(bookRepo.updateMetadataFields).toHaveBeenNthCalledWith(
+ 2,
+ 5,
+ expect.objectContaining({ chapters: null, chaptersSource: null, chaptersUpdatedBy: null }),
+ expect.anything(),
+ );
+ });
+
it('updateMetadata with empty communityRatings clears all existing ratings', async () => {
const { service, bookRepo } = makeService();
const user = makeUser();
@@ -5155,6 +5235,39 @@ describe('BookService', () => {
{ title: 'Bonus', startMs: 90_000 },
]);
expect(result.files.map((file) => file.filename)).toEqual(['Bonus.mp3', 'Part 01.mp3', 'Part 02.mp3']);
+ expect(result.audioMetadata).toMatchObject({ chaptersSource: 'tracks', chaptersUpdatedAt: null });
+ });
+
+ it('serves a stored chapter list cleaned for clients, with its source', async () => {
+ const { service, bookRepo } = makeService();
+ vi.spyOn(service, 'verifyBookAccess').mockResolvedValue(undefined);
+ mockAudiobookDetail(bookRepo, [
+ { id: 1, updatedAt: new Date('2026-01-01'), absolutePath: '/audio/Book.m4b', durationSeconds: 600, sortOrder: null },
+ ]);
+ const found = await bookRepo.findById(21);
+ found.book.book_metadata = {
+ title: 'Tracks',
+ durationSeconds: 600,
+ chapters: [
+ { title: 'Later', startMs: 5_000, durationMs: 100 },
+ { title: '', startMs: 0 },
+ { title: 'Duplicate', startMs: 5_000 },
+ ],
+ chaptersSource: 'provider',
+ chaptersUpdatedAt: new Date('2026-03-01T00:00:00.000Z'),
+ };
+ bookRepo.findById.mockResolvedValue(found);
+
+ const result = await service.getDetail(21, makeUser());
+
+ expect(result.audioMetadata).toMatchObject({
+ chapters: [
+ { title: 'Chapter 1', startMs: 0 },
+ { title: 'Later', startMs: 5_000 },
+ ],
+ chaptersSource: 'provider',
+ chaptersUpdatedAt: '2026-03-01T00:00:00.000Z',
+ });
});
it('does not synthesize chapters while any track duration is unknown', async () => {
diff --git a/server/src/modules/book/book.service.ts b/server/src/modules/book/book.service.ts
index 04488895a..c95135665 100644
--- a/server/src/modules/book/book.service.ts
+++ b/server/src/modules/book/book.service.ts
@@ -15,7 +15,8 @@ import { mkdtemp, rm, stat, rename } from 'fs/promises';
import { inArray, type SQL } from 'drizzle-orm';
import { MAX_BOOK_QUERY_OFFSET_ROWS, isBookQueryOffsetWithinLimit } from '../../common/constants/pagination.constants';
-import { compareAudioTracks, coverFetchInputs, resolveIsAudiobook } from '../../common/utils/book-media.utils';
+import { chapterProvenance, normalizeAudiobookChapters, resolveStoredAudiobookChapters } from '../../common/utils/audiobook-chapters.utils';
+import { coverFetchInputs, resolveIsAudiobook } from '../../common/utils/book-media.utils';
import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils';
import { mapWithConcurrency } from '../../common/utils/batch.utils';
import { SelfWriteRegistry } from '../../common/services/self-write-registry.service';
@@ -54,6 +55,7 @@ import {
import type {
AccessLevel,
AudiobookChapter,
+ AudiobookChapterSource,
BookCommunityRating,
BookKoboState,
BookMetadataRefreshPreviewFields,
@@ -91,7 +93,7 @@ import { MetadataExtractionService } from '../metadata/metadata-extraction.servi
import { MetadataScoreService } from '../metadata-score/metadata-score.service';
import { LibraryService } from '../library/library.service';
import { books } from '../../db/schema';
-import { ExistingMetadataFields, MetadataFetchPipeline, ResolvedMetadataFields } from '../metadata-fetch/metadata-fetch-pipeline';
+import { MetadataFetchPipeline, ResolvedMetadataFields, type ExistingMetadataFields } from '../metadata-fetch/metadata-fetch-pipeline';
import type { CacheDirective } from '../../common/provider-cache/provider-cache-spec';
import { splitMetadataIsbn } from '../../common/text-match/isbn-normalize';
import type { MetadataSearchParams } from '../metadata-fetch/providers/metadata-search-params';
@@ -109,6 +111,7 @@ import { BookMetadataLockService } from '../book-metadata-lock/book-metadata-loc
import { BookCoverStore } from '../book-cover-store/book-cover-store.service';
import { selectEmbeddedCoverSources } from '../book-cover-store/cover-sources';
import { CoverSlotReconciler } from '../metadata/cover-slot-reconciler.service';
+import { AudiobookChaptersService } from '../audiobook-chapters/audiobook-chapters.service';
import { BookQueryBuilder } from './book-query-builder.service';
import { AudiobookEbookProgressSyncService } from './audiobook-ebook-progress-sync.service';
import type { DeliveryArtifact } from './delivery-artifact.service';
@@ -338,6 +341,7 @@ export class BookService {
@Optional() private readonly deliverySelection?: DeliverySelectionService,
@Optional() private readonly metadataExtraction?: MetadataExtractionService,
@Optional() private readonly positionConverter?: PositionConverterService,
+ @Optional() private readonly audiobookChapters?: AudiobookChaptersService,
) {
this.appDataPath = this.config.get('storage.appDataPath')!;
}
@@ -1810,7 +1814,7 @@ export class BookService {
id: number,
dto: UpdateBookMetadataDto,
user: RequestUser,
- options: { postSaveMode?: PostMetadataSaveMode } = {},
+ options: { postSaveMode?: PostMetadataSaveMode; chaptersSource?: AudiobookChapterSource } = {},
): Promise {
const event = 'book.update_metadata';
const startedAt = Date.now();
@@ -1820,6 +1824,7 @@ export class BookService {
await this.bookMetadataLockService.assertManualUpdateAllowed(id, dto);
const { detail, scalarFieldCount, write, libraryAutoWriteEnabled } = await this.persistMetadataUpdate(id, dto, user, {
postSaveMode: options.postSaveMode ?? 'schedule',
+ chaptersSource: options.chaptersSource,
});
this.logger.log(
`[${event}] [end] bookId=${id} durationMs=${Date.now() - startedAt} scalarFields=${scalarFieldCount} authorsUpdated=${dto.authors !== undefined} narratorsUpdated=${dto.audioMetadata?.narrators !== undefined} genresUpdated=${dto.genres !== undefined} tagsUpdated=${dto.tags !== undefined} audioMetadataUpdated=${dto.audioMetadata !== undefined} comicMetadataUpdated=${dto.comicMetadata !== undefined} customMetadataUpdated=${dto.customMetadata !== undefined} - metadata update completed`,
@@ -1889,7 +1894,7 @@ export class BookService {
id: number,
dto: UpdateBookMetadataDto,
user: RequestUser,
- options: { lockedFields?: readonly string[]; postSaveMode?: PostMetadataSaveMode } = {},
+ options: { lockedFields?: readonly string[]; postSaveMode?: PostMetadataSaveMode; chaptersSource?: AudiobookChapterSource } = {},
): Promise<{
detail: BookDetailDto;
scalarFieldCount: number;
@@ -1940,7 +1945,14 @@ export class BookService {
if (dto.audioMetadata) {
if (dto.audioMetadata.durationSeconds !== undefined) scalarFields.durationSeconds = dto.audioMetadata.durationSeconds ?? null;
if (dto.audioMetadata.abridged !== undefined) scalarFields.abridged = dto.audioMetadata.abridged ?? false;
- if (dto.audioMetadata.chapters !== undefined) scalarFields.chapters = dto.audioMetadata.chapters ?? null;
+ if (dto.audioMetadata.chapters !== undefined) {
+ // Cleaned rather than rejected: this field shipped without ordering or title rules, and
+ // released clients must keep getting a 200 for a body that used to be accepted.
+ const chapters = dto.audioMetadata.chapters?.length ? normalizeAudiobookChapters(dto.audioMetadata.chapters) : null;
+ scalarFields.chapters = chapters;
+ Object.assign(scalarFields, chapterProvenance(chapters, options.chaptersSource ?? 'manual', user.id));
+ await this.audiobookChapters?.recordCurrentRevision(id);
+ }
}
const communityRatings = this.normalizeCommunityRatingInputs(dto.communityRatings);
@@ -3104,7 +3116,7 @@ export class BookService {
// Without these a fill-missing narrators rule saw none and wrote over the book's own narrators
// and chapters, which the queued fetch never did because it always passed them.
narrators: narratorRows.map((narrator) => narrator.name),
- chapters: meta?.chapters,
+ chapters: meta?.chapters ?? undefined,
};
if (meta?.lockedFields?.length) existingFields.lockedFields = meta.lockedFields;
const coverState = await this.coverStore.fetchState(id);
@@ -3169,7 +3181,7 @@ export class BookService {
const updatedFields = Object.keys(dto).length;
let detail: BookDetailDto | undefined;
if (updatedFields > 0) {
- const saveResult = await this.updateMetadata(id, dto, user, { postSaveMode: 'schedule' });
+ const saveResult = await this.updateMetadata(id, dto, user, { postSaveMode: 'schedule', chaptersSource: 'provider' });
detail = saveResult.book;
}
@@ -3495,7 +3507,10 @@ export class BookService {
const customMetadata = await this.customMetadataService.getBookValues(id, book.books.libraryId);
const hasAudioFiles = fileRows.some((f) => f.format && isAudioFormat(f.format));
const orderedFileRows = hasAudioFiles ? [...fileRows].sort((a, b) => compareBookFilePaths(a.absolutePath, b.absolutePath)) : fileRows;
- const resolvedChapters = this.resolveChapters(meta?.chapters as AudiobookChapter[] | null | undefined, orderedFileRows);
+ const storedChapters = meta?.chapters as AudiobookChapter[] | null | undefined;
+ const resolvedChapters = resolveStoredAudiobookChapters(storedChapters, orderedFileRows);
+ const chapters = resolvedChapters ? normalizeAudiobookChapters(resolvedChapters) : null;
+ const hasStoredChapters = (storedChapters?.length ?? 0) > 0;
const supplementalFields = buildBookDetailSupplementalFields({
readStatus,
hasAudioFiles,
@@ -3503,8 +3518,10 @@ export class BookService {
audioMeta: {
durationSeconds: meta?.durationSeconds,
abridged: meta?.abridged,
+ chaptersSource: hasStoredChapters ? (meta?.chaptersSource ?? null) : chapters?.length ? 'tracks' : null,
+ chaptersUpdatedAt: hasStoredChapters ? (meta?.chaptersUpdatedAt ?? null) : null,
},
- chapters: resolvedChapters,
+ chapters,
comicMeta,
collections: collectionRows,
});
@@ -3942,26 +3959,4 @@ export class BookService {
await this.scoreService.calculateAndSaveMany(bookIds);
}
-
- private resolveChapters(
- stored: AudiobookChapter[] | null | undefined,
- fileRows: { absolutePath: string; format: string | null; durationSeconds: number | null; sortOrder?: number | null }[],
- ): AudiobookChapter[] | null {
- if (stored && stored.length > 0) return stored;
-
- const audioFiles = fileRows.filter((f) => f.format && isAudioFormat(f.format)).sort(compareAudioTracks);
- if (audioFiles.length < 2) return stored ?? null;
- // Offsets past a track of unknown length would be wrong; clients fall back to one entry per track.
- if (audioFiles.some((f) => f.durationSeconds === null || f.durationSeconds <= 0)) return stored ?? null;
-
- const chapters: AudiobookChapter[] = [];
- let offsetMs = 0;
- for (const f of audioFiles) {
- const nameWithExt = basename(f.absolutePath);
- const title = nameWithExt.replace(/\.[^.]+$/, '');
- chapters.push({ title, startMs: offsetMs });
- offsetMs += Math.round((f.durationSeconds ?? 0) * 1000);
- }
- return chapters;
- }
}
diff --git a/server/src/modules/book/dto/book-detail.dto.ts b/server/src/modules/book/dto/book-detail.dto.ts
index 51ab4fb8b..7be1d4ab0 100644
--- a/server/src/modules/book/dto/book-detail.dto.ts
+++ b/server/src/modules/book/dto/book-detail.dto.ts
@@ -1,5 +1,6 @@
import type {
AudiobookChapter,
+ AudiobookChapterSource,
BookFileWriteStatus,
BookSeriesMembership,
BookMetadataLockField,
@@ -35,6 +36,8 @@ export class AudioMetadataDto {
durationSeconds: number | null;
abridged: boolean;
chapters: AudiobookChapter[] | null;
+ chaptersSource: AudiobookChapterSource | null;
+ chaptersUpdatedAt: string | null;
}
export class BookDetailDto {
diff --git a/server/src/modules/book/utils/build-book-detail-supplemental-fields.test.ts b/server/src/modules/book/utils/build-book-detail-supplemental-fields.test.ts
index b9466c053..f44f5def0 100644
--- a/server/src/modules/book/utils/build-book-detail-supplemental-fields.test.ts
+++ b/server/src/modules/book/utils/build-book-detail-supplemental-fields.test.ts
@@ -56,6 +56,8 @@ describe('buildBookDetailSupplementalFields', () => {
durationSeconds: 5400,
abridged: false,
chapters: [{ title: 'Chapter 1', startMs: 0 }],
+ chaptersSource: null,
+ chaptersUpdatedAt: null,
});
expect(result.collections).toEqual([{ id: 9, name: 'Favorites' }]);
});
diff --git a/server/src/modules/book/utils/build-book-detail-supplemental-fields.ts b/server/src/modules/book/utils/build-book-detail-supplemental-fields.ts
index f4537ce98..7c65d8fd5 100644
--- a/server/src/modules/book/utils/build-book-detail-supplemental-fields.ts
+++ b/server/src/modules/book/utils/build-book-detail-supplemental-fields.ts
@@ -1,4 +1,4 @@
-import type { AudiobookChapter, ComicMetadataFields, NarratorRef, UserBookStatus } from '@bookorbit/types';
+import type { AudiobookChapter, AudiobookChapterSource, ComicMetadataFields, NarratorRef, UserBookStatus } from '@bookorbit/types';
import type { BookDetailDto } from '../dto/book-detail.dto';
type NarratorRow = {
@@ -10,6 +10,8 @@ type NarratorRow = {
type AudioMetaSource = {
durationSeconds: number | null | undefined;
abridged: boolean | null | undefined;
+ chaptersSource?: AudiobookChapterSource | null;
+ chaptersUpdatedAt?: Date | null;
};
type ComicMetaSource = {
@@ -59,6 +61,8 @@ function buildAudioMetadata(
durationSeconds: audioMeta.durationSeconds ?? null,
abridged: audioMeta.abridged ?? false,
chapters,
+ chaptersSource: audioMeta.chaptersSource ?? null,
+ chaptersUpdatedAt: audioMeta.chaptersUpdatedAt?.toISOString() ?? null,
};
}
diff --git a/server/src/modules/chapter-editor/chapter-auto-apply.test.ts b/server/src/modules/chapter-editor/chapter-auto-apply.test.ts
new file mode 100644
index 000000000..e6e9a128f
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-auto-apply.test.ts
@@ -0,0 +1,259 @@
+import type { ChapterLookupNode, ChapterLookupResult } from '@bookorbit/types';
+
+import { flattenLookupTree, planAutoApply } from './chapter-auto-apply';
+
+function node(title: string, startMs: number, children: ChapterLookupNode[] = []): ChapterLookupNode {
+ return { title, startMs, lengthMs: null, children };
+}
+
+// A real release: Audible's runtime counts a 4.04 s "This is Audible" lead-in and a 5.06 s sign-off.
+const RUNTIME_MS = 37_206_520;
+const INTRO_MS = 4_040;
+const OUTRO_MS = 5_061;
+const WITHOUT_BRANDING_MS = RUNTIME_MS - INTRO_MS - OUTRO_MS;
+
+const CHAPTERS = [
+ node('Opening Credits', 0),
+ node('Chapter 1', 21_610),
+ node('Chapter 2', 2_480_310),
+ node('Chapter 3', 5_102_844),
+ node('Chapter 4', 9_870_115),
+ node('End Credits', RUNTIME_MS - 40_000),
+];
+
+function lookup(overrides: Partial = {}): ChapterLookupResult {
+ return {
+ provider: 'audible',
+ asin: 'B0TESTASIN',
+ region: 'us',
+ chapters: CHAPTERS,
+ runtimeMs: RUNTIME_MS,
+ brandIntroMs: INTRO_MS,
+ brandOutroMs: OUTRO_MS,
+ isAccurate: true,
+ ...overrides,
+ };
+}
+
+/** `count` chapters spread evenly over `totalMs`, the first at 0. */
+function evenChapters(count: number, totalMs: number): ChapterLookupNode[] {
+ return Array.from({ length: count }, (_, index) => node(`Chapter ${index + 1}`, Math.floor((index * totalMs) / count)));
+}
+
+describe('flattenLookupTree', () => {
+ it('lists parts and their chapters in reading order, keeping each part heading as a chapter of its own', () => {
+ const tree = [
+ node('Opening Credits', 0),
+ node('Part One', 18_000, [node('Chapter 1', 19_200), node('Chapter 2', 1_200_000, [node('Interlude', 1_500_000)])]),
+ node('Part Two', 2_400_000, [node('Chapter 3', 2_401_500)]),
+ ];
+
+ expect(flattenLookupTree(tree)).toEqual([
+ { title: 'Opening Credits', startMs: 0 },
+ { title: 'Part One', startMs: 18_000 },
+ { title: 'Chapter 1', startMs: 19_200 },
+ { title: 'Chapter 2', startMs: 1_200_000 },
+ { title: 'Interlude', startMs: 1_500_000 },
+ { title: 'Part Two', startMs: 2_400_000 },
+ { title: 'Chapter 3', startMs: 2_401_500 },
+ ]);
+ });
+
+ it('returns nothing for an empty tree', () => {
+ expect(flattenLookupTree([])).toEqual([]);
+ });
+});
+
+describe('planAutoApply', () => {
+ it('leaves combined part and leaf titles that exceed the editor limit for review', () => {
+ expect(planAutoApply(lookup({ chapters: [node('P'.repeat(300), 0, [node('C'.repeat(300), 0)]), node('Second', 60_000)] }), RUNTIME_MS)).toEqual({
+ kind: 'review',
+ reason: 'implausible',
+ });
+ });
+
+ it.each([
+ { name: 'unknown', totalDurationMs: null },
+ { name: 'zero', totalDurationMs: 0 },
+ { name: 'negative', totalDurationMs: -1 },
+ ])('leaves a book whose length is $name for review', ({ totalDurationMs }) => {
+ expect(planAutoApply(lookup(), totalDurationMs)).toEqual({ kind: 'review', reason: 'unknown_length' });
+ });
+
+ it.each([
+ { name: 'inexact', isAccurate: false },
+ { name: 'unrated', isAccurate: null },
+ ])('leaves timings Audible calls $name for review, even when the runtime matches', ({ isAccurate }) => {
+ expect(planAutoApply(lookup({ isAccurate }), WITHOUT_BRANDING_MS)).toEqual({ kind: 'review', reason: 'not_exact' });
+ expect(planAutoApply(lookup({ isAccurate }), RUNTIME_MS)).toEqual({ kind: 'review', reason: 'not_exact' });
+ });
+
+ it('leaves a lookup without a runtime for review, since nothing can be compared', () => {
+ expect(planAutoApply(lookup({ runtimeMs: null }), WITHOUT_BRANDING_MS)).toEqual({ kind: 'review', reason: 'runtime_mismatch' });
+ });
+
+ it('leaves a book 7.4 s shorter than the stripped runtime for review', () => {
+ // 37,190,000 against 37,197,419 without branding (7.4 s) and 37,206,520 with it (16.5 s).
+ expect(planAutoApply(lookup(), 37_190_000)).toEqual({ kind: 'review', reason: 'runtime_mismatch' });
+ });
+
+ it('applies to a file without branding, pulling every chapter earlier by the lead-in and the first one to 0', () => {
+ const plan = planAutoApply(lookup(), WITHOUT_BRANDING_MS);
+
+ expect(plan).toEqual({
+ kind: 'apply',
+ offsetMs: -INTRO_MS,
+ chapters: [
+ { title: 'Opening Credits', startMs: 0 },
+ { title: 'Chapter 1', startMs: 17_570 },
+ { title: 'Chapter 2', startMs: 2_476_270 },
+ { title: 'Chapter 3', startMs: 5_098_804 },
+ { title: 'Chapter 4', startMs: 9_866_075 },
+ { title: 'End Credits', startMs: RUNTIME_MS - 40_000 - INTRO_MS },
+ ],
+ });
+ });
+
+ it('applies to a file that kept its branding, with the timings as Audible gives them', () => {
+ const plan = planAutoApply(lookup(), RUNTIME_MS - 1_500);
+
+ expect(plan).toEqual({
+ kind: 'apply',
+ offsetMs: 0,
+ chapters: CHAPTERS.map((chapter) => ({ title: chapter.title, startMs: chapter.startMs })),
+ });
+ });
+
+ it('accepts a difference of exactly 2 s either side, and nothing more', () => {
+ expect(planAutoApply(lookup(), WITHOUT_BRANDING_MS + 2_000)).toMatchObject({ kind: 'apply', offsetMs: -INTRO_MS });
+ expect(planAutoApply(lookup(), WITHOUT_BRANDING_MS - 2_000)).toMatchObject({ kind: 'apply', offsetMs: -INTRO_MS });
+ expect(planAutoApply(lookup(), RUNTIME_MS + 2_000)).toMatchObject({ kind: 'apply', offsetMs: 0 });
+ expect(planAutoApply(lookup(), WITHOUT_BRANDING_MS - 2_001)).toEqual({ kind: 'review', reason: 'runtime_mismatch' });
+ expect(planAutoApply(lookup(), RUNTIME_MS + 2_001)).toEqual({ kind: 'review', reason: 'runtime_mismatch' });
+ });
+
+ it('prefers the stripped reading when the branding is short enough for both to match', () => {
+ const plan = planAutoApply(lookup({ brandIntroMs: 800, brandOutroMs: 700 }), RUNTIME_MS - 1_000);
+
+ expect(plan).toMatchObject({ kind: 'apply', offsetMs: -800 });
+ });
+
+ it('starts the first chapter at 0 even when Audible places it later', () => {
+ const chapters = [node('Chapter 1', 12_000), ...CHAPTERS.slice(2)];
+
+ const plan = planAutoApply(lookup({ chapters, brandIntroMs: 0, brandOutroMs: 0 }), RUNTIME_MS);
+
+ expect(plan.kind).toBe('apply');
+ expect(plan.kind === 'apply' && plan.chapters.slice(0, 2)).toEqual([
+ { title: 'Chapter 1', startMs: 0 },
+ { title: 'Chapter 2', startMs: 2_480_310 },
+ ]);
+ });
+
+ it('applies a flattened tree, part headings kept, trimmed and in order', () => {
+ const chapters = [
+ node('Opening Credits', 0),
+ node(' Part One ', 30_000, [node('Chapter 1', 31_500), node('Chapter 2', 4_000_000)]),
+ node('Part Two', 18_000_000, [node('Chapter 3', 18_001_200)]),
+ ];
+
+ const plan = planAutoApply(lookup({ chapters }), RUNTIME_MS);
+
+ expect(plan).toEqual({
+ kind: 'apply',
+ offsetMs: 0,
+ chapters: [
+ { title: 'Opening Credits', startMs: 0 },
+ { title: 'Part One', startMs: 30_000 },
+ { title: 'Chapter 1', startMs: 31_500 },
+ { title: 'Chapter 2', startMs: 4_000_000 },
+ { title: 'Part Two', startMs: 18_000_000 },
+ { title: 'Chapter 3', startMs: 18_001_200 },
+ ],
+ });
+ });
+
+ it('drops chapters the shift moves before 0 onto one start, keeping the first title', () => {
+ const chapters = [node('Opening Credits', 0), node('Prologue', 3_000), node('Chapter 1', 600_000)];
+
+ const plan = planAutoApply(lookup({ chapters }), WITHOUT_BRANDING_MS);
+
+ expect(plan).toMatchObject({
+ kind: 'apply',
+ chapters: [
+ { title: 'Opening Credits', startMs: 0 },
+ { title: 'Chapter 1', startMs: 600_000 - INTRO_MS },
+ ],
+ });
+ });
+
+ it('drops chapters starting at or after the end of the book', () => {
+ // Shifted by the lead-in, the sign-off starts exactly where the stripped file ends.
+ const chapters = [...CHAPTERS.slice(0, 5), node('End Credits', WITHOUT_BRANDING_MS + INTRO_MS)];
+
+ const plan = planAutoApply(lookup({ chapters }), WITHOUT_BRANDING_MS);
+
+ expect(plan).toMatchObject({ kind: 'apply' });
+ expect(plan.kind === 'apply' && plan.chapters.map((chapter) => chapter.title)).toEqual([
+ 'Opening Credits',
+ 'Chapter 1',
+ 'Chapter 2',
+ 'Chapter 3',
+ 'Chapter 4',
+ ]);
+ });
+
+ it.each([
+ { name: 'a single chapter', chapters: [node('Chapter 1', 0)] },
+ {
+ name: 'one chapter left once those past the end are dropped',
+ chapters: [node('Chapter 1', 0), node('Chapter 2', RUNTIME_MS + 10_000)],
+ },
+ { name: 'a blank title', chapters: [node('Chapter 1', 0), node(' ', 600_000), node('Chapter 3', 1_200_000)] },
+ { name: 'a blank part heading', chapters: [node('Chapter 1', 0), node('', 600_000, [node('Chapter 2', 601_000)])] },
+ { name: 'chapters averaging under 30 s', chapters: evenChapters(1_241, RUNTIME_MS) },
+ ])('leaves a list with $name for review', ({ chapters }) => {
+ expect(planAutoApply(lookup({ chapters }), RUNTIME_MS)).toEqual({ kind: 'review', reason: 'implausible' });
+ });
+
+ it('leaves two chapters the shift moves onto one start for review, since one chapter is left', () => {
+ const chapters = [node('Opening Credits', 0), node('Chapter 1', 2_000)];
+
+ expect(planAutoApply(lookup({ chapters }), WITHOUT_BRANDING_MS)).toEqual({ kind: 'review', reason: 'implausible' });
+ });
+
+ it('accepts chapters averaging exactly 30 s', () => {
+ const totalMs = 30_000 * 40;
+
+ const plan = planAutoApply(lookup({ chapters: evenChapters(40, totalMs), runtimeMs: totalMs, brandIntroMs: 0, brandOutroMs: 0 }), totalMs);
+
+ expect(plan.kind).toBe('apply');
+ expect(plan.kind === 'apply' && plan.chapters).toHaveLength(40);
+ });
+
+ it('reads the provider list without changing it', () => {
+ const chapters = [node(' Opening Credits ', 0), node('Part One', 30_000, [node('Chapter 1', 31_500)])];
+ const before = JSON.stringify(chapters);
+
+ planAutoApply(lookup({ chapters }), WITHOUT_BRANDING_MS);
+
+ expect(JSON.stringify(chapters)).toBe(before);
+ });
+});
+
+describe('Chapter lookup regressions', () => {
+ it('never auto applies a response the parser truncated', () => {
+ expect(planAutoApply(lookup({ truncated: true }), RUNTIME_MS).kind).toBe('review');
+ });
+ it('keeps the full runtime clock when it matches more closely than stripped branding', () => {
+ const plan = planAutoApply(lookup(), RUNTIME_MS);
+ expect(plan.kind).toBe('apply');
+ if (plan.kind === 'apply') expect(plan.chapters[1].startMs).toBe(CHAPTERS[1].startMs);
+ });
+ it('preserves the first child name when a part heading shares its start', () => {
+ expect(flattenLookupTree([node('Part One', 0, [node('Chapter One', 0), node('Chapter Two', 60_000)])])).toEqual([
+ { title: 'Part One: Chapter One', startMs: 0 },
+ { title: 'Chapter Two', startMs: 60_000 },
+ ]);
+ });
+});
diff --git a/server/src/modules/chapter-editor/chapter-auto-apply.ts b/server/src/modules/chapter-editor/chapter-auto-apply.ts
new file mode 100644
index 000000000..754739633
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-auto-apply.ts
@@ -0,0 +1,57 @@
+import {
+ AUDIOBOOK_CHAPTER_LIMITS,
+ type AudiobookChapter,
+ type ChapterLookupNode,
+ type ChapterLookupResult,
+ type ChapterLookupReviewReason,
+} from '@bookorbit/types';
+
+import { normalizeAudiobookChapters } from '../../common/utils/audiobook-chapters.utils';
+
+/** How far the runtimes may differ for the timings to be taken as they are, without a stretch. */
+const RUNTIME_TOLERANCE_MS = 2_000;
+/** Chapters averaging shorter than this read as a list of tracks or sections, not chapters. */
+const MIN_AVERAGE_CHAPTER_MS = 30_000;
+
+export type AutoApplyPlan = { kind: 'apply'; chapters: AudiobookChapter[]; offsetMs: number } | { kind: 'review'; reason: ChapterLookupReviewReason };
+
+/** Audible's tree as one list, part headings kept as chapters of their own, as the review sheet does by default. */
+export function flattenLookupTree(nodes: readonly ChapterLookupNode[]): AudiobookChapter[] {
+ return nodes.flatMap((node) => {
+ const children = flattenLookupTree(node.children);
+ if (children[0]?.startMs === node.startMs) {
+ return [{ ...children[0], title: `${node.title}: ${children[0].title}` }, ...children.slice(1)];
+ }
+ return [{ title: node.title, startMs: node.startMs }, ...children];
+ });
+}
+
+/**
+ * Whether a lookup can be applied with nobody looking. Only when Audible calls its timings exact,
+ * the book's length matches Audible's runtime either with its branding or without it, and the list
+ * looks like chapters. Anything else is left for a person with the reason it stopped.
+ */
+export function planAutoApply(lookup: ChapterLookupResult, totalDurationMs: number | null): AutoApplyPlan {
+ if (!totalDurationMs || totalDurationMs <= 0) return { kind: 'review', reason: 'unknown_length' };
+ if (lookup.truncated) return { kind: 'review', reason: 'implausible' };
+ if (lookup.isAccurate !== true) return { kind: 'review', reason: 'not_exact' };
+ if (lookup.runtimeMs === null) return { kind: 'review', reason: 'runtime_mismatch' };
+
+ const withoutBranding = lookup.runtimeMs - lookup.brandIntroMs - lookup.brandOutroMs;
+ const strippedError = Math.abs(totalDurationMs - withoutBranding);
+ const fullError = Math.abs(totalDurationMs - lookup.runtimeMs);
+ if (Math.min(strippedError, fullError) > RUNTIME_TOLERANCE_MS) return { kind: 'review', reason: 'runtime_mismatch' };
+ if (strippedError === fullError && lookup.brandIntroMs > 0) return { kind: 'review', reason: 'runtime_mismatch' };
+ const offsetMs = strippedError < fullError ? -lookup.brandIntroMs : 0;
+
+ const shifted = flattenLookupTree(lookup.chapters)
+ .map((chapter) => ({ title: chapter.title.trim(), startMs: Math.max(0, Math.round(chapter.startMs + offsetMs)) }))
+ .filter((chapter) => chapter.startMs < totalDurationMs);
+ if (shifted.some((chapter) => !chapter.title || chapter.title.length > AUDIOBOOK_CHAPTER_LIMITS.maxTitleLength))
+ return { kind: 'review', reason: 'implausible' };
+ // Judged after normalizing: chapters the shift clamps onto one start collapse into one.
+ const chapters = normalizeAudiobookChapters(shifted);
+ if (chapters.length < 2 || totalDurationMs / chapters.length < MIN_AVERAGE_CHAPTER_MS) return { kind: 'review', reason: 'implausible' };
+ chapters[0] = { ...chapters[0]!, startMs: 0 };
+ return { kind: 'apply', chapters, offsetMs };
+}
diff --git a/server/src/modules/chapter-editor/chapter-editor.controller.ts b/server/src/modules/chapter-editor/chapter-editor.controller.ts
new file mode 100644
index 000000000..044a69bf7
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-editor.controller.ts
@@ -0,0 +1,167 @@
+import { Body, Controller, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Post, Put, Query, ParseUUIDPipe, Req, Res } from '@nestjs/common';
+import type { FastifyRequest, FastifyReply } from 'fastify';
+import { withRequestAbort } from '../../common/utils/request-abort.utils';
+import { AuditAction, AuditResource, Permission } from '@bookorbit/types';
+
+import { Auditable } from '../../common/decorators/auditable.decorator';
+import { CurrentUser } from '../../common/decorators/current-user.decorator';
+import { RequirePermission } from '../../common/decorators/require-permission.decorator';
+import type { RequestUser } from '../../common/types/request-user';
+import { ChapterJobService } from './chapter-job.service';
+import { ChapterEditorService } from './chapter-editor.service';
+import { AnalyzeBookChaptersDto } from './dto/analyze-book-chapters.dto';
+import { EbookBookChaptersDto } from './dto/ebook-book-chapters.dto';
+import { LookupBookChaptersDto } from './dto/lookup-book-chapters.dto';
+import { RevertBookChaptersDto } from './dto/revert-book-chapters.dto';
+import { WaveformBookChaptersDto } from './dto/waveform-book-chapters.dto';
+import { SaveBookChaptersDto } from './dto/save-book-chapters.dto';
+import { StartChapterJobDto } from './dto/start-chapter-job.dto';
+import { WriteBookChaptersDto } from './dto/write-book-chapters.dto';
+
+@Controller('books/:bookId/chapters')
+export class ChapterEditorController {
+ constructor(
+ private readonly service: ChapterEditorService,
+ private readonly jobs: ChapterJobService,
+ ) {}
+
+ @Post('jobs/write')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ @Auditable({
+ action: AuditAction.BookWriteAndRename,
+ resource: AuditResource.Book,
+ getResourceId: (req) => parseInt(req.params['bookId'] as string, 10),
+ description: (req) => `Queued chapter file write for book #${req.params['bookId']}`,
+ })
+ startWrite(@Param('bookId', ParseIntPipe) bookId: number, @Body() dto: StartChapterJobDto, @CurrentUser() user: RequestUser) {
+ return this.jobs.start(bookId, 'write', dto.baseRevision, user, dto.retry);
+ }
+
+ @Post('jobs/read')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ startRead(@Param('bookId', ParseIntPipe) bookId: number, @Body() dto: StartChapterJobDto, @CurrentUser() user: RequestUser) {
+ return this.jobs.start(bookId, 'read', dto.baseRevision, user, dto.retry);
+ }
+
+ @Post('jobs/revert')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ @Auditable({
+ action: AuditAction.BookMetadataUpdate,
+ resource: AuditResource.Book,
+ getResourceId: (req) => parseInt(req.params['bookId'] as string, 10),
+ description: (req) => `Queued chapter revert for book #${req.params['bookId']}`,
+ })
+ startRevert(@Param('bookId', ParseIntPipe) bookId: number, @Body() dto: StartChapterJobDto, @CurrentUser() user: RequestUser) {
+ return this.jobs.start(bookId, 'revert', dto.baseRevision, user, dto.retry);
+ }
+
+ @Get('jobs')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ latestJob(@Param('bookId', ParseIntPipe) bookId: number, @CurrentUser() user: RequestUser) {
+ return this.jobs.latest(bookId, user);
+ }
+
+ @Get('jobs/:jobId')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ jobStatus(@Param('bookId', ParseIntPipe) bookId: number, @Param('jobId', ParseUUIDPipe) jobId: string, @CurrentUser() user: RequestUser) {
+ return this.jobs.status(bookId, jobId, user);
+ }
+
+ @Get()
+ getState(@Param('bookId', ParseIntPipe) bookId: number, @CurrentUser() user: RequestUser) {
+ return this.service.getState(bookId, user);
+ }
+
+ @Put()
+ @RequirePermission(Permission.LibraryEditMetadata)
+ @Auditable({
+ action: AuditAction.BookMetadataUpdate,
+ resource: AuditResource.Book,
+ getResourceId: (req) => parseInt(req.params['bookId'] as string, 10),
+ description: (req) => `Updated chapters for book #${req.params['bookId']}`,
+ })
+ save(@Param('bookId', ParseIntPipe) bookId: number, @Body() dto: SaveBookChaptersDto, @CurrentUser() user: RequestUser) {
+ return this.service.save(bookId, dto, user);
+ }
+
+ @Post('revert')
+ @HttpCode(HttpStatus.OK)
+ @RequirePermission(Permission.LibraryEditMetadata)
+ @Auditable({
+ action: AuditAction.BookMetadataUpdate,
+ resource: AuditResource.Book,
+ getResourceId: (req) => parseInt(req.params['bookId'] as string, 10),
+ description: (req) => `Reverted chapters to the audio files for book #${req.params['bookId']}`,
+ })
+ revert(@Param('bookId', ParseIntPipe) bookId: number, @Body() dto: RevertBookChaptersDto, @CurrentUser() user: RequestUser) {
+ return this.service.revert(bookId, dto, user);
+ }
+
+ @Post('write')
+ @HttpCode(HttpStatus.OK)
+ @RequirePermission(Permission.LibraryEditMetadata)
+ @Auditable({
+ action: AuditAction.BookWriteAndRename,
+ resource: AuditResource.Book,
+ getResourceId: (req) => parseInt(req.params['bookId'] as string, 10),
+ description: (req) => `Wrote chapters into the audio files of book #${req.params['bookId']}`,
+ })
+ writeToFiles(@Param('bookId', ParseIntPipe) bookId: number, @Body() dto: WriteBookChaptersDto, @CurrentUser() user: RequestUser) {
+ return this.service.writeToFiles(bookId, dto, user);
+ }
+
+ @Get('embedded')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ readEmbedded(@Param('bookId', ParseIntPipe) bookId: number, @CurrentUser() user: RequestUser) {
+ return this.service.readEmbedded(bookId, user);
+ }
+
+ @Get('lookup')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ lookup(@Param('bookId', ParseIntPipe) bookId: number, @Query() dto: LookupBookChaptersDto, @CurrentUser() user: RequestUser) {
+ return this.service.lookup(bookId, dto, user);
+ }
+
+ @Get('ebook')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ readEbook(@Param('bookId', ParseIntPipe) bookId: number, @Query() dto: EbookBookChaptersDto, @CurrentUser() user: RequestUser) {
+ return this.service.readEbook(bookId, dto.source, user);
+ }
+
+ @Post('analyze')
+ @HttpCode(HttpStatus.OK)
+ @RequirePermission(Permission.LibraryEditMetadata)
+ analyze(
+ @Param('bookId', ParseIntPipe) bookId: number,
+ @Body() dto: AnalyzeBookChaptersDto,
+ @CurrentUser() user: RequestUser,
+ @Req() request: FastifyRequest,
+ @Res({ passthrough: true }) reply: FastifyReply,
+ ) {
+ return withRequestAbort(request, reply, (signal) => this.service.analyze(bookId, dto, user, signal));
+ }
+
+ @Get('waveform')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ waveform(
+ @Param('bookId', ParseIntPipe) bookId: number,
+ @Query() dto: WaveformBookChaptersDto,
+ @CurrentUser() user: RequestUser,
+ @Req() request: FastifyRequest,
+ @Res({ passthrough: true }) reply: FastifyReply,
+ ) {
+ return withRequestAbort(request, reply, (signal) => this.service.waveform(bookId, dto, user, signal));
+ }
+
+ @Get('versions')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ listVersions(@Param('bookId', ParseIntPipe) bookId: number, @CurrentUser() user: RequestUser) {
+ return this.service.listVersions(bookId, user);
+ }
+
+ @Get('versions/:versionId')
+ @RequirePermission(Permission.LibraryEditMetadata)
+ getVersion(@Param('bookId', ParseIntPipe) bookId: number, @Param('versionId', ParseIntPipe) versionId: number, @CurrentUser() user: RequestUser) {
+ return this.service.getVersion(bookId, versionId, user);
+ }
+}
diff --git a/server/src/modules/chapter-editor/chapter-editor.module.ts b/server/src/modules/chapter-editor/chapter-editor.module.ts
new file mode 100644
index 000000000..e6205bd33
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-editor.module.ts
@@ -0,0 +1,34 @@
+import { Module } from '@nestjs/common';
+
+import { AudiobookChaptersModule } from '../audiobook-chapters/audiobook-chapters.module';
+import { BookMetadataLockModule } from '../book-metadata-lock/book-metadata-lock.module';
+import { BookModule } from '../book/book.module';
+import { FileWriteModule } from '../file-write/file-write.module';
+import { MetadataModule } from '../metadata/metadata.module';
+import { MetadataPreferencesModule } from '../metadata-preferences/metadata-preferences.module';
+import { UserModule } from '../user/user.module';
+import { EpubModule } from '../reader/epub/epub.module';
+import { ChapterJobRepository } from './chapter-job.repository';
+import { ChapterJobService } from './chapter-job.service';
+import { ChapterEditorController } from './chapter-editor.controller';
+import { ChapterEditorRepository } from './chapter-editor.repository';
+import { ChapterEditorService } from './chapter-editor.service';
+import { ChapterLookupService } from './chapter-lookup.service';
+import { ChapterPauseService } from './chapter-pause.service';
+
+@Module({
+ imports: [
+ UserModule,
+ AudiobookChaptersModule,
+ BookMetadataLockModule,
+ BookModule,
+ EpubModule,
+ FileWriteModule,
+ MetadataModule,
+ MetadataPreferencesModule,
+ ],
+ controllers: [ChapterEditorController],
+ providers: [ChapterJobRepository, ChapterJobService, ChapterEditorRepository, ChapterEditorService, ChapterLookupService, ChapterPauseService],
+ exports: [ChapterEditorService],
+})
+export class ChapterEditorModule {}
diff --git a/server/src/modules/chapter-editor/chapter-editor.repository.ts b/server/src/modules/chapter-editor/chapter-editor.repository.ts
new file mode 100644
index 000000000..62fabe40d
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-editor.repository.ts
@@ -0,0 +1,92 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type { AudiobookChapter } from '@bookorbit/types';
+import { and, count, desc, eq } from 'drizzle-orm';
+import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
+
+import { DB } from '../../db';
+import * as schema from '../../db/schema';
+import { bookChapterVersions, bookFiles, bookMetadata, users } from '../../db/schema';
+
+type Db = NodePgDatabase;
+
+// Writes keep 20 per book; this only bounds the read if rows ever arrive another way.
+const VERSIONS_LISTED = 50;
+
+const VERSION_COLUMNS = {
+ id: bookChapterVersions.id,
+ source: bookChapterVersions.source,
+ chapterCount: bookChapterVersions.chapterCount,
+ createdAt: bookChapterVersions.createdAt,
+ createdById: bookChapterVersions.createdBy,
+ createdByName: users.name,
+};
+
+@Injectable()
+export class ChapterEditorRepository {
+ constructor(@Inject(DB) private readonly db: Db) {}
+
+ async findChapterRow(bookId: number) {
+ const [row] = await this.db
+ .select({
+ chapters: bookMetadata.chapters,
+ source: bookMetadata.chaptersSource,
+ updatedAt: bookMetadata.chaptersUpdatedAt,
+ updatedById: bookMetadata.chaptersUpdatedBy,
+ updatedByName: users.name,
+ audibleId: bookMetadata.audibleId,
+ durationSeconds: bookMetadata.durationSeconds,
+ })
+ .from(bookMetadata)
+ .leftJoin(users, eq(users.id, bookMetadata.chaptersUpdatedBy))
+ .where(eq(bookMetadata.bookId, bookId))
+ .limit(1);
+ return row ? { ...row, chapters: (row.chapters as AudiobookChapter[] | null) ?? null } : null;
+ }
+
+ findTrackRows(bookId: number) {
+ return this.db
+ .select({
+ id: bookFiles.id,
+ fileHash: bookFiles.fileHash,
+ absolutePath: bookFiles.absolutePath,
+ format: bookFiles.format,
+ durationSeconds: bookFiles.durationSeconds,
+ sortOrder: bookFiles.sortOrder,
+ })
+ .from(bookFiles)
+ .where(eq(bookFiles.bookId, bookId));
+ }
+
+ findEbookFiles(bookId: number) {
+ return this.db
+ .select({ id: bookFiles.id, format: bookFiles.format, role: bookFiles.role, mediaOverlayAvailable: bookFiles.mediaOverlayAvailable })
+ .from(bookFiles)
+ .where(and(eq(bookFiles.bookId, bookId), eq(bookFiles.format, 'epub')))
+ .orderBy(bookFiles.id);
+ }
+
+ async countVersions(bookId: number): Promise {
+ const [row] = await this.db.select({ total: count() }).from(bookChapterVersions).where(eq(bookChapterVersions.bookId, bookId));
+ return row?.total ?? 0;
+ }
+
+ listVersions(bookId: number) {
+ return this.db
+ .select(VERSION_COLUMNS)
+ .from(bookChapterVersions)
+ .leftJoin(users, eq(users.id, bookChapterVersions.createdBy))
+ .where(eq(bookChapterVersions.bookId, bookId))
+ .orderBy(desc(bookChapterVersions.createdAt), desc(bookChapterVersions.id))
+ .limit(VERSIONS_LISTED);
+ }
+
+ async findVersion(bookId: number, versionId: number) {
+ const [row] = await this.db
+ .select({ ...VERSION_COLUMNS, chapters: bookChapterVersions.chapters })
+ .from(bookChapterVersions)
+ .leftJoin(users, eq(users.id, bookChapterVersions.createdBy))
+ .where(and(eq(bookChapterVersions.bookId, bookId), eq(bookChapterVersions.id, versionId)))
+ .limit(1);
+ return row ?? null;
+ }
+}
diff --git a/server/src/modules/chapter-editor/chapter-editor.service.test.ts b/server/src/modules/chapter-editor/chapter-editor.service.test.ts
new file mode 100644
index 000000000..75c0a9da0
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-editor.service.test.ts
@@ -0,0 +1,1179 @@
+import { ConflictException, HttpException, NotFoundException } from '@nestjs/common';
+import {
+ Permission,
+ BOOK_CHAPTERS_ERROR_CODES,
+ CHAPTER_PAUSE_LIMITS,
+ type AudiobookChapter,
+ type AudiobookChapterSource,
+ type BookChapterWriteBack,
+ type ChapterLookupResult,
+ type ChapterPause,
+} from '@bookorbit/types';
+
+import type { RequestUser } from '../../common/types/request-user';
+import { ChapterWriteUnavailableError } from '../file-write/file-write.service';
+import { ChapterEditorService } from './chapter-editor.service';
+import { AudioToolMissingError, type PauseSegment } from './chapter-pause.service';
+
+const BOOK_ID = 9;
+const UPDATED_AT = new Date('2026-09-30T12:00:00.000Z');
+
+const USER = {
+ id: 7,
+ username: 'editor',
+ name: 'Editor',
+ email: null,
+ active: true,
+ isSuperuser: false,
+ isDefaultPassword: false,
+ tokenVersion: 1,
+ settings: {},
+ avatarUrl: null,
+ provisioningMethod: 'local',
+ permissions: [],
+ contentFilters: null,
+} as unknown as RequestUser;
+
+interface ChapterRowInput {
+ chapters?: AudiobookChapter[] | null;
+ source?: AudiobookChapterSource | null;
+ updatedAt?: Date | null;
+ updatedById?: number | null;
+ updatedByName?: string | null;
+ audibleId?: string | null;
+ durationSeconds?: number | null;
+}
+
+function chapterRow(input: ChapterRowInput = {}) {
+ return {
+ chapters: null,
+ source: null,
+ updatedAt: null,
+ updatedById: null,
+ updatedByName: null,
+ audibleId: null,
+ durationSeconds: null,
+ ...input,
+ };
+}
+
+function track(id: number, name: string, durationSeconds: number | null, sortOrder: number, format = 'mp3') {
+ return { id, absolutePath: `/library/book/${name}`, format, durationSeconds, sortOrder };
+}
+
+const TWO_TRACKS = [track(2, 'Part 2.mp3', 40, 2), track(1, 'Part 1.mp3', 60, 1), track(3, 'cover.jpg', null, 0, 'jpg')];
+
+const STORED = chapterRow({
+ chapters: [
+ { title: 'Opening', startMs: 0 },
+ { title: 'Middle', startMs: 30_000 },
+ ],
+ source: 'embedded',
+ updatedAt: UPDATED_AT,
+ updatedById: 4,
+ updatedByName: 'Ann',
+ audibleId: 'B000TEST',
+ durationSeconds: 100,
+});
+
+const LOOKUP_RESULT: ChapterLookupResult = {
+ provider: 'audible',
+ asin: 'B0TESTASIN',
+ region: 'uk',
+ chapters: [{ title: 'Opening', startMs: 0, lengthMs: 1_000, children: [] }],
+ runtimeMs: 1_000,
+ brandIntroMs: 0,
+ brandOutroMs: 0,
+ isAccurate: true,
+};
+
+const WRITE_BACK: BookChapterWriteBack = { available: true, reason: null, fileCount: 2 };
+
+function ebookFile(id: number, mediaOverlayAvailable: boolean, role = 'content') {
+ return { id, format: 'epub', role, mediaOverlayAvailable };
+}
+
+function makeService(
+ options: {
+ row?: ReturnType | null;
+ tracks?: ReturnType[];
+ lockedFields?: string[];
+ ebookFiles?: ReturnType[];
+ audibleDomain?: string;
+ } = {},
+) {
+ const repo = {
+ findChapterRow: vi.fn().mockResolvedValue(options.row === undefined ? STORED : options.row),
+ findTrackRows: vi.fn().mockResolvedValue(options.tracks ?? TWO_TRACKS),
+ countVersions: vi.fn().mockResolvedValue(3),
+ listVersions: vi.fn().mockResolvedValue([]),
+ findVersion: vi.fn().mockResolvedValue(null),
+ findEbookFiles: vi.fn().mockResolvedValue(options.ebookFiles ?? []),
+ };
+ const bookService = { verifyBookAccess: vi.fn().mockResolvedValue(undefined) };
+ const audiobookChapters = {
+ saveEditedChapters: vi.fn().mockResolvedValue(undefined),
+ saveLookedUpChapters: vi.fn().mockResolvedValue(undefined),
+ restoreFileChapters: vi.fn().mockResolvedValue(undefined),
+ };
+ const metadataService = { readEmbeddedChapters: vi.fn().mockResolvedValue([]) };
+ const lockedFields = options.lockedFields ?? [];
+ const lockService = {
+ isFieldLocked: vi.fn().mockImplementation((_bookId: number, field: string) => Promise.resolve(lockedFields.includes(field))),
+ getLockedFields: vi.fn().mockResolvedValue(lockedFields),
+ replaceLockedFields: vi.fn().mockImplementation((_bookId: number, fields: string[]) => Promise.resolve(fields)),
+ };
+ const lookupService = { lookup: vi.fn().mockResolvedValue(LOOKUP_RESULT) };
+ const epubService = {
+ getBookInfo: vi.fn().mockResolvedValue({ toc: null }),
+ getMediaOverlayPlaylist: vi.fn().mockResolvedValue({ sections: [], durationSeconds: null }),
+ };
+ const providerConfig = { getConfig: vi.fn().mockResolvedValue({ audible: { enabled: true, domain: options.audibleDomain ?? 'com' } }) };
+ const pauseService = {
+ findPauses: vi.fn<(segments: readonly PauseSegment[]) => Promise>().mockResolvedValue([]),
+ readPeaks: vi.fn<(segments: readonly PauseSegment[], buckets: number) => Promise>().mockResolvedValue([]),
+ };
+ const fileWriteService = {
+ resolveChapterWriteStatus: vi.fn().mockResolvedValue(WRITE_BACK),
+ writeChapters: vi.fn().mockResolvedValue({ status: 'success', filesWritten: 2, filesTotal: 2 }),
+ };
+ const service = new ChapterEditorService(
+ repo as never,
+ bookService as never,
+ audiobookChapters as never,
+ metadataService as never,
+ lockService as never,
+ lookupService as never,
+ epubService as never,
+ providerConfig as never,
+ pauseService as never,
+ fileWriteService as never,
+ { findByIdWithPermissions: vi.fn().mockResolvedValue({ ...USER, permissions: [Permission.LibraryEditMetadata] }) } as never,
+ );
+ return {
+ service,
+ repo,
+ bookService,
+ audiobookChapters,
+ metadataService,
+ lockService,
+ lookupService,
+ epubService,
+ providerConfig,
+ pauseService,
+ fileWriteService,
+ };
+}
+
+async function currentRevision(service: ChapterEditorService): Promise {
+ return (await service.getState(BOOK_ID, USER)).revision;
+}
+
+async function refusal(run: Promise): Promise<{ status: number; body: { errorCode?: string; errorMeta?: Record } }> {
+ const error = await run.then(
+ () => null,
+ (caught: unknown) => caught,
+ );
+ expect(error).toBeInstanceOf(HttpException);
+ const exception = error as HttpException;
+ return { status: exception.getStatus(), body: exception.getResponse() as { errorCode?: string; errorMeta?: Record } };
+}
+
+describe('ChapterEditorService.getState', () => {
+ it('serves a stored list with who saved it and the track timeline', async () => {
+ const { service, bookService, fileWriteService } = makeService({ lockedFields: ['chapters'] });
+
+ const state = await service.getState(BOOK_ID, USER);
+
+ expect(bookService.verifyBookAccess).toHaveBeenCalledWith(BOOK_ID, USER);
+ expect(fileWriteService.resolveChapterWriteStatus).toHaveBeenCalledWith(BOOK_ID, [1, 2]);
+ expect(state).toEqual({
+ chapters: [
+ { title: 'Opening', startMs: 0 },
+ { title: 'Middle', startMs: 30_000 },
+ ],
+ source: 'embedded',
+ derived: false,
+ locked: true,
+ revision: expect.stringMatching(/^[0-9a-f]{64}$/),
+ updatedAt: UPDATED_AT.toISOString(),
+ updatedBy: { id: 4, name: 'Ann' },
+ totalDurationMs: 100_000,
+ tracks: [
+ { fileId: 1, name: 'Part 1.mp3', startMs: 0, durationMs: 60_000 },
+ { fileId: 2, name: 'Part 2.mp3', startMs: 60_000, durationMs: 40_000 },
+ ],
+ audibleId: 'B000TEST',
+ versionCount: 3,
+ audibleRegion: 'us',
+ sources: { ebookFileId: null, readAlongFileId: null },
+ writeBack: WRITE_BACK,
+ });
+ });
+
+ it('says why the chapters cannot be written into the files', async () => {
+ const { service, fileWriteService } = makeService();
+ fileWriteService.resolveChapterWriteStatus.mockResolvedValue({ available: false, reason: 'format_not_supported', fileCount: 2 });
+
+ await expect(service.getState(BOOK_ID, USER)).resolves.toMatchObject({
+ writeBack: { available: false, reason: 'format_not_supported', fileCount: 2 },
+ });
+ });
+
+ it('names the lookup storefront from the Audible provider setting and the EPUBs that can supply chapters', async () => {
+ const { service, repo } = makeService({
+ audibleDomain: 'audible.co.uk',
+ ebookFiles: [ebookFile(20, true), ebookFile(21, false, 'supplement'), ebookFile(22, false, 'primary')],
+ });
+
+ const state = await service.getState(BOOK_ID, USER);
+
+ expect(repo.findEbookFiles).toHaveBeenCalledWith(BOOK_ID);
+ expect(state.audibleRegion).toBe('uk');
+ expect(state.sources).toEqual({ ebookFileId: 22, readAlongFileId: 20 });
+ });
+
+ it('lets a lone read-along EPUB supply both the table of contents and the timings', async () => {
+ const { service } = makeService({ ebookFiles: [ebookFile(20, true)] });
+
+ await expect(service.getState(BOOK_ID, USER)).resolves.toMatchObject({ sources: { ebookFileId: 20, readAlongFileId: 20 } });
+ });
+
+ it('derives one chapter per track when nothing is stored, without crediting an editor', async () => {
+ const { service } = makeService({
+ row: chapterRow({ chapters: [], source: 'manual', updatedAt: UPDATED_AT, updatedById: 4, updatedByName: 'Ann' }),
+ });
+
+ const state = await service.getState(BOOK_ID, USER);
+
+ expect(state).toMatchObject({
+ chapters: [
+ { title: 'Part 1', startMs: 0 },
+ { title: 'Part 2', startMs: 60_000 },
+ ],
+ source: 'tracks',
+ derived: true,
+ locked: false,
+ updatedAt: null,
+ updatedBy: null,
+ });
+ });
+
+ it('stops placing tracks after one of unknown length and falls back to the declared duration', async () => {
+ const { service } = makeService({
+ row: chapterRow({ durationSeconds: 7_200 }),
+ tracks: [track(1, 'Part 1.mp3', 60, 1), track(2, 'Part 2.mp3', null, 2), track(3, 'Part 3.mp3', 30, 3)],
+ });
+
+ const state = await service.getState(BOOK_ID, USER);
+
+ expect(state.tracks).toEqual([
+ { fileId: 1, name: 'Part 1.mp3', startMs: 0, durationMs: 60_000 },
+ { fileId: 2, name: 'Part 2.mp3', startMs: 60_000, durationMs: null },
+ { fileId: 3, name: 'Part 3.mp3', startMs: null, durationMs: 30_000 },
+ ]);
+ expect(state.totalDurationMs).toBe(7_200_000);
+ expect(state).toMatchObject({ chapters: [], source: null, derived: false });
+ });
+
+ it('has no total when a track length and the declared duration are both unknown', async () => {
+ const { service } = makeService({ row: null, tracks: [track(1, 'Part 1.mp3', null, 1)] });
+
+ const state = await service.getState(BOOK_ID, USER);
+
+ expect(state.totalDurationMs).toBeNull();
+ expect(state.audibleId).toBeNull();
+ });
+
+ it('refuses a book without audio tracks', async () => {
+ const { service } = makeService({ tracks: [track(3, 'book.epub', null, 0, 'epub')] });
+
+ await expect(service.getState(BOOK_ID, USER)).rejects.toThrow(NotFoundException);
+ });
+
+ it('changes the revision whenever the stored list, its source or its save time changes', async () => {
+ const base = await currentRevision(makeService().service);
+
+ expect(await currentRevision(makeService().service)).toBe(base);
+ expect(await currentRevision(makeService({ row: { ...STORED, chapters: [{ title: 'Opening', startMs: 0 }] } }).service)).not.toBe(base);
+ expect(await currentRevision(makeService({ row: { ...STORED, source: 'manual' } }).service)).not.toBe(base);
+ expect(await currentRevision(makeService({ row: { ...STORED, updatedAt: new Date('2026-10-01T00:00:00.000Z') } }).service)).not.toBe(base);
+ });
+});
+
+describe('ChapterEditorService.save', () => {
+ const EDITED: AudiobookChapter[] = [
+ { title: ' Intro ', startMs: 0 },
+ { title: 'Body', startMs: 50_000 },
+ ];
+
+ it('refuses a stale revision without writing', async () => {
+ const { service, audiobookChapters, lockService } = makeService();
+
+ const { status, body } = await refusal(service.save(BOOK_ID, { chapters: EDITED, baseRevision: 'f'.repeat(64) }, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.stale);
+ expect(audiobookChapters.saveEditedChapters).not.toHaveBeenCalled();
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ {
+ name: 'an empty title',
+ chapters: [
+ { title: 'One', startMs: 0 },
+ { title: ' ', startMs: 10_000 },
+ ],
+ code: BOOK_CHAPTERS_ERROR_CODES.emptyTitle,
+ index: 1,
+ },
+ { name: 'a first chapter after zero', chapters: [{ title: 'One', startMs: 500 }], code: BOOK_CHAPTERS_ERROR_CODES.firstNotZero, index: 0 },
+ {
+ name: 'a start that does not move forward',
+ chapters: [
+ { title: 'One', startMs: 0 },
+ { title: 'Two', startMs: 20_000 },
+ { title: 'Three', startMs: 20_000 },
+ ],
+ code: BOOK_CHAPTERS_ERROR_CODES.notAscending,
+ index: 2,
+ },
+ {
+ name: 'a start at the end of the audio',
+ chapters: [
+ { title: 'One', startMs: 0 },
+ { title: 'Two', startMs: 100_000 },
+ ],
+ code: BOOK_CHAPTERS_ERROR_CODES.pastEnd,
+ index: 1,
+ },
+ ])('refuses $name with its code and index', async ({ chapters, code, index }) => {
+ const { service, audiobookChapters } = makeService();
+ const baseRevision = await currentRevision(service);
+
+ const { status, body } = await refusal(service.save(BOOK_ID, { chapters, baseRevision }, USER));
+
+ expect(status).toBe(400);
+ expect(body).toMatchObject({ errorCode: code, errorMeta: { index } });
+ expect(audiobookChapters.saveEditedChapters).not.toHaveBeenCalled();
+ });
+
+ it('accepts any start when the length of the audio is unknown', async () => {
+ const { service, audiobookChapters } = makeService({ row: chapterRow(), tracks: [track(1, 'Part 1.mp3', null, 1)] });
+ const baseRevision = await currentRevision(service);
+ const chapters = [
+ { title: 'One', startMs: 0 },
+ { title: 'Two', startMs: 99_000_000 },
+ ];
+
+ await service.save(BOOK_ID, { chapters, baseRevision }, USER);
+
+ expect(audiobookChapters.saveEditedChapters).toHaveBeenCalledWith(
+ BOOK_ID,
+ chapters,
+ USER.id,
+ expect.objectContaining({ expectedRevision: expect.any(String), locked: true }),
+ );
+ });
+
+ it('saves trimmed titles as the user and locks chapters by default', async () => {
+ const { service, audiobookChapters, lockService, bookService } = makeService({ lockedFields: ['title'] });
+ const baseRevision = await currentRevision(service);
+
+ const state = await service.save(BOOK_ID, { chapters: EDITED, baseRevision }, USER);
+
+ expect(bookService.verifyBookAccess).toHaveBeenLastCalledWith(BOOK_ID, USER, 'editor');
+ expect(audiobookChapters.saveEditedChapters).toHaveBeenCalledWith(
+ BOOK_ID,
+ [
+ { title: 'Intro', startMs: 0 },
+ { title: 'Body', startMs: 50_000 },
+ ],
+ USER.id,
+ expect.objectContaining({ expectedRevision: expect.any(String), locked: true }),
+ );
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ expect(state.revision).toMatch(/^[0-9a-f]{64}$/);
+ });
+
+ it('unlocks chapters when the save asks not to lock', async () => {
+ const { service, lockService } = makeService({ lockedFields: ['title', 'chapters'] });
+ const baseRevision = await currentRevision(service);
+
+ await service.save(BOOK_ID, { chapters: EDITED, baseRevision, lock: false }, USER);
+
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ { lock: true, lockedFields: ['chapters'] },
+ { lock: false, lockedFields: ['title'] },
+ ])('writes no locks when lock=$lock already matches', async ({ lock, lockedFields }) => {
+ const { service, audiobookChapters, lockService } = makeService({ lockedFields });
+ const baseRevision = await currentRevision(service);
+
+ await service.save(BOOK_ID, { chapters: EDITED, baseRevision, lock }, USER);
+
+ expect(audiobookChapters.saveEditedChapters).toHaveBeenCalled();
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+
+ it('writes nothing for a book the user cannot reach', async () => {
+ const { service, bookService, audiobookChapters } = makeService();
+ const baseRevision = await currentRevision(service);
+ bookService.verifyBookAccess.mockRejectedValueOnce(new NotFoundException('Book 9 not found'));
+
+ await expect(service.save(BOOK_ID, { chapters: EDITED, baseRevision }, USER)).rejects.toThrow(NotFoundException);
+ expect(audiobookChapters.saveEditedChapters).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService.revert', () => {
+ it('refuses when an audio file cannot be read, leaving chapters and locks alone', async () => {
+ const { service, metadataService, audiobookChapters, lockService } = makeService({ lockedFields: ['chapters'] });
+ metadataService.readEmbeddedChapters.mockResolvedValue(null);
+ const baseRevision = await currentRevision(service);
+
+ const { status, body } = await refusal(service.revert(BOOK_ID, { baseRevision }, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.filesUnreadable);
+ expect(audiobookChapters.restoreFileChapters).not.toHaveBeenCalled();
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+
+ it('refuses a stale revision before reading any file', async () => {
+ const { service, metadataService } = makeService();
+
+ const { status, body } = await refusal(service.revert(BOOK_ID, { baseRevision: '0'.repeat(64) }, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.stale);
+ expect(metadataService.readEmbeddedChapters).not.toHaveBeenCalled();
+ });
+
+ it('stores what the files carry in playback order, then unlocks chapters', async () => {
+ const { service, metadataService, audiobookChapters, lockService } = makeService({ lockedFields: ['title', 'chapters'] });
+ const embedded = [
+ { title: 'File One', startMs: 0 },
+ { title: 'File Two', startMs: 60_000 },
+ ];
+ metadataService.readEmbeddedChapters.mockResolvedValue(embedded);
+ const baseRevision = await currentRevision(service);
+
+ await service.revert(BOOK_ID, { baseRevision }, USER);
+
+ expect(metadataService.readEmbeddedChapters).toHaveBeenCalledWith(['/library/book/Part 1.mp3', '/library/book/Part 2.mp3'], BOOK_ID);
+ expect(audiobookChapters.restoreFileChapters).toHaveBeenCalledWith(
+ BOOK_ID,
+ embedded,
+ USER.id,
+ expect.objectContaining({ expectedRevision: expect.any(String), locked: false }),
+ );
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService.writeToFiles', () => {
+ it('writes the saved chapters on the track timeline, in playback order', async () => {
+ const { service, fileWriteService } = makeService();
+ const baseRevision = await currentRevision(service);
+
+ await expect(service.writeToFiles(BOOK_ID, { baseRevision }, USER)).resolves.toEqual({ status: 'success', filesWritten: 2, filesTotal: 2 });
+
+ expect(fileWriteService.writeChapters).toHaveBeenCalledWith(
+ BOOK_ID,
+ [
+ { id: 1, timelineStartMs: 0 },
+ { id: 2, timelineStartMs: 60_000 },
+ ],
+ [
+ { title: 'Opening', startMs: 0 },
+ { title: 'Middle', startMs: 30_000 },
+ ],
+ USER.id,
+ expect.any(Function),
+ undefined,
+ );
+ });
+
+ it('writes the list the editor shows when nothing is stored', async () => {
+ const { service, fileWriteService } = makeService({ row: chapterRow() });
+ const baseRevision = await currentRevision(service);
+
+ await service.writeToFiles(BOOK_ID, { baseRevision }, USER);
+
+ expect(fileWriteService.writeChapters).toHaveBeenCalledWith(
+ BOOK_ID,
+ expect.any(Array),
+ [
+ { title: 'Part 1', startMs: 0 },
+ { title: 'Part 2', startMs: 60_000 },
+ ],
+ USER.id,
+ expect.any(Function),
+ undefined,
+ );
+ });
+
+ it('refuses a stale revision without writing', async () => {
+ const { service, fileWriteService } = makeService();
+
+ const { status, body } = await refusal(service.writeToFiles(BOOK_ID, { baseRevision: 'e'.repeat(64) }, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.stale);
+ expect(fileWriteService.writeChapters).not.toHaveBeenCalled();
+ });
+
+ it('refuses when there are no chapters to write', async () => {
+ const { service, fileWriteService } = makeService({ row: chapterRow(), tracks: [track(1, 'Part 1.mp3', 60, 1)] });
+ const baseRevision = await currentRevision(service);
+
+ const { status, body } = await refusal(service.writeToFiles(BOOK_ID, { baseRevision }, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.nothingToWrite);
+ expect(fileWriteService.writeChapters).not.toHaveBeenCalled();
+ });
+
+ it('refuses when a track cannot be placed because an earlier length is unknown', async () => {
+ const { service, fileWriteService } = makeService({
+ tracks: [track(1, 'Part 1.mp3', 60, 1), track(2, 'Part 2.mp3', null, 2), track(3, 'Part 3.mp3', 30, 3)],
+ });
+ const baseRevision = await currentRevision(service);
+
+ const { status, body } = await refusal(service.writeToFiles(BOOK_ID, { baseRevision }, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.filesUnreadable);
+ expect(fileWriteService.writeChapters).not.toHaveBeenCalled();
+ });
+
+ it('answers 409 with the reason when the files cannot take chapters', async () => {
+ const { service, fileWriteService } = makeService();
+ fileWriteService.writeChapters.mockRejectedValue(new ChapterWriteUnavailableError('format_not_supported'));
+ const baseRevision = await currentRevision(service);
+
+ const { status, body } = await refusal(service.writeToFiles(BOOK_ID, { baseRevision }, USER));
+
+ expect(status).toBe(409);
+ expect(body).toMatchObject({ errorCode: BOOK_CHAPTERS_ERROR_CODES.writeUnavailable, errorMeta: { reason: 'format_not_supported' } });
+ });
+
+ it('passes any other failure through unchanged', async () => {
+ const { service, fileWriteService } = makeService();
+ const failure = new Error('database down');
+ fileWriteService.writeChapters.mockRejectedValue(failure);
+ const baseRevision = await currentRevision(service);
+
+ await expect(service.writeToFiles(BOOK_ID, { baseRevision }, USER)).rejects.toBe(failure);
+ });
+
+ it('checks access before reading anything', async () => {
+ const { service, bookService, repo, fileWriteService } = makeService();
+ bookService.verifyBookAccess.mockRejectedValueOnce(new NotFoundException('Book 9 not found'));
+
+ await expect(service.writeToFiles(BOOK_ID, { baseRevision: 'a'.repeat(64) }, USER)).rejects.toThrow(NotFoundException);
+ expect(repo.findChapterRow).not.toHaveBeenCalled();
+ expect(repo.findTrackRows).not.toHaveBeenCalled();
+ expect(fileWriteService.writeChapters).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService versions and embedded reads', () => {
+ it('answers 404 for a version the book does not have', async () => {
+ const { service, repo } = makeService();
+
+ await expect(service.getVersion(BOOK_ID, 55, USER)).rejects.toThrow(NotFoundException);
+ expect(repo.findVersion).toHaveBeenCalledWith(BOOK_ID, 55);
+ });
+
+ it('maps a version, crediting only an editor whose account still exists', async () => {
+ const { service, repo } = makeService();
+ const createdAt = new Date('2026-09-01T08:00:00.000Z');
+ repo.findVersion.mockResolvedValue({
+ id: 55,
+ source: 'manual',
+ chapterCount: 1,
+ createdAt,
+ createdById: 4,
+ createdByName: 'Ann',
+ chapters: [{ title: 'Only', startMs: 0 }],
+ });
+ repo.listVersions.mockResolvedValue([{ id: 54, source: 'embedded', chapterCount: 2, createdAt, createdById: null, createdByName: null }]);
+
+ await expect(service.getVersion(BOOK_ID, 55, USER)).resolves.toEqual({
+ id: 55,
+ source: 'manual',
+ chapterCount: 1,
+ createdAt: createdAt.toISOString(),
+ createdBy: { id: 4, name: 'Ann' },
+ chapters: [{ title: 'Only', startMs: 0 }],
+ });
+ await expect(service.listVersions(BOOK_ID, USER)).resolves.toEqual([
+ { id: 54, source: 'embedded', chapterCount: 2, createdAt: createdAt.toISOString(), createdBy: null },
+ ]);
+ });
+
+ it('cleans the embedded preview and refuses it when a file cannot be read', async () => {
+ const { service, metadataService } = makeService();
+ metadataService.readEmbeddedChapters.mockResolvedValueOnce([
+ { title: 'Late', startMs: 9_000 },
+ { title: '', startMs: 0 },
+ ]);
+
+ await expect(service.readEmbedded(BOOK_ID, USER)).resolves.toEqual({
+ chapters: [
+ { title: 'Chapter 1', startMs: 0 },
+ { title: 'Late', startMs: 9_000 },
+ ],
+ });
+
+ metadataService.readEmbeddedChapters.mockResolvedValueOnce(null);
+ await expect(service.readEmbedded(BOOK_ID, USER)).rejects.toThrow(ConflictException);
+ });
+});
+
+describe('ChapterEditorService.lookup', () => {
+ it('asks the region the request names', async () => {
+ const { service, bookService, lookupService, providerConfig } = makeService({ audibleDomain: 'audible.de' });
+
+ await expect(service.lookup(BOOK_ID, { asin: 'B0TESTASIN', region: 'uk' }, USER)).resolves.toBe(LOOKUP_RESULT);
+
+ expect(bookService.verifyBookAccess).toHaveBeenCalledWith(BOOK_ID, USER, 'editor');
+ expect(lookupService.lookup).toHaveBeenCalledWith('B0TESTASIN', 'uk');
+ expect(providerConfig.getConfig).not.toHaveBeenCalled();
+ });
+
+ it('defaults to the storefront the Audible provider is set to', async () => {
+ const { service, lookupService } = makeService({ audibleDomain: 'audible.co.jp' });
+
+ await service.lookup(BOOK_ID, { asin: 'B0TESTASIN' }, USER);
+
+ expect(lookupService.lookup).toHaveBeenCalledWith('B0TESTASIN', 'jp');
+ });
+
+ it('asks nothing for a book the user cannot reach', async () => {
+ const { service, bookService, lookupService } = makeService();
+ bookService.verifyBookAccess.mockRejectedValueOnce(new NotFoundException('Book 9 not found'));
+
+ await expect(service.lookup(BOOK_ID, { asin: 'B0TESTASIN', region: 'us' }, USER)).rejects.toThrow(NotFoundException);
+ expect(lookupService.lookup).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService.readEbook', () => {
+ const EPUBS = [ebookFile(30, false), ebookFile(31, true)];
+
+ it('lists the table of contents in reading order, flattening nesting and skipping front matter', async () => {
+ const { service, epubService, bookService } = makeService({ ebookFiles: EPUBS });
+ epubService.getBookInfo.mockResolvedValue({
+ toc: {
+ label: 'Table of Contents',
+ children: [
+ { label: 'Cover', href: 'cover.xhtml' },
+ { label: 'Title Page', href: 'title.xhtml' },
+ { label: 'Copyright', href: 'copyright.xhtml' },
+ { label: 'Contents', href: 'toc.xhtml' },
+ { label: 'Also by Jane Doe', href: 'also.xhtml' },
+ { label: 'Praise for The Book', href: 'praise.xhtml' },
+ {
+ label: ' Part One ',
+ href: 'p1.xhtml',
+ children: [
+ { label: 'Chapter 1', href: 'c1.xhtml' },
+ { label: ' ', href: 'blank.xhtml', children: [{ label: 'Chapter 1.5', href: 'c1-5.xhtml' }] },
+ ],
+ },
+ { label: 'Chapter 2', href: 'c2.xhtml' },
+ { label: 'Copyright Notice Is Not Front Matter Here', href: 'x.xhtml' },
+ ],
+ },
+ });
+
+ const result = await service.readEbook(BOOK_ID, 'toc', USER);
+
+ expect(bookService.verifyBookAccess).toHaveBeenCalledWith(BOOK_ID, USER, 'editor');
+ expect(epubService.getBookInfo).toHaveBeenCalledWith(BOOK_ID, 30, USER);
+ expect(result).toEqual({
+ source: 'toc',
+ fileId: 30,
+ titlesOnly: true,
+ runtimeMs: null,
+ chapters: [
+ { title: 'Part One', startMs: 0 },
+ { title: 'Chapter 1', startMs: 0 },
+ { title: 'Chapter 1.5', startMs: 0 },
+ { title: 'Chapter 2', startMs: 0 },
+ { title: 'Copyright Notice Is Not Front Matter Here', startMs: 0 },
+ ],
+ });
+ });
+
+ it('places read-along sections by their narration start and reports the narration length', async () => {
+ const { service, epubService } = makeService({ ebookFiles: EPUBS });
+ epubService.getMediaOverlayPlaylist.mockResolvedValue({
+ durationSeconds: 3_600.4,
+ sections: [
+ { index: 0, label: null, startSeconds: 0 },
+ { index: 1, label: ' Chapter One ', startSeconds: 12.3456 },
+ { index: 2, label: ' ', startSeconds: 40 },
+ { index: 3, label: 'Chapter Two', startSeconds: 1_800 },
+ ],
+ });
+
+ const result = await service.readEbook(BOOK_ID, 'read_along', USER);
+
+ expect(epubService.getMediaOverlayPlaylist).toHaveBeenCalledWith(BOOK_ID, 31, USER);
+ expect(result).toEqual({
+ source: 'read_along',
+ fileId: 31,
+ titlesOnly: false,
+ runtimeMs: 3_600_400,
+ chapters: [
+ { title: 'Chapter One', startMs: 12_346 },
+ { title: 'Chapter Two', startMs: 1_800_000 },
+ ],
+ });
+ });
+
+ it('reports no runtime when the narration length is unknown', async () => {
+ const { service, epubService } = makeService({ ebookFiles: EPUBS });
+ epubService.getMediaOverlayPlaylist.mockResolvedValue({ durationSeconds: null, sections: [{ index: 0, label: 'Only', startSeconds: 0 }] });
+
+ await expect(service.readEbook(BOOK_ID, 'read_along', USER)).resolves.toMatchObject({
+ runtimeMs: null,
+ chapters: [{ title: 'Only', startMs: 0 }],
+ });
+ });
+
+ it.each([
+ { name: 'no EPUB at all', source: 'toc' as const, files: [] },
+ { name: 'no read-along EPUB', source: 'read_along' as const, files: [ebookFile(30, false)] },
+ { name: 'only a supplementary EPUB', source: 'toc' as const, files: [ebookFile(30, false, 'supplement')] },
+ ])('refuses $name without opening a file', async ({ source, files }) => {
+ const { service, epubService } = makeService({ ebookFiles: files });
+
+ const { status, body } = await refusal(service.readEbook(BOOK_ID, source, USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.ebookUnavailable);
+ expect(epubService.getBookInfo).not.toHaveBeenCalled();
+ expect(epubService.getMediaOverlayPlaylist).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ { name: 'no table of contents', toc: null },
+ {
+ name: 'a table of contents of front matter only',
+ toc: { label: 'Table of Contents', children: [{ label: 'Cover' }, { label: 'Table of Contents' }] },
+ },
+ { name: 'an empty table of contents', toc: { label: 'Table of Contents', children: [] } },
+ ])('refuses an EPUB with $name', async ({ toc }) => {
+ const { service, epubService } = makeService({ ebookFiles: EPUBS });
+ epubService.getBookInfo.mockResolvedValue({ toc });
+
+ const { status, body } = await refusal(service.readEbook(BOOK_ID, 'toc', USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.ebookUnavailable);
+ });
+
+ it('refuses a read-along EPUB that names none of its sections', async () => {
+ const { service, epubService } = makeService({ ebookFiles: EPUBS });
+ epubService.getMediaOverlayPlaylist.mockResolvedValue({ durationSeconds: 60, sections: [{ index: 0, label: null, startSeconds: 0 }] });
+
+ const { status, body } = await refusal(service.readEbook(BOOK_ID, 'read_along', USER));
+
+ expect(status).toBe(409);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.ebookUnavailable);
+ });
+
+ it('reads nothing for a book the user cannot reach', async () => {
+ const { service, bookService, repo, epubService } = makeService({ ebookFiles: EPUBS });
+ bookService.verifyBookAccess.mockRejectedValueOnce(new NotFoundException('Book 9 not found'));
+
+ await expect(service.readEbook(BOOK_ID, 'toc', USER)).rejects.toThrow(NotFoundException);
+ expect(repo.findEbookFiles).not.toHaveBeenCalled();
+ expect(epubService.getBookInfo).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService.analyze', () => {
+ const ONE_TRACK = [track(1, 'Part 1.mp3', 60, 1)];
+ const PART_1 = '/library/book/Part 1.mp3';
+ const PART_2 = '/library/book/Part 2.mp3';
+
+ it('centres a window on each point, clipped to the book, and moves its pauses onto the book timeline', async () => {
+ const { service, bookService, pauseService } = makeService({ row: chapterRow(), tracks: ONE_TRACK });
+ pauseService.findPauses.mockResolvedValue([{ startMs: 1_000, endMs: 1_500 }]);
+
+ const result = await service.analyze(BOOK_ID, { points: [1_000, 30_000, 59_000, 75_000], windowMs: 6_000 }, USER);
+
+ expect(bookService.verifyBookAccess).toHaveBeenCalledWith(BOOK_ID, USER, 'editor');
+ expect(result.windows).toEqual([
+ { pointMs: 1_000, fromMs: 0, toMs: 4_000, pauses: [{ startMs: 1_000, endMs: 1_500 }], failed: false },
+ { pointMs: 30_000, fromMs: 27_000, toMs: 33_000, pauses: [{ startMs: 28_000, endMs: 28_500 }], failed: false },
+ { pointMs: 59_000, fromMs: 56_000, toMs: 60_000, pauses: [{ startMs: 57_000, endMs: 57_500 }], failed: false },
+ { pointMs: 60_000, fromMs: 57_000, toMs: 60_000, pauses: [{ startMs: 58_000, endMs: 58_500 }], failed: false },
+ ]);
+ expect(pauseService.findPauses).toHaveBeenCalledTimes(4);
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_1, offsetMs: 0, durationMs: 4_000 }], undefined);
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_1, offsetMs: 27_000, durationMs: 6_000 }], undefined);
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_1, offsetMs: 56_000, durationMs: 4_000 }], undefined);
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_1, offsetMs: 57_000, durationMs: 3_000 }], undefined);
+ });
+
+ it('splits a window that crosses a file boundary into both files, in playback order', async () => {
+ const { service, pauseService } = makeService();
+ pauseService.findPauses.mockImplementation((segments) => Promise.resolve(segments.length === 2 ? [{ startMs: 2_500, endMs: 4_000 }] : []));
+
+ const result = await service.analyze(BOOK_ID, { points: [62_000, 55_000, 80_000], windowMs: 10_000 }, USER);
+
+ expect(pauseService.findPauses).toHaveBeenCalledWith(
+ [
+ { absolutePath: PART_1, offsetMs: 57_000, durationMs: 3_000 },
+ { absolutePath: PART_2, offsetMs: 0, durationMs: 7_000 },
+ ],
+ undefined,
+ );
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_1, offsetMs: 50_000, durationMs: 10_000 }], undefined);
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_2, offsetMs: 15_000, durationMs: 10_000 }], undefined);
+ expect(result.windows).toEqual([
+ { pointMs: 62_000, fromMs: 57_000, toMs: 67_000, pauses: [{ startMs: 59_500, endMs: 61_000 }], failed: false },
+ { pointMs: 55_000, fromMs: 50_000, toMs: 60_000, pauses: [], failed: false },
+ { pointMs: 80_000, fromMs: 75_000, toMs: 85_000, pauses: [], failed: false },
+ ]);
+ });
+
+ it('cannot place a window that reaches into a file of unknown length with more after it', async () => {
+ const tracks = [track(1, 'Part 1.mp3', 60, 1), track(2, 'Part 2.mp3', null, 2), track(3, 'Part 3.mp3', 60, 3)];
+ const { service, pauseService } = makeService({ row: chapterRow(), tracks });
+
+ const result = await service.analyze(BOOK_ID, { points: [30_000, 61_000], windowMs: 10_000 }, USER);
+
+ expect(result.windows.map((window) => window.failed)).toEqual([false, true]);
+ expect(pauseService.findPauses).toHaveBeenCalledTimes(1);
+ });
+
+ it('listens to 10 seconds around a point by default', async () => {
+ const { service, pauseService } = makeService();
+
+ const result = await service.analyze(BOOK_ID, { points: [30_000] }, USER);
+
+ expect(pauseService.findPauses).toHaveBeenCalledWith([{ absolutePath: PART_1, offsetMs: 25_000, durationMs: 10_000 }], undefined);
+ expect(result.windows).toEqual([{ pointMs: 30_000, fromMs: 25_000, toMs: 35_000, pauses: [], failed: false }]);
+ });
+
+ it('marks only the window whose audio will not decode as failed', async () => {
+ const { service, pauseService } = makeService();
+ pauseService.findPauses.mockImplementation((segments) =>
+ segments[0]!.absolutePath === PART_2
+ ? Promise.reject(new Error('Command failed: ffmpeg\nInvalid data found when processing input'))
+ : Promise.resolve([{ startMs: 0, endMs: 400 }]),
+ );
+
+ const result = await service.analyze(BOOK_ID, { points: [10_000, 70_000] }, USER);
+
+ expect(result.windows).toEqual([
+ { pointMs: 10_000, fromMs: 5_000, toMs: 15_000, pauses: [{ startMs: 5_000, endMs: 5_400 }], failed: false },
+ { pointMs: 70_000, fromMs: 65_000, toMs: 75_000, pauses: [], failed: true },
+ ]);
+ });
+
+ it('refuses more audio than one analysis may decode, before decoding any of it', async () => {
+ const { service, pauseService } = makeService({ row: chapterRow(), tracks: [track(1, 'Part 1.mp3', 36_000, 1)] });
+ const windowMs = CHAPTER_PAUSE_LIMITS.maxWindowMs;
+ const points = (count: number) => Array.from({ length: count }, (_, index) => windowMs * (index + 1));
+ const fits = CHAPTER_PAUSE_LIMITS.maxAudioMs / windowMs;
+
+ const { status, body } = await refusal(service.analyze(BOOK_ID, { points: points(fits + 1), windowMs }, USER));
+
+ expect(status).toBe(400);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.analysisTooLarge);
+ expect(pauseService.findPauses).not.toHaveBeenCalled();
+
+ const atLimit = await service.analyze(BOOK_ID, { points: points(fits), windowMs }, USER);
+
+ expect(atLimit.windows).toHaveLength(fits);
+ expect(pauseService.findPauses).toHaveBeenCalledTimes(fits);
+ });
+
+ it('answers 503 when the server has no ffmpeg', async () => {
+ const { service, pauseService } = makeService();
+ pauseService.findPauses.mockRejectedValue(new AudioToolMissingError());
+
+ const { status, body } = await refusal(service.analyze(BOOK_ID, { points: [10_000, 70_000] }, USER));
+
+ expect(status).toBe(503);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.analysisUnavailable);
+ });
+
+ it('reads nothing for a book the user cannot reach', async () => {
+ const { service, bookService, repo, pauseService } = makeService();
+ bookService.verifyBookAccess.mockRejectedValueOnce(new NotFoundException('Book 9 not found'));
+
+ await expect(service.analyze(BOOK_ID, { points: [10_000] }, USER)).rejects.toThrow(NotFoundException);
+ expect(repo.findTrackRows).not.toHaveBeenCalled();
+ expect(pauseService.findPauses).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService.waveform', () => {
+ const PART_1 = '/library/book/Part 1.mp3';
+ const PART_2 = '/library/book/Part 2.mp3';
+
+ it('reads 16 s around the point in 320 slices by default, across a file boundary in playback order', async () => {
+ const { service, bookService, pauseService } = makeService();
+ pauseService.readPeaks.mockResolvedValue([0.25, 0.5]);
+
+ const result = await service.waveform(BOOK_ID, { atMs: 62_000 }, USER);
+
+ expect(bookService.verifyBookAccess).toHaveBeenCalledWith(BOOK_ID, USER, 'editor');
+ expect(pauseService.readPeaks).toHaveBeenCalledWith(
+ [
+ { absolutePath: PART_1, offsetMs: 54_000, durationMs: 6_000 },
+ { absolutePath: PART_2, offsetMs: 0, durationMs: 10_000 },
+ ],
+ 320,
+ undefined,
+ );
+ expect(result).toEqual({ fromMs: 54_000, toMs: 70_000, peaks: [0.25, 0.5], failed: false });
+ });
+
+ it('honours the window and slice count asked for, clipping the window to the book', async () => {
+ const { service, pauseService } = makeService();
+
+ await expect(service.waveform(BOOK_ID, { atMs: 99_000, windowMs: 4_000, buckets: 20 }, USER)).resolves.toEqual({
+ fromMs: 97_000,
+ toMs: 100_000,
+ peaks: [],
+ failed: false,
+ });
+ expect(pauseService.readPeaks).toHaveBeenLastCalledWith([{ absolutePath: PART_2, offsetMs: 37_000, durationMs: 3_000 }], 20, undefined);
+
+ await expect(service.waveform(BOOK_ID, { atMs: 150_000, windowMs: 4_000, buckets: 20 }, USER)).resolves.toMatchObject({
+ fromMs: 98_000,
+ toMs: 100_000,
+ });
+ await expect(service.waveform(BOOK_ID, { atMs: 1_000, windowMs: 4_000, buckets: 20 }, USER)).resolves.toMatchObject({
+ fromMs: 0,
+ toMs: 3_000,
+ });
+ expect(pauseService.readPeaks).toHaveBeenLastCalledWith([{ absolutePath: PART_1, offsetMs: 0, durationMs: 3_000 }], 20, undefined);
+ });
+
+ it('marks a window it cannot place on the timeline as failed without decoding', async () => {
+ const tracks = [track(1, 'Part 1.mp3', 60, 1), track(2, 'Part 2.mp3', null, 2), track(3, 'Part 3.mp3', 60, 3)];
+ const { service, pauseService } = makeService({ row: chapterRow(), tracks });
+
+ await expect(service.waveform(BOOK_ID, { atMs: 61_000, windowMs: 10_000 }, USER)).resolves.toEqual({
+ fromMs: 56_000,
+ toMs: 66_000,
+ peaks: [],
+ failed: true,
+ });
+ expect(pauseService.readPeaks).not.toHaveBeenCalled();
+ });
+
+ it('marks a window whose audio will not decode as failed', async () => {
+ const { service, pauseService } = makeService();
+ pauseService.readPeaks.mockRejectedValue(new Error('Command failed: ffmpeg\nInvalid data found when processing input'));
+
+ await expect(service.waveform(BOOK_ID, { atMs: 30_000 }, USER)).resolves.toEqual({
+ fromMs: 22_000,
+ toMs: 38_000,
+ peaks: [],
+ failed: true,
+ });
+ });
+
+ it('answers 503 when the server has no ffmpeg', async () => {
+ const { service, pauseService } = makeService();
+ pauseService.readPeaks.mockRejectedValue(new AudioToolMissingError());
+
+ const { status, body } = await refusal(service.waveform(BOOK_ID, { atMs: 30_000 }, USER));
+
+ expect(status).toBe(503);
+ expect(body.errorCode).toBe(BOOK_CHAPTERS_ERROR_CODES.analysisUnavailable);
+ });
+
+ it('reads nothing for a book the user cannot reach', async () => {
+ const { service, bookService, repo, pauseService } = makeService();
+ bookService.verifyBookAccess.mockRejectedValueOnce(new NotFoundException('Book 9 not found'));
+
+ await expect(service.waveform(BOOK_ID, { atMs: 30_000 }, USER)).rejects.toThrow(NotFoundException);
+ expect(repo.findTrackRows).not.toHaveBeenCalled();
+ expect(pauseService.readPeaks).not.toHaveBeenCalled();
+ });
+});
+
+describe('ChapterEditorService.bulkLookup', () => {
+ // TWO_TRACKS play for 100 s; Audible's runtime adds a 2 s lead-in and a 3 s sign-off.
+ const CONFIDENT: ChapterLookupResult = {
+ provider: 'audible',
+ asin: 'B000TEST',
+ region: 'us',
+ chapters: [
+ { title: 'Opening Credits', startMs: 0, lengthMs: 30_000, children: [] },
+ {
+ title: 'Part One',
+ startMs: 30_000,
+ lengthMs: 70_000,
+ children: [{ title: 'Chapter 1', startMs: 65_000, lengthMs: 40_000, children: [] }],
+ },
+ ],
+ runtimeMs: 105_000,
+ brandIntroMs: 2_000,
+ brandOutroMs: 3_000,
+ isAccurate: true,
+ };
+
+ it('applies a confident lookup as provider chapters on behalf of the queuer, then locks them', async () => {
+ const { service, bookService, lookupService, audiobookChapters, lockService } = makeService({ lockedFields: ['title'] });
+ lookupService.lookup.mockResolvedValue(CONFIDENT);
+
+ const outcome = await service.bulkLookup(BOOK_ID, USER.id);
+
+ const applied = [
+ { title: 'Opening Credits', startMs: 0 },
+ { title: 'Part One', startMs: 28_000 },
+ { title: 'Chapter 1', startMs: 63_000 },
+ ];
+ expect(audiobookChapters.saveLookedUpChapters).toHaveBeenCalledWith(
+ BOOK_ID,
+ applied,
+ USER.id,
+ expect.objectContaining({ expectedRevision: expect.any(String), requireUnlocked: true, locked: true }),
+ );
+ expect(audiobookChapters.saveEditedChapters).not.toHaveBeenCalled();
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+
+ expect(outcome).toEqual({
+ status: 'applied',
+ reason: null,
+ result: { provider: 'audible', chapterCount: 3, runtimeMs: 105_000, totalDurationMs: 100_000, isAccurate: true },
+ });
+ expect(bookService.verifyBookAccess).toHaveBeenCalledTimes(2);
+ });
+
+ it('skips mutation when the queuer is gone', async () => {
+ const { service, lookupService, audiobookChapters } = makeService();
+ lookupService.lookup.mockResolvedValue(CONFIDENT);
+
+ await expect(service.bulkLookup(BOOK_ID, null)).resolves.toMatchObject({ status: 'skipped' });
+ expect(audiobookChapters.saveLookedUpChapters).not.toHaveBeenCalled();
+ });
+
+ it('looks the stored ASIN up, trimmed and upper-cased, on the storefront the Audible provider is set to', async () => {
+ const { service, lookupService } = makeService({ row: chapterRow({ audibleId: ' b0testasin ' }), audibleDomain: 'co.uk' });
+ lookupService.lookup.mockResolvedValue(CONFIDENT);
+
+ await service.bulkLookup(BOOK_ID, USER.id);
+
+ expect(lookupService.lookup).toHaveBeenCalledWith('B0TESTASIN', 'uk');
+ });
+
+ it('leaves a list protected by the chapters lock for review without replacing it', async () => {
+ const { service, lookupService, audiobookChapters, lockService } = makeService({ lockedFields: ['chapters'] });
+ lookupService.lookup.mockResolvedValue(CONFIDENT);
+
+ const outcome = await service.bulkLookup(BOOK_ID, USER.id);
+
+ expect(outcome).toEqual({
+ status: 'review',
+ reason: 'locked',
+ // Counted with the parts opened up, the way the review sheet lists them.
+ result: { provider: 'audible', chapterCount: 3, runtimeMs: 105_000, totalDurationMs: 100_000, isAccurate: true },
+ });
+ expect(audiobookChapters.saveLookedUpChapters).not.toHaveBeenCalled();
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+
+ it.each([
+ { name: 'inexact timings', lookup: { isAccurate: false }, reason: 'not_exact' },
+ { name: 'a runtime 5 s away from the book', lookup: { runtimeMs: 110_000 }, reason: 'runtime_mismatch' },
+ {
+ name: 'a single chapter',
+ lookup: { chapters: [{ title: 'Whole Book', startMs: 0, lengthMs: 105_000, children: [] }] },
+ reason: 'implausible',
+ },
+ ])('leaves $name for review with what was found, saving nothing', async ({ lookup, reason }) => {
+ const { service, lookupService, audiobookChapters, lockService } = makeService();
+ const found = { ...CONFIDENT, chapters: CONFIDENT.chapters.slice(0, 1), ...lookup };
+ lookupService.lookup.mockResolvedValue(found);
+
+ const outcome = await service.bulkLookup(BOOK_ID, USER.id);
+
+ expect(outcome).toEqual({
+ status: 'review',
+ reason,
+ result: {
+ provider: 'audible',
+ chapterCount: found.chapters.length,
+ runtimeMs: found.runtimeMs,
+ totalDurationMs: 100_000,
+ isAccurate: found.isAccurate,
+ },
+ });
+ expect(audiobookChapters.saveLookedUpChapters).not.toHaveBeenCalled();
+ expect(lockService.replaceLockedFields).not.toHaveBeenCalled();
+ });
+
+ it('leaves a book whose length is unknown for review', async () => {
+ const { service, lookupService } = makeService({ row: chapterRow({ audibleId: 'B000TEST' }), tracks: [track(1, 'Part 1.mp3', null, 1)] });
+ lookupService.lookup.mockResolvedValue(CONFIDENT);
+
+ await expect(service.bulkLookup(BOOK_ID, USER.id)).resolves.toMatchObject({
+ status: 'review',
+ reason: 'unknown_length',
+ result: { totalDurationMs: null },
+ });
+ });
+
+ it('reports a 404 lookup as not found', async () => {
+ const { service, lookupService, audiobookChapters } = makeService();
+ lookupService.lookup.mockRejectedValue(
+ new HttpException({ message: 'No chapters were found for that ASIN', errorCode: BOOK_CHAPTERS_ERROR_CODES.lookupNotFound }, 404),
+ );
+
+ await expect(service.bulkLookup(BOOK_ID, USER.id)).resolves.toEqual({ status: 'not_found', reason: null, result: null });
+ expect(audiobookChapters.saveLookedUpChapters).not.toHaveBeenCalled();
+ });
+
+ it('throws when the chapter sources cannot be reached, so the book is tried again', async () => {
+ const { service, lookupService } = makeService();
+ const unreachable = new HttpException({ message: 'The chapter sources could not be reached' }, 502);
+ lookupService.lookup.mockRejectedValue(unreachable);
+
+ await expect(service.bulkLookup(BOOK_ID, USER.id)).rejects.toBe(unreachable);
+ });
+
+ it.each([
+ { name: 'no metadata row', row: null },
+ { name: 'no ASIN', row: chapterRow({ audibleId: null }) },
+ { name: 'a blank ASIN', row: chapterRow({ audibleId: ' ' }) },
+ ])('skips a book with $name without asking Audible', async ({ row }) => {
+ const { service, lookupService } = makeService({ row });
+
+ await expect(service.bulkLookup(BOOK_ID, USER.id)).resolves.toEqual({ status: 'skipped', reason: 'no_asin', result: null });
+ expect(lookupService.lookup).not.toHaveBeenCalled();
+ });
+
+ it('skips a book that no longer has audio tracks', async () => {
+ const { service, lookupService } = makeService({ tracks: [track(3, 'cover.jpg', null, 0, 'jpg')] });
+
+ await expect(service.bulkLookup(BOOK_ID, USER.id)).resolves.toEqual({ status: 'skipped', reason: 'no_audio', result: null });
+ expect(lookupService.lookup).not.toHaveBeenCalled();
+ });
+
+ it('lets any other failure loading the book through, so it is tried again', async () => {
+ const { service, repo } = makeService();
+ const failure = new Error('connection terminated');
+ repo.findTrackRows.mockRejectedValue(failure);
+
+ await expect(service.bulkLookup(BOOK_ID, USER.id)).rejects.toBe(failure);
+ });
+});
diff --git a/server/src/modules/chapter-editor/chapter-editor.service.ts b/server/src/modules/chapter-editor/chapter-editor.service.ts
new file mode 100644
index 000000000..e17411045
--- /dev/null
+++ b/server/src/modules/chapter-editor/chapter-editor.service.ts
@@ -0,0 +1,591 @@
+import { basename } from 'node:path';
+
+import {
+ BadRequestException,
+ ConflictException,
+ ForbiddenException,
+ HttpException,
+ Injectable,
+ Logger,
+ NotFoundException,
+ ServiceUnavailableException,
+} from '@nestjs/common';
+import {
+ AUDIOBOOK_CHAPTER_LIMITS,
+ Permission,
+ BOOK_CHAPTERS_ERROR_CODES,
+ CHAPTER_PAUSE_LIMITS,
+ type AnalyzeBookChaptersResult,
+ type AudiobookChapter,
+ type ChapterPauseWindow,
+ type ChapterWaveform,
+ type ChapterLookupQueueStatus,
+ type ChapterLookupReviewReason,
+ type ChapterLookupReviewSummary,
+ type WriteBookChaptersResult,
+ type BookChapterSources,
+ type ChapterLookupResult,
+ type EbookBookChapters,
+ type EbookChapterSource,
+ type EpubTocItem,
+ type BookChapterEditor,
+ type BookChaptersErrorCode,
+ type BookChaptersState,
+ type BookChapterTrack,
+ type BookChapterVersion,
+ type BookChapterVersionSummary,
+ type EmbeddedBookChapters,
+} from '@bookorbit/types';
+
+import type { RequestUser } from '../../common/types/request-user';
+import { mapWithConcurrency } from '../../common/utils/batch.utils';
+import {
+ computeChapterEditorRevision,
+ normalizeAudiobookChapters,
+ orderAudiobookTracks,
+ resolveStoredAudiobookChapters,
+} from '../../common/utils/audiobook-chapters.utils';
+import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils';
+import { AudiobookChaptersService } from '../audiobook-chapters/audiobook-chapters.service';
+import { BookMetadataLockService } from '../book-metadata-lock/book-metadata-lock.service';
+import { BookService } from '../book/book.service';
+import { ChapterWriteUnavailableError, FileWriteService } from '../file-write/file-write.service';
+import { MetadataService } from '../metadata/metadata.service';
+import { ProviderConfigService } from '../metadata-preferences/provider-config.service';
+import { UserService } from '../user/user.service';
+import { EpubService } from '../reader/epub/epub.service';
+import type { AnalyzeBookChaptersDto } from './dto/analyze-book-chapters.dto';
+import type { LookupBookChaptersDto } from './dto/lookup-book-chapters.dto';
+import { audibleRegionForDomain, ChapterLookupService } from './chapter-lookup.service';
+import { AudioToolMissingError, ChapterPauseService, type PauseSegment } from './chapter-pause.service';
+import type { SaveBookChaptersDto } from './dto/save-book-chapters.dto';
+import type { WaveformBookChaptersDto } from './dto/waveform-book-chapters.dto';
+import { flattenLookupTree, planAutoApply } from './chapter-auto-apply';
+import type { WriteBookChaptersDto } from './dto/write-book-chapters.dto';
+import type { RevertBookChaptersDto } from './dto/revert-book-chapters.dto';
+import { ChapterEditorRepository } from './chapter-editor.repository';
+
+type ChapterRow = NonNullable>>;
+type TrackRow = Awaited>[number];
+type VersionRow = Awaited>[number];
+type EbookFileRow = Awaited>[number];
+
+const DEFAULT_PAUSE_WINDOW_MS = 10_000;
+const DEFAULT_WAVEFORM_WINDOW_MS = 16_000;
+const DEFAULT_WAVEFORM_BUCKETS = 320;
+
+/** Table-of-contents entries that never name a chapter of the narration. */
+const FRONT_MATTER_LABEL = /^(cover|title page|copyright( page)?|contents|table of contents|about the book|also by .+|praise for .+|praise)$/i;
+
+/** How one book's bulk lookup ended. A thrown error means it may be tried again. */
+export interface BulkLookupOutcome {
+ status: Extract;
+ reason: ChapterLookupReviewReason | 'no_asin' | 'no_audio' | 'access_changed' | null;
+ result: ChapterLookupReviewSummary | null;
+}
+
+interface PauseWindowPlan {
+ pointMs: number;
+ fromMs: number;
+ toMs: number;
+ /** Null when the files under the window cannot be placed on the timeline. */
+ segments: PauseSegment[] | null;
+}
+
+interface EditorContext {
+ bookId: number;
+ row: ChapterRow | null;
+ rows: TrackRow[];
+ tracks: TrackRow[];
+ revision: string;
+}
+
+@Injectable()
+export class ChapterEditorService {
+ private readonly logger = new Logger(ChapterEditorService.name);
+
+ constructor(
+ private readonly repo: ChapterEditorRepository,
+ private readonly bookService: BookService,
+ private readonly audiobookChapters: AudiobookChaptersService,
+ private readonly metadataService: MetadataService,
+ private readonly lockService: BookMetadataLockService,
+ private readonly lookupService: ChapterLookupService,
+ private readonly epubService: EpubService,
+ private readonly providerConfig: ProviderConfigService,
+ private readonly pauseService: ChapterPauseService,
+ private readonly fileWriteService: FileWriteService,
+ private readonly userService: UserService,
+ ) {}
+
+ async getState(bookId: number, user: RequestUser): Promise {
+ await this.bookService.verifyBookAccess(bookId, user);
+ return this.buildState(bookId, await this.loadContext(bookId));
+ }
+
+ async save(bookId: number, dto: SaveBookChaptersDto, user: RequestUser): Promise {
+ const event = 'chapter_editor.save';
+ const startedAt = Date.now();
+ const lock = dto.lock ?? true;
+ this.logger.log(`[${event}] [start] bookId=${bookId} userId=${user.id} chapters=${dto.chapters.length} lock=${lock} - chapter save started`);
+ try {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ const context = await this.loadContext(bookId);
+ this.assertCurrent(context, dto.baseRevision);
+ const chapters = this.validate(dto.chapters, this.timeline(context).totalDurationMs);
+
+ await this.audiobookChapters.saveEditedChapters(bookId, chapters, user.id, { expectedRevision: dto.baseRevision, locked: lock });
+
+ this.logger.log(`[${event}] [end] bookId=${bookId} durationMs=${Date.now() - startedAt} chapters=${chapters.length} - chapters saved`);
+ return this.buildState(bookId, await this.loadContext(bookId));
+ } catch (error) {
+ this.logFailure(event, bookId, startedAt, error);
+ throw error;
+ }
+ }
+
+ async revert(
+ bookId: number,
+ dto: RevertBookChaptersDto,
+ user: RequestUser,
+ onProgress?: (done: number, total: number) => Promise,
+ ): Promise {
+ const event = 'chapter_editor.revert';
+ const startedAt = Date.now();
+ this.logger.log(`[${event}] [start] bookId=${bookId} userId=${user.id} - chapter revert started`);
+ try {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ const context = await this.loadContext(bookId);
+ this.assertCurrent(context, dto.baseRevision);
+ const embedded = await this.readFileChapters(context, onProgress);
+
+ await this.audiobookChapters.restoreFileChapters(bookId, embedded, user.id, { expectedRevision: dto.baseRevision, locked: false });
+
+ this.logger.log(
+ `[${event}] [end] bookId=${bookId} durationMs=${Date.now() - startedAt} chapters=${embedded.length} - chapters reverted to the files`,
+ );
+ return this.buildState(bookId, await this.loadContext(bookId));
+ } catch (error) {
+ this.logFailure(event, bookId, startedAt, error);
+ throw error;
+ }
+ }
+
+ /** Writes the saved chapters into the audio files. The list must be the one the caller last saw. */
+ async writeToFiles(
+ bookId: number,
+ dto: WriteBookChaptersDto,
+ user: RequestUser,
+ onProgress?: (written: number, total: number) => Promise,
+ ): Promise {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ const context = await this.loadContext(bookId);
+ this.assertCurrent(context, dto.baseRevision);
+ const chapters = this.editorChapters(context);
+ if (chapters.length === 0) throw this.coded(ConflictException, BOOK_CHAPTERS_ERROR_CODES.nothingToWrite, 'There are no chapters to write');
+ const { tracks } = this.timeline(context);
+ if (tracks.some((track) => track.startMs === null)) {
+ throw this.coded(ConflictException, BOOK_CHAPTERS_ERROR_CODES.filesUnreadable, 'An audio file length is unknown');
+ }
+ try {
+ return await this.fileWriteService.writeChapters(
+ bookId,
+ tracks.map((track) => ({ id: track.fileId, timelineStartMs: track.startMs! })),
+ chapters,
+ user.id,
+ async () => {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ const fresh = await this.loadContext(bookId);
+ this.assertCurrent(fresh, dto.baseRevision);
+ return {
+ files: this.timeline(fresh).tracks.map((track) => ({ id: track.fileId, timelineStartMs: track.startMs! })),
+ chapters: this.editorChapters(fresh),
+ };
+ },
+ onProgress,
+ );
+ } catch (error) {
+ if (error instanceof ChapterWriteUnavailableError) {
+ throw new ConflictException({
+ message: error.message,
+ errorCode: BOOK_CHAPTERS_ERROR_CODES.writeUnavailable,
+ errorMeta: { reason: error.reason },
+ });
+ }
+ throw error;
+ }
+ }
+
+ async readEmbedded(bookId: number, user: RequestUser, onProgress?: (done: number, total: number) => Promise): Promise {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ const chapters = await this.readFileChapters(await this.loadContext(bookId), onProgress);
+ return { chapters: normalizeAudiobookChapters(chapters) };
+ }
+
+ async lookup(bookId: number, dto: LookupBookChaptersDto, user: RequestUser): Promise {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ return this.lookupService.lookup(dto.asin, dto.region ?? (await this.defaultAudibleRegion()));
+ }
+
+ async readEbook(bookId: number, source: EbookChapterSource, user: RequestUser): Promise {
+ await this.bookService.verifyBookAccess(bookId, user, 'editor');
+ const sources = this.ebookSources(await this.repo.findEbookFiles(bookId));
+ const fileId = source === 'toc' ? sources.ebookFileId : sources.readAlongFileId;
+ if (fileId === null) {
+ throw this.coded(ConflictException, BOOK_CHAPTERS_ERROR_CODES.ebookUnavailable, 'This book has no matching EPUB');
+ }
+
+ if (source === 'toc') {
+ const info = await this.epubService.getBookInfo(bookId, fileId, user);
+ const chapters = tocTitles(info.toc).map((title) => ({ title, startMs: 0 }));
+ if (chapters.length === 0) throw this.coded(ConflictException, BOOK_CHAPTERS_ERROR_CODES.ebookUnavailable, 'The EPUB has no table of contents');
+ return { source, fileId, chapters, titlesOnly: true, runtimeMs: null };
+ }
+
+ const playlist = await this.epubService.getMediaOverlayPlaylist(bookId, fileId, user);
+ const chapters = normalizeAudiobookChapters(
+ playlist.sections
+ .filter((section) => section.label?.trim())
+ .map((section) => ({ title: section.label!.trim(), startMs: Math.round(section.startSeconds * 1000) })),
+ );
+ if (chapters.length === 0)
+ throw this.coded(ConflictException, BOOK_CHAPTERS_ERROR_CODES.ebookUnavailable, 'The read-along EPUB names no sections');
+ return {
+ source,
+ fileId,
+ chapters,
+ titlesOnly: false,
+ runtimeMs: playlist.durationSeconds === null ? null : Math.round(playlist.durationSeconds * 1000),
+ };
+ }
+
+ /** Pauses near each point, for snapping chapter starts. A window whose audio will not decode is marked failed. */
+ async analyze(bookId: number, dto: AnalyzeBookChaptersDto, user: RequestUser, signal?: AbortSignal): Promise