From ff880a470c4c4dc92505478e4cc53a23b55a3c5b Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:27:03 +0000 Subject: [PATCH] feat(annotations): add book journal, starred highlights and colour names Add per-book journal entries keyed by a client id, a star toggle that does not bump the annotation version pushed to devices, a per-user colour legend, and an edit route so bookmark title and note changes reach the server. All of it is additive and advertised through app-info features, so iOS sends new fields only to servers that support them. Also sort annotations by numeric CFI steps instead of comparing CFI strings. --- packages/types/src/annotation.ts | 19 + packages/types/src/app-info.ts | 13 + packages/types/src/book-journal.ts | 50 + packages/types/src/index.ts | 1 + scripts/e2e/suite-registry.mjs | 22 + server/src/app.module.ts | 2 + ..._add_book_journal_and_annotation_stars.sql | 26 + .../src/db/migrations/meta/0102_snapshot.json | 22688 ++++++++++++++++ server/src/db/migrations/meta/_journal.json | 7 + server/src/db/schema/reader.ts | 51 + .../annotation/annotation-hub.service.test.ts | 25 + .../annotation/annotation-hub.service.ts | 3 + .../annotation/annotation.repository.test.ts | 124 +- .../annotation/annotation.repository.ts | 110 +- .../annotation/annotation.service.test.ts | 45 + .../modules/annotation/annotation.service.ts | 9 +- .../dto/annotation-dto-validation.test.ts | 29 + .../annotation/dto/annotation-hub.dto.ts | 6 +- .../annotation/dto/annotation-response.dto.ts | 2 + .../annotation/dto/update-annotation.dto.ts | 7 +- .../modules/app-info/app-info.service.test.ts | 17 + .../src/modules/app-info/app-info.service.ts | 3 +- .../book-journal.controller.test.ts | 92 + .../book-journal/book-journal.controller.ts | 74 + .../book-journal/book-journal.module.test.ts | 18 + .../book-journal/book-journal.module.ts | 13 + .../book-journal.repository.test.ts | 175 + .../book-journal/book-journal.repository.ts | 115 + .../book-journal/book-journal.service.test.ts | 315 + .../book-journal/book-journal.service.ts | 141 + .../dto/book-journal-dto-validation.test.ts | 105 + .../dto/book-journal-query.dto.ts | 9 + .../dto/create-book-journal-entry.dto.ts | 58 + .../dto/journal-text.transform.ts | 6 + .../dto/update-book-journal-entry.dto.ts | 53 + .../bookmark/bookmark-response.dto.test.ts | 36 + .../modules/bookmark/bookmark-sync.service.ts | 5 +- .../bookmark/bookmark.controller.test.ts | 9 + .../modules/bookmark/bookmark.controller.ts | 13 +- .../src/modules/bookmark/bookmark.dto.test.ts | 34 + .../bookmark/bookmark.repository.test.ts | 50 +- .../modules/bookmark/bookmark.repository.ts | 34 + .../modules/bookmark/bookmark.service.test.ts | 51 + .../src/modules/bookmark/bookmark.service.ts | 19 + .../bookmark/dto/bookmark-response.dto.ts | 10 + .../bookmark/dto/update-bookmark.dto.ts | 33 + .../koreader-bookmark-exchange.service.ts | 3 +- .../user-preferences.controller.test.ts | 18 + .../user-preferences.controller.ts | 12 + .../user-preferences.service.test.ts | 66 +- .../user-preferences.service.ts | 70 + server/test/book-notebook.e2e-spec.ts | 337 + .../route-inventory/manifest.json | 4 +- .../route-inventory/routes-02.json | 12 + .../route-inventory/routes-09.json | 24 + .../route-inventory/routes-10.json | 72 + 56 files changed, 25306 insertions(+), 39 deletions(-) create mode 100644 packages/types/src/book-journal.ts create mode 100644 server/src/db/migrations/0102_add_book_journal_and_annotation_stars.sql create mode 100644 server/src/db/migrations/meta/0102_snapshot.json create mode 100644 server/src/modules/book-journal/book-journal.controller.test.ts create mode 100644 server/src/modules/book-journal/book-journal.controller.ts create mode 100644 server/src/modules/book-journal/book-journal.module.test.ts create mode 100644 server/src/modules/book-journal/book-journal.module.ts create mode 100644 server/src/modules/book-journal/book-journal.repository.test.ts create mode 100644 server/src/modules/book-journal/book-journal.repository.ts create mode 100644 server/src/modules/book-journal/book-journal.service.test.ts create mode 100644 server/src/modules/book-journal/book-journal.service.ts create mode 100644 server/src/modules/book-journal/dto/book-journal-dto-validation.test.ts create mode 100644 server/src/modules/book-journal/dto/book-journal-query.dto.ts create mode 100644 server/src/modules/book-journal/dto/create-book-journal-entry.dto.ts create mode 100644 server/src/modules/book-journal/dto/journal-text.transform.ts create mode 100644 server/src/modules/book-journal/dto/update-book-journal-entry.dto.ts create mode 100644 server/src/modules/bookmark/dto/update-bookmark.dto.ts create mode 100644 server/test/book-notebook.e2e-spec.ts diff --git a/packages/types/src/annotation.ts b/packages/types/src/annotation.ts index f6cb76aa9..ebe3b5ae8 100644 --- a/packages/types/src/annotation.ts +++ b/packages/types/src/annotation.ts @@ -101,6 +101,8 @@ export interface AnnotationItem { updatedAt?: string | null; /** Present for PDF highlights; null/absent for EPUB and device-synced formats. */ pdf?: AnnotationPdfPosition | null; + /** When the reader starred it; null when not starred. Absent from servers older than stars. */ + starredAt?: string | null; } /** @@ -232,6 +234,23 @@ export interface AnnotationHubBookFacet { count: number; } +/** Bulk actions on `POST /annotations/bulk`. `star` and `unstar` touch only active rows. */ +export const ANNOTATION_BULK_ACTIONS = ["trash", "restore", "restyle", "star", "unstar"] as const; +export type AnnotationBulkAction = (typeof ANNOTATION_BULK_ACTIONS)[number]; + +export const ANNOTATION_COLOR_NAME_MAX_ENTRIES = 32; +export const ANNOTATION_COLOR_NAME_MAX_LENGTH = 40; + +/** + * A reader's own labels for highlight colours, keyed by uppercase `#RRGGBB`. Stored per user + * under the `annotation-colors` preference category. + */ +export interface AnnotationColorNamePreferences { + names: Record; +} + +export const ANNOTATION_COLOR_NAME_PREFERENCES_DEFAULTS: AnnotationColorNamePreferences = { names: {} }; + export type AnnotationPositionFormat = "cfi" | "xpointer" | "pdf" | "kobo_span"; export interface AnnotationPositionInfo { diff --git a/packages/types/src/app-info.ts b/packages/types/src/app-info.ts index 6e2638896..f308d435f 100644 --- a/packages/types/src/app-info.ts +++ b/packages/types/src/app-info.ts @@ -1,6 +1,19 @@ +/** + * Capabilities this server supports, advertised so a client can tell a newer server from an + * older one before sending a field or calling a route the older one would reject. Append only: + * a released client reads this list, so a value is never renamed or removed. + * + * Named apart from `APP_FEATURES` in app-features.ts, which holds the build-time feature flags. + */ +export const SERVER_FEATURES = ["annotation-stars", "annotation-color-names", "annotation-trash", "book-journal", "bookmark-edit"] as const; + +export type ServerFeature = (typeof SERVER_FEATURES)[number]; + export interface AppInfoResponse { version: string; updateAvailable: boolean | null; latestVersion: string | null; maxUploadSizeMb: number; + /** Absent on servers older than the field itself, which a client treats as an empty list. */ + features?: ServerFeature[]; } diff --git a/packages/types/src/book-journal.ts b/packages/types/src/book-journal.ts new file mode 100644 index 000000000..bcd164bcb --- /dev/null +++ b/packages/types/src/book-journal.ts @@ -0,0 +1,50 @@ +export const BOOK_JOURNAL_BODY_MAX_LENGTH = 10000; +export const BOOK_JOURNAL_QUOTE_MAX_LENGTH = 5000; +export const BOOK_JOURNAL_CHAPTER_TITLE_MAX_LENGTH = 500; +export const BOOK_JOURNAL_CFI_MAX_LENGTH = 2000; +/** Defensive ceiling on one book's list; a per-book journal is expected to stay far below it. */ +export const BOOK_JOURNAL_LIST_LIMIT = 2000; + +export const BOOK_JOURNAL_STATUSES = ["active", "trashed"] as const; +export type BookJournalStatus = (typeof BOOK_JOURNAL_STATUSES)[number]; + +/** A free-form note a reader keeps about one book, optionally anchored to where they were. */ +export interface BookJournalEntry { + id: number; + /** Client-generated UUID; the identity a client addresses the entry by in every route. */ + clientId: string; + bookId: number; + body: string; + quote: string | null; + chapterTitle: string | null; + /** 0 to 100. */ + positionPercent: number | null; + cfi: string | null; + positionSeconds: number | null; + createdAt: string; + updatedAt: string; + /** Set while the entry is in the trash. */ + deletedAt: string | null; +} + +export interface BookJournalEntryCreate { + clientId: string; + body: string; + quote?: string | null; + chapterTitle?: string | null; + positionPercent?: number | null; + cfi?: string | null; + positionSeconds?: number | null; + /** When the entry was written, for an entry captured offline. Clamped to now when ahead of it. */ + createdAt?: string; +} + +/** Every field optional; `null` clears a nullable field, and `body` cannot be cleared. */ +export interface BookJournalEntryUpdate { + body?: string; + quote?: string | null; + chapterTitle?: string | null; + positionPercent?: number | null; + cfi?: string | null; + positionSeconds?: number | null; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3398b3c28..99c9da05d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -72,6 +72,7 @@ export * from "./reading-session"; export * from "./reading-session-source-bucket"; export * from "./shared-reading-insights"; export * from "./annotation"; +export * from "./book-journal"; export * from "./hardcover"; export * from "./readwise"; export * from "./storygraph"; diff --git a/scripts/e2e/suite-registry.mjs b/scripts/e2e/suite-registry.mjs index e3614e7dd..3f5e199c2 100644 --- a/scripts/e2e/suite-registry.mjs +++ b/scripts/e2e/suite-registry.mjs @@ -831,6 +831,28 @@ export const E2E_SUITES = Object.freeze({ ...SHARED_DB_AND_HELPER_PATHS, ], }, + "book-notebook": { + id: "book-notebook", + name: "Book Notebook", + timeout: 45, + lane: "full", + description: "Book journal, annotation stars and reading order, bookmark edits and colour names suite", + vitestTarget: "test/book-notebook.e2e-spec.ts", + junitOutput: `${TEST_RESULTS_DIR}/book-notebook-e2e-junit.xml`, + prepareDedicatedDatabase: true, + useDedicatedDatabase: true, + changedPaths: [ + "server/src/modules/book-journal/**", + "server/src/modules/annotation/**", + "server/src/modules/bookmark/**", + "server/src/modules/user-preferences/**", + "server/src/modules/app-info/**", + "server/test/book-notebook.e2e-spec.ts", + "server/test/e2e/reader-state-isolation/**", + ...SHARED_DB_AND_HELPER_PATHS, + ...SHARED_TYPES_PATHS, + ], + }, "kobo-annotation-sync": { id: "kobo-annotation-sync", name: "Kobo Annotation Sync", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 00555564d..ca0ca60d8 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -37,6 +37,7 @@ import { CollectionModule } from './modules/collection/collection.module'; import { AuthModule } from './modules/auth/auth.module'; import { CbzModule } from './modules/reader/cbz/cbz.module'; import { BookmarkModule } from './modules/bookmark/bookmark.module'; +import { BookJournalModule } from './modules/book-journal/book-journal.module'; import { BookModule } from './modules/book/book.module'; import { CatalogModule } from './modules/catalog/catalog.module'; import { CoverModule } from './modules/cover/cover.module'; @@ -157,6 +158,7 @@ import { PodcastModule } from './modules/podcast/podcast.module'; KoboModule, OpdsModule, BookmarkModule, + BookJournalModule, AnnotationModule, DashboardModule, HealthModule, diff --git a/server/src/db/migrations/0102_add_book_journal_and_annotation_stars.sql b/server/src/db/migrations/0102_add_book_journal_and_annotation_stars.sql new file mode 100644 index 000000000..042356dcb --- /dev/null +++ b/server/src/db/migrations/0102_add_book_journal_and_annotation_stars.sql @@ -0,0 +1,26 @@ +CREATE TABLE "book_journal_entries" ( + "id" serial PRIMARY KEY NOT NULL, + "client_id" uuid NOT NULL, + "user_id" integer NOT NULL, + "book_id" integer NOT NULL, + "body" text NOT NULL, + "quote" text, + "chapter_title" varchar(500), + "position_percent" real, + "cfi" varchar(2000), + "position_seconds" real, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "deleted_at" timestamp with time zone, + CONSTRAINT "book_journal_entries_body_length_chk" CHECK (char_length("book_journal_entries"."body") between 1 and 10000), + CONSTRAINT "book_journal_entries_quote_length_chk" CHECK ("book_journal_entries"."quote" is null or char_length("book_journal_entries"."quote") <= 5000), + CONSTRAINT "book_journal_entries_position_percent_range_chk" CHECK ("book_journal_entries"."position_percent" is null or ("book_journal_entries"."position_percent" >= 0 and "book_journal_entries"."position_percent" <= 100)), + CONSTRAINT "book_journal_entries_position_seconds_nonnegative_chk" CHECK ("book_journal_entries"."position_seconds" is null or "book_journal_entries"."position_seconds" >= 0) +); +--> statement-breakpoint +ALTER TABLE "annotations" ADD COLUMN "starred_at" timestamp with time zone;--> statement-breakpoint +ALTER TABLE "book_journal_entries" ADD CONSTRAINT "book_journal_entries_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_journal_entries" ADD CONSTRAINT "book_journal_entries_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "book_journal_entries_user_client_id_uidx" ON "book_journal_entries" USING btree ("user_id","client_id");--> statement-breakpoint +CREATE INDEX "book_journal_entries_user_book_created_active_idx" ON "book_journal_entries" USING btree ("user_id","book_id","created_at") WHERE "book_journal_entries"."deleted_at" is null;--> statement-breakpoint +CREATE INDEX "book_journal_entries_book_id_idx" ON "book_journal_entries" USING btree ("book_id"); \ No newline at end of file diff --git a/server/src/db/migrations/meta/0102_snapshot.json b/server/src/db/migrations/meta/0102_snapshot.json new file mode 100644 index 000000000..69f8e8547 --- /dev/null +++ b/server/src/db/migrations/meta/0102_snapshot.json @@ -0,0 +1,22688 @@ +{ + "id": "83654d7f-8cfb-42e4-8a28-774fa4f7a102", + "prevId": "75fcd920-9917-42ba-b01c-f7baccc37d6c", + "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 + }, + "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_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" + }, + "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_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 + }, + "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 + }, + "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_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 + }, + "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 + }, + "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_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 + }, + "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_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_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_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" + } + }, + "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 + }, + "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.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 + }, + "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 + }, + "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_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 + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "bookmarks_origin_chk": { + "name": "bookmarks_origin_chk", + "value": "\"bookmarks\".\"origin\" in ('web', 'koreader')" + } + }, + "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 + } + }, + "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 + }, + "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_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_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_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 + }, + "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 + } + }, + "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 + }, + "store_sync": { + "name": "store_sync", + "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": {}, + "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 + }, + "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_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" + } + }, + "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 + }, + "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 + }, + "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 + }, + "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')" + } + }, + "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 + }, + "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')" + } + }, + "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_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_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 + }, + "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 + }, + "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": {}, + "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_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.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 + } + }, + "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 9e37316de..3f0db9311 100644 --- a/server/src/db/migrations/meta/_journal.json +++ b/server/src/db/migrations/meta/_journal.json @@ -715,6 +715,13 @@ "when": 1790876695526, "tag": "0101_refresh_unmeasured_media_overlay_durations", "breakpoints": true + }, + { + "idx": 102, + "version": "7", + "when": 1791126075111, + "tag": "0102_add_book_journal_and_annotation_stars", + "breakpoints": true } ] } diff --git a/server/src/db/schema/reader.ts b/server/src/db/schema/reader.ts index ed310f6ea..b5006dad5 100644 --- a/server/src/db/schema/reader.ts +++ b/server/src/db/schema/reader.ts @@ -156,6 +156,54 @@ export const userBookNotes = pgTable( export type UserBookNoteRow = typeof userBookNotes.$inferSelect; export type NewUserBookNote = typeof userBookNotes.$inferInsert; +export const bookJournalEntries = pgTable( + 'book_journal_entries', + { + id: serial('id').primaryKey(), + // Client-generated, so an entry written offline can be retried without duplicating it. + clientId: uuid('client_id').notNull(), + userId: integer('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + bookId: integer('book_id') + .notNull() + .references(() => books.id, { onDelete: 'cascade' }), + body: text('body').notNull(), + quote: text('quote'), + chapterTitle: varchar('chapter_title', { length: 500 }), + positionPercent: real('position_percent'), + cfi: varchar('cfi', { length: 2000 }), + positionSeconds: real('position_seconds'), + // Client-supplied for an entry captured offline, so it gets the decoder that reads any year. + createdAt: timestamptz('created_at') + .default(sql`now()`) + .notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdateFn(() => new Date()), + // Trash. Restorable until permanently deleted. + deletedAt: timestamp('deleted_at', { withTimezone: true }), + }, + (t) => [ + uniqueIndex('book_journal_entries_user_client_id_uidx').on(t.userId, t.clientId), + index('book_journal_entries_user_book_created_active_idx') + .on(t.userId, t.bookId, t.createdAt) + .where(sql`${t.deletedAt} is null`), + index('book_journal_entries_book_id_idx').on(t.bookId), + check('book_journal_entries_body_length_chk', sql`char_length(${t.body}) between 1 and 10000`), + check('book_journal_entries_quote_length_chk', sql`${t.quote} is null or char_length(${t.quote}) <= 5000`), + check( + 'book_journal_entries_position_percent_range_chk', + sql`${t.positionPercent} is null or (${t.positionPercent} >= 0 and ${t.positionPercent} <= 100)`, + ), + check('book_journal_entries_position_seconds_nonnegative_chk', sql`${t.positionSeconds} is null or ${t.positionSeconds} >= 0`), + ], +); + +export type BookJournalEntryRow = typeof bookJournalEntries.$inferSelect; +export type NewBookJournalEntry = typeof bookJournalEntries.$inferInsert; + export const readingProgress = pgTable( 'reading_progress', { @@ -480,6 +528,9 @@ export const annotations = pgTable( deviceCreatedAt: varchar('device_created_at', { length: 19 }), deviceUpdatedAt: varchar('device_updated_at', { length: 19 }), sourceCreatedAt: timestamp('source_created_at', { withTimezone: true }), + // Server-only reader metadata. Deliberately outside `version`, which drives device push-down: + // no device carries a star, so starring must not re-send the highlight to every device. + starredAt: timestamp('starred_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }) .notNull() diff --git a/server/src/modules/annotation/annotation-hub.service.test.ts b/server/src/modules/annotation/annotation-hub.service.test.ts index 3f3d4ae11..3c426dac1 100644 --- a/server/src/modules/annotation/annotation-hub.service.test.ts +++ b/server/src/modules/annotation/annotation-hub.service.test.ts @@ -42,6 +42,7 @@ function makeService(rebuilderRegistered = false) { findHubBookFacet: vi.fn().mockResolvedValue(null), bulkSetDeleted: vi.fn().mockResolvedValue(2), bulkRestyle: vi.fn().mockResolvedValue(2), + bulkSetStarred: vi.fn().mockResolvedValue(2), restore: vi.fn().mockResolvedValue(makeHubRow({ deletedAt: null })), findHubById: vi.fn().mockResolvedValue(makeHubRow()), purge: vi.fn().mockResolvedValue('purged'), @@ -88,6 +89,18 @@ describe('AnnotationHubService', () => { }); describe('list', () => { + it('returns starredAt on hub items as an ISO string or null', async () => { + const { service, annotationRepo } = makeService(); + annotationRepo.findHubPaginated.mockResolvedValueOnce({ + items: [makeHubRow({ starredAt: new Date('2026-02-02T02:02:02Z') }), makeHubRow({ id: 2, starredAt: null })], + total: 2, + }); + + const result = await service.list(10, {}); + + expect(result.items.map((item) => item.starredAt)).toEqual(['2026-02-02T02:02:02.000Z', null]); + }); + it('maps the date range to Date objects and the notes-only flag into repository filters', async () => { const { service, annotationRepo } = makeService(); @@ -293,6 +306,18 @@ describe('AnnotationHubService', () => { expect(annotationRepo.bulkRestyle).toHaveBeenCalledWith(10, [1], { color: '#38BDF8', style: 'underline' }); }); + + it('stars and unstars the given ids without touching deletion or style', async () => { + const { service, annotationRepo } = makeService(); + + expect(await service.bulk(10, { ids: [1, 2], action: 'star' })).toEqual({ affected: 2 }); + await service.bulk(10, { ids: [3], action: 'unstar' }); + + expect(annotationRepo.bulkSetStarred).toHaveBeenNthCalledWith(1, 10, [1, 2], true); + expect(annotationRepo.bulkSetStarred).toHaveBeenNthCalledWith(2, 10, [3], false); + expect(annotationRepo.bulkSetDeleted).not.toHaveBeenCalled(); + expect(annotationRepo.bulkRestyle).not.toHaveBeenCalled(); + }); }); describe('restore', () => { diff --git a/server/src/modules/annotation/annotation-hub.service.ts b/server/src/modules/annotation/annotation-hub.service.ts index 80eca9d12..5cc96466e 100644 --- a/server/src/modules/annotation/annotation-hub.service.ts +++ b/server/src/modules/annotation/annotation-hub.service.ts @@ -147,6 +147,8 @@ export class AnnotationHubService { affected = await this.annotationRepo.bulkSetDeleted(userId, dto.ids, true); } else if (dto.action === 'restore') { affected = await this.annotationRepo.bulkSetDeleted(userId, dto.ids, false); + } else if (dto.action === 'star' || dto.action === 'unstar') { + affected = await this.annotationRepo.bulkSetStarred(userId, dto.ids, dto.action === 'star'); } else { affected = await this.annotationRepo.bulkRestyle(userId, dto.ids, { color: dto.color, style: dto.style }); } @@ -275,6 +277,7 @@ export class AnnotationHubService { jumpFileId: row.jumpFileId, jumpFileFormat: row.jumpFileFormat, pageno: row.pageno, + starredAt: row.starredAt ? row.starredAt.toISOString() : null, }; } diff --git a/server/src/modules/annotation/annotation.repository.test.ts b/server/src/modules/annotation/annotation.repository.test.ts index 0e728bfd8..9c390fa3f 100644 --- a/server/src/modules/annotation/annotation.repository.test.ts +++ b/server/src/modules/annotation/annotation.repository.test.ts @@ -1,9 +1,9 @@ import { Test } from '@nestjs/testing'; -import type { SQL } from 'drizzle-orm'; +import { sql, type SQL } from 'drizzle-orm'; import { PgDialect } from 'drizzle-orm/pg-core'; import { DB } from '../../db'; -import { AnnotationRepository } from './annotation.repository'; +import { AnnotationRepository, cfiStepsSortKey } from './annotation.repository'; function makeRow(overrides?: Record) { return { @@ -238,6 +238,53 @@ describe('AnnotationRepository', () => { expect(result).toBeNull(); }); + + it('writes a null note through, which is how a note is cleared', async () => { + const db = makeDb([{ id: 1 }], [makeRow()]); + const repo = await makeRepository(db); + + await repo.update(5, 1, 10, { note: null }); + + const setCall = db._queries[0].set.mock.calls[0][0] as Record; + expect(setCall).toHaveProperty('note', null); + expect(setCall).toHaveProperty('version'); + expect(setCall).not.toHaveProperty('starredAt'); + }); + + it('stars without bumping the version devices sync from, keeping an existing star time', async () => { + const db = makeDb([{ id: 1 }], [makeRow()]); + const repo = await makeRepository(db); + + await repo.update(5, 1, 10, {}, { starred: true }); + + const setCall = db._queries[0].set.mock.calls[0][0] as Record; + expect(setCall).not.toHaveProperty('version'); + expect(setCall).toHaveProperty('updatedAt'); + expect(compileSql([setCall.starredAt])).toBe('coalesce("annotations"."starred_at", now())'); + }); + + it('unstars by clearing the star time, still without a version bump', async () => { + const db = makeDb([{ id: 1 }], [makeRow()]); + const repo = await makeRepository(db); + + await repo.update(5, 1, 10, {}, { starred: false }); + + const setCall = db._queries[0].set.mock.calls[0][0] as Record; + expect(setCall).toHaveProperty('starredAt', null); + expect(setCall).not.toHaveProperty('version'); + }); + + it('bumps the version when a star arrives together with a content change', async () => { + const db = makeDb([{ id: 1 }], [makeRow()]); + const repo = await makeRepository(db); + + await repo.update(5, 1, 10, { color: '#4ADE80' }, { starred: true }); + + const setCall = db._queries[0].set.mock.calls[0][0] as Record; + expect(setCall).toHaveProperty('color', '#4ADE80'); + expect(setCall).toHaveProperty('version'); + expect(setCall).toHaveProperty('starredAt'); + }); }); describe('softDelete', () => { @@ -359,6 +406,19 @@ describe('AnnotationRepository', () => { expect(db._queries[0].orderBy.mock.calls[0]).toHaveLength(4); }); + + it('orders CFI positions by their numeric steps rather than as strings', async () => { + const db = makeDb([], [{ count: 0 }]); + const repo = await makeRepository(db); + + await repo.findPaginated(5, 10, {}, { by: 'position', dir: 'desc' }, 1, 25); + + const terms = db._queries[0].orderBy.mock.calls[0].map((fragment: unknown) => compileSql([fragment])); + expect(terms[2]).toContain('regexp_matches("annotation_positions"."pos0", \'[/:]([0-9]+)\', \'g\') with ordinality'); + expect(terms[2]).toMatch(/desc nulls last$/); + expect(terms.some((term: string) => /^"annotation_positions"."pos0" (asc|desc)/.test(term))).toBe(false); + expect(terms[3]).toBe('"annotations"."id" desc'); + }); }); describe('getStats', () => { @@ -595,6 +655,22 @@ describe('AnnotationRepository', () => { expect(db.select.mock.calls[0][0]).toHaveProperty('xpointer'); expect(compileSql(db._queries[0].orderBy.mock.calls[0])).toContain('coalesce("annotations"."source_created_at", "annotations"."created_at")'); }); + + it('orders each book by reading position, not by chapter title', async () => { + const db = makeDb([]); + const repo = await makeRepository(db); + + await repo.findHubAll(10, { status: 'active' }); + + const terms = db._queries[0].orderBy.mock.calls[0].map((fragment: unknown) => compileSql([fragment])); + expect(terms[0]).toBe('"book_metadata"."title" asc'); + expect(terms[1]).toBe('"annotations"."book_id" asc'); + expect(terms.join(' ')).toContain("ap_pdf.format = 'pdf'"); + expect(terms.join(' ')).toContain('regexp_matches("annotation_positions"."pos0"'); + expect(terms.join(' ')).not.toContain('"annotations"."chapter_title"'); + expect(terms.at(-1)).toBe('"annotations"."id" asc'); + expect(db._queries[0].limit).toHaveBeenCalledWith(5000); + }); }); describe('findHubById', () => { @@ -707,6 +783,50 @@ describe('AnnotationRepository', () => { }); }); + describe('bulkSetStarred', () => { + it('returns 0 for an empty id list without querying', async () => { + const db = makeDb(); + const repo = await makeRepository(db); + + expect(await repo.bulkSetStarred(10, [], true)).toBe(0); + expect(db.update).not.toHaveBeenCalled(); + }); + + it('stars only active, unstarred rows the user owns, without a version bump', async () => { + const db = makeDb([{ id: 1 }, { id: 2 }]); + const repo = await makeRepository(db); + + expect(await repo.bulkSetStarred(10, [1, 2, 3], true)).toBe(2); + + const setCall = db._queries[0].set.mock.calls[0][0] as Record; + expect(setCall).not.toHaveProperty('version'); + expect(compileSql([setCall.starredAt])).toBe('coalesce("annotations"."starred_at", now())'); + const where = compileSql(db._queries[0].where.mock.calls[0]); + expect(where).toContain('"annotations"."user_id" = $'); + expect(where).toContain('"annotations"."deleted_at" is null'); + expect(where).toContain('"annotations"."starred_at" is null'); + }); + + it('unstars only rows that are starred', async () => { + const db = makeDb([{ id: 1 }]); + const repo = await makeRepository(db); + + expect(await repo.bulkSetStarred(10, [1], false)).toBe(1); + + expect(db._queries[0].set.mock.calls[0][0]).toHaveProperty('starredAt', null); + expect(compileSql(db._queries[0].where.mock.calls[0])).toContain('"annotations"."starred_at" is not null'); + }); + }); + + describe('cfiStepsSortKey', () => { + it('keeps the step pattern intact through the template literal', () => { + const compiled = compileSql([cfiStepsSortKey(sql`p.pos0`)]); + expect(compiled).toContain("regexp_matches(p.pos0, '[/:]([0-9]+)', 'g') with ordinality as step(m, n)"); + expect(compiled).toContain('order by step.n'); + expect(compiled).toContain("'{}'::numeric[]"); + }); + }); + describe('bulkRestyle', () => { it('returns 0 when there are no ids or no patch fields', async () => { const db = makeDb(); diff --git a/server/src/modules/annotation/annotation.repository.ts b/server/src/modules/annotation/annotation.repository.ts index b6a1e710b..5cc503e5a 100644 --- a/server/src/modules/annotation/annotation.repository.ts +++ b/server/src/modules/annotation/annotation.repository.ts @@ -59,6 +59,11 @@ function highlightedAt(): SQL { return sql`coalesce(${annotations.sourceCreatedAt}, ${annotations.createdAt})`; } +/** Starring an already starred row keeps its original timestamp. */ +function starredAtValue(starred: boolean): SQL | null { + return starred ? sql`coalesce(${annotations.starredAt}, now())` : null; +} + export interface HubFilters { bookId?: number; colors?: string[]; @@ -363,15 +368,28 @@ export class AnnotationRepository { }); } + /** + * `version` drives device push-down, so it moves for a content change and stays put for a + * request that only stars or unstars. A request with no star field keeps the old behaviour of + * always bumping, so nothing an older client sends changes meaning. + */ async update( bookId: number, annotationId: number, userId: number, data: Partial>, + options: { starred?: boolean } = {}, ): Promise { + const contentChanged = data.note !== undefined || data.color !== undefined || data.style !== undefined; + const bumpVersion = options.starred === undefined || contentChanged; const [row] = await this.db .update(annotations) - .set({ ...data, version: sql`${annotations.version} + 1`, updatedAt: sql`now()` }) + .set({ + ...data, + ...(bumpVersion && { version: sql`${annotations.version} + 1` }), + ...(options.starred !== undefined && { starredAt: starredAtValue(options.starred) }), + updatedAt: sql`now()`, + }) .where(and(eq(annotations.id, annotationId), ...this.baseConditions(bookId, userId))) .returning(); if (!row) return null; @@ -495,7 +513,9 @@ export class AnnotationRepository { .leftJoin(bookMetadata, eq(bookMetadata.bookId, annotations.bookId)) .leftJoin(books, eq(books.id, annotations.bookId)) .where(and(...conditions)) - .orderBy(asc(bookMetadata.title), asc(annotations.chapterTitle), asc(highlightedAt())) + // Book by book, then each book in reading order, so chapters come out as they are read + // rather than alphabetically. The book id keeps two books sharing a title apart. + .orderBy(asc(bookMetadata.title), asc(annotations.bookId), ...readingPositionOrder('asc'), asc(highlightedAt()), asc(annotations.id)) .limit(limit); return rows as HubAnnotationRow[]; } @@ -667,6 +687,24 @@ export class AnnotationRepository { return result.length; } + /** Active rows only, and only those whose star actually changes; never bumps the version. */ + async bulkSetStarred(userId: number, ids: number[], starred: boolean): Promise { + if (ids.length === 0) return 0; + const result = await this.db + .update(annotations) + .set({ starredAt: starredAtValue(starred), updatedAt: sql`now()` }) + .where( + and( + inArray(annotations.id, ids), + eq(annotations.userId, userId), + isNull(annotations.deletedAt), + starred ? isNull(annotations.starredAt) : isNotNull(annotations.starredAt), + ), + ) + .returning({ id: annotations.id }); + return result.length; + } + async bulkRestyle(userId: number, ids: number[], patch: { color?: string; style?: string }): Promise { if (ids.length === 0 || (patch.color === undefined && patch.style === undefined)) return 0; const result = await this.db @@ -756,29 +794,53 @@ export class AnnotationRepository { private buildOrderBy(sort: AnnotationSort) { const direction = sort.dir === 'desc' ? desc : asc; if (sort.by === 'position') { - const sqlDirection = sql.raw(sort.dir === 'desc' ? 'desc' : 'asc'); - const pdfPage = sql`( - select case - when ap_pdf.extras ->> 'pageno' ~ '^[0-9]+$' then (ap_pdf.extras ->> 'pageno')::int - else null - end - from ${annotationPositions} ap_pdf - where ap_pdf.annotation_id = ${annotations.id} and ap_pdf.format = 'pdf' - limit 1 - )`; - const pdfY = sql`( - select ((regexp_match(ap_pdf.pos0, '"y"[[:space:]]*:[[:space:]]*(-?[0-9]+(?:[.][0-9]+)?)'))[1])::numeric - from ${annotationPositions} ap_pdf - where ap_pdf.annotation_id = ${annotations.id} and ap_pdf.format = 'pdf' - limit 1 - )`; - return [ - sql`${pdfPage} ${sqlDirection} nulls last`, - sql`${pdfY} ${sqlDirection} nulls last`, - sql`${annotationPositions.pos0} ${sqlDirection} nulls last`, - direction(annotations.id), - ]; + return [...readingPositionOrder(sort.dir), direction(annotations.id)]; } return [direction(highlightedAt()), direction(annotations.id)]; } } + +/** + * The numeric steps of a CFI as an array, which Postgres compares element by element, so `/6/8` + * sorts before `/6/10` where the raw string would put it after. Null when the position is missing + * or carries no steps, so those rows still fall to the end under `nulls last`. The ordinality + * keeps the steps in the order they appear in the CFI, and `numeric` rather than `bigint` means a + * malformed CFI with an absurdly long digit run cannot overflow and fail the whole listing. + * + * Character classes rather than backslash escapes: inside a template literal JavaScript eats `\d`. + */ +export function cfiStepsSortKey(pos0: SQL | typeof annotationPositions.pos0): SQL { + return sql`nullif(array( + select (step.m)[1]::numeric + from regexp_matches(${pos0}, '[/:]([0-9]+)', 'g') with ordinality as step(m, n) + order by step.n + ), '{}'::numeric[])`; +} + +/** + * Where a highlight sits in the book: PDF page, then the vertical offset on that page, then the + * CFI steps. Requires the canonical `cfi` position joined as `annotation_positions`. + */ +export function readingPositionOrder(dir: 'asc' | 'desc'): SQL[] { + const sqlDirection = sql.raw(dir === 'desc' ? 'desc' : 'asc'); + const pdfPage = sql`( + select case + when ap_pdf.extras ->> 'pageno' ~ '^[0-9]+$' then (ap_pdf.extras ->> 'pageno')::int + else null + end + from ${annotationPositions} ap_pdf + where ap_pdf.annotation_id = ${annotations.id} and ap_pdf.format = 'pdf' + limit 1 + )`; + const pdfY = sql`( + select ((regexp_match(ap_pdf.pos0, '"y"[[:space:]]*:[[:space:]]*(-?[0-9]+(?:[.][0-9]+)?)'))[1])::numeric + from ${annotationPositions} ap_pdf + where ap_pdf.annotation_id = ${annotations.id} and ap_pdf.format = 'pdf' + limit 1 + )`; + return [ + sql`${pdfPage} ${sqlDirection} nulls last`, + sql`${pdfY} ${sqlDirection} nulls last`, + sql`${cfiStepsSortKey(annotationPositions.pos0)} ${sqlDirection} nulls last`, + ]; +} diff --git a/server/src/modules/annotation/annotation.service.test.ts b/server/src/modules/annotation/annotation.service.test.ts index fe84d14e2..84c0a0911 100644 --- a/server/src/modules/annotation/annotation.service.test.ts +++ b/server/src/modules/annotation/annotation.service.test.ts @@ -118,6 +118,18 @@ describe('AnnotationService', () => { expect(result[0].bookId).toBe(5); }); + it('returns starredAt on the unpaginated list, null when not starred', async () => { + const { service, annotationRepo } = await makeService(); + const starredAt = new Date('2026-03-04T05:06:07Z'); + annotationRepo.findByBookId.mockResolvedValue([makeAnnotationRow({ starredAt }), makeAnnotationRow({ id: 11, starredAt: null })]); + + const result = await service.getAnnotations(5, makeUser()); + + expect(result[0].starredAt).toEqual(starredAt); + expect(JSON.parse(JSON.stringify(result[0])).starredAt).toBe('2026-03-04T05:06:07.000Z'); + expect(result[1].starredAt).toBeNull(); + }); + it('verifies book access before querying', async () => { const { service, annotationRepo, bookService } = await makeService(); annotationRepo.findByBookId.mockResolvedValue([]); @@ -441,6 +453,25 @@ describe('AnnotationService', () => { expect(callArg).not.toHaveProperty('note'); }); + it('passes a star toggle through to the repository alongside the content patch', async () => { + const { service, annotationRepo } = await makeService(); + annotationRepo.update.mockResolvedValue(makeAnnotationRow({ starredAt: new Date('2026-03-04T05:06:07Z') })); + + const result = await service.updateAnnotation(5, 10, makeUser(), { starred: true }); + + expect(annotationRepo.update).toHaveBeenCalledWith(5, 10, 1, {}, { starred: true }); + expect(result.starredAt).toEqual(new Date('2026-03-04T05:06:07Z')); + }); + + it('passes an unstar with a note change as one request', async () => { + const { service, annotationRepo } = await makeService(); + annotationRepo.update.mockResolvedValue(makeAnnotationRow()); + + await service.updateAnnotation(5, 10, makeUser(), { starred: false, note: null }); + + expect(annotationRepo.update).toHaveBeenCalledWith(5, 10, 1, { note: null }, { starred: false }); + }); + it('throws NotFoundException when annotation is not found', async () => { const { service, annotationRepo } = await makeService(); annotationRepo.update.mockResolvedValue(null); @@ -669,6 +700,20 @@ describe('AnnotationService', () => { expect(result.stats.chapters).toEqual(['Chapter 1', 'Chapter 2']); }); + it('carries starredAt on paginated items as an ISO string or null', async () => { + const { service, annotationRepo } = await makeService(); + annotationRepo.findPaginated.mockResolvedValue({ + items: [makeAnnotationRow({ starredAt: new Date('2026-03-04T05:06:07Z') }), makeAnnotationRow({ id: 11 })], + total: 2, + }); + annotationRepo.getStats.mockResolvedValue(makeStatsResult()); + annotationRepo.getDistinctChapters.mockResolvedValue([]); + + const result = await service.getAnnotationsPaginated(5, makeUser(), { page: 1 }); + + expect(result.items.map((item) => item.starredAt)).toEqual(['2026-03-04T05:06:07.000Z', null]); + }); + it('verifies book access before querying', async () => { const { service, annotationRepo, bookService } = await makeService(); annotationRepo.findPaginated.mockResolvedValue({ items: [], total: 0 }); diff --git a/server/src/modules/annotation/annotation.service.ts b/server/src/modules/annotation/annotation.service.ts index 297f0356d..a939a8c88 100644 --- a/server/src/modules/annotation/annotation.service.ts +++ b/server/src/modules/annotation/annotation.service.ts @@ -70,6 +70,7 @@ export class AnnotationService { highlightedAt: dto.highlightedAt.toISOString(), createdAt: dto.createdAt instanceof Date ? dto.createdAt.toISOString() : String(dto.createdAt), pdf: dto.pdf, + starredAt: dto.starredAt ? dto.starredAt.toISOString() : null, }; }), total, @@ -129,11 +130,15 @@ export class AnnotationService { async updateAnnotation(bookId: number, annotationId: number, user: RequestUser, dto: UpdateAnnotationDto): Promise { await this.bookService.verifyBookAccess(bookId, user); - const row = await this.annotationRepo.update(bookId, annotationId, user.id, { + const content = { ...(dto.note !== undefined && { note: dto.note }), ...(dto.color !== undefined && { color: dto.color }), ...(dto.style !== undefined && { style: dto.style }), - }); + }; + const row = + dto.starred === undefined + ? await this.annotationRepo.update(bookId, annotationId, user.id, content) + : await this.annotationRepo.update(bookId, annotationId, user.id, content, { starred: dto.starred }); if (!row) throw new NotFoundException(this.notFoundMessage(bookId, annotationId)); return AnnotationResponseDto.from(row); } diff --git a/server/src/modules/annotation/dto/annotation-dto-validation.test.ts b/server/src/modules/annotation/dto/annotation-dto-validation.test.ts index ed14ddd60..8b29fb71a 100644 --- a/server/src/modules/annotation/dto/annotation-dto-validation.test.ts +++ b/server/src/modules/annotation/dto/annotation-dto-validation.test.ts @@ -261,4 +261,33 @@ describe('AnnotationHubBooksQueryDto validation', () => { expect(await errorsFor(AnnotationHubBooksQueryDto, { status: 'trashed' })).toHaveLength(0); expect((await errorsFor(AnnotationHubBooksQueryDto, { status: 'gone' })).length).toBeGreaterThan(0); }); + + describe('notebook additions', () => { + async function strictErrors(cls: new () => T, value: Record) { + return validate(plainToInstance(cls, value), { whitelist: true, forbidNonWhitelisted: true }); + } + + it('accepts a null note under the global whitelist settings, which is how a note is cleared', async () => { + const dto = plainToInstance(UpdateAnnotationDto, { note: null }); + expect(await strictErrors(UpdateAnnotationDto, { note: null })).toHaveLength(0); + expect(dto.note).toBeNull(); + }); + + it('accepts a boolean star toggle on its own or with content changes', async () => { + expect(await strictErrors(UpdateAnnotationDto, { starred: true })).toHaveLength(0); + expect(await strictErrors(UpdateAnnotationDto, { starred: false, note: null, color: '#4ADE80' })).toHaveLength(0); + }); + + it('rejects a non-boolean or null star toggle rather than reading null as unstar', async () => { + expect((await strictErrors(UpdateAnnotationDto, { starred: 'yes' })).map((error) => error.property)).toEqual(['starred']); + expect((await strictErrors(UpdateAnnotationDto, { starred: null })).map((error) => error.property)).toEqual(['starred']); + }); + + it('accepts star and unstar as bulk actions next to the existing ones', async () => { + for (const action of ['trash', 'restore', 'restyle', 'star', 'unstar']) { + expect(await strictErrors(AnnotationBulkDto, { ids: [1, 2], action })).toHaveLength(0); + } + expect((await strictErrors(AnnotationBulkDto, { ids: [1], action: 'favourite' })).map((error) => error.property)).toEqual(['action']); + }); + }); }); diff --git a/server/src/modules/annotation/dto/annotation-hub.dto.ts b/server/src/modules/annotation/dto/annotation-hub.dto.ts index 54d871efa..3234d1b55 100644 --- a/server/src/modules/annotation/dto/annotation-hub.dto.ts +++ b/server/src/modules/annotation/dto/annotation-hub.dto.ts @@ -1,6 +1,8 @@ import { Transform, Type } from 'class-transformer'; import { ArrayMaxSize, ArrayMinSize, IsArray, IsBoolean, IsIn, IsInt, IsISO8601, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; +import { ANNOTATION_BULK_ACTIONS, type AnnotationBulkAction } from '@bookorbit/types'; + import { ANNOTATION_POSITION_FORMATS, ANNOTATION_STYLES, type AnnotationPositionFormat } from '../annotation.constants'; export class AnnotationHubQueryDto { @@ -125,8 +127,8 @@ export class AnnotationBulkDto { @Min(1, { each: true }) ids!: number[]; - @IsIn(['trash', 'restore', 'restyle']) - action!: 'trash' | 'restore' | 'restyle'; + @IsIn(ANNOTATION_BULK_ACTIONS) + action!: AnnotationBulkAction; @IsOptional() @IsString() diff --git a/server/src/modules/annotation/dto/annotation-response.dto.ts b/server/src/modules/annotation/dto/annotation-response.dto.ts index b798e81d3..4ac5e26a8 100644 --- a/server/src/modules/annotation/dto/annotation-response.dto.ts +++ b/server/src/modules/annotation/dto/annotation-response.dto.ts @@ -20,6 +20,7 @@ export class AnnotationResponseDto { createdAt!: Date; pdf!: AnnotationPdfPosition | null; updatedAt!: Date | null; + starredAt!: Date | null; static from(row: AnnotationWithCfi): AnnotationResponseDto { const dto = new AnnotationResponseDto(); @@ -41,6 +42,7 @@ export class AnnotationResponseDto { dto.highlightedAt = row.sourceCreatedAt ?? row.createdAt; dto.createdAt = row.createdAt; dto.updatedAt = row.updatedAt ?? null; + dto.starredAt = row.starredAt ?? null; return dto; } } diff --git a/server/src/modules/annotation/dto/update-annotation.dto.ts b/server/src/modules/annotation/dto/update-annotation.dto.ts index 31ef174d0..e982f9ba4 100644 --- a/server/src/modules/annotation/dto/update-annotation.dto.ts +++ b/server/src/modules/annotation/dto/update-annotation.dto.ts @@ -1,4 +1,4 @@ -import { IsIn, IsOptional, IsString, MaxLength, ValidateIf } from 'class-validator'; +import { IsBoolean, IsIn, IsOptional, IsString, MaxLength, ValidateIf } from 'class-validator'; import { ANNOTATION_STYLES } from '../annotation.constants'; @@ -16,4 +16,9 @@ export class UpdateAnnotationDto { @IsOptional() @IsIn(ANNOTATION_STYLES) style?: string; + + /** Server-only metadata: toggling it alone does not bump the version devices sync from. */ + @ValidateIf((o: UpdateAnnotationDto) => o.starred !== undefined) + @IsBoolean() + starred?: boolean; } diff --git a/server/src/modules/app-info/app-info.service.test.ts b/server/src/modules/app-info/app-info.service.test.ts index 81c8fdf0a..3a6964a4d 100644 --- a/server/src/modules/app-info/app-info.service.test.ts +++ b/server/src/modules/app-info/app-info.service.test.ts @@ -1,5 +1,6 @@ import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { SERVER_FEATURES } from '@bookorbit/types'; import { AppSettingsService } from '../app-settings/app-settings.service'; import { GITHUB_RELEASES_API } from './app-info.constants'; @@ -73,6 +74,22 @@ describe('AppInfoService', () => { expect(info).not.toHaveProperty('bookDockPath'); }); + it('advertises every server feature so clients can detect support', async () => { + const service = new AppInfoService(makeConfig('Local build'), makeAppSettings()); + const info = await service.getAppInfo(); + expect(info.features).toEqual([...SERVER_FEATURES]); + expect(info.features).toEqual( + expect.arrayContaining(['annotation-stars', 'annotation-color-names', 'annotation-trash', 'book-journal', 'bookmark-edit']), + ); + }); + + it('returns a fresh features array per call so a caller cannot mutate the shared list', async () => { + const service = new AppInfoService(makeConfig('Local build'), makeAppSettings()); + const first = await service.getAppInfo(); + first.features?.push('book-journal'); + expect((await service.getAppInfo()).features).toEqual([...SERVER_FEATURES]); + }); + it('fetches from GitHub if checked for the first time', async () => { vi.spyOn(global, 'fetch').mockResolvedValue(makeGithubResponse('v1.3.0')); const service = new AppInfoService(makeConfig('v1.2.3'), makeAppSettings(true)); diff --git a/server/src/modules/app-info/app-info.service.ts b/server/src/modules/app-info/app-info.service.ts index 51421d0a3..febbcec52 100644 --- a/server/src/modules/app-info/app-info.service.ts +++ b/server/src/modules/app-info/app-info.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import type { AppInfoResponse } from '@bookorbit/types'; +import { SERVER_FEATURES, type AppInfoResponse } from '@bookorbit/types'; import { sanitizeLogValue } from '../../common/utils/log-sanitize.utils'; import { AppSettingsService } from '../app-settings/app-settings.service'; @@ -49,6 +49,7 @@ export class AppInfoService implements OnApplicationBootstrap { updateAvailable: this.updateAvailable, latestVersion: this.latestVersion, maxUploadSizeMb, + features: [...SERVER_FEATURES], }; } diff --git a/server/src/modules/book-journal/book-journal.controller.test.ts b/server/src/modules/book-journal/book-journal.controller.test.ts new file mode 100644 index 000000000..52ef5be04 --- /dev/null +++ b/server/src/modules/book-journal/book-journal.controller.test.ts @@ -0,0 +1,92 @@ +import 'reflect-metadata'; + +import { HttpStatus, ParseUUIDPipe, RequestMethod } from '@nestjs/common'; +import { HTTP_CODE_METADATA, METHOD_METADATA, PATH_METADATA, ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; + +import { EMPTY_CONTENT_FILTER_RULES } from '@bookorbit/types'; + +import type { RequestUser } from '../../common/types/request-user'; +import { BookJournalController } from './book-journal.controller'; + +const CLIENT_ID = '4f9c7a52-3d1e-4b6a-9a51-0c2f6e8d7b13'; + +const user: RequestUser = { + id: 8, + username: 'journal-user', + name: 'Journal User', + email: null, + active: true, + isDefaultPassword: false, + tokenVersion: 1, + settings: {}, + avatarUrl: null, + provisioningMethod: 'local', + isSuperuser: false, + permissions: [], + contentFilters: EMPTY_CONTENT_FILTER_RULES, +}; + +describe('BookJournalController', () => { + const service = { + list: vi.fn(), + create: vi.fn(), + update: vi.fn(), + trash: vi.fn(), + restore: vi.fn(), + purge: vi.fn(), + }; + const controller = new BookJournalController(service as never); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('is mounted under the book it belongs to', () => { + expect(Reflect.getMetadata(PATH_METADATA, BookJournalController)).toBe('books/:bookId/journal'); + }); + + it.each([ + ['list', RequestMethod.GET, '/', undefined], + ['create', RequestMethod.POST, '/', undefined], + ['update', RequestMethod.PATCH, ':clientId', undefined], + ['trash', RequestMethod.DELETE, ':clientId', HttpStatus.NO_CONTENT], + ['restore', RequestMethod.POST, ':clientId/restore', HttpStatus.OK], + ['purge', RequestMethod.DELETE, ':clientId/permanent', HttpStatus.NO_CONTENT], + ] as const)('declares %s as the expected route', (method, httpMethod, path, status) => { + const handler = BookJournalController.prototype[method]; + expect(Reflect.getMetadata(METHOD_METADATA, handler)).toBe(httpMethod); + expect(Reflect.getMetadata(PATH_METADATA, handler)).toBe(path); + expect(Reflect.getMetadata(HTTP_CODE_METADATA, handler)).toBe(status); + }); + + it.each(['update', 'trash', 'restore', 'purge'] as const)('validates the client id of %s as a UUID', (method) => { + const args = Reflect.getMetadata(ROUTE_ARGS_METADATA, BookJournalController, method) as Record; + const clientIdArg = Object.values(args).find((arg) => arg.data === 'clientId'); + expect(clientIdArg?.pipes.some((pipe) => pipe instanceof ParseUUIDPipe)).toBe(true); + }); + + it('defaults the list to active entries', async () => { + service.list.mockResolvedValue([]); + + await controller.list(5, user, {}); + await controller.list(5, user, { status: 'trashed' }); + + expect(service.list).toHaveBeenNthCalledWith(1, 5, user, 'active'); + expect(service.list).toHaveBeenNthCalledWith(2, 5, user, 'trashed'); + }); + + it('delegates create, update, trash, restore and purge with the current user', async () => { + const createDto = { clientId: CLIENT_ID, body: 'note' }; + await controller.create(5, createDto, user); + await controller.update(5, CLIENT_ID, { body: 'edited' }, user); + await controller.trash(5, CLIENT_ID, user); + await controller.restore(5, CLIENT_ID, user); + await controller.purge(5, CLIENT_ID, user); + + expect(service.create).toHaveBeenCalledWith(5, user, createDto); + expect(service.update).toHaveBeenCalledWith(5, CLIENT_ID, user, { body: 'edited' }); + expect(service.trash).toHaveBeenCalledWith(5, CLIENT_ID, user); + expect(service.restore).toHaveBeenCalledWith(5, CLIENT_ID, user); + expect(service.purge).toHaveBeenCalledWith(5, CLIENT_ID, user); + }); +}); diff --git a/server/src/modules/book-journal/book-journal.controller.ts b/server/src/modules/book-journal/book-journal.controller.ts new file mode 100644 index 000000000..f27f85d02 --- /dev/null +++ b/server/src/modules/book-journal/book-journal.controller.ts @@ -0,0 +1,74 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; + +import type { BookJournalEntry } from '@bookorbit/types'; + +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import type { RequestUser } from '../../common/types/request-user'; +import { BookJournalService } from './book-journal.service'; +import { BookJournalQueryDto } from './dto/book-journal-query.dto'; +import { CreateBookJournalEntryDto } from './dto/create-book-journal-entry.dto'; +import { UpdateBookJournalEntryDto } from './dto/update-book-journal-entry.dto'; + +@Controller('books/:bookId/journal') +export class BookJournalController { + constructor(private readonly journalService: BookJournalService) {} + + @Get() + list( + @Param('bookId', ParseIntPipe) bookId: number, + @CurrentUser() user: RequestUser, + @Query() query: BookJournalQueryDto, + ): Promise { + return this.journalService.list(bookId, user, query.status ?? 'active'); + } + + /** Always 201, including an idempotent replay of a client id this book already holds. */ + @Post() + create( + @Param('bookId', ParseIntPipe) bookId: number, + @Body() dto: CreateBookJournalEntryDto, + @CurrentUser() user: RequestUser, + ): Promise { + return this.journalService.create(bookId, user, dto); + } + + @Patch(':clientId') + update( + @Param('bookId', ParseIntPipe) bookId: number, + @Param('clientId', new ParseUUIDPipe()) clientId: string, + @Body() dto: UpdateBookJournalEntryDto, + @CurrentUser() user: RequestUser, + ): Promise { + return this.journalService.update(bookId, clientId, user, dto); + } + + @Delete(':clientId') + @HttpCode(HttpStatus.NO_CONTENT) + async trash( + @Param('bookId', ParseIntPipe) bookId: number, + @Param('clientId', new ParseUUIDPipe()) clientId: string, + @CurrentUser() user: RequestUser, + ): Promise { + await this.journalService.trash(bookId, clientId, user); + } + + @Post(':clientId/restore') + @HttpCode(HttpStatus.OK) + restore( + @Param('bookId', ParseIntPipe) bookId: number, + @Param('clientId', new ParseUUIDPipe()) clientId: string, + @CurrentUser() user: RequestUser, + ): Promise { + return this.journalService.restore(bookId, clientId, user); + } + + @Delete(':clientId/permanent') + @HttpCode(HttpStatus.NO_CONTENT) + async purge( + @Param('bookId', ParseIntPipe) bookId: number, + @Param('clientId', new ParseUUIDPipe()) clientId: string, + @CurrentUser() user: RequestUser, + ): Promise { + await this.journalService.purge(bookId, clientId, user); + } +} diff --git a/server/src/modules/book-journal/book-journal.module.test.ts b/server/src/modules/book-journal/book-journal.module.test.ts new file mode 100644 index 000000000..589705373 --- /dev/null +++ b/server/src/modules/book-journal/book-journal.module.test.ts @@ -0,0 +1,18 @@ +import 'reflect-metadata'; + +vi.mock('../book/book.module', () => ({ BookModule: class BookModule {} })); + +import { MODULE_METADATA } from '@nestjs/common/constants'; + +import { BookJournalController } from './book-journal.controller'; +import { BookJournalModule } from './book-journal.module'; +import { BookJournalRepository } from './book-journal.repository'; +import { BookJournalService } from './book-journal.service'; + +describe('BookJournalModule', () => { + it('registers the controller, service and repository without exporting internals', () => { + expect(Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, BookJournalModule)).toEqual([BookJournalController]); + expect(Reflect.getMetadata(MODULE_METADATA.PROVIDERS, BookJournalModule)).toEqual([BookJournalService, BookJournalRepository]); + expect(Reflect.getMetadata(MODULE_METADATA.EXPORTS, BookJournalModule)).toBeUndefined(); + }); +}); diff --git a/server/src/modules/book-journal/book-journal.module.ts b/server/src/modules/book-journal/book-journal.module.ts new file mode 100644 index 000000000..fb812d2d3 --- /dev/null +++ b/server/src/modules/book-journal/book-journal.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { BookModule } from '../book/book.module'; +import { BookJournalController } from './book-journal.controller'; +import { BookJournalRepository } from './book-journal.repository'; +import { BookJournalService } from './book-journal.service'; + +@Module({ + imports: [BookModule], + controllers: [BookJournalController], + providers: [BookJournalService, BookJournalRepository], +}) +export class BookJournalModule {} diff --git a/server/src/modules/book-journal/book-journal.repository.test.ts b/server/src/modules/book-journal/book-journal.repository.test.ts new file mode 100644 index 000000000..29a227e7f --- /dev/null +++ b/server/src/modules/book-journal/book-journal.repository.test.ts @@ -0,0 +1,175 @@ +import type { SQL } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + +import { bookJournalEntries } from '../../db/schema'; +import { BookJournalRepository } from './book-journal.repository'; + +const CLIENT_ID = '4f9c7a52-3d1e-4b6a-9a51-0c2f6e8d7b13'; + +type ChainQuery = Record> & { + then: (resolve: (value: unknown) => unknown, reject?: (error: unknown) => unknown) => Promise; +}; + +function mockQuery(result: unknown): ChainQuery { + const query = {} as ChainQuery; + for (const method of ['from', 'where', 'orderBy', 'limit', 'values', 'set', 'returning', 'onConflictDoNothing']) { + query[method] = vi.fn().mockReturnValue(query); + } + query.then = (resolve, reject) => Promise.resolve(result).then(resolve, reject); + return query; +} + +function makeDb(...results: unknown[]) { + const queue = [...results]; + const queries: ChainQuery[] = []; + const next = () => { + const query = mockQuery(queue.length > 0 ? queue.shift() : []); + queries.push(query); + return query; + }; + return { + select: vi.fn().mockImplementation(next), + insert: vi.fn().mockImplementation(next), + update: vi.fn().mockImplementation(next), + delete: vi.fn().mockImplementation(next), + _queries: queries, + }; +} + +function whereSql(query: ChainQuery): { sql: string; params: unknown[] } { + const compiled = new PgDialect().sqlToQuery(query.where.mock.calls[0][0] as SQL); + return { sql: compiled.sql, params: compiled.params }; +} + +describe('BookJournalRepository', () => { + it('lists active entries for one user and book, oldest first, capped', async () => { + const db = makeDb([{ id: 1 }]); + const repo = new BookJournalRepository(db as never); + + const rows = await repo.findByBook(7, 5, 'active', 2000); + + expect(rows).toEqual([{ id: 1 }]); + const query = db._queries[0]; + const { sql, params } = whereSql(query); + expect(sql).toContain('"book_journal_entries"."user_id" = $1'); + expect(sql).toContain('"book_journal_entries"."book_id" = $2'); + expect(sql).toContain('"book_journal_entries"."deleted_at" is null'); + expect(params).toEqual([7, 5]); + const order = query.orderBy.mock.calls[0].map((fragment: SQL) => new PgDialect().sqlToQuery(fragment).sql); + expect(order).toEqual(['"book_journal_entries"."created_at" asc', '"book_journal_entries"."id" asc']); + expect(query.limit).toHaveBeenCalledWith(2000); + }); + + it('lists only trashed entries for the trashed status', async () => { + const db = makeDb([]); + const repo = new BookJournalRepository(db as never); + + await repo.findByBook(7, 5, 'trashed', 2000); + + expect(whereSql(db._queries[0]).sql).toContain('"book_journal_entries"."deleted_at" is not null'); + }); + + it('looks a client id up for the caller only, across books', async () => { + const db = makeDb([]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.findByClientId(7, CLIENT_ID)).resolves.toBeNull(); + + const { sql, params } = whereSql(db._queries[0]); + expect(sql).toContain('"book_journal_entries"."user_id" = $1'); + expect(sql).toContain('"book_journal_entries"."client_id" = $2'); + expect(sql).not.toContain('book_id'); + expect(params).toEqual([7, CLIENT_ID]); + }); + + it('inserts without overwriting an existing client id', async () => { + const row = { id: 3 }; + const db = makeDb([row]); + const repo = new BookJournalRepository(db as never); + const values = { clientId: CLIENT_ID, userId: 7, bookId: 5, body: 'x' }; + + await expect(repo.insert(values)).resolves.toEqual(row); + + const query = db._queries[0]; + expect(db.insert).toHaveBeenCalledWith(bookJournalEntries); + expect(query.values).toHaveBeenCalledWith(values); + expect(query.onConflictDoNothing).toHaveBeenCalledWith({ target: [bookJournalEntries.userId, bookJournalEntries.clientId] }); + }); + + it('returns null from insert when the client id already exists', async () => { + const db = makeDb([]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.insert({ clientId: CLIENT_ID, userId: 7, bookId: 5, body: 'x' })).resolves.toBeNull(); + }); + + it('updates only an active entry the caller owns on that book', async () => { + const db = makeDb([{ id: 1 }]); + const repo = new BookJournalRepository(db as never); + + await repo.updateActive(7, 5, CLIENT_ID, { body: 'edited' }); + + const query = db._queries[0]; + expect(query.set).toHaveBeenCalledWith({ body: 'edited' }); + const { sql, params } = whereSql(query); + expect(sql).toContain('"book_journal_entries"."user_id" = $1'); + expect(sql).toContain('"book_journal_entries"."book_id" = $2'); + expect(sql).toContain('"book_journal_entries"."client_id" = $3'); + expect(sql).toContain('"book_journal_entries"."deleted_at" is null'); + expect(params).toEqual([7, 5, CLIENT_ID]); + }); + + describe('trash', () => { + it('reports trashed when the entry moved', async () => { + const db = makeDb([{ id: 1 }]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.trash(7, 5, CLIENT_ID)).resolves.toBe('trashed'); + expect(db._queries[0].set).toHaveBeenCalledWith({ deletedAt: expect.any(Date) }); + expect(whereSql(db._queries[0]).sql).toContain('"book_journal_entries"."deleted_at" is null'); + expect(db.select).not.toHaveBeenCalled(); + }); + + it('reports already_trashed when the entry exists but was not active', async () => { + const db = makeDb([], [{ id: 1 }]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.trash(7, 5, CLIENT_ID)).resolves.toBe('already_trashed'); + expect(whereSql(db._queries[1]).params).toEqual([7, 5, CLIENT_ID]); + }); + + it('reports not_found when nothing matches', async () => { + const db = makeDb([], []); + const repo = new BookJournalRepository(db as never); + + await expect(repo.trash(7, 5, CLIENT_ID)).resolves.toBe('not_found'); + }); + }); + + it('restores only a trashed entry', async () => { + const db = makeDb([]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.restore(7, 5, CLIENT_ID)).resolves.toBeNull(); + expect(db._queries[0].set).toHaveBeenCalledWith({ deletedAt: null }); + expect(whereSql(db._queries[0]).sql).toContain('"book_journal_entries"."deleted_at" is not null'); + }); + + it('purges only a trashed entry', async () => { + const db = makeDb([{ id: 1 }]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.purge(7, 5, CLIENT_ID)).resolves.toBe(true); + expect(db.delete).toHaveBeenCalledWith(bookJournalEntries); + const { sql, params } = whereSql(db._queries[0]); + expect(sql).toContain('"book_journal_entries"."deleted_at" is not null'); + expect(params).toEqual([7, 5, CLIENT_ID]); + }); + + it('reports false when there is nothing in the trash to purge', async () => { + const db = makeDb([]); + const repo = new BookJournalRepository(db as never); + + await expect(repo.purge(7, 5, CLIENT_ID)).resolves.toBe(false); + }); +}); diff --git a/server/src/modules/book-journal/book-journal.repository.ts b/server/src/modules/book-journal/book-journal.repository.ts new file mode 100644 index 000000000..56d2c05fe --- /dev/null +++ b/server/src/modules/book-journal/book-journal.repository.ts @@ -0,0 +1,115 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { type SQL, and, asc, eq, isNotNull, isNull } from 'drizzle-orm'; +import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; + +import type { BookJournalStatus } from '@bookorbit/types'; + +import { DB } from '../../db'; +import * as schema from '../../db/schema'; +import { bookJournalEntries, type BookJournalEntryRow, type NewBookJournalEntry } from '../../db/schema'; + +type Db = NodePgDatabase; + +export type BookJournalEntryInsert = Omit; +export type BookJournalEntryPatch = Partial< + Pick +>; + +@Injectable() +export class BookJournalRepository { + constructor(@Inject(DB) private readonly db: Db) {} + + /** Oldest first, the order a journal is read in. */ + findByBook(userId: number, bookId: number, status: BookJournalStatus, limit: number): Promise { + const trashed = status === 'trashed'; + return this.db + .select() + .from(bookJournalEntries) + .where( + and( + eq(bookJournalEntries.userId, userId), + eq(bookJournalEntries.bookId, bookId), + trashed ? isNotNull(bookJournalEntries.deletedAt) : isNull(bookJournalEntries.deletedAt), + ), + ) + .orderBy(asc(bookJournalEntries.createdAt), asc(bookJournalEntries.id)) + .limit(limit); + } + + /** Any state and any book: the client id is unique per user, not per book. */ + async findByClientId(userId: number, clientId: string): Promise { + const [row] = await this.db + .select() + .from(bookJournalEntries) + .where(and(eq(bookJournalEntries.userId, userId), eq(bookJournalEntries.clientId, clientId))) + .limit(1); + return row ?? null; + } + + /** Returns null when the user already has an entry with this client id. */ + async insert(values: BookJournalEntryInsert): Promise { + const [row] = await this.db + .insert(bookJournalEntries) + .values(values) + .onConflictDoNothing({ target: [bookJournalEntries.userId, bookJournalEntries.clientId] }) + .returning(); + return row ?? null; + } + + async findActive(userId: number, bookId: number, clientId: string): Promise { + const [row] = await this.db + .select() + .from(bookJournalEntries) + .where(and(...this.identity(userId, bookId, clientId), isNull(bookJournalEntries.deletedAt))) + .limit(1); + return row ?? null; + } + + async updateActive(userId: number, bookId: number, clientId: string, patch: BookJournalEntryPatch): Promise { + const [row] = await this.db + .update(bookJournalEntries) + .set(patch) + .where(and(...this.identity(userId, bookId, clientId), isNull(bookJournalEntries.deletedAt))) + .returning(); + return row ?? null; + } + + /** `trashed` when this call moved it, `already_trashed` when it was in the trash before. */ + async trash(userId: number, bookId: number, clientId: string): Promise<'trashed' | 'already_trashed' | 'not_found'> { + const moved = await this.db + .update(bookJournalEntries) + .set({ deletedAt: new Date() }) + .where(and(...this.identity(userId, bookId, clientId), isNull(bookJournalEntries.deletedAt))) + .returning({ id: bookJournalEntries.id }); + if (moved.length > 0) return 'trashed'; + + const [existing] = await this.db + .select({ id: bookJournalEntries.id }) + .from(bookJournalEntries) + .where(and(...this.identity(userId, bookId, clientId))) + .limit(1); + return existing ? 'already_trashed' : 'not_found'; + } + + async restore(userId: number, bookId: number, clientId: string): Promise { + const [row] = await this.db + .update(bookJournalEntries) + .set({ deletedAt: null }) + .where(and(...this.identity(userId, bookId, clientId), isNotNull(bookJournalEntries.deletedAt))) + .returning(); + return row ?? null; + } + + /** Hard delete, only from the trash. */ + async purge(userId: number, bookId: number, clientId: string): Promise { + const deleted = await this.db + .delete(bookJournalEntries) + .where(and(...this.identity(userId, bookId, clientId), isNotNull(bookJournalEntries.deletedAt))) + .returning({ id: bookJournalEntries.id }); + return deleted.length > 0; + } + + private identity(userId: number, bookId: number, clientId: string): SQL[] { + return [eq(bookJournalEntries.userId, userId), eq(bookJournalEntries.bookId, bookId), eq(bookJournalEntries.clientId, clientId)]; + } +} diff --git a/server/src/modules/book-journal/book-journal.service.test.ts b/server/src/modules/book-journal/book-journal.service.test.ts new file mode 100644 index 000000000..ddd6ab184 --- /dev/null +++ b/server/src/modules/book-journal/book-journal.service.test.ts @@ -0,0 +1,315 @@ +import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common'; + +import { BOOK_JOURNAL_LIST_LIMIT, EMPTY_CONTENT_FILTER_RULES } from '@bookorbit/types'; + +import type { RequestUser } from '../../common/types/request-user'; +import type { BookJournalEntryRow } from '../../db/schema'; +import { BOOK_JOURNAL_FUTURE_SKEW_MS, BookJournalService, resolveCreatedAt, toBookJournalEntry } from './book-journal.service'; +import type { CreateBookJournalEntryDto } from './dto/create-book-journal-entry.dto'; + +const CLIENT_ID = '4f9c7a52-3d1e-4b6a-9a51-0c2f6e8d7b13'; +const NOW = new Date('2026-10-04T12:00:00.000Z'); + +function makeUser(overrides?: Partial): RequestUser { + return { + id: 7, + username: 'journaler', + name: 'Journal Reader', + email: null, + active: true, + isSuperuser: false, + isDefaultPassword: false, + tokenVersion: 1, + settings: {}, + avatarUrl: null, + provisioningMethod: 'local', + permissions: [], + contentFilters: EMPTY_CONTENT_FILTER_RULES, + ...overrides, + }; +} + +function makeRow(overrides?: Partial): BookJournalEntryRow { + return { + id: 1, + clientId: CLIENT_ID, + userId: 7, + bookId: 5, + body: 'A thought about chapter two', + quote: null, + chapterTitle: null, + positionPercent: null, + cfi: null, + positionSeconds: null, + createdAt: new Date('2026-10-01T08:00:00.000Z'), + updatedAt: new Date('2026-10-01T08:00:00.000Z'), + deletedAt: null, + ...overrides, + }; +} + +function makeService() { + const repo = { + findByBook: vi.fn().mockResolvedValue([]), + findByClientId: vi.fn().mockResolvedValue(null), + insert: vi.fn(), + findActive: vi.fn().mockResolvedValue(null), + updateActive: vi.fn().mockResolvedValue(null), + trash: vi.fn(), + restore: vi.fn().mockResolvedValue(null), + purge: vi.fn().mockResolvedValue(false), + }; + const bookService = { verifyBookAccess: vi.fn().mockResolvedValue(undefined) }; + const service = new BookJournalService(repo as never, bookService as never); + return { service, repo, bookService }; +} + +function createDto(overrides?: Partial): CreateBookJournalEntryDto { + return { clientId: CLIENT_ID, body: 'A thought about chapter two', ...overrides } as CreateBookJournalEntryDto; +} + +describe('BookJournalService', () => { + describe('list', () => { + it('verifies access, scopes by the caller and maps rows to the shared shape', async () => { + const { service, repo, bookService } = makeService(); + const user = makeUser(); + repo.findByBook.mockResolvedValue([makeRow({ quote: 'quoted', positionPercent: 12.5 })]); + + const result = await service.list(5, user); + + expect(bookService.verifyBookAccess).toHaveBeenCalledWith(5, user); + expect(repo.findByBook).toHaveBeenCalledWith(7, 5, 'active', BOOK_JOURNAL_LIST_LIMIT); + expect(result).toEqual([ + { + id: 1, + clientId: CLIENT_ID, + bookId: 5, + body: 'A thought about chapter two', + quote: 'quoted', + chapterTitle: null, + positionPercent: 12.5, + cfi: null, + positionSeconds: null, + createdAt: '2026-10-01T08:00:00.000Z', + updatedAt: '2026-10-01T08:00:00.000Z', + deletedAt: null, + }, + ]); + expect(result[0]).not.toHaveProperty('userId'); + }); + + it('lists the trash when asked', async () => { + const { service, repo } = makeService(); + await service.list(5, makeUser(), 'trashed'); + expect(repo.findByBook).toHaveBeenCalledWith(7, 5, 'trashed', BOOK_JOURNAL_LIST_LIMIT); + }); + + it('does not query when the book is not accessible', async () => { + const { service, repo, bookService } = makeService(); + bookService.verifyBookAccess.mockRejectedValue(new NotFoundException('Book 5 not found')); + + await expect(service.list(5, makeUser())).rejects.toThrow(NotFoundException); + expect(repo.findByBook).not.toHaveBeenCalled(); + }); + }); + + describe('create', () => { + it('inserts a normalized entry owned by the caller', async () => { + const { service, repo } = makeService(); + repo.insert.mockResolvedValue(makeRow()); + + await service.create( + 5, + makeUser(), + createDto({ + body: ' trimmed body ', + quote: ' quoted line ', + chapterTitle: ' ', + positionPercent: 42, + cfi: 'epubcfi(/6/8!/4/2/1:0)', + positionSeconds: 0, + }), + NOW, + ); + + expect(repo.insert).toHaveBeenCalledWith({ + clientId: CLIENT_ID, + userId: 7, + bookId: 5, + body: 'trimmed body', + quote: 'quoted line', + chapterTitle: null, + positionPercent: 42, + cfi: 'epubcfi(/6/8!/4/2/1:0)', + positionSeconds: 0, + createdAt: NOW, + }); + }); + + it('keeps an offline createdAt that is in the past', async () => { + const { service, repo } = makeService(); + repo.insert.mockResolvedValue(makeRow()); + + await service.create(5, makeUser(), createDto({ createdAt: '2026-09-30T21:15:00.000Z' }), NOW); + + expect(repo.insert.mock.calls[0][0].createdAt).toEqual(new Date('2026-09-30T21:15:00.000Z')); + }); + + it('rejects a body that is empty after trimming', async () => { + const { service, repo } = makeService(); + + await expect(service.create(5, makeUser(), createDto({ body: ' ' }), NOW)).rejects.toThrow(BadRequestException); + expect(repo.insert).not.toHaveBeenCalled(); + }); + + it('returns the stored entry unchanged when the client id is replayed for the same book', async () => { + const { service, repo } = makeService(); + const stored = makeRow({ body: 'original body', deletedAt: new Date('2026-10-02T00:00:00.000Z') }); + repo.insert.mockResolvedValue(null); + repo.findByClientId.mockResolvedValue(stored); + + const result = await service.create(5, makeUser(), createDto({ body: 'a different body' }), NOW); + + expect(repo.findByClientId).toHaveBeenCalledWith(7, CLIENT_ID); + expect(result).toEqual(toBookJournalEntry(stored)); + expect(result.body).toBe('original body'); + }); + + it('rejects a client id that already belongs to another book with 409', async () => { + const { service, repo } = makeService(); + repo.insert.mockResolvedValue(null); + repo.findByClientId.mockResolvedValue(makeRow({ bookId: 99 })); + + await expect(service.create(5, makeUser(), createDto(), NOW)).rejects.toThrow(ConflictException); + }); + + it('answers 409 when the conflicting row vanished before it could be read', async () => { + const { service, repo } = makeService(); + repo.insert.mockResolvedValue(null); + + await expect(service.create(5, makeUser(), createDto(), NOW)).rejects.toThrow(ConflictException); + }); + + it('does not insert when the book is not accessible', async () => { + const { service, repo, bookService } = makeService(); + bookService.verifyBookAccess.mockRejectedValue(new NotFoundException('Book 5 not found')); + + await expect(service.create(5, makeUser(), createDto(), NOW)).rejects.toThrow(NotFoundException); + expect(repo.insert).not.toHaveBeenCalled(); + }); + }); + + describe('update', () => { + it('patches only the fields sent and lets null clear a nullable field', async () => { + const { service, repo } = makeService(); + repo.updateActive.mockResolvedValue(makeRow({ body: 'edited' })); + + const result = await service.update(5, CLIENT_ID, makeUser(), { body: ' edited ', quote: null, positionPercent: null, cfi: '' }); + + expect(repo.updateActive).toHaveBeenCalledWith(7, 5, CLIENT_ID, { body: 'edited', quote: null, positionPercent: null, cfi: null }); + expect(result.body).toBe('edited'); + }); + + it('reads the active row instead of writing when nothing was sent', async () => { + const { service, repo } = makeService(); + repo.findActive.mockResolvedValue(makeRow()); + + await service.update(5, CLIENT_ID, makeUser(), {}); + + expect(repo.updateActive).not.toHaveBeenCalled(); + expect(repo.findActive).toHaveBeenCalledWith(7, 5, CLIENT_ID); + }); + + it('answers 404 for a missing or trashed entry', async () => { + const { service } = makeService(); + + await expect(service.update(5, CLIENT_ID, makeUser(), { body: 'x' })).rejects.toThrow(NotFoundException); + await expect(service.update(5, CLIENT_ID, makeUser(), {})).rejects.toThrow(NotFoundException); + }); + + it('rejects an edited body that is empty after trimming', async () => { + const { service, repo } = makeService(); + + await expect(service.update(5, CLIENT_ID, makeUser(), { body: ' ' })).rejects.toThrow(BadRequestException); + expect(repo.updateActive).not.toHaveBeenCalled(); + }); + }); + + describe('trash', () => { + it('succeeds when the entry moves to the trash and when it was already there', async () => { + const { service, repo } = makeService(); + repo.trash.mockResolvedValueOnce('trashed').mockResolvedValueOnce('already_trashed'); + + await expect(service.trash(5, CLIENT_ID, makeUser())).resolves.toBeUndefined(); + await expect(service.trash(5, CLIENT_ID, makeUser())).resolves.toBeUndefined(); + expect(repo.trash).toHaveBeenCalledWith(7, 5, CLIENT_ID); + }); + + it('answers 404 for an entry that does not exist', async () => { + const { service, repo } = makeService(); + repo.trash.mockResolvedValue('not_found'); + + await expect(service.trash(5, CLIENT_ID, makeUser())).rejects.toThrow(NotFoundException); + }); + }); + + describe('restore', () => { + it('returns the restored entry', async () => { + const { service, repo } = makeService(); + repo.restore.mockResolvedValue(makeRow()); + + const result = await service.restore(5, CLIENT_ID, makeUser()); + + expect(repo.restore).toHaveBeenCalledWith(7, 5, CLIENT_ID); + expect(result.deletedAt).toBeNull(); + }); + + it('answers 404 unless the entry is in the trash', async () => { + const { service } = makeService(); + await expect(service.restore(5, CLIENT_ID, makeUser())).rejects.toThrow(NotFoundException); + }); + }); + + describe('purge', () => { + it('hard-deletes a trashed entry', async () => { + const { service, repo } = makeService(); + repo.purge.mockResolvedValue(true); + + await expect(service.purge(5, CLIENT_ID, makeUser())).resolves.toBeUndefined(); + expect(repo.purge).toHaveBeenCalledWith(7, 5, CLIENT_ID); + }); + + it('answers 404 unless the entry is in the trash', async () => { + const { service } = makeService(); + await expect(service.purge(5, CLIENT_ID, makeUser())).rejects.toThrow(NotFoundException); + }); + + it('checks access before touching the trash', async () => { + const { service, repo, bookService } = makeService(); + bookService.verifyBookAccess.mockRejectedValue(new NotFoundException('Book 5 not found')); + + await expect(service.purge(5, CLIENT_ID, makeUser())).rejects.toThrow(NotFoundException); + expect(repo.purge).not.toHaveBeenCalled(); + }); + }); +}); + +describe('resolveCreatedAt', () => { + it('defaults to now', () => { + expect(resolveCreatedAt(undefined, NOW)).toBe(NOW); + }); + + it('keeps a timestamp within the allowed clock skew', () => { + const ahead = new Date(NOW.getTime() + BOOK_JOURNAL_FUTURE_SKEW_MS); + expect(resolveCreatedAt(ahead.toISOString(), NOW)).toEqual(ahead); + }); + + it('clamps a timestamp further in the future to now', () => { + const farAhead = new Date(NOW.getTime() + BOOK_JOURNAL_FUTURE_SKEW_MS + 1); + expect(resolveCreatedAt(farAhead.toISOString(), NOW)).toBe(NOW); + }); + + it('rejects a timestamp before 1970', () => { + expect(() => resolveCreatedAt('1969-12-31T23:59:59.000Z', NOW)).toThrow(BadRequestException); + }); +}); diff --git a/server/src/modules/book-journal/book-journal.service.ts b/server/src/modules/book-journal/book-journal.service.ts new file mode 100644 index 000000000..643f2987b --- /dev/null +++ b/server/src/modules/book-journal/book-journal.service.ts @@ -0,0 +1,141 @@ +import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; + +import { BOOK_JOURNAL_LIST_LIMIT, type BookJournalEntry, type BookJournalStatus } from '@bookorbit/types'; + +import type { RequestUser } from '../../common/types/request-user'; +import type { BookJournalEntryRow } from '../../db/schema'; +import { BookService } from '../book/book.service'; +import { BookJournalRepository, type BookJournalEntryPatch } from './book-journal.repository'; +import type { CreateBookJournalEntryDto } from './dto/create-book-journal-entry.dto'; +import type { UpdateBookJournalEntryDto } from './dto/update-book-journal-entry.dto'; + +/** How far ahead of the server clock an offline entry's own timestamp may be before it is clamped. */ +export const BOOK_JOURNAL_FUTURE_SKEW_MS = 5 * 60 * 1000; +const EARLIEST_CREATED_AT_MS = Date.UTC(1970, 0, 1); + +@Injectable() +export class BookJournalService { + constructor( + private readonly repo: BookJournalRepository, + private readonly bookService: BookService, + ) {} + + async list(bookId: number, user: RequestUser, status: BookJournalStatus = 'active'): Promise { + await this.bookService.verifyBookAccess(bookId, user); + const rows = await this.repo.findByBook(user.id, bookId, status, BOOK_JOURNAL_LIST_LIMIT); + return rows.map(toBookJournalEntry); + } + + /** + * Idempotent on the client id, so an offline client can retry a create it never saw answered. + * A replay returns the stored entry untouched, even if it has since been edited or trashed. + */ + async create(bookId: number, user: RequestUser, dto: CreateBookJournalEntryDto, now = new Date()): Promise { + await this.bookService.verifyBookAccess(bookId, user); + + const inserted = await this.repo.insert({ + clientId: dto.clientId, + userId: user.id, + bookId, + body: requireBody(dto.body), + quote: optionalText(dto.quote), + chapterTitle: optionalText(dto.chapterTitle), + positionPercent: dto.positionPercent ?? null, + cfi: optionalText(dto.cfi), + positionSeconds: dto.positionSeconds ?? null, + createdAt: resolveCreatedAt(dto.createdAt, now), + }); + if (inserted) return toBookJournalEntry(inserted); + + const existing = await this.repo.findByClientId(user.id, dto.clientId); + if (!existing) throw new ConflictException('Journal entry could not be created; retry the request'); + if (existing.bookId !== bookId) throw new ConflictException('This journal entry id already belongs to another book'); + return toBookJournalEntry(existing); + } + + async update(bookId: number, clientId: string, user: RequestUser, dto: UpdateBookJournalEntryDto): Promise { + await this.bookService.verifyBookAccess(bookId, user); + const patch = buildPatch(dto); + + const row = + Object.keys(patch).length === 0 + ? await this.repo.findActive(user.id, bookId, clientId) + : await this.repo.updateActive(user.id, bookId, clientId, patch); + if (!row) throw new NotFoundException(notFoundMessage(bookId, clientId)); + return toBookJournalEntry(row); + } + + /** Moves the entry to the trash. Trashing an entry already there is a no-op, not an error. */ + async trash(bookId: number, clientId: string, user: RequestUser): Promise { + await this.bookService.verifyBookAccess(bookId, user); + const outcome = await this.repo.trash(user.id, bookId, clientId); + if (outcome === 'not_found') throw new NotFoundException(notFoundMessage(bookId, clientId)); + } + + async restore(bookId: number, clientId: string, user: RequestUser): Promise { + await this.bookService.verifyBookAccess(bookId, user); + const row = await this.repo.restore(user.id, bookId, clientId); + if (!row) throw new NotFoundException(`Journal entry ${clientId} is not in the trash for book ${bookId}`); + return toBookJournalEntry(row); + } + + async purge(bookId: number, clientId: string, user: RequestUser): Promise { + await this.bookService.verifyBookAccess(bookId, user); + const purged = await this.repo.purge(user.id, bookId, clientId); + if (!purged) throw new NotFoundException(`Journal entry ${clientId} is not in the trash for book ${bookId}`); + } +} + +export function toBookJournalEntry(row: BookJournalEntryRow): BookJournalEntry { + return { + id: row.id, + clientId: row.clientId, + bookId: row.bookId, + body: row.body, + quote: row.quote ?? null, + chapterTitle: row.chapterTitle ?? null, + positionPercent: row.positionPercent ?? null, + cfi: row.cfi ?? null, + positionSeconds: row.positionSeconds ?? null, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + deletedAt: row.deletedAt ? row.deletedAt.toISOString() : null, + }; +} + +/** A client clock running ahead must not date an entry into the future; one far behind is a bug. */ +export function resolveCreatedAt(raw: string | undefined, now: Date): Date { + if (raw === undefined) return now; + const parsed = new Date(raw); + const ms = parsed.getTime(); + if (Number.isNaN(ms) || ms < EARLIEST_CREATED_AT_MS) throw new BadRequestException('createdAt is out of range'); + return ms > now.getTime() + BOOK_JOURNAL_FUTURE_SKEW_MS ? now : parsed; +} + +function requireBody(raw: string): string { + const body = raw.trim(); + if (body === '') throw new BadRequestException('body must not be empty'); + return body; +} + +/** Blank optional text is stored as absent rather than as an empty string. */ +function optionalText(raw: string | null | undefined): string | null { + if (raw == null) return null; + const trimmed = raw.trim(); + return trimmed === '' ? null : trimmed; +} + +function buildPatch(dto: UpdateBookJournalEntryDto): BookJournalEntryPatch { + const patch: BookJournalEntryPatch = {}; + if (dto.body !== undefined) patch.body = requireBody(dto.body); + if (dto.quote !== undefined) patch.quote = optionalText(dto.quote); + if (dto.chapterTitle !== undefined) patch.chapterTitle = optionalText(dto.chapterTitle); + if (dto.positionPercent !== undefined) patch.positionPercent = dto.positionPercent; + if (dto.cfi !== undefined) patch.cfi = optionalText(dto.cfi); + if (dto.positionSeconds !== undefined) patch.positionSeconds = dto.positionSeconds; + return patch; +} + +function notFoundMessage(bookId: number, clientId: string): string { + return `Journal entry ${clientId} not found for book ${bookId}`; +} diff --git a/server/src/modules/book-journal/dto/book-journal-dto-validation.test.ts b/server/src/modules/book-journal/dto/book-journal-dto-validation.test.ts new file mode 100644 index 000000000..77f407749 --- /dev/null +++ b/server/src/modules/book-journal/dto/book-journal-dto-validation.test.ts @@ -0,0 +1,105 @@ +import 'reflect-metadata'; + +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; + +import { BookJournalQueryDto } from './book-journal-query.dto'; +import { CreateBookJournalEntryDto } from './create-book-journal-entry.dto'; +import { UpdateBookJournalEntryDto } from './update-book-journal-entry.dto'; + +const CLIENT_ID = '4f9c7a52-3d1e-4b6a-9a51-0c2f6e8d7b13'; + +async function errorsFor(cls: new () => T, value: Record) { + return validate(plainToInstance(cls, value), { whitelist: true, forbidNonWhitelisted: true }); +} + +async function failedProperties(cls: new () => T, value: Record) { + return (await errorsFor(cls, value)).map((error) => error.property); +} + +describe('Book journal DTO validation', () => { + describe('CreateBookJournalEntryDto', () => { + it('accepts a full payload, with every optional field null or set', async () => { + expect( + await errorsFor(CreateBookJournalEntryDto, { + clientId: CLIENT_ID, + body: 'A note', + quote: 'Quoted line', + chapterTitle: 'Chapter 4', + positionPercent: 37.5, + cfi: 'epubcfi(/6/8!/4/2/1:0)', + positionSeconds: 12.25, + createdAt: '2026-10-01T08:30:00.000Z', + }), + ).toHaveLength(0); + expect( + await errorsFor(CreateBookJournalEntryDto, { + clientId: CLIENT_ID, + body: 'A note', + quote: null, + chapterTitle: null, + positionPercent: null, + cfi: null, + positionSeconds: null, + }), + ).toHaveLength(0); + }); + + it('accepts an uppercase UUID as Swift generates it', async () => { + expect(await errorsFor(CreateBookJournalEntryDto, { clientId: CLIENT_ID.toUpperCase(), body: 'A note' })).toHaveLength(0); + }); + + it('trims the body and rejects one that is blank', async () => { + const dto = plainToInstance(CreateBookJournalEntryDto, { clientId: CLIENT_ID, body: ' padded ' }); + expect(dto.body).toBe('padded'); + expect(await failedProperties(CreateBookJournalEntryDto, { clientId: CLIENT_ID, body: ' \n ' })).toEqual(['body']); + expect(await failedProperties(CreateBookJournalEntryDto, { clientId: CLIENT_ID, body: '' })).toEqual(['body']); + expect(await failedProperties(CreateBookJournalEntryDto, { clientId: CLIENT_ID })).toEqual(['body']); + }); + + it('requires a UUID client id', async () => { + expect(await failedProperties(CreateBookJournalEntryDto, { body: 'x' })).toEqual(['clientId']); + expect(await failedProperties(CreateBookJournalEntryDto, { clientId: 'not-a-uuid', body: 'x' })).toEqual(['clientId']); + }); + + it('enforces length and range bounds', async () => { + const base = { clientId: CLIENT_ID, body: 'x' }; + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, body: 'a'.repeat(10001) })).toEqual(['body']); + expect(await errorsFor(CreateBookJournalEntryDto, { ...base, body: 'a'.repeat(10000) })).toHaveLength(0); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, quote: 'a'.repeat(5001) })).toEqual(['quote']); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, chapterTitle: 'a'.repeat(501) })).toEqual(['chapterTitle']); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, cfi: 'a'.repeat(2001) })).toEqual(['cfi']); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, positionPercent: 100.5 })).toEqual(['positionPercent']); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, positionPercent: -1 })).toEqual(['positionPercent']); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, positionSeconds: -0.5 })).toEqual(['positionSeconds']); + expect(await failedProperties(CreateBookJournalEntryDto, { ...base, createdAt: 'yesterday' })).toEqual(['createdAt']); + }); + + it('rejects fields the contract does not know', async () => { + expect(await failedProperties(CreateBookJournalEntryDto, { clientId: CLIENT_ID, body: 'x', mood: 'happy' })).toEqual(['mood']); + }); + }); + + describe('UpdateBookJournalEntryDto', () => { + it('accepts an empty patch and nulls that clear nullable fields', async () => { + expect(await errorsFor(UpdateBookJournalEntryDto, {})).toHaveLength(0); + expect( + await errorsFor(UpdateBookJournalEntryDto, { quote: null, chapterTitle: null, positionPercent: null, cfi: null, positionSeconds: null }), + ).toHaveLength(0); + }); + + it('rejects a null or blank body, which cannot be cleared', async () => { + expect(await failedProperties(UpdateBookJournalEntryDto, { body: null })).toEqual(['body']); + expect(await failedProperties(UpdateBookJournalEntryDto, { body: ' ' })).toEqual(['body']); + expect(await errorsFor(UpdateBookJournalEntryDto, { body: ' edited ' })).toHaveLength(0); + }); + }); + + describe('BookJournalQueryDto', () => { + it('accepts the two statuses only', async () => { + expect(await errorsFor(BookJournalQueryDto, {})).toHaveLength(0); + expect(await errorsFor(BookJournalQueryDto, { status: 'trashed' })).toHaveLength(0); + expect(await failedProperties(BookJournalQueryDto, { status: 'all' })).toEqual(['status']); + }); + }); +}); diff --git a/server/src/modules/book-journal/dto/book-journal-query.dto.ts b/server/src/modules/book-journal/dto/book-journal-query.dto.ts new file mode 100644 index 000000000..e7c8c9bde --- /dev/null +++ b/server/src/modules/book-journal/dto/book-journal-query.dto.ts @@ -0,0 +1,9 @@ +import { IsIn, IsOptional } from 'class-validator'; + +import { BOOK_JOURNAL_STATUSES, type BookJournalStatus } from '@bookorbit/types'; + +export class BookJournalQueryDto { + @IsOptional() + @IsIn(BOOK_JOURNAL_STATUSES) + status?: BookJournalStatus; +} diff --git a/server/src/modules/book-journal/dto/create-book-journal-entry.dto.ts b/server/src/modules/book-journal/dto/create-book-journal-entry.dto.ts new file mode 100644 index 000000000..28cf3c773 --- /dev/null +++ b/server/src/modules/book-journal/dto/create-book-journal-entry.dto.ts @@ -0,0 +1,58 @@ +import { Transform } from 'class-transformer'; +import { IsISO8601, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, Max, MaxLength, Min, ValidateIf } from 'class-validator'; + +import { + BOOK_JOURNAL_BODY_MAX_LENGTH, + BOOK_JOURNAL_CFI_MAX_LENGTH, + BOOK_JOURNAL_CHAPTER_TITLE_MAX_LENGTH, + BOOK_JOURNAL_QUOTE_MAX_LENGTH, + type BookJournalEntryCreate, +} from '@bookorbit/types'; + +import { trimJournalText } from './journal-text.transform'; + +export class CreateBookJournalEntryDto implements BookJournalEntryCreate { + @IsUUID() + clientId!: string; + + @Transform(trimJournalText) + @IsString() + @IsNotEmpty() + @MaxLength(BOOK_JOURNAL_BODY_MAX_LENGTH) + body!: string; + + @IsOptional() + @ValidateIf((o: CreateBookJournalEntryDto) => o.quote !== null) + @IsString() + @MaxLength(BOOK_JOURNAL_QUOTE_MAX_LENGTH) + quote?: string | null; + + @IsOptional() + @ValidateIf((o: CreateBookJournalEntryDto) => o.chapterTitle !== null) + @IsString() + @MaxLength(BOOK_JOURNAL_CHAPTER_TITLE_MAX_LENGTH) + chapterTitle?: string | null; + + @IsOptional() + @ValidateIf((o: CreateBookJournalEntryDto) => o.positionPercent !== null) + @IsNumber({ allowNaN: false, allowInfinity: false }) + @Min(0) + @Max(100) + positionPercent?: number | null; + + @IsOptional() + @ValidateIf((o: CreateBookJournalEntryDto) => o.cfi !== null) + @IsString() + @MaxLength(BOOK_JOURNAL_CFI_MAX_LENGTH) + cfi?: string | null; + + @IsOptional() + @ValidateIf((o: CreateBookJournalEntryDto) => o.positionSeconds !== null) + @IsNumber({ allowNaN: false, allowInfinity: false }) + @Min(0) + positionSeconds?: number | null; + + @IsOptional() + @IsISO8601({ strict: true }) + createdAt?: string; +} diff --git a/server/src/modules/book-journal/dto/journal-text.transform.ts b/server/src/modules/book-journal/dto/journal-text.transform.ts new file mode 100644 index 000000000..367b4b4f9 --- /dev/null +++ b/server/src/modules/book-journal/dto/journal-text.transform.ts @@ -0,0 +1,6 @@ +import type { TransformFnParams } from 'class-transformer'; + +/** Trims before validation, so a body of only whitespace fails the non-empty rule with a 400. */ +export function trimJournalText({ value }: TransformFnParams): unknown { + return typeof value === 'string' ? value.trim() : value; +} diff --git a/server/src/modules/book-journal/dto/update-book-journal-entry.dto.ts b/server/src/modules/book-journal/dto/update-book-journal-entry.dto.ts new file mode 100644 index 000000000..39f1db0ce --- /dev/null +++ b/server/src/modules/book-journal/dto/update-book-journal-entry.dto.ts @@ -0,0 +1,53 @@ +import { Transform } from 'class-transformer'; +import { IsNotEmpty, IsNumber, IsOptional, IsString, Max, MaxLength, Min, ValidateIf } from 'class-validator'; + +import { + BOOK_JOURNAL_BODY_MAX_LENGTH, + BOOK_JOURNAL_CFI_MAX_LENGTH, + BOOK_JOURNAL_CHAPTER_TITLE_MAX_LENGTH, + BOOK_JOURNAL_QUOTE_MAX_LENGTH, + type BookJournalEntryUpdate, +} from '@bookorbit/types'; + +import { trimJournalText } from './journal-text.transform'; + +export class UpdateBookJournalEntryDto implements BookJournalEntryUpdate { + // Not nullable: an entry always has a body, so `null` is rejected rather than read as "clear". + @ValidateIf((o: UpdateBookJournalEntryDto) => o.body !== undefined) + @Transform(trimJournalText) + @IsString() + @IsNotEmpty() + @MaxLength(BOOK_JOURNAL_BODY_MAX_LENGTH) + body?: string; + + @IsOptional() + @ValidateIf((o: UpdateBookJournalEntryDto) => o.quote !== null) + @IsString() + @MaxLength(BOOK_JOURNAL_QUOTE_MAX_LENGTH) + quote?: string | null; + + @IsOptional() + @ValidateIf((o: UpdateBookJournalEntryDto) => o.chapterTitle !== null) + @IsString() + @MaxLength(BOOK_JOURNAL_CHAPTER_TITLE_MAX_LENGTH) + chapterTitle?: string | null; + + @IsOptional() + @ValidateIf((o: UpdateBookJournalEntryDto) => o.positionPercent !== null) + @IsNumber({ allowNaN: false, allowInfinity: false }) + @Min(0) + @Max(100) + positionPercent?: number | null; + + @IsOptional() + @ValidateIf((o: UpdateBookJournalEntryDto) => o.cfi !== null) + @IsString() + @MaxLength(BOOK_JOURNAL_CFI_MAX_LENGTH) + cfi?: string | null; + + @IsOptional() + @ValidateIf((o: UpdateBookJournalEntryDto) => o.positionSeconds !== null) + @IsNumber({ allowNaN: false, allowInfinity: false }) + @Min(0) + positionSeconds?: number | null; +} diff --git a/server/src/modules/bookmark/bookmark-response.dto.test.ts b/server/src/modules/bookmark/bookmark-response.dto.test.ts index 3603c9a04..125f51dbc 100644 --- a/server/src/modules/bookmark/bookmark-response.dto.test.ts +++ b/server/src/modules/bookmark/bookmark-response.dto.test.ts @@ -3,14 +3,23 @@ import { BookmarkResponseDto } from './dto/bookmark-response.dto'; describe('BookmarkResponseDto', () => { it('maps nullable bookmark fields from repository rows', () => { const createdAt = new Date('2026-01-01T00:00:00Z'); + const updatedAt = new Date('2026-01-02T00:00:00Z'); const dto = BookmarkResponseDto.from({ id: 11, + clientId: '0d2a4c8e-7b1f-4f55-9c3a-6e1b2d9f0a47', userId: 22, bookId: 33, cfi: null, title: '00:01:23', + note: null, + chapterId: null, positionSeconds: null, + origin: 'web', + devicePos: 'device-only', + pageno: 4, createdAt, + updatedAt, + deletedAt: null, } as never); expect(dto).toEqual({ @@ -20,7 +29,34 @@ describe('BookmarkResponseDto', () => { title: '00:01:23', positionSeconds: null, createdAt, + note: null, + updatedAt, + clientId: '0d2a4c8e-7b1f-4f55-9c3a-6e1b2d9f0a47', + origin: 'web', + chapterId: null, }); + expect(dto).not.toHaveProperty('userId'); + expect(dto).not.toHaveProperty('devicePos'); + expect(dto).not.toHaveProperty('deletedAt'); + }); + + it('carries the note, origin and chapter id of an edited device bookmark', () => { + const dto = BookmarkResponseDto.from({ + id: 9, + clientId: '0d2a4c8e-7b1f-4f55-9c3a-6e1b2d9f0a47', + userId: 7, + bookId: 5, + cfi: 'epubcfi(/6/2)', + title: 'Renamed', + note: 'Why I marked this', + chapterId: 'chapter-3', + positionSeconds: null, + origin: 'koreader', + createdAt: new Date('2026-02-01T00:00:00Z'), + updatedAt: new Date('2026-02-03T00:00:00Z'), + } as never); + + expect(dto).toMatchObject({ note: 'Why I marked this', origin: 'koreader', chapterId: 'chapter-3', title: 'Renamed' }); }); it('preserves provided CFI and position seconds values', () => { diff --git a/server/src/modules/bookmark/bookmark-sync.service.ts b/server/src/modules/bookmark/bookmark-sync.service.ts index 0985616c4..fe4f2e6a4 100644 --- a/server/src/modules/bookmark/bookmark-sync.service.ts +++ b/server/src/modules/bookmark/bookmark-sync.service.ts @@ -45,7 +45,10 @@ export class BookmarkSyncService { return existing ? { row: existing, outcome: 'matched' } : null; } - /** Device-side rename. The web has no rename surface, so the device always wins. */ + /** + * Device-side rename. The device wins, including over a rename made through the API: the + * exchange never pushes a rename down, so a device re-uploading its own title is all it knows. + */ applyDeviceEdit(userId: number, bookmarkId: number, values: { title?: string; devicePos?: string; pageno?: number | null }) { return this.bookmarkRepo.updateFromDevice(userId, bookmarkId, values); } diff --git a/server/src/modules/bookmark/bookmark.controller.test.ts b/server/src/modules/bookmark/bookmark.controller.test.ts index a9e881893..1f8514471 100644 --- a/server/src/modules/bookmark/bookmark.controller.test.ts +++ b/server/src/modules/bookmark/bookmark.controller.test.ts @@ -24,6 +24,7 @@ describe('BookmarkController', () => { const service = { getBookmarks: vi.fn(), createBookmark: vi.fn(), + updateBookmark: vi.fn(), deleteBookmark: vi.fn(), }; @@ -50,6 +51,14 @@ describe('BookmarkController', () => { expect(service.createBookmark).toHaveBeenCalledWith(5, user, dto); }); + it('delegates updateBookmark with book, bookmark, payload and user', async () => { + service.updateBookmark.mockResolvedValue({ id: 15 }); + + await controller.updateBookmark(5, 15, { title: 'Renamed', note: null }, user); + + expect(service.updateBookmark).toHaveBeenCalledWith(5, 15, user, { title: 'Renamed', note: null }); + }); + it('delegates deleteBookmark with book, bookmark, and user', async () => { service.deleteBookmark.mockResolvedValue(undefined); diff --git a/server/src/modules/bookmark/bookmark.controller.ts b/server/src/modules/bookmark/bookmark.controller.ts index dbbaea72b..c064ad1f1 100644 --- a/server/src/modules/bookmark/bookmark.controller.ts +++ b/server/src/modules/bookmark/bookmark.controller.ts @@ -1,9 +1,10 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Post } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; import { CurrentUser } from '../../common/decorators/current-user.decorator'; import type { RequestUser } from '../../common/types/request-user'; import { BookmarkService } from './bookmark.service'; import { CreateBookmarkDto } from './dto/create-bookmark.dto'; +import { UpdateBookmarkDto } from './dto/update-bookmark.dto'; @Controller('books/:bookId/bookmarks') export class BookmarkController { @@ -19,6 +20,16 @@ export class BookmarkController { return this.bookmarkService.createBookmark(bookId, user, dto); } + @Patch(':bookmarkId') + updateBookmark( + @Param('bookId', ParseIntPipe) bookId: number, + @Param('bookmarkId', ParseIntPipe) bookmarkId: number, + @Body() dto: UpdateBookmarkDto, + @CurrentUser() user: RequestUser, + ) { + return this.bookmarkService.updateBookmark(bookId, bookmarkId, user, dto); + } + @Delete(':bookmarkId') @HttpCode(HttpStatus.NO_CONTENT) async deleteBookmark( diff --git a/server/src/modules/bookmark/bookmark.dto.test.ts b/server/src/modules/bookmark/bookmark.dto.test.ts index f9b8eba0e..8cb6f1c5d 100644 --- a/server/src/modules/bookmark/bookmark.dto.test.ts +++ b/server/src/modules/bookmark/bookmark.dto.test.ts @@ -4,6 +4,7 @@ import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import { CreateBookmarkDto } from './dto/create-bookmark.dto'; +import { UpdateBookmarkDto } from './dto/update-bookmark.dto'; async function errorsFor(value: Record) { const dto = plainToInstance(CreateBookmarkDto, value); @@ -39,4 +40,37 @@ describe('Bookmark DTO validation', () => { expect((await errorsFor({ title: 'x', cfi: 'a'.repeat(2001) })).length).toBeGreaterThan(0); expect((await errorsFor({ title: 'a'.repeat(501), cfi: 'epubcfi(/6/2)' })).length).toBeGreaterThan(0); }); + + describe('UpdateBookmarkDto', () => { + async function updateErrors(value: Record) { + return validate(plainToInstance(UpdateBookmarkDto, value), { whitelist: true, forbidNonWhitelisted: true }); + } + + it('accepts an empty patch, a title, a note and a null note', async () => { + expect(await updateErrors({})).toHaveLength(0); + expect(await updateErrors({ title: 'Renamed' })).toHaveLength(0); + expect(await updateErrors({ note: 'Why I marked this' })).toHaveLength(0); + expect(await updateErrors({ note: null })).toHaveLength(0); + }); + + it('trims the title and turns a blank note into null', () => { + const dto = plainToInstance(UpdateBookmarkDto, { title: ' Renamed ', note: ' ' }); + expect(dto.title).toBe('Renamed'); + expect(dto.note).toBeNull(); + expect(plainToInstance(UpdateBookmarkDto, { note: ' kept ' }).note).toBe('kept'); + }); + + it('rejects a blank or null title, and over-long fields', async () => { + expect((await updateErrors({ title: ' ' })).map((error) => error.property)).toEqual(['title']); + expect((await updateErrors({ title: null })).map((error) => error.property)).toEqual(['title']); + expect((await updateErrors({ title: 'a'.repeat(501) })).map((error) => error.property)).toEqual(['title']); + expect(await updateErrors({ title: 'a'.repeat(500) })).toHaveLength(0); + expect((await updateErrors({ note: 'a'.repeat(4001) })).map((error) => error.property)).toEqual(['note']); + expect(await updateErrors({ note: 'a'.repeat(4000) })).toHaveLength(0); + }); + + it('rejects fields it does not know, including a location change', async () => { + expect((await updateErrors({ cfi: 'epubcfi(/6/4)' })).map((error) => error.property)).toEqual(['cfi']); + }); + }); }); diff --git a/server/src/modules/bookmark/bookmark.repository.test.ts b/server/src/modules/bookmark/bookmark.repository.test.ts index 8c4f373f8..494372599 100644 --- a/server/src/modules/bookmark/bookmark.repository.test.ts +++ b/server/src/modules/bookmark/bookmark.repository.test.ts @@ -1,3 +1,6 @@ +import type { SQL } from 'drizzle-orm'; +import { PgDialect } from 'drizzle-orm/pg-core'; + import { BookmarkRepository } from './bookmark.repository'; function makeRow(overrides?: Record) { @@ -21,12 +24,13 @@ function makeRow(overrides?: Record) { function makeDb() { const selectResult = { from: vi.fn() }; const fromResult = { where: vi.fn() }; - const whereResult = { orderBy: vi.fn() }; + const whereResult = { orderBy: vi.fn(), limit: vi.fn() }; const orderByResult = { limit: vi.fn() }; selectResult.from.mockReturnValue(fromResult); fromResult.where.mockReturnValue(whereResult); whereResult.orderBy.mockResolvedValue([]); + whereResult.limit.mockResolvedValue([]); orderByResult.limit.mockResolvedValue([]); const insertResult = { values: vi.fn() }; @@ -212,6 +216,50 @@ describe('BookmarkRepository', () => { }); }); + describe('findLive and update', () => { + const liveConditionsSql = (call: unknown) => new PgDialect().sqlToQuery(call as SQL); + + it('finds only a live, located bookmark the user owns on that book', async () => { + const { repo, db } = makeRepository(); + db._where.limit.mockResolvedValue([makeRow()]); + + await expect(repo.findLive(5, 1, 10)).resolves.toEqual(makeRow()); + + const fromResult = db.select.mock.results[0].value.from.mock.results[0].value as { where: ReturnType }; + const compiled = liveConditionsSql(fromResult.where.mock.calls[0][0]); + expect(compiled.sql).toContain('"bookmarks"."id" = $1'); + expect(compiled.sql).toContain('"bookmarks"."book_id" = $2'); + expect(compiled.sql).toContain('"bookmarks"."user_id" = $3'); + expect(compiled.sql).toContain('"bookmarks"."cfi" is not null'); + expect(compiled.sql).toContain('"bookmarks"."deleted_at" is null'); + expect(compiled.params).toEqual([1, 5, 10]); + }); + + it('returns null from findLive when nothing matches', async () => { + const { repo } = makeRepository(); + await expect(repo.findLive(5, 1, 10)).resolves.toBeNull(); + }); + + it('updates the title and note and stamps updatedAt so device exchange can see the change', async () => { + const { repo, db } = makeRepository(); + const updated = makeRow({ title: 'Renamed', note: 'note' }); + db._updateWhere.returning.mockResolvedValue([updated]); + + await expect(repo.update(5, 1, 10, { title: 'Renamed', note: 'note' })).resolves.toEqual(updated); + + expect(db._update.set).toHaveBeenCalledWith({ title: 'Renamed', note: 'note', updatedAt: expect.any(Date) }); + const setResult = db._update.set.mock.results[0].value as { where: ReturnType }; + const compiled = liveConditionsSql(setResult.where.mock.calls[0][0]); + expect(compiled.sql).toContain('"bookmarks"."deleted_at" is null'); + expect(compiled.params).toEqual([1, 5, 10]); + }); + + it('returns null from update for a missing, tombstoned or foreign bookmark', async () => { + const { repo } = makeRepository(); + await expect(repo.update(5, 99, 10, { note: null })).resolves.toBeNull(); + }); + }); + describe('sync operations', () => { it('lists a bounded, tombstone-inclusive working set', async () => { const { repo, db } = makeRepository(); diff --git a/server/src/modules/bookmark/bookmark.repository.ts b/server/src/modules/bookmark/bookmark.repository.ts index 62e70100d..29322c647 100644 --- a/server/src/modules/bookmark/bookmark.repository.ts +++ b/server/src/modules/bookmark/bookmark.repository.ts @@ -102,6 +102,30 @@ export class BookmarkRepository { return row ?? null; } + /** A live, located (CFI) bookmark the user owns; the same rows the web list and delete see. */ + async findLive(bookId: number, bookmarkId: number, userId: number): Promise { + const [row] = await this.db + .select() + .from(bookmarks) + .where(and(...this.liveConditions(bookId, bookmarkId, userId))) + .limit(1); + return row ?? null; + } + + async update( + bookId: number, + bookmarkId: number, + userId: number, + values: Partial>, + ): Promise { + const [row] = await this.db + .update(bookmarks) + .set({ ...values, updatedAt: new Date() }) + .where(and(...this.liveConditions(bookId, bookmarkId, userId))) + .returning(); + return row ?? null; + } + /** Soft delete: devices still holding the bookmark learn about it on their next exchange. */ async softDelete(bookId: number, bookmarkId: number, userId: number) { const result = await this.db @@ -120,6 +144,16 @@ export class BookmarkRepository { return result.length > 0; } + private liveConditions(bookId: number, bookmarkId: number, userId: number) { + return [ + eq(bookmarks.id, bookmarkId), + eq(bookmarks.bookId, bookId), + eq(bookmarks.userId, userId), + isNotNull(bookmarks.cfi), + isNull(bookmarks.deletedAt), + ]; + } + // Sync-facing operations. The bookmarks table stays owned here; the KOReader // module drives these through BookmarkSyncService. diff --git a/server/src/modules/bookmark/bookmark.service.test.ts b/server/src/modules/bookmark/bookmark.service.test.ts index 14fdebda8..1308ad378 100644 --- a/server/src/modules/bookmark/bookmark.service.test.ts +++ b/server/src/modules/bookmark/bookmark.service.test.ts @@ -46,6 +46,8 @@ function makeService() { create: vi.fn(), restoreAtLocation: vi.fn().mockResolvedValue(null), softDelete: vi.fn(), + findLive: vi.fn().mockResolvedValue(null), + update: vi.fn().mockResolvedValue(null), }; const bookService = { verifyBookAccess: vi.fn().mockResolvedValue(undefined), @@ -153,6 +155,55 @@ describe('BookmarkService', () => { }); }); + describe('updateBookmark', () => { + it('verifies access, patches only the sent fields and returns the mapped bookmark', async () => { + const { service, bookmarkRepo, bookService } = makeService(); + const user = makeUser(); + bookmarkRepo.update.mockResolvedValue(makeBookmarkRow({ title: 'Renamed', note: 'Why', updatedAt: new Date('2026-02-01T00:00:00Z') })); + + const result = await service.updateBookmark(5, 10, user, { title: 'Renamed', note: 'Why' }); + + expect(bookService.verifyBookAccess).toHaveBeenCalledWith(5, user); + expect(bookmarkRepo.update).toHaveBeenCalledWith(5, 10, 1, { title: 'Renamed', note: 'Why' }); + expect(result).toBeInstanceOf(BookmarkResponseDto); + expect(result).toMatchObject({ title: 'Renamed', note: 'Why' }); + }); + + it('clears the note with null and leaves the title alone', async () => { + const { service, bookmarkRepo } = makeService(); + bookmarkRepo.update.mockResolvedValue(makeBookmarkRow({ note: null })); + + await service.updateBookmark(5, 10, makeUser(), { note: null }); + + expect(bookmarkRepo.update).toHaveBeenCalledWith(5, 10, 1, { note: null }); + }); + + it('reads the live bookmark instead of writing when nothing was sent', async () => { + const { service, bookmarkRepo } = makeService(); + bookmarkRepo.findLive.mockResolvedValue(makeBookmarkRow()); + + await service.updateBookmark(5, 10, makeUser(), {}); + + expect(bookmarkRepo.update).not.toHaveBeenCalled(); + expect(bookmarkRepo.findLive).toHaveBeenCalledWith(5, 10, 1); + }); + + it('throws NotFoundException for a missing, tombstoned or foreign bookmark', async () => { + const { service } = makeService(); + + await expect(service.updateBookmark(5, 99, makeUser(), { title: 'x' })).rejects.toThrow('Bookmark 99 not found for book 5'); + await expect(service.updateBookmark(5, 99, makeUser(), {})).rejects.toThrow(NotFoundException); + }); + + it('propagates access errors and skips the update', async () => { + const { service, bookService, bookmarkRepo } = makeService(); + bookService.verifyBookAccess.mockRejectedValue(new ForbiddenException()); + + await expect(service.updateBookmark(5, 10, makeUser(), { title: 'x' })).rejects.toThrow(ForbiddenException); + expect(bookmarkRepo.update).not.toHaveBeenCalled(); + }); + }); + describe('deleteBookmark', () => { it('verifies access and soft deletes the bookmark', async () => { const { service, bookmarkRepo, bookService } = makeService(); diff --git a/server/src/modules/bookmark/bookmark.service.ts b/server/src/modules/bookmark/bookmark.service.ts index 7595291cd..bb9af41a2 100644 --- a/server/src/modules/bookmark/bookmark.service.ts +++ b/server/src/modules/bookmark/bookmark.service.ts @@ -6,6 +6,7 @@ import { BookService } from '../book/book.service'; import { BookmarkRepository } from './bookmark.repository'; import { BookmarkResponseDto } from './dto/bookmark-response.dto'; import { CreateBookmarkDto } from './dto/create-bookmark.dto'; +import { UpdateBookmarkDto } from './dto/update-bookmark.dto'; const BOOKMARK_CONFLICT_MESSAGE = 'Bookmark already exists'; @@ -48,6 +49,24 @@ export class BookmarkService { throw new ConflictException(BOOKMARK_CONFLICT_MESSAGE); } + /** + * Renames a bookmark or edits its note. A KOReader device that already holds the dogear does not + * receive the change: bookmark exchange pushes only bookmarks a device has never seen. + */ + async updateBookmark(bookId: number, bookmarkId: number, user: RequestUser, dto: UpdateBookmarkDto): Promise { + await this.bookService.verifyBookAccess(bookId, user); + const patch = { + ...(dto.title !== undefined && { title: dto.title }), + ...(dto.note !== undefined && { note: dto.note }), + }; + const row = + Object.keys(patch).length === 0 + ? await this.bookmarkRepo.findLive(bookId, bookmarkId, user.id) + : await this.bookmarkRepo.update(bookId, bookmarkId, user.id, patch); + if (!row) throw new NotFoundException(this.notFoundMessage(bookId, bookmarkId)); + return BookmarkResponseDto.from(row); + } + async deleteBookmark(bookId: number, bookmarkId: number, user: RequestUser): Promise { await this.bookService.verifyBookAccess(bookId, user); const deleted = await this.bookmarkRepo.softDelete(bookId, bookmarkId, user.id); diff --git a/server/src/modules/bookmark/dto/bookmark-response.dto.ts b/server/src/modules/bookmark/dto/bookmark-response.dto.ts index a1b408f79..a8da6c461 100644 --- a/server/src/modules/bookmark/dto/bookmark-response.dto.ts +++ b/server/src/modules/bookmark/dto/bookmark-response.dto.ts @@ -7,6 +7,11 @@ export class BookmarkResponseDto { title!: string; positionSeconds!: number | null; createdAt!: Date; + note!: string | null; + updatedAt!: Date; + clientId!: string; + origin!: string; + chapterId!: string | null; static from(row: BookmarkRow): BookmarkResponseDto { const dto = new BookmarkResponseDto(); @@ -16,6 +21,11 @@ export class BookmarkResponseDto { dto.title = row.title; dto.positionSeconds = row.positionSeconds ?? null; dto.createdAt = row.createdAt; + dto.note = row.note ?? null; + dto.updatedAt = row.updatedAt; + dto.clientId = row.clientId; + dto.origin = row.origin; + dto.chapterId = row.chapterId ?? null; return dto; } } diff --git a/server/src/modules/bookmark/dto/update-bookmark.dto.ts b/server/src/modules/bookmark/dto/update-bookmark.dto.ts new file mode 100644 index 000000000..d151ceaf1 --- /dev/null +++ b/server/src/modules/bookmark/dto/update-bookmark.dto.ts @@ -0,0 +1,33 @@ +import { Transform } from 'class-transformer'; +import { IsNotEmpty, IsOptional, IsString, MaxLength, ValidateIf } from 'class-validator'; + +export const BOOKMARK_TITLE_MAX_LENGTH = 500; +export const BOOKMARK_NOTE_MAX_LENGTH = 4000; + +function trimText({ value }: { value: unknown }): unknown { + return typeof value === 'string' ? value.trim() : value; +} + +/** A blank note clears it, the same as sending null. */ +function trimNote({ value }: { value: unknown }): unknown { + if (typeof value !== 'string') return value; + const trimmed = value.trim(); + return trimmed === '' ? null : trimmed; +} + +export class UpdateBookmarkDto { + // Not nullable: a bookmark always has a title. + @ValidateIf((o: UpdateBookmarkDto) => o.title !== undefined) + @Transform(trimText) + @IsString() + @IsNotEmpty() + @MaxLength(BOOKMARK_TITLE_MAX_LENGTH) + title?: string; + + @IsOptional() + @Transform(trimNote) + @ValidateIf((o: UpdateBookmarkDto) => o.note !== null) + @IsString() + @MaxLength(BOOKMARK_NOTE_MAX_LENGTH) + note?: string | null; +} diff --git a/server/src/modules/koreader/koreader-bookmark-exchange.service.ts b/server/src/modules/koreader/koreader-bookmark-exchange.service.ts index b122c5e31..c34abd850 100644 --- a/server/src/modules/koreader/koreader-bookmark-exchange.service.ts +++ b/server/src/modules/koreader/koreader-bookmark-exchange.service.ts @@ -229,7 +229,8 @@ export class KoreaderBookmarkExchangeService { const known = linkByKey.get(key); if (known) { const bookmark = working.byId.get(known.bookmarkId); - // Only the device can rename a dogear, so a differing title is always its edit. + // A differing title is treated as the device's edit. Renames made through the API are + // never pushed down, so the device's title wins the next time it uploads this dogear. if (bookmark && !bookmark.deletedAt && bookmark.title !== title) { await this.bookmarkSync.applyDeviceEdit(userId, known.bookmarkId, { title, pageno: change.pageno ?? null }); mutated = true; diff --git a/server/src/modules/user-preferences/user-preferences.controller.test.ts b/server/src/modules/user-preferences/user-preferences.controller.test.ts index 35a26afad..4139a0676 100644 --- a/server/src/modules/user-preferences/user-preferences.controller.test.ts +++ b/server/src/modules/user-preferences/user-preferences.controller.test.ts @@ -92,6 +92,8 @@ describe('UserPreferencesController', () => { upsertLocalePreferences: vi.fn(), upsertCoverSearchPreferences: vi.fn(), upsertBookRequestPreferences: vi.fn(), + getAnnotationColorNamePreferences: vi.fn(), + upsertAnnotationColorNamePreferences: vi.fn(), } as unknown as Mocked; controller = new UserPreferencesController(service); @@ -212,4 +214,20 @@ describe('UserPreferencesController', () => { expect(service.upsertLocalePreferences).toHaveBeenCalledWith(99, validLocalePreferences); }); + + it('GET /annotation-colors wraps the named colours in settings', async () => { + service.getAnnotationColorNamePreferences.mockResolvedValueOnce({ names: { '#FACC15': 'Ideas' } }); + + await expect(controller.getAnnotationColorNamePreferences(makeUser({ id: 11 }))).resolves.toEqual({ + settings: { names: { '#FACC15': 'Ideas' } }, + }); + expect(service.getAnnotationColorNamePreferences).toHaveBeenCalledWith(11); + }); + + it('PUT /annotation-colors forwards the current user id and settings', async () => { + const settings = { names: { '#facc15': 'Ideas' } }; + + await expect(controller.upsertAnnotationColorNamePreferences({ settings }, makeUser({ id: 42 }))).resolves.toBeUndefined(); + expect(service.upsertAnnotationColorNamePreferences).toHaveBeenCalledWith(42, settings); + }); }); diff --git a/server/src/modules/user-preferences/user-preferences.controller.ts b/server/src/modules/user-preferences/user-preferences.controller.ts index 0a3bdcc07..50211df0e 100644 --- a/server/src/modules/user-preferences/user-preferences.controller.ts +++ b/server/src/modules/user-preferences/user-preferences.controller.ts @@ -81,6 +81,18 @@ export class UserPreferencesController { await this.userPreferencesService.upsertServerFontPreferences(user.id, dto.settings); } + @Get('annotation-colors') + async getAnnotationColorNamePreferences(@CurrentUser() user: RequestUser) { + const settings = await this.userPreferencesService.getAnnotationColorNamePreferences(user.id); + return { settings }; + } + + @Put('annotation-colors') + @HttpCode(204) + async upsertAnnotationColorNamePreferences(@Body() dto: UpsertUserPreferenceDto, @CurrentUser() user: RequestUser) { + await this.userPreferencesService.upsertAnnotationColorNamePreferences(user.id, dto.settings); + } + @Get('whats-new') async getWhatsNewPreferences(@CurrentUser() user: RequestUser) { const settings = await this.userPreferencesService.getWhatsNewPreferences(user.id); diff --git a/server/src/modules/user-preferences/user-preferences.service.test.ts b/server/src/modules/user-preferences/user-preferences.service.test.ts index d50ede3f8..1f657476f 100644 --- a/server/src/modules/user-preferences/user-preferences.service.test.ts +++ b/server/src/modules/user-preferences/user-preferences.service.test.ts @@ -10,7 +10,7 @@ import type { ServerFontPreferences, ThemePreferences, } from '@bookorbit/types'; -import { MAX_SERVER_FONTS, SUPPORTED_LOCALES } from '@bookorbit/types'; +import { ANNOTATION_COLOR_NAME_MAX_ENTRIES, ANNOTATION_COLOR_NAME_PREFERENCES_DEFAULTS, MAX_SERVER_FONTS, SUPPORTED_LOCALES } from '@bookorbit/types'; import { UserPreferencesRepository } from './user-preferences.repository'; import { UserPreferencesService } from './user-preferences.service'; @@ -761,6 +761,70 @@ describe('UserPreferencesService', () => { }); }); + describe('annotation colour name preferences', () => { + const hexKey = (i: number) => `#${i.toString(16).padStart(6, '0').toUpperCase()}`; + + it('defaults to no names when the reader has never set any', async () => { + repo.findByCategory.mockResolvedValue(undefined); + + const result = await service.getAnnotationColorNamePreferences(11); + + expect(result).toEqual({ names: {} }); + expect(repo.findByCategory).toHaveBeenCalledWith(11, 'annotation-colors'); + result.names['#FFFFFF'] = 'mutated'; + expect(ANNOTATION_COLOR_NAME_PREFERENCES_DEFAULTS).toEqual({ names: {} }); + }); + + it('returns the stored names', async () => { + repo.findByCategory.mockResolvedValue({ data: { names: { '#FACC15': 'Ideas', '#4ADE80': 'Quotes' } } } as never); + + await expect(service.getAnnotationColorNamePreferences(11)).resolves.toEqual({ names: { '#FACC15': 'Ideas', '#4ADE80': 'Quotes' } }); + }); + + it('falls back to the defaults when the stored payload no longer validates', async () => { + repo.findByCategory.mockResolvedValue({ data: { names: { yellow: 'Ideas' } } as never }); + + await expect(service.getAnnotationColorNamePreferences(11)).resolves.toEqual({ names: {} }); + }); + + it('normalises keys to uppercase, trims labels and drops blank ones', async () => { + await service.upsertAnnotationColorNamePreferences(11, { names: { '#facc15': ' Ideas ', '#4ade80': ' ', '#38BDF8': 'Quotes' } }); + + expect(repo.upsert).toHaveBeenCalledWith(11, 'annotation-colors', { names: { '#FACC15': 'Ideas', '#38BDF8': 'Quotes' } }); + }); + + it('stores an empty map, which clears every name', async () => { + await service.upsertAnnotationColorNamePreferences(11, { names: {} }); + + expect(repo.upsert).toHaveBeenCalledWith(11, 'annotation-colors', { names: {} }); + }); + + it.each([ + ['a named colour that is not #RRGGBB', { names: { yellow: 'Ideas' } }], + ['a three-digit hex colour', { names: { '#FC1': 'Ideas' } }], + ['a label longer than the cap', { names: { '#FACC15': 'x'.repeat(41) } }], + ['a non-string label', { names: { '#FACC15': 7 } }], + ['two keys naming the same colour', { names: { '#facc15': 'One', '#FACC15': 'Two' } }], + ['a missing names map', {}], + ['an unknown key', { names: {}, extra: true }], + ])('rejects %s with 400', async (_label, settings) => { + await expect(service.upsertAnnotationColorNamePreferences(11, settings as Record)).rejects.toBeInstanceOf(BadRequestException); + expect(repo.upsert).not.toHaveBeenCalled(); + }); + + it('accepts a label exactly at the length cap', async () => { + await expect(service.upsertAnnotationColorNamePreferences(11, { names: { '#FACC15': 'x'.repeat(40) } })).resolves.toBeUndefined(); + }); + + it('caps the number of named colours, counting only labels that are kept', async () => { + const atCap = Object.fromEntries(Array.from({ length: ANNOTATION_COLOR_NAME_MAX_ENTRIES }, (_, i) => [hexKey(i), `Name ${i}`])); + await expect(service.upsertAnnotationColorNamePreferences(11, { names: { ...atCap, '#ABCDEF': ' ' } })).resolves.toBeUndefined(); + + const overCap = { ...atCap, '#ABCDEF': 'One too many' }; + await expect(service.upsertAnnotationColorNamePreferences(11, { names: overCap })).rejects.toBeInstanceOf(BadRequestException); + }); + }); + it('getPodcastPlaylistPreferences returns an empty list when nothing is stored', async () => { await expect(service.getPodcastPlaylistPreferences(7)).resolves.toEqual({ playlists: [] }); expect(repo.findByCategory).toHaveBeenCalledWith(7, 'podcast-playlists'); diff --git a/server/src/modules/user-preferences/user-preferences.service.ts b/server/src/modules/user-preferences/user-preferences.service.ts index eaf33f24c..4036fed2a 100644 --- a/server/src/modules/user-preferences/user-preferences.service.ts +++ b/server/src/modules/user-preferences/user-preferences.service.ts @@ -1,5 +1,8 @@ import { ACCENT_IDS, + ANNOTATION_COLOR_NAME_MAX_ENTRIES, + ANNOTATION_COLOR_NAME_MAX_LENGTH, + ANNOTATION_COLOR_NAME_PREFERENCES_DEFAULTS, AUTHOR_COVER_SHAPES, PODCAST_PLAYLIST_MAX_SAVED, PODCAST_PLAYLIST_MAX_SHOWS, @@ -28,6 +31,7 @@ import { SURFACE_OPACITY_MIN, TABLE_DENSITIES, THEME_IDS, + type AnnotationColorNamePreferences, type BookRequestPreferences, type DisplayPreferences, type CoverSearchPreferences, @@ -177,6 +181,36 @@ const SERVER_FONT_PREFERENCES_SCHEMA = z }) .strict(); +const ANNOTATION_COLOR_KEY = /^#[0-9a-fA-F]{6}$/; + +/** + * Keys normalise to uppercase so `#facc15` and `#FACC15` name one colour, and a blank label drops + * its entry, which is how a client clears a name. The cap applies to what is actually stored. + */ +const ANNOTATION_COLOR_NAME_PREFERENCES_SCHEMA = z + .object({ + names: z + .record(z.string().regex(ANNOTATION_COLOR_KEY, 'must be a #RRGGBB colour'), z.string().trim().max(ANNOTATION_COLOR_NAME_MAX_LENGTH)) + .transform((names, ctx) => { + const normalized: Record = {}; + for (const [key, label] of Object.entries(names)) { + if (label === '') continue; + const color = key.toUpperCase(); + if (Object.hasOwn(normalized, color)) { + ctx.addIssue({ code: 'custom', path: [key], message: 'names the same colour as another key' }); + return z.NEVER; + } + normalized[color] = label; + } + if (Object.keys(normalized).length > ANNOTATION_COLOR_NAME_MAX_ENTRIES) { + ctx.addIssue({ code: 'custom', message: `must name at most ${ANNOTATION_COLOR_NAME_MAX_ENTRIES} colours` }); + return z.NEVER; + } + return normalized; + }), + }) + .strict() satisfies z.ZodType; + const PODCAST_PLAYLIST_RULES_SCHEMA = z .object({ filter: z.enum(['latest', 'downloaded', 'in_progress', 'unplayed', 'finished']), @@ -315,6 +349,42 @@ export class UserPreferencesService { } } + async getAnnotationColorNamePreferences(userId: number): Promise { + const row = await this.repo.findByCategory(userId, 'annotation-colors'); + if (!row) return { names: { ...ANNOTATION_COLOR_NAME_PREFERENCES_DEFAULTS.names } }; + const stored = ANNOTATION_COLOR_NAME_PREFERENCES_SCHEMA.safeParse(row.data); + return stored.success ? stored.data : { names: { ...ANNOTATION_COLOR_NAME_PREFERENCES_DEFAULTS.names } }; + } + + async upsertAnnotationColorNamePreferences(userId: number, data: Record): Promise { + const start = Date.now(); + this.logger.log(`[user_preferences.upsert_annotation_colors] [start] userId=${userId} - upsert annotation colour names started`); + + const result = ANNOTATION_COLOR_NAME_PREFERENCES_SCHEMA.safeParse(data); + if (!result.success) { + const firstIssue = result.error.issues[0]; + const issuePath = firstIssue?.path.length ? firstIssue.path.join('.') : 'settings'; + const issueMessage = firstIssue?.message ?? 'Invalid settings payload'; + throw new BadRequestException(`Invalid annotation colour preferences at "${issuePath}": ${issueMessage}`); + } + + try { + await this.repo.upsert(userId, 'annotation-colors', { names: result.data.names }); + const durationMs = Date.now() - start; + this.logger.log( + `[user_preferences.upsert_annotation_colors] [end] userId=${userId} durationMs=${durationMs} namedCount=${Object.keys(result.data.names).length} - upsert annotation colour names completed`, + ); + } catch (err) { + const durationMs = Date.now() - start; + const errorClass = err instanceof Error ? err.constructor.name : 'UnknownError'; + const error = sanitizeLogValue(err instanceof Error ? err.message : String(err)); + this.logger.error( + `[user_preferences.upsert_annotation_colors] [fail] userId=${userId} durationMs=${durationMs} errorClass=${errorClass} error="${error}" - upsert annotation colour names failed`, + ); + throw err; + } + } + async getLocalePreferences(userId: number): Promise { const row = await this.repo.findByCategory(userId, 'locale'); return row ? (row.data as LocalePreferences) : null; diff --git a/server/test/book-notebook.e2e-spec.ts b/server/test/book-notebook.e2e-spec.ts new file mode 100644 index 000000000..cbeab0b81 --- /dev/null +++ b/server/test/book-notebook.e2e-spec.ts @@ -0,0 +1,337 @@ +import { randomUUID } from 'crypto'; +import { eq } from 'drizzle-orm'; + +import { SERVER_FEATURES, type AnnotationItem, type BookJournalEntry } from '@bookorbit/types'; + +import * as schema from '../src/db/schema'; +import { createEpubFixture } from './e2e/reader-state-isolation/reader-state-isolation-fixture-builder'; +import { + authHeader, + closeReaderStateIsolationE2EContext, + createLibraryWithFolder, + createReaderStateIsolationE2EContext, + createUserAndLogin, + grantLibraryAccess, + locateBookByAbsolutePath, + triggerAndWaitForLibraryScan, + type LocatedBookFile, + type ReaderStateIsolationE2EContext, + type TestUserSession, +} from './e2e/reader-state-isolation/reader-state-isolation-harness'; + +type InjectMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; + +describe('Book notebook (e2e)', { timeout: 120_000 }, () => { + let ctx!: ReaderStateIsolationE2EContext; + let bookA!: LocatedBookFile; + let bookB!: LocatedBookFile; + let owner!: TestUserSession; + let peer!: TestUserSession; + let outsider!: TestUserSession; + + beforeAll(async () => { + ctx = await createReaderStateIsolationE2EContext(); + const library = await createLibraryWithFolder(ctx, { name: `notebook-${randomUUID()}` }); + const pathA = await createEpubFixture(library.folderPath, 'notebook-a.epub', { + title: `Notebook A ${randomUUID()}`, + uid: `urn:uuid:${randomUUID()}`, + }); + const pathB = await createEpubFixture(library.folderPath, 'notebook-b.epub', { + title: `Notebook B ${randomUUID()}`, + uid: `urn:uuid:${randomUUID()}`, + }); + await triggerAndWaitForLibraryScan(ctx, library.libraryId); + bookA = await locateBookByAbsolutePath(ctx, pathA); + bookB = await locateBookByAbsolutePath(ctx, pathB); + + owner = await createUserAndLogin(ctx); + peer = await createUserAndLogin(ctx); + outsider = await createUserAndLogin(ctx); + await grantLibraryAccess(ctx, owner.userId, library.libraryId, 'viewer'); + await grantLibraryAccess(ctx, peer.userId, library.libraryId, 'viewer'); + }, 120_000); + + afterAll(async () => { + if (ctx) await closeReaderStateIsolationE2EContext(ctx); + }); + + function call(user: TestUserSession, method: InjectMethod, url: string, payload?: Record) { + return ctx.app.inject({ method, url: `/api/v1${url}`, headers: authHeader(user.accessToken), ...(payload !== undefined && { payload }) }); + } + + it('advertises the notebook capabilities on app-info', async () => { + const response = await call(owner, 'GET', '/app-info'); + expect(response.statusCode).toBe(200); + expect((response.json() as { features: string[] }).features).toEqual([...SERVER_FEATURES]); + }); + + describe('journal', () => { + it('creates idempotently on the client id and lists oldest first', async () => { + const clientId = randomUUID(); + const created = await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { + clientId, + body: ' First thought ', + quote: 'A quoted line', + chapterTitle: 'Chapter 1', + positionPercent: 12.5, + cfi: 'epubcfi(/6/8!/4/2/1:0)', + createdAt: '2026-01-02T03:04:05.000Z', + }); + expect(created.statusCode).toBe(201); + const entry = created.json() as BookJournalEntry; + expect(entry).toMatchObject({ + clientId, + bookId: bookA.bookId, + body: 'First thought', + quote: 'A quoted line', + chapterTitle: 'Chapter 1', + positionPercent: 12.5, + cfi: 'epubcfi(/6/8!/4/2/1:0)', + positionSeconds: null, + createdAt: '2026-01-02T03:04:05.000Z', + deletedAt: null, + }); + + const replay = await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId: clientId.toUpperCase(), body: 'Different body' }); + expect(replay.statusCode).toBe(201); + expect(replay.json()).toEqual(entry); + + const later = await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId: randomUUID(), body: 'Second thought' }); + expect(later.statusCode).toBe(201); + + const list = await call(owner, 'GET', `/books/${bookA.bookId}/journal`); + expect(list.statusCode).toBe(200); + const bodies = (list.json() as BookJournalEntry[]).map((item) => item.body); + expect(bodies.indexOf('First thought')).toBeLessThan(bodies.indexOf('Second thought')); + expect(bodies.filter((body) => body === 'First thought')).toHaveLength(1); + }); + + it('clamps a createdAt far in the future to the server clock', async () => { + const before = Date.now(); + const response = await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { + clientId: randomUUID(), + body: 'From a fast clock', + createdAt: new Date(before + 24 * 60 * 60 * 1000).toISOString(), + }); + expect(response.statusCode).toBe(201); + const createdAt = Date.parse((response.json() as BookJournalEntry).createdAt); + expect(createdAt).toBeGreaterThanOrEqual(before - 1000); + expect(createdAt).toBeLessThanOrEqual(Date.now() + 1000); + }); + + it('answers 409 when a client id is reused on another book', async () => { + const clientId = randomUUID(); + expect((await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId, body: 'On A' })).statusCode).toBe(201); + + const conflict = await call(owner, 'POST', `/books/${bookB.bookId}/journal`, { clientId, body: 'On B' }); + expect(conflict.statusCode).toBe(409); + }); + + it('rejects blank bodies, malformed ids and unknown fields with 400', async () => { + expect((await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId: randomUUID(), body: ' ' })).statusCode).toBe(400); + expect((await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId: 'nope', body: 'x' })).statusCode).toBe(400); + expect((await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId: randomUUID(), body: 'x', mood: 'calm' })).statusCode).toBe(400); + expect( + (await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId: randomUUID(), body: 'x', positionPercent: 101 })).statusCode, + ).toBe(400); + expect((await call(owner, 'PATCH', `/books/${bookA.bookId}/journal/not-a-uuid`, { body: 'x' })).statusCode).toBe(400); + expect((await call(owner, 'GET', `/books/${bookA.bookId}/journal?status=everything`)).statusCode).toBe(400); + }); + + it('edits, trashes, restores and permanently deletes through the trash only', async () => { + const clientId = randomUUID(); + const base = `/books/${bookA.bookId}/journal/${clientId}`; + await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId, body: 'Lifecycle', quote: 'Quote to clear' }); + + const edited = await call(owner, 'PATCH', base, { body: 'Lifecycle edited', quote: null, positionSeconds: 30 }); + expect(edited.statusCode).toBe(200); + expect(edited.json()).toMatchObject({ body: 'Lifecycle edited', quote: null, positionSeconds: 30 }); + expect((await call(owner, 'PATCH', base, { body: null })).statusCode).toBe(400); + + expect((await call(owner, 'DELETE', `${base}/permanent`)).statusCode).toBe(404); + expect((await call(owner, 'DELETE', base)).statusCode).toBe(204); + expect((await call(owner, 'DELETE', base)).statusCode).toBe(204); + expect((await call(owner, 'PATCH', base, { body: 'Edit in trash' })).statusCode).toBe(404); + + const active = (await call(owner, 'GET', `/books/${bookA.bookId}/journal`)).json() as BookJournalEntry[]; + expect(active.some((item) => item.clientId === clientId)).toBe(false); + const trashed = (await call(owner, 'GET', `/books/${bookA.bookId}/journal?status=trashed`)).json() as BookJournalEntry[]; + const inTrash = trashed.find((item) => item.clientId === clientId); + expect(inTrash?.deletedAt).toEqual(expect.any(String)); + + const restored = await call(owner, 'POST', `${base}/restore`); + expect(restored.statusCode).toBe(200); + expect(restored.json()).toMatchObject({ clientId, deletedAt: null, body: 'Lifecycle edited' }); + expect((await call(owner, 'POST', `${base}/restore`)).statusCode).toBe(404); + + expect((await call(owner, 'DELETE', base)).statusCode).toBe(204); + expect((await call(owner, 'DELETE', `${base}/permanent`)).statusCode).toBe(204); + expect((await call(owner, 'DELETE', `${base}/permanent`)).statusCode).toBe(404); + expect((await call(owner, 'DELETE', base)).statusCode).toBe(404); + const [row] = await ctx.db.select().from(schema.bookJournalEntries).where(eq(schema.bookJournalEntries.clientId, clientId)); + expect(row).toBeUndefined(); + }); + + it('keeps every entry private to its author and the book behind library access', async () => { + const clientId = randomUUID(); + await call(owner, 'POST', `/books/${bookA.bookId}/journal`, { clientId, body: 'Private' }); + + const peerList = (await call(peer, 'GET', `/books/${bookA.bookId}/journal`)).json() as BookJournalEntry[]; + expect(peerList.some((item) => item.clientId === clientId)).toBe(false); + expect((await call(peer, 'PATCH', `/books/${bookA.bookId}/journal/${clientId}`, { body: 'Hijack' })).statusCode).toBe(404); + expect((await call(peer, 'DELETE', `/books/${bookA.bookId}/journal/${clientId}`)).statusCode).toBe(404); + + const peerOwn = await call(peer, 'POST', `/books/${bookA.bookId}/journal`, { clientId, body: 'Same id, different reader' }); + expect(peerOwn.statusCode).toBe(201); + expect((peerOwn.json() as BookJournalEntry).body).toBe('Same id, different reader'); + + expect((await call(outsider, 'GET', `/books/${bookA.bookId}/journal`)).statusCode).toBe(404); + expect((await call(outsider, 'POST', `/books/${bookA.bookId}/journal`, { clientId: randomUUID(), body: 'x' })).statusCode).toBe(404); + }); + }); + + describe('annotations', () => { + async function createAnnotation(cfi: string, note: string | null = null): Promise { + const response = await call(owner, 'POST', `/books/${bookB.bookId}/annotations`, { cfi, text: `text at ${cfi}`, color: '#FACC15', note }); + expect(response.statusCode).toBe(201); + return (response.json() as { id: number }).id; + } + + async function storedVersion(id: number) { + const [row] = await ctx.db + .select({ version: schema.annotations.version, starredAt: schema.annotations.starredAt }) + .from(schema.annotations) + .where(eq(schema.annotations.id, id)); + return row; + } + + let tenId: number; + let eightId: number; + let eightLaterId: number; + + beforeAll(async () => { + tenId = await createAnnotation('epubcfi(/6/10!/4/2/1:0)', 'remove me'); + eightLaterId = await createAnnotation('epubcfi(/6/8!/4/2/1:12)'); + eightId = await createAnnotation('epubcfi(/6/8!/4/2/1:9)'); + }); + + it('sorts by reading position numerically, so /6/8 comes before /6/10', async () => { + const response = await call(owner, 'GET', `/books/${bookB.bookId}/annotations?page=1&pageSize=25&sortBy=position&sortDir=asc`); + expect(response.statusCode).toBe(200); + const ids = (response.json() as { items: AnnotationItem[] }).items.map((item) => item.id); + expect(ids).toEqual([eightId, eightLaterId, tenId]); + }); + + it('exports a book in reading order', async () => { + const response = await call(owner, 'GET', `/annotations/export?format=json&bookId=${bookB.bookId}`); + expect(response.statusCode).toBe(200); + const ids = (JSON.parse(response.body) as { id: number }[]).map((row) => row.id); + expect(ids).toEqual([eightId, eightLaterId, tenId]); + }); + + it('stars and unstars without bumping the version devices sync from', async () => { + const before = await storedVersion(eightId); + + const starred = await call(owner, 'PATCH', `/books/${bookB.bookId}/annotations/${eightId}`, { starred: true }); + expect(starred.statusCode).toBe(200); + const starredAt = (starred.json() as AnnotationItem).starredAt; + expect(starredAt).toEqual(expect.any(String)); + expect((await storedVersion(eightId)).version).toBe(before.version); + + const again = await call(owner, 'PATCH', `/books/${bookB.bookId}/annotations/${eightId}`, { starred: true }); + expect((again.json() as AnnotationItem).starredAt).toBe(starredAt); + + const list = (await call(owner, 'GET', `/books/${bookB.bookId}/annotations`)).json() as AnnotationItem[]; + expect(list.find((item) => item.id === eightId)?.starredAt).toBe(starredAt); + expect(list.find((item) => item.id === tenId)?.starredAt).toBeNull(); + + const unstarred = await call(owner, 'PATCH', `/books/${bookB.bookId}/annotations/${eightId}`, { starred: false }); + expect((unstarred.json() as AnnotationItem).starredAt).toBeNull(); + expect((await storedVersion(eightId)).version).toBe(before.version); + + expect((await call(owner, 'PATCH', `/books/${bookB.bookId}/annotations/${eightId}`, { starred: null })).statusCode).toBe(400); + }); + + it('clears a note with null and bumps the version for that content change', async () => { + const before = await storedVersion(tenId); + + const response = await call(owner, 'PATCH', `/books/${bookB.bookId}/annotations/${tenId}`, { note: null }); + + expect(response.statusCode).toBe(200); + expect((response.json() as AnnotationItem).note).toBeNull(); + expect((await storedVersion(tenId)).version).toBe(before.version + 1); + }); + + it('bulk stars active rows only and reports what changed', async () => { + const trashedId = await createAnnotation('epubcfi(/6/12!/4/2/1:0)'); + expect((await call(owner, 'DELETE', `/books/${bookB.bookId}/annotations/${trashedId}`)).statusCode).toBe(204); + const versionBefore = (await storedVersion(eightLaterId)).version; + + const star = await call(owner, 'POST', '/annotations/bulk', { ids: [eightLaterId, tenId, trashedId], action: 'star' }); + expect(star.statusCode).toBe(200); + expect(star.json()).toEqual({ affected: 2 }); + expect((await storedVersion(trashedId)).starredAt).toBeNull(); + expect((await storedVersion(eightLaterId)).version).toBe(versionBefore); + + const hub = (await call(owner, 'GET', `/annotations?bookId=${bookB.bookId}`)).json() as { items: AnnotationItem[] }; + expect(hub.items.find((item) => item.id === tenId)?.starredAt).toEqual(expect.any(String)); + + const peerStar = await call(peer, 'POST', '/annotations/bulk', { ids: [eightId], action: 'star' }); + expect(peerStar.json()).toEqual({ affected: 0 }); + + const unstar = await call(owner, 'POST', '/annotations/bulk', { ids: [eightLaterId, tenId, eightId], action: 'unstar' }); + expect(unstar.json()).toEqual({ affected: 2 }); + }); + }); + + describe('bookmarks', () => { + it('renames and annotates a bookmark, and reports the new fields', async () => { + const created = await call(owner, 'POST', `/books/${bookA.bookId}/bookmarks`, { cfi: 'epubcfi(/6/14!/4/2/1:0)', title: 'Chapter 7' }); + expect(created.statusCode).toBe(201); + const bookmark = created.json() as { + id: number; + note: string | null; + updatedAt: string; + clientId: string; + origin: string; + chapterId: string | null; + }; + expect(bookmark).toMatchObject({ note: null, origin: 'web', chapterId: null, clientId: expect.any(String), updatedAt: expect.any(String) }); + + const edited = await call(owner, 'PATCH', `/books/${bookA.bookId}/bookmarks/${bookmark.id}`, { title: ' The duel ', note: 'Reread this' }); + expect(edited.statusCode).toBe(200); + const after = edited.json() as typeof bookmark & { title: string }; + expect(after).toMatchObject({ id: bookmark.id, title: 'The duel', note: 'Reread this', clientId: bookmark.clientId }); + expect(Date.parse(after.updatedAt)).toBeGreaterThanOrEqual(Date.parse(bookmark.updatedAt)); + + const cleared = await call(owner, 'PATCH', `/books/${bookA.bookId}/bookmarks/${bookmark.id}`, { note: ' ' }); + expect(cleared.json()).toMatchObject({ title: 'The duel', note: null }); + + const list = (await call(owner, 'GET', `/books/${bookA.bookId}/bookmarks`)).json() as { id: number; title: string; note: string | null }[]; + expect(list.find((item) => item.id === bookmark.id)).toMatchObject({ title: 'The duel', note: null }); + + expect((await call(owner, 'PATCH', `/books/${bookA.bookId}/bookmarks/${bookmark.id}`, { title: '' })).statusCode).toBe(400); + expect((await call(peer, 'PATCH', `/books/${bookA.bookId}/bookmarks/${bookmark.id}`, { title: 'Hijack' })).statusCode).toBe(404); + expect((await call(owner, 'DELETE', `/books/${bookA.bookId}/bookmarks/${bookmark.id}`)).statusCode).toBe(204); + expect((await call(owner, 'PATCH', `/books/${bookA.bookId}/bookmarks/${bookmark.id}`, { title: 'Gone' })).statusCode).toBe(404); + }); + }); + + describe('annotation colour names', () => { + it('defaults to none, stores normalised names and rejects invalid ones', async () => { + const initial = await call(owner, 'GET', '/user-preferences/annotation-colors'); + expect(initial.statusCode).toBe(200); + expect(initial.json()).toEqual({ settings: { names: {} } }); + + const saved = await call(owner, 'PUT', '/user-preferences/annotation-colors', { settings: { names: { '#facc15': ' Ideas ', '#4ADE80': '' } } }); + expect(saved.statusCode).toBe(204); + expect((await call(owner, 'GET', '/user-preferences/annotation-colors')).json()).toEqual({ settings: { names: { '#FACC15': 'Ideas' } } }); + expect((await call(peer, 'GET', '/user-preferences/annotation-colors')).json()).toEqual({ settings: { names: {} } }); + + expect((await call(owner, 'PUT', '/user-preferences/annotation-colors', { settings: { names: { yellow: 'Ideas' } } })).statusCode).toBe(400); + const tooMany = Object.fromEntries(Array.from({ length: 33 }, (_, i) => [`#${i.toString(16).padStart(6, '0')}`, `Name ${i}`])); + expect((await call(owner, 'PUT', '/user-preferences/annotation-colors', { settings: { names: tooMany } })).statusCode).toBe(400); + expect((await call(owner, 'GET', '/user-preferences/annotation-colors')).json()).toEqual({ settings: { names: { '#FACC15': 'Ideas' } } }); + }); + }); +}); diff --git a/server/test/e2e/authorization-matrix/route-inventory/manifest.json b/server/test/e2e/authorization-matrix/route-inventory/manifest.json index 479c4b0b3..1ac9294af 100644 --- a/server/test/e2e/authorization-matrix/route-inventory/manifest.json +++ b/server/test/e2e/authorization-matrix/route-inventory/manifest.json @@ -1,7 +1,7 @@ { - "totalRoutes": 614, + "totalRoutes": 623, "byPermission": { - "(none)": 262, + "(none)": 271, "Permission.ManageMetadataConfig": 34, "Permission.EmailSend": 30, "Permission.LibraryEditMetadata": 35, diff --git a/server/test/e2e/authorization-matrix/route-inventory/routes-02.json b/server/test/e2e/authorization-matrix/route-inventory/routes-02.json index 8df409e7e..3acbd787c 100644 --- a/server/test/e2e/authorization-matrix/route-inventory/routes-02.json +++ b/server/test/e2e/authorization-matrix/route-inventory/routes-02.json @@ -671,6 +671,18 @@ "allowDefault": false, "useGuards": [] }, + { + "file": "server/src/modules/bookmark/bookmark.controller.ts", + "className": "BookmarkController", + "methodName": "updateBookmark", + "httpMethod": "PATCH", + "path": "/books/:bookId/bookmarks/:bookmarkId", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, { "file": "server/src/modules/bookmark/bookmark.controller.ts", "className": "BookmarkController", diff --git a/server/test/e2e/authorization-matrix/route-inventory/routes-09.json b/server/test/e2e/authorization-matrix/route-inventory/routes-09.json index cdd8ae342..f7e2f3e41 100644 --- a/server/test/e2e/authorization-matrix/route-inventory/routes-09.json +++ b/server/test/e2e/authorization-matrix/route-inventory/routes-09.json @@ -778,5 +778,29 @@ "isPublic": false, "allowDefault": false, "useGuards": [] + }, + { + "file": "server/src/modules/user-preferences/user-preferences.controller.ts", + "className": "UserPreferencesController", + "methodName": "getAnnotationColorNamePreferences", + "httpMethod": "GET", + "path": "/user-preferences/annotation-colors", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, + { + "file": "server/src/modules/user-preferences/user-preferences.controller.ts", + "className": "UserPreferencesController", + "methodName": "upsertAnnotationColorNamePreferences", + "httpMethod": "PUT", + "path": "/user-preferences/annotation-colors", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] } ] diff --git a/server/test/e2e/authorization-matrix/route-inventory/routes-10.json b/server/test/e2e/authorization-matrix/route-inventory/routes-10.json index ec593b24c..c004a8ef4 100644 --- a/server/test/e2e/authorization-matrix/route-inventory/routes-10.json +++ b/server/test/e2e/authorization-matrix/route-inventory/routes-10.json @@ -766,5 +766,77 @@ "isPublic": false, "allowDefault": false, "useGuards": [] + }, + { + "file": "server/src/modules/book-journal/book-journal.controller.ts", + "className": "BookJournalController", + "methodName": "list", + "httpMethod": "GET", + "path": "/books/:bookId/journal", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, + { + "file": "server/src/modules/book-journal/book-journal.controller.ts", + "className": "BookJournalController", + "methodName": "create", + "httpMethod": "POST", + "path": "/books/:bookId/journal", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, + { + "file": "server/src/modules/book-journal/book-journal.controller.ts", + "className": "BookJournalController", + "methodName": "update", + "httpMethod": "PATCH", + "path": "/books/:bookId/journal/:clientId", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, + { + "file": "server/src/modules/book-journal/book-journal.controller.ts", + "className": "BookJournalController", + "methodName": "trash", + "httpMethod": "DELETE", + "path": "/books/:bookId/journal/:clientId", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, + { + "file": "server/src/modules/book-journal/book-journal.controller.ts", + "className": "BookJournalController", + "methodName": "restore", + "httpMethod": "POST", + "path": "/books/:bookId/journal/:clientId/restore", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] + }, + { + "file": "server/src/modules/book-journal/book-journal.controller.ts", + "className": "BookJournalController", + "methodName": "purge", + "httpMethod": "DELETE", + "path": "/books/:bookId/journal/:clientId/permanent", + "permissions": [], + "libraryAccess": [], + "isPublic": false, + "allowDefault": false, + "useGuards": [] } ]