diff --git a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue index ca25e0b8d..ecbe7073c 100644 --- a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue +++ b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue @@ -53,6 +53,7 @@ const DIRECT_PATCH_FIELDS = [ 'itunesId', 'audibleId', 'comicvineId', + 'ranobedbId', ] as const const COMIC_FIELD_MAP = { @@ -112,6 +113,7 @@ const providerIdFields = [ { field: 'itunesId' as const, label: 'iTunes' }, { field: 'audibleId' as const, label: 'Audible' }, { field: 'comicvineId' as const, label: 'ComicVine' }, + { field: 'ranobedbId' as const, label: 'RanobeDB' }, ] function setIntField(field: 'publishedYear' | 'pageCount' | 'durationSeconds', e: Event) { @@ -310,6 +312,7 @@ function buildPreviewPatch(preview: MetadataRefreshPreview): MetadataPatch { itunesId: preview.itunesId, audibleId: preview.audibleId, comicvineId: preview.comicvineId, + ranobedbId: preview.ranobedbId, comicMetadata: preview.comicMetadata, narrators: preview.audioMetadata?.narrators, durationSeconds: preview.audioMetadata?.durationSeconds ?? undefined, diff --git a/client/src/features/book/components/detail/tabs/MetadataSearchDrawer.vue b/client/src/features/book/components/detail/tabs/MetadataSearchDrawer.vue index ee57576a6..46a0860b7 100644 --- a/client/src/features/book/components/detail/tabs/MetadataSearchDrawer.vue +++ b/client/src/features/book/components/detail/tabs/MetadataSearchDrawer.vue @@ -74,7 +74,7 @@ function handleSearch(params: { title: string; author: string; isbn: string }) { const isAudiobook = props.book.files.some((f) => f.format != null && isAudioFormat(f.format)) selectedCandidate.value = null view.value = 'search' - search({ ...params, bookId: props.book.id, isAudiobook }) + search({ ...params, isAudiobook }) } function handleSelect(candidate: MetadataCandidate) { diff --git a/client/src/features/book/composables/useMetadataDiff.ts b/client/src/features/book/composables/useMetadataDiff.ts index db1c86c63..412e6a60f 100644 --- a/client/src/features/book/composables/useMetadataDiff.ts +++ b/client/src/features/book/composables/useMetadataDiff.ts @@ -94,6 +94,7 @@ export interface MetadataPatch { itunesId?: string | null audibleId?: string | null comicvineId?: string | null + ranobedbId?: string | null comicMetadata?: ComicMetadataFields } @@ -154,6 +155,7 @@ export type ProviderIdPatchField = | 'itunesId' | 'audibleId' | 'comicvineId' + | 'ranobedbId' export const PROVIDER_ID_FIELD: Record = { google: 'googleBooksId', @@ -165,6 +167,7 @@ export const PROVIDER_ID_FIELD: Record(Object.values(PROVIDER_ID_FIELD).filter((v): v is ProviderIdPatchField => v !== undefined)) @@ -183,6 +186,7 @@ export const PROVIDER_ID_LABEL: Record = { audible: 'Audible ID', audnexus: 'AudNexus ID', comicvine: 'ComicVine ID', + ranobedb: 'RanobeDB ID', } export function getCandidateValueFrom(candidate: MetadataCandidate, key: DiffFieldKey): string { diff --git a/client/src/features/book/composables/useMetadataEditor.ts b/client/src/features/book/composables/useMetadataEditor.ts index af14b5b3e..6a463b94f 100644 --- a/client/src/features/book/composables/useMetadataEditor.ts +++ b/client/src/features/book/composables/useMetadataEditor.ts @@ -26,6 +26,7 @@ const ROOT_FIELDS = [ 'itunesId', 'audibleId', 'comicvineId', + 'ranobedbId', ] as const const COMIC_FIELDS = { @@ -79,6 +80,7 @@ export function useMetadataEditor() { itunesId: null as string | null, audibleId: null as string | null, comicvineId: null as string | null, + ranobedbId: null as string | null, comicIssueNumber: null as string | null, comicVolumeName: null as string | null, comicStoryArcs: [] as string[], @@ -124,6 +126,7 @@ export function useMetadataEditor() { form.itunesId = book.providerIds.itunes ?? null form.audibleId = book.providerIds.audible ?? null form.comicvineId = book.providerIds.comicvine ?? null + form.ranobedbId = book.providerIds.ranobedb ?? null const cm = book.comicMetadata form.comicIssueNumber = cm?.issueNumber ?? null form.comicVolumeName = cm?.volumeName ?? null diff --git a/client/src/features/book/composables/useRefreshMetadata.ts b/client/src/features/book/composables/useRefreshMetadata.ts index fb27be09c..9f490ffd2 100644 --- a/client/src/features/book/composables/useRefreshMetadata.ts +++ b/client/src/features/book/composables/useRefreshMetadata.ts @@ -26,6 +26,7 @@ export interface MetadataRefreshPreview { itunesId?: string | null audibleId?: string | null comicvineId?: string | null + ranobedbId?: string | null audioMetadata?: { narrators?: string[] durationSeconds?: number | null diff --git a/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue b/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue index 67a42bfdd..1fca9d962 100644 --- a/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue +++ b/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue @@ -171,6 +171,7 @@ const rows: RowDef[] = [ blockedMessage: 'ComicVine requires an API key before it can be enabled', }, }, + { key: 'ranobedb', label: 'RanobeDB', hint: 'Japanese light novel metadata from RanobeDB. No setup required.', fields: [] }, ] const TESTABLE_PROVIDERS: MetadataProviderKey[] = [MetadataProviderKey.AMAZON, MetadataProviderKey.HARDCOVER] diff --git a/client/src/lib/__tests__/provider-colors.spec.ts b/client/src/lib/__tests__/provider-colors.spec.ts new file mode 100644 index 000000000..90348ec2e --- /dev/null +++ b/client/src/lib/__tests__/provider-colors.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest' + +import { getProviderColor, providerBadgeStyle, providerChipStyle, PROVIDER_SHORT_LABELS } from '../provider-colors' + +const RANOBEDB_COLOR = '#a78cff' + +describe('getProviderColor', () => { + it('returns correct color for ranobedb', () => { + expect(getProviderColor('ranobedb')).toBe(RANOBEDB_COLOR) + }) + + it('returns correct color for comicvine', () => { + expect(getProviderColor('comicvine')).toBe('#ffdb0f') + }) + + it('returns default color for unknown provider', () => { + const result = getProviderColor('unknown-provider') + expect(result).toBeTruthy() + expect(result).not.toBe(RANOBEDB_COLOR) + }) + + it('returns a color for all known providers', () => { + const providers = ['google', 'amazon', 'goodreads', 'hardcover', 'openLibrary', 'itunes', 'audible', 'audnexus', 'comicvine', 'ranobedb', 'auto'] + for (const provider of providers) { + expect(getProviderColor(provider)).toBeTruthy() + } + }) +}) + +describe('PROVIDER_SHORT_LABELS', () => { + it('has a label for ranobedb', () => { + expect(PROVIDER_SHORT_LABELS['ranobedb']).toBe('RanobeDB') + }) + + it('has labels for all providers', () => { + const providers = ['google', 'amazon', 'goodreads', 'hardcover', 'openLibrary', 'itunes', 'audible', 'audnexus', 'comicvine', 'ranobedb'] + for (const provider of providers) { + expect(PROVIDER_SHORT_LABELS[provider]).toBeTruthy() + } + }) +}) + +describe('providerBadgeStyle', () => { + it('returns a style object for ranobedb', () => { + const style = providerBadgeStyle('ranobedb') + expect(style).toHaveProperty('backgroundColor') + expect(style).toHaveProperty('color') + expect(style).toHaveProperty('outlineColor') + expect(style.color).toBe(RANOBEDB_COLOR) + }) + + it('returns a style object for unknown providers without throwing', () => { + expect(() => providerBadgeStyle('unknown')).not.toThrow() + }) +}) + +describe('providerChipStyle', () => { + it('returns a style object for ranobedb when not disabled', () => { + const style = providerChipStyle('ranobedb', false) + expect(style).toHaveProperty('backgroundColor') + expect(style.color).toBe(RANOBEDB_COLOR) + expect(style.opacity).toBeUndefined() + }) + + it('sets opacity when disabled', () => { + const style = providerChipStyle('ranobedb', true) + expect(style.opacity).toBe('0.75') + }) +}) diff --git a/client/src/lib/provider-colors.ts b/client/src/lib/provider-colors.ts index 865a82e42..498f842c5 100644 --- a/client/src/lib/provider-colors.ts +++ b/client/src/lib/provider-colors.ts @@ -10,6 +10,7 @@ const PROVIDER_HEX: Record = { audible: '#FF8A00', audnexus: '#FF5ADD', comicvine: '#ffdb0f', + ranobedb: '#a78cff', auto: '#8B5CF6', } @@ -25,6 +26,7 @@ export const PROVIDER_SHORT_LABELS: Record = { audible: 'Audible', audnexus: 'AudNexus', comicvine: 'ComicVine', + ranobedb: 'RanobeDB', auto: 'Fetched', } diff --git a/packages/types/src/metadata-fetch.ts b/packages/types/src/metadata-fetch.ts index ebcad2a92..2d8d2f6ca 100644 --- a/packages/types/src/metadata-fetch.ts +++ b/packages/types/src/metadata-fetch.ts @@ -10,6 +10,7 @@ export const MetadataProviderKey = { AUDIBLE: "audible", AUDNEXUS: "audnexus", COMICVINE: "comicvine", + RANOBEDB: "ranobedb", } as const; export interface ComicMetadataFields { diff --git a/packages/types/src/metadata-lock.ts b/packages/types/src/metadata-lock.ts index 85c677acb..1dc56165c 100644 --- a/packages/types/src/metadata-lock.ts +++ b/packages/types/src/metadata-lock.ts @@ -25,6 +25,7 @@ export const BOOK_METADATA_LOCK_FIELDS = [ "itunesId", "audibleId", "comicvineId", + "ranobedbId", "comicIssueNumber", "comicVolumeName", "comicStoryArcs", @@ -73,7 +74,7 @@ export const BOOK_METADATA_LOCK_GROUPS: BookMetadataLockGroup[] = [ { key: "providers", label: "Provider IDs", - fields: ["googleBooksId", "goodreadsId", "amazonId", "hardcoverId", "openLibraryId", "itunesId", "audibleId", "comicvineId"], + fields: ["googleBooksId", "goodreadsId", "amazonId", "hardcoverId", "openLibraryId", "itunesId", "audibleId", "comicvineId", "ranobedbId"], }, { key: "comic", diff --git a/packages/types/src/metadata-preferences.ts b/packages/types/src/metadata-preferences.ts index e67ec4099..30a600811 100644 --- a/packages/types/src/metadata-preferences.ts +++ b/packages/types/src/metadata-preferences.ts @@ -118,6 +118,7 @@ export interface ProviderConfigurations { audible: AudibleProviderConfig; audnexus: AudnexusProviderConfig; comicvine: ComicVineProviderConfig; + ranobedb: SimpleProviderConfig; } export interface ProviderStatus { diff --git a/server/src/db/migrations/0011_add_ranobedb_id.sql b/server/src/db/migrations/0011_add_ranobedb_id.sql new file mode 100644 index 000000000..bfe26e49d --- /dev/null +++ b/server/src/db/migrations/0011_add_ranobedb_id.sql @@ -0,0 +1 @@ +ALTER TABLE "book_metadata" ADD COLUMN "ranobedb_id" varchar(50); \ No newline at end of file diff --git a/server/src/db/migrations/meta/0011_snapshot.json b/server/src/db/migrations/meta/0011_snapshot.json new file mode 100644 index 000000000..1eed628dc --- /dev/null +++ b/server/src/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,10256 @@ +{ + "id": "faf29503-ed28-40a0-aa32-f227fec606b5", + "prevId": "83f0250b-9d64-484b-93d7-e2fa62c523e1", + "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_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_action": { + "name": "idx_audit_action", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "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": {} + } + }, + "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.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 + }, + "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 + }, + "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_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" + } + }, + "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\"" + } + }, + "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'" + }, + "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": {} + } + }, + "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" + } + }, + "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.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 + }, + "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 + }, + "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'" + }, + "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": "integer", + "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_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_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_organization_mode_chk": { + "name": "libraries_organization_mode_chk", + "value": "\"libraries\".\"organization_mode\" in ('book_per_folder', 'book_per_file')" + }, + "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" + } + }, + "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 + }, + "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": {}, + "isRLSEnabled": false + }, + "public.book_files": { + "name": "book_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "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": "bigint", + "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 + }, + "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_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_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": "no action", + "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" + } + }, + "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 + }, + "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_idx": { + "name": "books_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": {} + }, + "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": {} + } + }, + "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.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()" + } + }, + "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": {} + } + }, + "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.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 + }, + "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 + }, + "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_name_uidx": { + "name": "collections_user_name_uidx", + "columns": [ + { + "expression": "user_id", + "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": {} + } + }, + "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": {}, + "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 + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "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 + }, + "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": { + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "smart_scopes_user_id_name_unique": { + "name": "smart_scopes_user_id_name_unique", + "nullsNotDistinct": false, + "columns": ["user_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "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": {} + } + }, + "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": {} + } + }, + "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_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_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_name": { + "name": "series_name", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false + }, + "series_index": { + "name": "series_index", + "type": "real", + "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 + }, + "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 + }, + "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 + }, + "comicvine_id": { + "name": "comicvine_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "ranobedb_id": { + "name": "ranobedb_id", + "type": "varchar(50)", + "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[]" + }, + "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_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_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_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_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_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_series_name_index_idx": { + "name": "bm_series_name_index_idx", + "columns": [ + { + "expression": "series_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "series_index", + "isExpression": false, + "asc": 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" + } + }, + "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_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_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": {} + } + }, + "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": {} + } + }, + "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": {} + } + }, + "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": {} + } + }, + "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.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 + }, + "cfi": { + "name": "cfi", + "type": "varchar(2000)", + "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 + }, + "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_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": {} + } + }, + "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')" + } + }, + "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 + }, + "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": {} + } + }, + "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" + } + }, + "isRLSEnabled": false + }, + "public.bookmarks": { + "name": "bookmarks", + "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 + }, + "cfi": { + "name": "cfi", + "type": "varchar(2000)", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": { + "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_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", + "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": {}, + "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": {} + } + }, + "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_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 + }, + "updated_at": { + "name": "updated_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_updated_at_idx": { + "name": "reading_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": {} + } + }, + "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" + } + }, + "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": 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 + }, + "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": {} + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "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_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": true + }, + "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\" >= 1 and \"user_book_ratings\".\"rating\" <= 5" + } + }, + "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": {} + } + }, + "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 + }, + "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_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_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_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 + }, + "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"] + } + }, + "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 + }, + "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_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": {}, + "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_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 + }, + "metadata_hash": { + "name": "metadata_hash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "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_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": false + }, + "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 + }, + "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 + }, + "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 + }, + "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_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"] + } + }, + "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.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": {} + } + }, + "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_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 + }, + "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": {} + } + }, + "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.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'" + }, + "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\" >= 100 and \"user_fonts\".\"weight\" <= 900 and \"user_fonts\".\"weight\" % 100 = 0" + }, + "user_fonts_style_chk": { + "name": "user_fonts_style_chk", + "value": "\"user_fonts\".\"style\" in ('normal', 'italic')" + } + }, + "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_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_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')" + }, + "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 + } + }, + "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": {} + } + }, + "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(10)", + "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 + }, + "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_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 + }, + "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.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 + } + }, + "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.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"] + } + }, + "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 5d24c90fb..ac654b89f 100644 --- a/server/src/db/migrations/meta/_journal.json +++ b/server/src/db/migrations/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1780190394625, "tag": "0010_fix_reading_threshold", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1780269274384, + "tag": "0011_add_ranobedb_id", + "breakpoints": true } ] } diff --git a/server/src/db/schema/metadata.ts b/server/src/db/schema/metadata.ts index b7bfc587c..32a34c0f8 100644 --- a/server/src/db/schema/metadata.ts +++ b/server/src/db/schema/metadata.ts @@ -64,6 +64,7 @@ export const bookMetadata = pgTable( abridged: boolean('abridged').notNull().default(false), audibleId: varchar('audible_id', { length: 20 }), comicvineId: varchar('comicvine_id', { length: 50 }), + ranobedbId: varchar('ranobedb_id', { length: 50 }), chapters: jsonb('chapters'), lockedFields: text('locked_fields') .array() diff --git a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts index 70681b4eb..d0f602328 100644 --- a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts +++ b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts @@ -289,6 +289,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst if (filteredProviderIds[MetadataProviderKey.ITUNES]) scalarFields.itunesId = filteredProviderIds[MetadataProviderKey.ITUNES]; if (filteredProviderIds[MetadataProviderKey.AUDIBLE]) scalarFields.audibleId = filteredProviderIds[MetadataProviderKey.AUDIBLE]; if (filteredProviderIds[MetadataProviderKey.COMICVINE]) scalarFields.comicvineId = filteredProviderIds[MetadataProviderKey.COMICVINE]; + if (filteredProviderIds[MetadataProviderKey.RANOBEDB]) scalarFields.ranobedbId = filteredProviderIds[MetadataProviderKey.RANOBEDB]; const duration = this.asNullableNumber(filteredResolved.duration); if (duration !== undefined) scalarFields.durationSeconds = duration; @@ -375,6 +376,8 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst openLibraryId?: string | null; itunesId?: string | null; audibleId?: string | null; + comicvineId?: string | null; + ranobedbId?: string | null; }): Partial> { const ids: Partial> = {}; if (meta.googleBooksId) ids[MetadataProviderKey.GOOGLE] = meta.googleBooksId; @@ -384,6 +387,8 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst if (meta.openLibraryId) ids[MetadataProviderKey.OPEN_LIBRARY] = meta.openLibraryId; if (meta.itunesId) ids[MetadataProviderKey.ITUNES] = meta.itunesId; if (meta.audibleId) ids[MetadataProviderKey.AUDIBLE] = meta.audibleId; + if (meta.comicvineId) ids[MetadataProviderKey.COMICVINE] = meta.comicvineId; + if (meta.ranobedbId) ids[MetadataProviderKey.RANOBEDB] = meta.ranobedbId; return ids; } diff --git a/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts b/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts index 964259cdd..c4c3275df 100644 --- a/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts +++ b/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts @@ -17,6 +17,7 @@ const PROVIDER_ID_LOCK_FIELDS_BY_PROVIDER: Partial = { @@ -237,6 +238,7 @@ export class BookMetadataLockService { this.copyUpdateField(filteredDto, dto, 'itunesId', 'itunesId', lockedSet, skippedFields); this.copyUpdateField(filteredDto, dto, 'audibleId', 'audibleId', lockedSet, skippedFields); this.copyUpdateField(filteredDto, dto, 'comicvineId', 'comicvineId', lockedSet, skippedFields); + this.copyUpdateField(filteredDto, dto, 'ranobedbId', 'ranobedbId', lockedSet, skippedFields); if (dto.audioMetadata) { const filteredAudioMetadata: NonNullable = {}; @@ -360,6 +362,7 @@ export class BookMetadataLockService { this.addFieldIfPresent(fields, dto, 'itunesId', 'itunesId'); this.addFieldIfPresent(fields, dto, 'audibleId', 'audibleId'); this.addFieldIfPresent(fields, dto, 'comicvineId', 'comicvineId'); + this.addFieldIfPresent(fields, dto, 'ranobedbId', 'ranobedbId'); if (dto.audioMetadata) { this.addFieldIfPresent(fields, dto.audioMetadata, 'narrators', 'narrators'); diff --git a/server/src/modules/book/book.service.ts b/server/src/modules/book/book.service.ts index 8cec80850..3e164c833 100644 --- a/server/src/modules/book/book.service.ts +++ b/server/src/modules/book/book.service.ts @@ -245,6 +245,7 @@ export class BookService { itunesId?: string | null; audibleId?: string | null; comicvineId?: string | null; + ranobedbId?: string | null; }): Partial> { const providerIds: Partial> = {}; if (meta.googleBooksId) providerIds[MetadataProviderKey.GOOGLE] = meta.googleBooksId; @@ -255,13 +256,14 @@ export class BookService { if (meta.itunesId) providerIds[MetadataProviderKey.ITUNES] = meta.itunesId; if (meta.audibleId) providerIds[MetadataProviderKey.AUDIBLE] = meta.audibleId; if (meta.comicvineId) providerIds[MetadataProviderKey.COMICVINE] = meta.comicvineId; + if (meta.ranobedbId) providerIds[MetadataProviderKey.RANOBEDB] = meta.ranobedbId; return providerIds; } private applyResolvedProviderIds( dto: Pick< UpdateBookMetadataDto, - 'googleBooksId' | 'goodreadsId' | 'amazonId' | 'hardcoverId' | 'openLibraryId' | 'itunesId' | 'audibleId' | 'comicvineId' + 'googleBooksId' | 'goodreadsId' | 'amazonId' | 'hardcoverId' | 'openLibraryId' | 'itunesId' | 'audibleId' | 'comicvineId' | 'ranobedbId' >, providerIds: Partial>, ): void { @@ -273,6 +275,7 @@ export class BookService { if (providerIds[MetadataProviderKey.ITUNES]) dto.itunesId = providerIds[MetadataProviderKey.ITUNES]; if (providerIds[MetadataProviderKey.AUDIBLE]) dto.audibleId = providerIds[MetadataProviderKey.AUDIBLE]; if (providerIds[MetadataProviderKey.COMICVINE]) dto.comicvineId = providerIds[MetadataProviderKey.COMICVINE]; + if (providerIds[MetadataProviderKey.RANOBEDB]) dto.ranobedbId = providerIds[MetadataProviderKey.RANOBEDB]; } async verifyBookAccess(bookId: number, user: RequestUser): Promise { @@ -1177,6 +1180,7 @@ export class BookService { if (dto.itunesId !== undefined) scalarFields.itunesId = dto.itunesId ?? null; if (dto.audibleId !== undefined) scalarFields.audibleId = dto.audibleId ?? null; if (dto.comicvineId !== undefined) scalarFields.comicvineId = dto.comicvineId ?? null; + if (dto.ranobedbId !== undefined) scalarFields.ranobedbId = dto.ranobedbId ?? null; if (dto.rating !== undefined) scalarFields.rating = dto.rating ?? null; if (dto.audioMetadata) { if (dto.audioMetadata.durationSeconds !== undefined) scalarFields.durationSeconds = dto.audioMetadata.durationSeconds ?? null; @@ -2312,6 +2316,7 @@ export class BookService { [MetadataProviderKey.ITUNES]: meta?.itunesId ?? null, [MetadataProviderKey.AUDIBLE]: meta?.audibleId ?? null, [MetadataProviderKey.COMICVINE]: meta?.comicvineId ?? null, + [MetadataProviderKey.RANOBEDB]: meta?.ranobedbId ?? null, }, authors: authorRows, genres: genreRows.map((g) => g.name), diff --git a/server/src/modules/book/dto/update-book-metadata.dto.ts b/server/src/modules/book/dto/update-book-metadata.dto.ts index 987a59891..0133719c5 100644 --- a/server/src/modules/book/dto/update-book-metadata.dto.ts +++ b/server/src/modules/book/dto/update-book-metadata.dto.ts @@ -53,5 +53,6 @@ export class UpdateBookMetadataDto { @IsOptional() @IsString() @MaxLength(20) audibleId?: string | null; @IsOptional() @ValidateNested() @Type(() => AudioMetadataDto) audioMetadata?: AudioMetadataDto; @IsOptional() @IsString() @MaxLength(50) comicvineId?: string | null; + @IsOptional() @IsString() @MaxLength(50) ranobedbId?: string | null; @IsOptional() @ValidateNested() @Type(() => ComicMetadataDto) comicMetadata?: ComicMetadataDto; } diff --git a/server/src/modules/cover/providers/itunes-cover-provider.test.ts b/server/src/modules/cover/providers/itunes-cover-provider.test.ts index a13cc160b..9a67ed296 100644 --- a/server/src/modules/cover/providers/itunes-cover-provider.test.ts +++ b/server/src/modules/cover/providers/itunes-cover-provider.test.ts @@ -13,6 +13,7 @@ const baseConfig: ProviderConfigurations = { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; function mockJsonResponse(body: unknown, status = 200): Response { diff --git a/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts b/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts index 67e94e14a..3236054e5 100644 --- a/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts +++ b/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts @@ -17,6 +17,7 @@ import { AudibleProvider } from './providers/audible/audible.provider'; import { AudnexusProvider } from './providers/audnexus/audnexus.provider'; import { HardcoverProvider } from './providers/hardcover/hardcover.provider'; import { ComicVineProvider } from './providers/comicvine/comicvine.provider'; +import { RanobeDbProvider } from './providers/ranobedb/ranobedb.provider'; describe('MetadataFetchModule', () => { it('registers provider classes and factory wiring for METADATA_PROVIDERS', () => { @@ -33,6 +34,7 @@ describe('MetadataFetchModule', () => { AudnexusProvider, HardcoverProvider, ComicVineProvider, + RanobeDbProvider, ProviderRegistry, MetadataFetchRepository, MetadataFetchService, @@ -56,6 +58,7 @@ describe('MetadataFetchModule', () => { AudnexusProvider, HardcoverProvider, ComicVineProvider, + RanobeDbProvider, ]); const google = { key: 'google' }; diff --git a/server/src/modules/metadata-fetch/metadata-fetch.module.ts b/server/src/modules/metadata-fetch/metadata-fetch.module.ts index 88c52de29..8113e6e20 100644 --- a/server/src/modules/metadata-fetch/metadata-fetch.module.ts +++ b/server/src/modules/metadata-fetch/metadata-fetch.module.ts @@ -20,6 +20,8 @@ import { HardcoverClient } from './providers/hardcover/hardcover.client'; import { HardcoverProvider } from './providers/hardcover/hardcover.provider'; import { ComicVineClient } from './providers/comicvine/comicvine.client'; import { ComicVineProvider } from './providers/comicvine/comicvine.provider'; +import { RanobeDbClient } from './providers/ranobedb/ranobedb.client'; +import { RanobeDbProvider } from './providers/ranobedb/ranobedb.provider'; const PROVIDER_CLASSES = [ GoogleProvider, @@ -31,6 +33,7 @@ const PROVIDER_CLASSES = [ AudnexusProvider, HardcoverProvider, ComicVineProvider, + RanobeDbProvider, ]; @Module({ @@ -44,6 +47,7 @@ const PROVIDER_CLASSES = [ }, HardcoverClient, ComicVineClient, + RanobeDbClient, ProviderRegistry, ProviderThrottleTracker, MetadataFetchRepository, diff --git a/server/src/modules/metadata-fetch/metadata-fetch.repository.ts b/server/src/modules/metadata-fetch/metadata-fetch.repository.ts index d10dc4fa4..41b1fbb08 100644 --- a/server/src/modules/metadata-fetch/metadata-fetch.repository.ts +++ b/server/src/modules/metadata-fetch/metadata-fetch.repository.ts @@ -18,6 +18,7 @@ export interface StoredProviderIdsRow { itunesId: string | null; audibleId: string | null; comicvineId: string | null; + ranobedbId: string | null; } @Injectable() @@ -36,6 +37,7 @@ export class MetadataFetchRepository { itunesId: bookMetadata.itunesId, audibleId: bookMetadata.audibleId, comicvineId: bookMetadata.comicvineId, + ranobedbId: bookMetadata.ranobedbId, }) .from(books) .leftJoin(bookMetadata, eq(bookMetadata.bookId, books.id)) diff --git a/server/src/modules/metadata-fetch/metadata-fetch.service.ts b/server/src/modules/metadata-fetch/metadata-fetch.service.ts index 267a4d1ac..ba2142871 100644 --- a/server/src/modules/metadata-fetch/metadata-fetch.service.ts +++ b/server/src/modules/metadata-fetch/metadata-fetch.service.ts @@ -70,6 +70,7 @@ export class MetadataFetchService { [MetadataProviderKey.ITUNES]: row.itunesId ?? undefined, [MetadataProviderKey.AUDIBLE]: row.audibleId ?? undefined, [MetadataProviderKey.COMICVINE]: row.comicvineId ?? undefined, + [MetadataProviderKey.RANOBEDB]: row.ranobedbId ?? undefined, }; } diff --git a/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts b/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts index 98889b870..92072eb91 100644 --- a/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts @@ -19,6 +19,7 @@ describe('AmazonProvider', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; beforeEach(async () => { diff --git a/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts b/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts index 3cb8af466..200674b0c 100644 --- a/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts @@ -37,6 +37,7 @@ const baseConfig: ProviderConfigurations = { audible: { enabled: true, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; function makeProvider(config: ProviderConfigurations = baseConfig): { diff --git a/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts b/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts index 0755a3fc8..c2b555bbe 100644 --- a/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts @@ -28,6 +28,7 @@ describe('AudnexusProvider', () => { audible: { enabled: false, domain: 'audible.com' }, audnexus: { enabled: true }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; beforeEach(async () => { diff --git a/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts b/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts index 6e9541c42..b9d8fc8c1 100644 --- a/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts @@ -94,7 +94,8 @@ describe('ComicVineProvider', () => { it('returns empty array when provider is disabled', async () => { vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({ ...mockConfig, - comicvine: { enabled: false, apiKey: 'test-key' }, + comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }); expect(await provider.search({ title: 'Batman #1' })).toEqual([]); @@ -308,7 +309,8 @@ describe('ComicVineProvider', () => { it('returns null when disabled', async () => { vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({ ...mockConfig, - comicvine: { enabled: false, apiKey: 'test-key' }, + comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }); expect(await provider.lookupById('100')).toBeNull(); diff --git a/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts b/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts index 577bd66ed..27c596f32 100644 --- a/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts @@ -18,6 +18,7 @@ describe('GoodreadsProvider', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; beforeEach(() => { diff --git a/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts b/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts index d1ff3a8d9..8512478a5 100644 --- a/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts @@ -18,6 +18,7 @@ describe('GoogleProvider', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; beforeEach(async () => { diff --git a/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts b/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts index 38d000cb8..068aa1072 100644 --- a/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts @@ -16,6 +16,7 @@ const mockConfig: ProviderConfigurations = { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; const mockDocument: HardcoverSearchDocument = { diff --git a/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts b/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts index 983ac97f4..1cdd8dc42 100644 --- a/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts @@ -18,6 +18,7 @@ describe('ITunesProvider', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; beforeEach(async () => { diff --git a/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts b/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts index df8473d48..185d6e610 100644 --- a/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts +++ b/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts @@ -17,6 +17,7 @@ describe('OpenLibraryProvider', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; beforeEach(() => { diff --git a/server/src/modules/metadata-fetch/providers/provider-constants.ts b/server/src/modules/metadata-fetch/providers/provider-constants.ts index 76572d4d5..ff2dc3c5e 100644 --- a/server/src/modules/metadata-fetch/providers/provider-constants.ts +++ b/server/src/modules/metadata-fetch/providers/provider-constants.ts @@ -7,6 +7,7 @@ export const PROVIDER_LIMITS = { AMAZON_MAX_RESULTS: 3, GOODREADS_MAX_RESULTS: 3, COMICVINE_MAX_RESULTS: 10, + RANOBEDB_MAX_RESULTS: 5, DEFAULT_SEARCH_RESULTS: 10, } as const; @@ -15,4 +16,5 @@ export const PROVIDER_DELAYS_MS = { GOODREADS_BETWEEN_REQUESTS: 600, HARDCOVER_RATE_LIMIT: 1_000, COMICVINE_VELOCITY_GUARD: 1_000, + RANOBEDB_BETWEEN_REQUESTS: 500, } as const; diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.client.test.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.client.test.ts new file mode 100644 index 000000000..29f41f339 --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.client.test.ts @@ -0,0 +1,171 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import * as fetchWithThrottleModule from '../../fetch-with-throttle'; +import { ProviderThrottleError } from '../../provider-throttle.error'; +import { RanobeDbClient } from './ranobedb.client'; + +vi.mock('../../fetch-with-throttle', () => ({ + fetchWithThrottle: vi.fn(), +})); + +vi.mock('../provider-utils', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + sleep: vi.fn().mockResolvedValue(undefined), + buildRequestSignal: vi.fn().mockReturnValue(new AbortController().signal), + }; +}); + +describe('RanobeDbClient', () => { + let client: RanobeDbClient; + let mockFetch: ReturnType>; + + beforeEach(() => { + client = new RanobeDbClient(); + mockFetch = vi.mocked(fetchWithThrottleModule.fetchWithThrottle); + vi.clearAllMocks(); + }); + + describe('search()', () => { + it('returns array of book IDs on success', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ books: [{ id: 1 }, { id: 2 }, { id: 3 }], count: 3, currentPage: 1, totalPages: 1 }), + } as Response); + + const result = await client.search('Sword Art Online'); + expect(result).toEqual([1, 2, 3]); + }); + + it('builds query parameters correctly', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ books: [], count: 0, currentPage: 1, totalPages: 0 }), + } as Response); + + await client.search('Re:Zero'); + + expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('q=Re%3AZero'), expect.any(Object)); + expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('limit=5'), expect.any(Object)); + }); + + it('sends correct User-Agent header', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ books: [], count: 0, currentPage: 1, totalPages: 0 }), + } as Response); + + await client.search('test'); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + 'User-Agent': expect.stringContaining('bookorbit'), + }), + }), + ); + }); + + it('returns empty array when response is not ok', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 503, + } as Response); + + const result = await client.search('query'); + expect(result).toEqual([]); + }); + + it('returns empty array when fetch throws a generic error', async () => { + mockFetch.mockRejectedValue(new Error('network failure')); + + const result = await client.search('query'); + expect(result).toEqual([]); + }); + + it('rethrows ProviderThrottleError', async () => { + mockFetch.mockRejectedValue(new ProviderThrottleError('ranobedb', 1000)); + + await expect(client.search('query')).rejects.toThrow(ProviderThrottleError); + }); + + it('returns empty array when books field is missing from response', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({}), + } as Response); + + const result = await client.search('query'); + expect(result).toEqual([]); + }); + }); + + describe('fetchBook()', () => { + it('returns book response on success', async () => { + const mockBook = { + id: 42, + title: 'Test Book', + romaji: null, + lang: 'ja', + c_release_date: 20220101, + image: null, + rating: null, + titles: [], + editions: [], + releases: [], + publishers: [], + series: null, + }; + + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ book: mockBook }), + } as Response); + + const result = await client.fetchBook(42); + expect(result).toEqual({ book: mockBook }); + }); + + it('calls correct URL', async () => { + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ book: { id: 42 } }), + } as Response); + + await client.fetchBook(42); + + expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/book/42'), expect.any(Object)); + }); + + it('returns null when response is not ok', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 404, + } as Response); + + const result = await client.fetchBook(999); + expect(result).toBeNull(); + }); + + it('returns null when fetch throws a generic error', async () => { + mockFetch.mockRejectedValue(new Error('timeout')); + + const result = await client.fetchBook(1); + expect(result).toBeNull(); + }); + + it('rethrows ProviderThrottleError', async () => { + mockFetch.mockRejectedValue(new ProviderThrottleError('ranobedb', 1000)); + + await expect(client.fetchBook(1)).rejects.toThrow(ProviderThrottleError); + }); + }); +}); diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.client.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.client.ts new file mode 100644 index 000000000..b636f461d --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.client.ts @@ -0,0 +1,72 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { fetchWithThrottle } from '../../fetch-with-throttle'; +import { ProviderThrottleError } from '../../provider-throttle.error'; +import { PROVIDER_DELAYS_MS, PROVIDER_TIMEOUT_MS } from '../provider-constants'; +import { buildRequestSignal, sanitizeLogError, sleep } from '../provider-utils'; +import { RanobeDbBookResponse, RanobeDbSearchResponse } from './ranobedb.types'; + +const BASE_URL = 'https://ranobedb.org/api/v0'; +const USER_AGENT = 'bookorbit/1.0 (+https://github.com/bookorbit/bookorbit)'; + +class RateLimiter { + private nextAllowedTime = 0; + + async throttle(signal?: AbortSignal): Promise { + const now = Date.now(); + const scheduled = Math.max(now, this.nextAllowedTime); + this.nextAllowedTime = scheduled + PROVIDER_DELAYS_MS.RANOBEDB_BETWEEN_REQUESTS; + const wait = scheduled - now; + if (wait > 0) { + await sleep(wait, signal); + } + } +} + +@Injectable() +export class RanobeDbClient { + private readonly logger = new Logger(RanobeDbClient.name); + private readonly rateLimiter = new RateLimiter(); + + async search(query: string, signal?: AbortSignal): Promise { + const params = new URLSearchParams({ + q: query, + limit: '5', + }); + const body = await this.get('search', `/books?${params.toString()}`, signal); + return body?.books?.map((b) => b.id) ?? []; + } + + async fetchBook(id: number, signal?: AbortSignal): Promise { + return this.get('fetchBook', `/book/${id}`, signal); + } + + private async get(op: string, path: string, signal?: AbortSignal): Promise { + await this.rateLimiter.throttle(signal); + const startedAt = Date.now(); + this.logger.log(`[ranobedb] [start] op=${op} path=${path}`); + + try { + const res = await fetchWithThrottle(`${BASE_URL}${path}`, { + headers: { 'User-Agent': USER_AGENT }, + signal: buildRequestSignal(PROVIDER_TIMEOUT_MS.DEFAULT, signal), + }); + + if (!res.ok) { + this.logger.warn(`[ranobedb] [fail] op=${op} status=${res.status} durationMs=${Date.now() - startedAt} error="non-ok response"`); + return null; + } + + const body = (await res.json()) as T; + this.logger.log(`[ranobedb] [end] op=${op} status=${res.status} durationMs=${Date.now() - startedAt}`); + return body; + } catch (err) { + if (err instanceof ProviderThrottleError) { + this.logger.warn(`[ranobedb] [fail] op=${op} durationMs=${Date.now() - startedAt} error="throttled"`); + throw err; + } + this.logger.warn(`[ranobedb] [fail] op=${op} durationMs=${Date.now() - startedAt} error="${sanitizeLogError(err)}"`); + return null; + } + } +} diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.test.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.test.ts new file mode 100644 index 000000000..fc1f40dac --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.test.ts @@ -0,0 +1,363 @@ +import { MetadataProviderKey } from '@bookorbit/types'; +import { describe, expect, it } from 'vitest'; + +import { mapRanobeDbBook, parseDateInt } from './ranobedb.mapper'; +import { RanobeDbBook } from './ranobedb.types'; + +const baseBook: RanobeDbBook = { + id: 1287, + title: 'ソードアート・オンライン', + romaji: 'Sword Art Online', + lang: 'ja', + description: 'A virtual reality adventure.', + description_ja: '仮想現実の冒険。', + c_release_date: 20120710, + image: { id: 10, filename: 'covers/sao.jpg', width: 300, height: 450, nsfw: false, spoiler: false }, + rating: { score: 8.5, count: 1200 }, + titles: [ + { book_id: 1287, lang: 'en', official: true, title: 'Sword Art Online Vol. 1', romaji: null }, + { book_id: 1287, lang: 'ja', official: true, title: 'ソードアート・オンライン', romaji: 'Sword Art Online' }, + ], + editions: [ + { + book_id: 1287, + lang: 'en', + title: 'English Edition', + eid: 1, + staff: [ + { role_type: 'author', romaji: 'Reki Kawahara', name: '川原礫', staff_id: 1 }, + { role_type: 'artist', romaji: 'abec', name: 'abec', staff_id: 2 }, + ], + }, + { + book_id: 1287, + lang: 'ja', + title: 'Japanese Edition', + eid: 2, + staff: [ + { role_type: 'author', romaji: null, name: '川原礫', staff_id: 1 }, + { role_type: 'translator', romaji: 'Some Translator', name: 'Some Translator', staff_id: 3 }, + ], + }, + ], + releases: [ + { + lang: 'en', + id: 100, + title: 'Sword Art Online Vol. 1', + release_date: 20121210, + isbn13: '9780316371247', + pages: 240, + format: 'print', + }, + { + lang: 'en', + id: 101, + title: 'Sword Art Online Vol. 1 Digital', + release_date: 20121215, + isbn13: null, + pages: null, + format: 'digital', + }, + { + lang: 'ja', + id: 200, + title: 'ソードアート・オンライン', + release_date: 20120710, + isbn13: '9784048913102', + pages: 248, + format: 'print', + }, + ], + publishers: [ + { lang: 'en', id: 10, romaji: null, name: 'Yen Press', publisher_type: 'publisher' }, + { lang: 'en', id: 11, romaji: null, name: 'Yen On', publisher_type: 'imprint' }, + { lang: 'ja', id: 20, romaji: null, name: 'KADOKAWA', publisher_type: 'publisher' }, + ], + series: { + id: 50, + title: 'Sword Art Online', + romaji: 'Sword Art Online', + books: [ + { id: 1287, lang: 'en', title: 'Vol. 1', romaji: null, image: null }, + { id: 1288, lang: 'en', title: 'Vol. 2', romaji: null, image: null }, + ], + tags: [ + { id: 1, name: 'Sci-Fi', ttype: 'genre' }, + { id: 2, name: 'Action', ttype: 'genre' }, + { id: 3, name: 'LGBTQ', ttype: 'genre' }, + { id: 4, name: 'Isekai', ttype: 'tag' }, + { id: 5, name: 'Shonen', ttype: 'demographic' }, + ], + }, +}; + +describe('parseDateInt', () => { + it('extracts year from valid date integer', () => { + expect(parseDateInt(20220624)).toBe(2022); + expect(parseDateInt(19991231)).toBe(1999); + expect(parseDateInt(20000101)).toBe(2000); + }); + + it('returns undefined for null', () => { + expect(parseDateInt(null)).toBeUndefined(); + }); + + it('returns undefined for undefined', () => { + expect(parseDateInt(undefined)).toBeUndefined(); + }); + + it('returns undefined for zero', () => { + expect(parseDateInt(0)).toBeUndefined(); + }); + + it('returns undefined for years out of plausible range', () => { + expect(parseDateInt(9990101)).toBeUndefined(); + expect(parseDateInt(9990101)).toBeUndefined(); + }); + + it('extracts year at boundary', () => { + expect(parseDateInt(10000101)).toBe(1000); + expect(parseDateInt(22000101)).toBe(2200); + }); +}); + +describe('mapRanobeDbBook', () => { + it('maps a complete book correctly', () => { + const result = mapRanobeDbBook(baseBook); + + expect(result).toMatchObject({ + provider: MetadataProviderKey.RANOBEDB, + providerId: '1287', + title: 'Sword Art Online Vol. 1', + authors: ['Reki Kawahara'], + publisher: 'Yen Press', + publishedYear: 2012, + language: 'en', + pageCount: 240, + isbn13: '9780316371247', + seriesName: 'Sword Art Online', + seriesIndex: 1, + coverUrl: 'https://images.ranobedb.org/covers/sao.jpg', + sourceUrl: 'https://ranobedb.org/book/1287', + }); + }); + + it('includes only genre-type tags (not tag/demographic)', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.genres).toEqual(['Sci-Fi', 'Action', 'LGBTQ']); + expect(result?.genres).not.toContain('Isekai'); + expect(result?.genres).not.toContain('Shonen'); + }); + + it('preserves original casing of genre names', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.genres).toContain('Sci-Fi'); + expect(result?.genres).toContain('LGBTQ'); + }); + + it('deduplicates authors across editions', () => { + const result = mapRanobeDbBook(baseBook); + const authorCount = result?.authors?.filter((a) => a === 'Reki Kawahara' || a === '川原礫').length ?? 0; + expect(authorCount).toBe(1); + }); + + it('prefers romaji over native name for authors', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.authors).toContain('Reki Kawahara'); + expect(result?.authors).not.toContain('川原礫'); + }); + + it('only includes authors (not artists/translators)', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.authors).not.toContain('abec'); + expect(result?.authors).not.toContain('Some Translator'); + }); + + it('prefers English print release over English digital for isbn/pages', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.isbn13).toBe('9780316371247'); + expect(result?.pageCount).toBe(240); + }); + + it('prefers English release publishedYear over c_release_date', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.publishedYear).toBe(2012); + }); + + it('falls back to c_release_date year when no English release has a date', () => { + const book: RanobeDbBook = { + ...baseBook, + releases: [{ lang: 'en', id: 100, title: null, release_date: null, isbn13: null, pages: null, format: 'print' }], + c_release_date: 20180501, + }; + const result = mapRanobeDbBook(book); + expect(result?.publishedYear).toBe(2018); + }); + + it('returns null when book has no id', () => { + const book = { ...baseBook, id: 0 }; + expect(mapRanobeDbBook(book)).toBeNull(); + }); + + it('sets language to "en" when any release has lang en', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.language).toBe('en'); + }); + + it('falls back to book.lang when no English releases exist', () => { + const book: RanobeDbBook = { + ...baseBook, + lang: 'ja', + releases: [{ lang: 'ja', id: 200, title: null, release_date: 20120710, isbn13: null, pages: null, format: 'print' }], + }; + const result = mapRanobeDbBook(book); + expect(result?.language).toBe('ja'); + }); + + it('falls back to romaji title when no official English title', () => { + const book: RanobeDbBook = { + ...baseBook, + titles: [{ book_id: 1287, lang: 'ja', official: true, title: 'ソードアート・オンライン', romaji: null }], + romaji: 'Sword Art Online', + }; + const result = mapRanobeDbBook(book); + expect(result?.title).toBe('Sword Art Online'); + }); + + it('falls back to native title when no romaji and no English title', () => { + const book: RanobeDbBook = { + ...baseBook, + titles: [], + romaji: null, + title: 'ソードアート・オンライン', + }; + const result = mapRanobeDbBook(book); + expect(result?.title).toBe('ソードアート・オンライン'); + }); + + it('resolves publisher from English publisher (not imprint)', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.publisher).toBe('Yen Press'); + expect(result?.publisher).not.toBe('Yen On'); + }); + + it('returns undefined publisher when no English publisher exists', () => { + const book: RanobeDbBook = { + ...baseBook, + publishers: [{ lang: 'ja', id: 20, romaji: null, name: 'KADOKAWA', publisher_type: 'publisher' }], + }; + const result = mapRanobeDbBook(book); + expect(result?.publisher).toBeUndefined(); + }); + + it('computes seriesIndex as 1-based position', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.seriesIndex).toBe(1); + }); + + it('returns undefined seriesIndex when book is not in series.books', () => { + const book: RanobeDbBook = { + ...baseBook, + series: { ...baseBook.series!, books: [{ id: 9999, lang: null, title: null, romaji: null, image: null }] }, + }; + const result = mapRanobeDbBook(book); + expect(result?.seriesIndex).toBeUndefined(); + }); + + it('returns undefined seriesIndex when series is null', () => { + const book: RanobeDbBook = { ...baseBook, series: null }; + const result = mapRanobeDbBook(book); + expect(result?.seriesIndex).toBeUndefined(); + }); + + it('returns empty genres array when series is null', () => { + const book: RanobeDbBook = { ...baseBook, series: null }; + const result = mapRanobeDbBook(book); + expect(result?.genres).toEqual([]); + }); + + it('returns no coverUrl when image is null', () => { + const book: RanobeDbBook = { ...baseBook, image: null }; + const result = mapRanobeDbBook(book); + expect(result?.coverUrl).toBeUndefined(); + }); + + it('returns empty authors array when no editions have authors', () => { + const book: RanobeDbBook = { + ...baseBook, + editions: [{ book_id: 1287, lang: 'en', title: null, eid: 1, staff: [{ role_type: 'artist', romaji: 'abec', name: 'abec', staff_id: 2 }] }], + }; + const result = mapRanobeDbBook(book); + expect(result?.authors).toEqual([]); + }); + + it('uses digital release when no print release is available', () => { + const book: RanobeDbBook = { + ...baseBook, + releases: [{ lang: 'en', id: 101, title: null, release_date: 20221201, isbn13: '9780000000001', pages: 300, format: 'digital' }], + }; + const result = mapRanobeDbBook(book); + expect(result?.isbn13).toBe('9780000000001'); + expect(result?.pageCount).toBe(300); + }); + + it('does not use audio release for isbn/pages selection', () => { + const book: RanobeDbBook = { + ...baseBook, + releases: [{ lang: 'en', id: 102, title: null, release_date: 20221201, isbn13: '9780000000002', pages: 999, format: 'audio' }], + }; + const result = mapRanobeDbBook(book); + expect(result?.isbn13).toBeUndefined(); + expect(result?.pageCount).toBeUndefined(); + }); + + it('deduplicates genres', () => { + const book: RanobeDbBook = { + ...baseBook, + series: { + ...baseBook.series!, + tags: [ + { id: 1, name: 'Action', ttype: 'genre' }, + { id: 2, name: 'Action', ttype: 'genre' }, + ], + }, + }; + const result = mapRanobeDbBook(book); + expect(result?.genres).toEqual(['Action']); + }); + + it('includes sourceUrl', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.sourceUrl).toBe('https://ranobedb.org/book/1287'); + }); + + it('maps English description when present', () => { + const result = mapRanobeDbBook(baseBook); + expect(result?.description).toBe('A virtual reality adventure.'); + }); + + it('falls back to description_ja when English description is empty', () => { + const book: RanobeDbBook = { ...baseBook, description: '', description_ja: '仮想現実の冒険。' }; + const result = mapRanobeDbBook(book); + expect(result?.description).toBe('仮想現実の冒険。'); + }); + + it('falls back to description_ja when English description is null', () => { + const book: RanobeDbBook = { ...baseBook, description: null, description_ja: '仮想現実の冒険。' }; + const result = mapRanobeDbBook(book); + expect(result?.description).toBe('仮想現実の冒険。'); + }); + + it('returns undefined description when both descriptions are empty', () => { + const book: RanobeDbBook = { ...baseBook, description: '', description_ja: '' }; + const result = mapRanobeDbBook(book); + expect(result?.description).toBeUndefined(); + }); + + it('returns undefined description when both descriptions are null', () => { + const book: RanobeDbBook = { ...baseBook, description: null, description_ja: null }; + const result = mapRanobeDbBook(book); + expect(result?.description).toBeUndefined(); + }); +}); diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.ts new file mode 100644 index 000000000..38d26d9e1 --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.mapper.ts @@ -0,0 +1,117 @@ +import { MetadataCandidate, MetadataProviderKey } from '@bookorbit/types'; + +import { RanobeDbBook, RanobeDbRelease } from './ranobedb.types'; + +const IMAGE_BASE_URL = 'https://images.ranobedb.org'; +const RANOBEDB_BASE_URL = 'https://ranobedb.org'; + +export function parseDateInt(value: number | null | undefined): number | undefined { + if (!value) return undefined; + const year = Math.floor(value / 10000); + return year >= 1000 && year <= 2200 ? year : undefined; +} + +function selectEnglishRelease(releases: RanobeDbRelease[]): RanobeDbRelease | undefined { + const english = releases.filter((r) => r.lang === 'en' && r.format !== 'audio'); + return english.find((r) => r.format === 'print') ?? english.find((r) => r.format === 'digital'); +} + +function resolveTitle(book: RanobeDbBook): string { + const englishTitle = book.titles.find((t) => t.lang === 'en' && t.official); + if (englishTitle) return englishTitle.title; + if (book.romaji) return book.romaji; + return book.title; +} + +function resolveSubtitle(book: RanobeDbBook): string | undefined { + const mainTitle = resolveTitle(book); + if (book.title_orig && book.title_orig !== mainTitle) { + return book.title_orig; + } + if (book.romaji_orig && book.romaji_orig !== mainTitle) { + return book.romaji_orig; + } + return undefined; +} + +function resolveAuthors(book: RanobeDbBook): string[] { + const seen = new Set(); + const seenNames = new Set(); + const authors: string[] = []; + for (const edition of book.editions) { + for (const staff of edition.staff) { + if (staff.role_type !== 'author') continue; + if (seen.has(staff.staff_id)) continue; + seen.add(staff.staff_id); + const name = staff.romaji ?? staff.name; + if (!seenNames.has(name)) { + seenNames.add(name); + authors.push(name); + } + } + } + return authors; +} + +function resolvePublisher(book: RanobeDbBook): string | undefined { + const pub = book.publishers.find((p) => p.lang === 'en' && p.publisher_type === 'publisher'); + if (!pub) return undefined; + return pub.romaji ?? pub.name; +} + +function resolveSeriesIndex(book: RanobeDbBook): number | undefined { + if (!book.series) return undefined; + const pos = book.series.books.findIndex((b) => b.id === book.id); + return pos === -1 ? undefined : pos + 1; +} + +function resolveGenres(book: RanobeDbBook): string[] { + if (!book.series) return []; + const seen = new Set(); + const genres: string[] = []; + for (const tag of book.series.tags) { + if (tag.ttype !== 'genre') continue; + const trimmed = tag.name.trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + genres.push(trimmed); + } + return genres; +} + +function resolvePublishedYear(book: RanobeDbBook, release: RanobeDbRelease | undefined): number | undefined { + if (release?.release_date) { + const year = parseDateInt(release.release_date); + if (year) return year; + } + if (book.olang && book.c_release_dates && book.c_release_dates[book.olang]) { + const year = parseDateInt(book.c_release_dates[book.olang]); + if (year) return year; + } + return parseDateInt(book.c_release_date); +} + +export function mapRanobeDbBook(book: RanobeDbBook): MetadataCandidate | null { + if (!book?.id) return null; + + const selectedRelease = selectEnglishRelease(book.releases); + + return { + provider: MetadataProviderKey.RANOBEDB, + providerId: String(book.id), + title: resolveTitle(book), + subtitle: resolveSubtitle(book), + authors: resolveAuthors(book), + publisher: resolvePublisher(book), + publishedYear: resolvePublishedYear(book, selectedRelease), + language: book.releases.some((r) => r.lang === 'en') ? 'en' : (book.olang ?? book.lang ?? undefined), + pageCount: selectedRelease?.pages ?? undefined, + isbn13: selectedRelease?.isbn13 ?? undefined, + seriesName: book.series?.title, + seriesIndex: resolveSeriesIndex(book), + description: book.description?.trim() || book.description_ja?.trim() || undefined, + genres: resolveGenres(book), + coverUrl: book.image ? `${IMAGE_BASE_URL}/${book.image.filename}` : undefined, + sourceUrl: `${RANOBEDB_BASE_URL}/book/${book.id}`, + }; +} diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts new file mode 100644 index 000000000..9f13082b3 --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts @@ -0,0 +1,254 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ProviderConfigurations } from '@bookorbit/types'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { ProviderConfigService } from '../../../metadata-preferences/provider-config.service'; +import { RanobeDbClient } from './ranobedb.client'; +import { RanobeDbProvider } from './ranobedb.provider'; +import { RanobeDbBook } from './ranobedb.types'; + +const enabledConfig: ProviderConfigurations = { + google: { enabled: false, apiKey: '' }, + amazon: { enabled: false, domain: 'amazon.com', cookie: '' }, + goodreads: { enabled: false }, + hardcover: { enabled: false, apiKey: '' }, + openLibrary: { enabled: false }, + itunes: { enabled: false, coverResolution: 'high' }, + audible: { enabled: false, domain: 'com' }, + audnexus: { enabled: false }, + comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: true }, +}; + +const disabledConfig: ProviderConfigurations = { + ...enabledConfig, + ranobedb: { enabled: false }, +}; + +const mockBook: RanobeDbBook = { + id: 1287, + title: 'ソードアート・オンライン', + romaji: 'Sword Art Online', + lang: 'ja', + c_release_date: 20120710, + image: { id: 10, filename: 'covers/sao.jpg', width: 300, height: 450, nsfw: false, spoiler: false }, + rating: null, + titles: [{ book_id: 1287, lang: 'en', official: true, title: 'Sword Art Online Vol. 1', romaji: null }], + editions: [ + { + book_id: 1287, + lang: 'en', + title: 'EN Edition', + eid: 1, + staff: [{ role_type: 'author', romaji: 'Reki Kawahara', name: '川原礫', staff_id: 1 }], + }, + ], + releases: [{ lang: 'en', id: 100, title: null, release_date: 20121210, isbn13: '9780316371247', pages: 240, format: 'print' }], + publishers: [{ lang: 'en', id: 10, romaji: null, name: 'Yen Press', publisher_type: 'publisher' }], + series: { id: 50, title: 'Sword Art Online', romaji: null, books: [{ id: 1287, lang: null, title: null, romaji: null, image: null }], tags: [] }, +}; + +describe('RanobeDbProvider', () => { + let provider: RanobeDbProvider; + let client: RanobeDbClient; + let providerConfig: ProviderConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RanobeDbProvider, + { + provide: RanobeDbClient, + useValue: { + search: vi.fn().mockResolvedValue([]), + fetchBook: vi.fn().mockResolvedValue(null), + }, + }, + { + provide: ProviderConfigService, + useValue: { + getConfig: vi.fn().mockResolvedValue(enabledConfig), + }, + }, + ], + }).compile(); + + provider = module.get(RanobeDbProvider); + client = module.get(RanobeDbClient); + providerConfig = module.get(ProviderConfigService); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('provider metadata', () => { + it('has key RANOBEDB', () => { + expect(provider.key).toBe('ranobedb'); + }); + + it('has label RanobeDB', () => { + expect(provider.label).toBe('RanobeDB'); + }); + + it('is identifiable', () => { + expect(provider.identifiable).toBe(true); + }); + }); + + describe('search()', () => { + it('returns empty array when provider is disabled', async () => { + vi.mocked(providerConfig.getConfig).mockResolvedValue(disabledConfig); + const result = await provider.search({ title: 'SAO' }); + expect(result).toEqual([]); + expect(client.search).not.toHaveBeenCalled(); + }); + + it('returns empty array when no title and no author', async () => { + const result = await provider.search({}); + expect(result).toEqual([]); + expect(client.search).not.toHaveBeenCalled(); + }); + + it('searches by title when only title provided', async () => { + vi.mocked(client.search).mockResolvedValue([1287]); + vi.mocked(client.fetchBook).mockResolvedValue({ book: mockBook }); + + const result = await provider.search({ title: 'Sword Art Online' }); + + expect(client.search).toHaveBeenCalledWith('Sword Art Online', undefined); + expect(result).toHaveLength(1); + expect(result[0].providerId).toBe('1287'); + }); + + it('combines title and author in query', async () => { + vi.mocked(client.search).mockResolvedValue([]); + + await provider.search({ title: 'SAO', author: 'Kawahara' }); + + expect(client.search).toHaveBeenCalledWith('SAO Kawahara', undefined); + }); + + it('uses author alone when no title provided', async () => { + vi.mocked(client.search).mockResolvedValue([]); + + await provider.search({ author: 'Kawahara' }); + + expect(client.search).toHaveBeenCalledWith('Kawahara', undefined); + }); + + it('returns empty array when client.search returns no IDs', async () => { + vi.mocked(client.search).mockResolvedValue([]); + + const result = await provider.search({ title: 'unknown' }); + expect(result).toEqual([]); + }); + + it('skips null candidates from fetchBook', async () => { + vi.mocked(client.search).mockResolvedValue([1287, 9999]); + vi.mocked(client.fetchBook).mockResolvedValueOnce({ book: mockBook }).mockResolvedValueOnce(null); + + const result = await provider.search({ title: 'SAO' }); + expect(result).toHaveLength(1); + }); + + it('skips books where fetchBook returns null book.id (mapper returns null)', async () => { + const zeroIdBook: RanobeDbBook = { ...mockBook, id: 0 }; + vi.mocked(client.search).mockResolvedValue([0]); + vi.mocked(client.fetchBook).mockResolvedValue({ book: zeroIdBook }); + + const result = await provider.search({ title: 'SAO' }); + expect(result).toEqual([]); + }); + + it('returns multiple candidates', async () => { + const book2: RanobeDbBook = { ...mockBook, id: 1288 }; + vi.mocked(client.search).mockResolvedValue([1287, 1288]); + vi.mocked(client.fetchBook).mockResolvedValueOnce({ book: mockBook }).mockResolvedValueOnce({ book: book2 }); + + const result = await provider.search({ title: 'SAO' }); + expect(result).toHaveLength(2); + }); + + it('passes signal to client', async () => { + const controller = new AbortController(); + vi.mocked(client.search).mockResolvedValue([1287]); + vi.mocked(client.fetchBook).mockResolvedValue({ book: mockBook }); + + await provider.search({ title: 'SAO', signal: controller.signal }); + + expect(client.search).toHaveBeenCalledWith('SAO', controller.signal); + expect(client.fetchBook).toHaveBeenCalledWith(1287, controller.signal); + }); + }); + + describe('lookupById()', () => { + it('returns null when provider is disabled', async () => { + vi.mocked(providerConfig.getConfig).mockResolvedValue(disabledConfig); + const result = await provider.lookupById('1287'); + expect(result).toBeNull(); + }); + + it('returns null for non-numeric ID', async () => { + const result = await provider.lookupById('abc'); + expect(result).toBeNull(); + expect(client.fetchBook).not.toHaveBeenCalled(); + }); + + it('returns null for ID with trailing non-numeric characters', async () => { + const result = await provider.lookupById('123abc'); + expect(result).toBeNull(); + expect(client.fetchBook).not.toHaveBeenCalled(); + }); + + it('returns null for empty string ID', async () => { + const result = await provider.lookupById(''); + expect(result).toBeNull(); + }); + + it('returns null for ID with leading/trailing spaces', async () => { + const result = await provider.lookupById(' 123 '); + expect(result).toBeNull(); + }); + + it('returns null when fetchBook returns null', async () => { + vi.mocked(client.fetchBook).mockResolvedValue(null); + const result = await provider.lookupById('1287'); + expect(result).toBeNull(); + }); + + it('returns mapped candidate for valid numeric ID', async () => { + vi.mocked(client.fetchBook).mockResolvedValue({ book: mockBook }); + const result = await provider.lookupById('1287'); + expect(result).not.toBeNull(); + expect(result?.providerId).toBe('1287'); + expect(result?.title).toBe('Sword Art Online Vol. 1'); + }); + + it('calls fetchBook with correct integer ID', async () => { + vi.mocked(client.fetchBook).mockResolvedValue({ book: mockBook }); + await provider.lookupById('42'); + expect(client.fetchBook).toHaveBeenCalledWith(42, undefined); + }); + + it('passes signal to fetchBook', async () => { + const controller = new AbortController(); + vi.mocked(client.fetchBook).mockResolvedValue({ book: mockBook }); + await provider.lookupById('1287', controller.signal); + expect(client.fetchBook).toHaveBeenCalledWith(1287, controller.signal); + }); + + it('returns null for unsafe integer (overflow)', async () => { + const unsafe = String(Number.MAX_SAFE_INTEGER + 1); + const result = await provider.lookupById(unsafe); + expect(result).toBeNull(); + }); + + it('returns null when mapper returns null (book id=0)', async () => { + const zeroIdBook: RanobeDbBook = { ...mockBook, id: 0 }; + vi.mocked(client.fetchBook).mockResolvedValue({ book: zeroIdBook }); + const result = await provider.lookupById('1287'); + expect(result).toBeNull(); + }); + }); +}); diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.ts new file mode 100644 index 000000000..7ba8535a7 --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { MetadataCandidate, MetadataProviderKey } from '@bookorbit/types'; + +import { ProviderConfigService } from '../../../metadata-preferences/provider-config.service'; +import { IdentifiableProvider } from '../metadata-provider'; +import { MetadataSearchParams } from '../metadata-search-params'; +import { RanobeDbClient } from './ranobedb.client'; +import { mapRanobeDbBook } from './ranobedb.mapper'; + +@Injectable() +export class RanobeDbProvider implements IdentifiableProvider { + readonly key = MetadataProviderKey.RANOBEDB; + readonly label = 'RanobeDB'; + readonly identifiable = true as const; + + private readonly logger = new Logger(RanobeDbProvider.name); + + constructor( + private readonly client: RanobeDbClient, + private readonly providerConfig: ProviderConfigService, + ) {} + + async search(params: MetadataSearchParams): Promise { + const { enabled } = await this.providerConfig.getConfig().then((c) => c.ranobedb); + if (!enabled) return []; + + if (!params.title && !params.author) return []; + + const query = params.author ? `${params.title ?? ''} ${params.author}`.trim() : (params.title ?? ''); + + const ids = await this.client.search(query, params.signal); + if (ids.length === 0) return []; + + const candidates: MetadataCandidate[] = []; + for (const id of ids) { + const response = await this.client.fetchBook(id, params.signal); + if (!response) continue; + const candidate = mapRanobeDbBook(response.book); + if (candidate) candidates.push(candidate); + } + return candidates; + } + + async lookupById(providerId: string, signal?: AbortSignal): Promise { + const { enabled } = await this.providerConfig.getConfig().then((c) => c.ranobedb); + if (!enabled) return null; + + if (!/^\d+$/.test(providerId) || !Number.isSafeInteger(Number(providerId))) { + this.logger.warn(`[ranobedb.lookup] invalid providerId="${providerId}"`); + return null; + } + + const response = await this.client.fetchBook(Number(providerId), signal); + if (!response) return null; + + return mapRanobeDbBook(response.book); + } +} diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.types.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.types.ts new file mode 100644 index 000000000..300008402 --- /dev/null +++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.types.ts @@ -0,0 +1,113 @@ +export type RoleType = 'author' | 'artist' | 'translator' | 'editor' | 'narrator' | 'staff'; +export type PublisherType = 'publisher' | 'imprint'; +export type TagType = 'genre' | 'demographic' | 'content' | 'tag'; +export type ReleaseFormat = 'digital' | 'print' | 'audio'; + +export interface RanobeDbSearchResponse { + books: { id: number }[]; + count: number; + currentPage: number; + totalPages: number; +} + +export interface RanobeDbBookResponse { + book: RanobeDbBook; +} + +export interface RanobeDbBook { + id: number; + title: string; + title_orig: string | null; + romaji: string | null; + romaji_orig: string | null; + lang: string | null; + olang: string | null; + description: string | null; + description_ja: string | null; + c_release_date: number | null; + c_release_dates: Record | null; + image: RanobeDbImage | null; + rating: RanobeDbRating | null; + titles: RanobeDbTitleEntry[]; + editions: RanobeDbEdition[]; + releases: RanobeDbRelease[]; + publishers: RanobeDbPublisher[]; + series: RanobeDbSeries | null; +} + +export interface RanobeDbTitleEntry { + book_id: number; + lang: string; + official: boolean; + title: string; + romaji: string | null; +} + +export interface RanobeDbEdition { + book_id: number; + lang: string | null; + title: string | null; + eid: number; + staff: RanobeDbStaff[]; +} + +export interface RanobeDbStaff { + role_type: RoleType; + romaji: string | null; + name: string; + staff_id: number; +} + +export interface RanobeDbRelease { + lang: string; + id: number; + title: string | null; + release_date: number | null; + isbn13: string | null; + pages: number | null; + format: ReleaseFormat; +} + +export interface RanobeDbPublisher { + lang: string; + id: number; + romaji: string | null; + name: string; + publisher_type: PublisherType; +} + +export interface RanobeDbSeries { + id: number; + title: string; + romaji: string | null; + books: RanobeDbSeriesBook[]; + tags: RanobeDbTag[]; +} + +export interface RanobeDbSeriesBook { + id: number; + lang: string | null; + title: string | null; + romaji: string | null; + image: RanobeDbImage | null; +} + +export interface RanobeDbTag { + id: number; + name: string; + ttype: TagType; +} + +export interface RanobeDbImage { + id: number; + filename: string; + width: number | null; + height: number | null; + nsfw: boolean; + spoiler: boolean; +} + +export interface RanobeDbRating { + score: number; + count: number; +} diff --git a/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts b/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts index d06b7822a..462d11b73 100644 --- a/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts +++ b/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts @@ -47,4 +47,5 @@ export class UpdateProviderConfigDto { @IsOptional() @ValidateNested() @Type(() => AudibleProviderConfigDto) audible?: AudibleProviderConfigDto; @IsOptional() @ValidateNested() @Type(() => SimpleProviderConfigDto) audnexus?: SimpleProviderConfigDto; @IsOptional() @ValidateNested() @Type(() => ComicVineProviderConfigDto) comicvine?: ComicVineProviderConfigDto; + @IsOptional() @ValidateNested() @Type(() => SimpleProviderConfigDto) ranobedb?: SimpleProviderConfigDto; } diff --git a/server/src/modules/metadata-preferences/provider-config.service.test.ts b/server/src/modules/metadata-preferences/provider-config.service.test.ts index fd435a1b6..35fe93509 100644 --- a/server/src/modules/metadata-preferences/provider-config.service.test.ts +++ b/server/src/modules/metadata-preferences/provider-config.service.test.ts @@ -72,6 +72,7 @@ describe('ProviderConfigService', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }); }); @@ -352,6 +353,7 @@ describe('ProviderConfigService', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }); expect(statuses.map((s) => s.key)).toEqual([ @@ -364,6 +366,7 @@ describe('ProviderConfigService', () => { MetadataProviderKey.AUDIBLE, MetadataProviderKey.AUDNEXUS, MetadataProviderKey.COMICVINE, + MetadataProviderKey.RANOBEDB, ]); expect(statuses.find((s) => s.key === MetadataProviderKey.GOOGLE)?.configured).toBe(false); expect(statuses.find((s) => s.key === MetadataProviderKey.GOOGLE)?.hint).toContain('API key required'); @@ -386,6 +389,7 @@ describe('ProviderConfigService', () => { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }); expect(statuses.find((s) => s.key === MetadataProviderKey.HARDCOVER)?.configured).toBe(true); diff --git a/server/src/modules/metadata-preferences/provider-config.service.ts b/server/src/modules/metadata-preferences/provider-config.service.ts index bcb8b9aba..1d611cf80 100644 --- a/server/src/modules/metadata-preferences/provider-config.service.ts +++ b/server/src/modules/metadata-preferences/provider-config.service.ts @@ -42,6 +42,7 @@ const DEFAULT_CONFIG: ProviderConfigurations = { audible: { enabled: false, domain: 'com' }, audnexus: { enabled: false }, comicvine: { enabled: false, apiKey: '' }, + ranobedb: { enabled: false }, }; function asObject(value: unknown): Record { @@ -124,6 +125,7 @@ const PROVIDER_LABELS: Record = { [MetadataProviderKey.AUDIBLE]: 'Audible', [MetadataProviderKey.AUDNEXUS]: 'AudNexus', [MetadataProviderKey.COMICVINE]: 'ComicVine', + [MetadataProviderKey.RANOBEDB]: 'RanobeDB', }; type ProviderEnableRule = { @@ -167,6 +169,7 @@ export class ProviderConfigService { audible: { ...DEFAULT_CONFIG.audible }, audnexus: { ...DEFAULT_CONFIG.audnexus }, comicvine: { ...DEFAULT_CONFIG.comicvine }, + ranobedb: { ...DEFAULT_CONFIG.ranobedb }, }; } @@ -182,6 +185,7 @@ export class ProviderConfigService { audible: mergeAudibleConfig(base.audible, next.audible), audnexus: mergeSimpleConfig(base.audnexus, next.audnexus), comicvine: mergeComicVineConfig(base.comicvine, next.comicvine), + ranobedb: mergeSimpleConfig(base.ranobedb, next.ranobedb), }; } @@ -383,6 +387,12 @@ export class ProviderConfigService { configured: !!this.getEnableRule('comicvine')?.canEnable(cfg), hint: !this.getEnableRule('comicvine')?.canEnable(cfg) ? this.getEnableRule('comicvine')?.setupHint : undefined, }, + { + key: MetadataProviderKey.RANOBEDB, + label: PROVIDER_LABELS[MetadataProviderKey.RANOBEDB], + enabled: cfg.ranobedb.enabled, + configured: true, + }, ]; }