diff --git a/client/public/assets/provider-icons/aladin.svg b/client/public/assets/provider-icons/aladin.svg
new file mode 100644
index 000000000..d08509c8e
--- /dev/null
+++ b/client/public/assets/provider-icons/aladin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/features/book/components/BookQuickView.vue b/client/src/features/book/components/BookQuickView.vue
index a9a35e6f3..1d2ac2585 100644
--- a/client/src/features/book/components/BookQuickView.vue
+++ b/client/src/features/book/components/BookQuickView.vue
@@ -154,6 +154,15 @@ const providerLinks = computed(() => {
fallback: 'LC',
})
}
+ if (ids.aladin) {
+ out.push({
+ key: 'aladin',
+ label: 'Aladin',
+ url: `https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=${ids.aladin}`,
+ iconUrl: providerIconPath('aladin'),
+ fallback: '알',
+ })
+ }
return out
})
diff --git a/client/src/features/book/components/detail/tabs/DetailsTab.vue b/client/src/features/book/components/detail/tabs/DetailsTab.vue
index 6f9644fb0..c8c4d335f 100644
--- a/client/src/features/book/components/detail/tabs/DetailsTab.vue
+++ b/client/src/features/book/components/detail/tabs/DetailsTab.vue
@@ -612,6 +612,15 @@ const providerLinks = computed(() => {
fallback: 'LC',
})
}
+ if (ids.aladin) {
+ out.push({
+ key: 'aladin',
+ label: 'Aladin',
+ url: `https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=${ids.aladin}`,
+ iconUrl: providerIconPath('aladin'),
+ fallback: '알',
+ })
+ }
return out
})
diff --git a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue
index 7457d51e3..c1bcf98c1 100644
--- a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue
+++ b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue
@@ -75,6 +75,7 @@ const DIRECT_PATCH_FIELDS = [
'comicvineId',
'ranobedbId',
'lubimyczytacId',
+ 'aladinId',
] as const
const COMIC_FIELD_MAP = {
@@ -173,6 +174,7 @@ const providerIdFields = [
{ field: 'comicvineId' as const, label: 'ComicVine' },
{ field: 'ranobedbId' as const, label: 'RanobeDB' },
{ field: 'lubimyczytacId' as const, label: 'LubimyCzytac' },
+ { field: 'aladinId' as const, label: 'Aladin' },
]
function setIntField(field: 'publishedYear' | 'pageCount' | 'durationSeconds', e: Event) {
@@ -463,6 +465,7 @@ function buildPreviewPatch(preview: MetadataRefreshPreview): MetadataPatch {
comicvineId: preview.comicvineId,
ranobedbId: preview.ranobedbId,
lubimyczytacId: preview.lubimyczytacId,
+ aladinId: preview.aladinId,
comicMetadata: preview.comicMetadata,
narrators: preview.audioMetadata?.narrators,
durationSeconds: preview.audioMetadata?.durationSeconds ?? undefined,
diff --git a/client/src/features/book/composables/useFileMetadata.ts b/client/src/features/book/composables/useFileMetadata.ts
index 7965becda..e7898fcd6 100644
--- a/client/src/features/book/composables/useFileMetadata.ts
+++ b/client/src/features/book/composables/useFileMetadata.ts
@@ -25,6 +25,7 @@ export interface FileMetadata {
comicvineId?: string | null
ranobedbId?: string | null
lubimyczytacId?: string | null
+ aladinId?: string | null
authors?: string[]
genres?: string[]
narrators?: string[]
diff --git a/client/src/features/book/composables/useMetadataDiff.ts b/client/src/features/book/composables/useMetadataDiff.ts
index 6bfef889e..53ca8d5d9 100644
--- a/client/src/features/book/composables/useMetadataDiff.ts
+++ b/client/src/features/book/composables/useMetadataDiff.ts
@@ -97,6 +97,7 @@ export interface MetadataPatch {
comicvineId?: string | null
ranobedbId?: string | null
lubimyczytacId?: string | null
+ aladinId?: string | null
comicMetadata?: ComicMetadataFields
}
@@ -160,6 +161,7 @@ export type ProviderIdPatchField =
| 'comicvineId'
| 'ranobedbId'
| 'lubimyczytacId'
+ | 'aladinId'
export const PROVIDER_ID_FIELD: Record = {
google: 'googleBooksId',
@@ -174,6 +176,7 @@ export const PROVIDER_ID_FIELD: Record(Object.values(PROVIDER_ID_FIELD).filter((v): v is ProviderIdPatchField => v !== undefined))
@@ -195,6 +198,7 @@ export const PROVIDER_ID_LABEL: Record = {
ranobedb: 'RanobeDB ID',
kobo: 'Kobo ID',
lubimyczytac: 'LubimyCzytac ID',
+ aladin: 'Aladin 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 5a185fc97..6382e836d 100644
--- a/client/src/features/book/composables/useMetadataEditor.ts
+++ b/client/src/features/book/composables/useMetadataEditor.ts
@@ -32,6 +32,7 @@ const ROOT_FIELDS = [
'comicvineId',
'ranobedbId',
'lubimyczytacId',
+ 'aladinId',
] as const
const COMIC_FIELDS = {
@@ -115,6 +116,7 @@ export function useMetadataEditor() {
comicvineId: null as string | null,
ranobedbId: null as string | null,
lubimyczytacId: null as string | null,
+ aladinId: null as string | null,
comicIssueNumber: null as string | null,
comicVolumeName: null as string | null,
comicStoryArcs: [] as string[],
@@ -164,6 +166,7 @@ export function useMetadataEditor() {
form.comicvineId = book.providerIds.comicvine ?? null
form.ranobedbId = book.providerIds.ranobedb ?? null
form.lubimyczytacId = book.providerIds.lubimyczytac ?? null
+ form.aladinId = book.providerIds.aladin ?? null
const cm = book.comicMetadata
form.comicIssueNumber = cm?.issueNumber ?? null
form.comicVolumeName = cm?.volumeName ?? null
diff --git a/client/src/features/book/composables/useMetadataSearch.ts b/client/src/features/book/composables/useMetadataSearch.ts
index a098a426a..f4e864372 100644
--- a/client/src/features/book/composables/useMetadataSearch.ts
+++ b/client/src/features/book/composables/useMetadataSearch.ts
@@ -96,6 +96,7 @@ export function useMetadataSearch() {
'itunes',
'kobo',
'openLibrary',
+ 'aladin',
]
function sortResults(list: MetadataCandidate[]): MetadataCandidate[] {
diff --git a/client/src/features/book/lib/file-metadata-patch.ts b/client/src/features/book/lib/file-metadata-patch.ts
index c1e22e1c2..27335ab4f 100644
--- a/client/src/features/book/lib/file-metadata-patch.ts
+++ b/client/src/features/book/lib/file-metadata-patch.ts
@@ -24,6 +24,7 @@ const FILE_METADATA_PATCH_FIELDS = [
'comicvineId',
'ranobedbId',
'lubimyczytacId',
+ 'aladinId',
'authors',
'genres',
'narrators',
diff --git a/client/src/features/book/lib/metadata-refresh-feedback.ts b/client/src/features/book/lib/metadata-refresh-feedback.ts
index a6fba64a5..ca867e944 100644
--- a/client/src/features/book/lib/metadata-refresh-feedback.ts
+++ b/client/src/features/book/lib/metadata-refresh-feedback.ts
@@ -18,6 +18,7 @@ const PROVIDER_LABELS: Record = {
[MetadataProviderKey.RANOBEDB]: 'RanobeDB',
[MetadataProviderKey.KOBO]: 'Kobo',
[MetadataProviderKey.LUBIMYCZYTAC]: 'LubimyCzytac',
+ [MetadataProviderKey.ALADIN]: 'Aladin',
}
export function metadataRefreshEmptyMessage(diagnostics: MetadataFetchDiagnostics, book: BookDetail): string {
diff --git a/client/src/features/book/lib/provider-icons.ts b/client/src/features/book/lib/provider-icons.ts
index a26577ebc..29a019cf0 100644
--- a/client/src/features/book/lib/provider-icons.ts
+++ b/client/src/features/book/lib/provider-icons.ts
@@ -11,6 +11,7 @@ const PROVIDER_ICON_PATHS = {
kobo: `${PROVIDER_ICON_BASE_PATH}/kobo.svg`,
ranobedb: `${PROVIDER_ICON_BASE_PATH}/ranobedb.svg`,
lubimyczytac: `${PROVIDER_ICON_BASE_PATH}/lubimyczytac.svg`,
+ aladin: `${PROVIDER_ICON_BASE_PATH}/aladin.svg`,
} as const
type ProviderIconKey = keyof typeof PROVIDER_ICON_PATHS
diff --git a/client/src/features/metadata-score/components/MetadataScoreBreakdown.vue b/client/src/features/metadata-score/components/MetadataScoreBreakdown.vue
index 02b6f61f3..28a6baf64 100644
--- a/client/src/features/metadata-score/components/MetadataScoreBreakdown.vue
+++ b/client/src/features/metadata-score/components/MetadataScoreBreakdown.vue
@@ -64,6 +64,8 @@ function isFieldFilled(field: MetadataScoreField): boolean {
return !!b.providerIds.openLibrary?.trim()
case 'itunesId':
return !!b.providerIds.itunes?.trim()
+ case 'aladinId':
+ return !!b.providerIds.aladin?.trim()
default:
return false
}
diff --git a/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue b/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue
index 163d32ceb..b6c4e638a 100644
--- a/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue
+++ b/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue
@@ -204,9 +204,19 @@ const rows: RowDef[] = [
hint: 'Polish book catalog (lubimyczytac.pl). Scrapes public book pages. No setup required.',
fields: [],
},
+ {
+ key: 'aladin',
+ label: 'Aladin',
+ hint: 'Korean book metadata from Aladin (알라딘). A TTB Key from aladin.co.kr is required.',
+ fields: [{ key: 'ttbKey', label: 'TTB Key', type: 'password', placeholder: 'ttb...', alwaysEditable: true }],
+ enableRequirement: {
+ isConfigured: (c) => !!c.aladin.ttbKey.trim(),
+ blockedMessage: 'Aladin requires a TTB Key before it can be enabled',
+ },
+ },
]
-const TESTABLE_PROVIDERS: MetadataProviderKey[] = [MetadataProviderKey.AMAZON, MetadataProviderKey.HARDCOVER]
+const TESTABLE_PROVIDERS: MetadataProviderKey[] = [MetadataProviderKey.AMAZON, MetadataProviderKey.HARDCOVER, MetadataProviderKey.ALADIN]
function statusFor(key: string) {
return props.statuses.find((s) => s.key === key)
diff --git a/client/src/lib/__tests__/provider-colors.spec.ts b/client/src/lib/__tests__/provider-colors.spec.ts
index 818dbab48..ae92d37e2 100644
--- a/client/src/lib/__tests__/provider-colors.spec.ts
+++ b/client/src/lib/__tests__/provider-colors.spec.ts
@@ -32,6 +32,7 @@ describe('getProviderColor', () => {
'comicvine',
'ranobedb',
'kobo',
+ 'aladin',
'auto',
]
for (const provider of providers) {
@@ -46,7 +47,20 @@ describe('PROVIDER_SHORT_LABELS', () => {
})
it('has labels for all providers', () => {
- const providers = ['google', 'amazon', 'goodreads', 'hardcover', 'openLibrary', 'itunes', 'audible', 'audnexus', 'comicvine', 'ranobedb', 'kobo']
+ const providers = [
+ 'google',
+ 'amazon',
+ 'goodreads',
+ 'hardcover',
+ 'openLibrary',
+ 'itunes',
+ 'audible',
+ 'audnexus',
+ 'comicvine',
+ 'ranobedb',
+ 'kobo',
+ 'aladin',
+ ]
for (const provider of providers) {
expect(PROVIDER_SHORT_LABELS[provider]).toBeTruthy()
}
diff --git a/client/src/lib/provider-colors.ts b/client/src/lib/provider-colors.ts
index d63316417..a611f6de6 100644
--- a/client/src/lib/provider-colors.ts
+++ b/client/src/lib/provider-colors.ts
@@ -13,6 +13,7 @@ const PROVIDER_HEX: Record = {
ranobedb: '#a78cff',
kobo: '#e23434',
lubimyczytac: '#f47373',
+ aladin: '#3E7FFF',
auto: '#8B5CF6',
}
@@ -31,6 +32,7 @@ export const PROVIDER_SHORT_LABELS: Record = {
ranobedb: 'RanobeDB',
kobo: 'Kobo',
lubimyczytac: 'LubimyCzytac',
+ aladin: 'Aladin',
auto: 'Fetched',
}
diff --git a/package.json b/package.json
index a875327c8..c7506de83 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,8 @@
"@esbuild-kit/core-utils>esbuild": "^0.25.12",
"fastify": "^5.8.5",
"fast-uri": "^3.1.1",
- "fast-xml-builder": "^1.1.7"
+ "fast-xml-builder": "^1.1.7",
+ "ws": "^8.21.0"
},
"onlyBuiltDependencies": [
"@nestjs/core",
diff --git a/packages/types/src/book.ts b/packages/types/src/book.ts
index 4bd6d83b0..a132010d8 100644
--- a/packages/types/src/book.ts
+++ b/packages/types/src/book.ts
@@ -177,6 +177,7 @@ export type BookMetadataRefreshPreviewFields = {
comicvineId?: string | null;
ranobedbId?: string | null;
lubimyczytacId?: string | null;
+ aladinId?: string | null;
audioMetadata?: {
narrators?: string[];
durationSeconds?: number | null;
diff --git a/packages/types/src/file-write.ts b/packages/types/src/file-write.ts
index df26895f9..37946bbea 100644
--- a/packages/types/src/file-write.ts
+++ b/packages/types/src/file-write.ts
@@ -25,6 +25,7 @@ export const COMMON_PROVIDER_BOOK_FILE_WRITE_FIELDS = [
"ranobedbId",
"koboId",
"lubimyczytacId",
+ "aladinId",
] as const;
export const COMIC_BOOK_FILE_WRITE_FIELDS = [
@@ -77,6 +78,7 @@ export const BOOK_FILE_WRITE_FIELD_LABELS = {
ranobedbId: "RanobeDB ID",
koboId: "Kobo ID",
lubimyczytacId: "LubimyCzytac ID",
+ aladinId: "Aladin ID",
comicIssueNumber: "Issue number",
comicVolumeName: "Volume",
comicPencillers: "Pencillers",
diff --git a/packages/types/src/metadata-fetch.ts b/packages/types/src/metadata-fetch.ts
index 35c1386a2..6d0ef5a45 100644
--- a/packages/types/src/metadata-fetch.ts
+++ b/packages/types/src/metadata-fetch.ts
@@ -13,6 +13,7 @@ export const MetadataProviderKey = {
RANOBEDB: "ranobedb",
KOBO: "kobo",
LUBIMYCZYTAC: "lubimyczytac",
+ ALADIN: "aladin",
} as const;
export interface ComicMetadataFields {
diff --git a/packages/types/src/metadata-lock.ts b/packages/types/src/metadata-lock.ts
index 5b421ae54..f0bdc9741 100644
--- a/packages/types/src/metadata-lock.ts
+++ b/packages/types/src/metadata-lock.ts
@@ -28,6 +28,7 @@ export const BOOK_METADATA_LOCK_FIELDS = [
"comicvineId",
"ranobedbId",
"lubimyczytacId",
+ "aladinId",
"comicIssueNumber",
"comicVolumeName",
"comicStoryArcs",
@@ -88,6 +89,7 @@ export const BOOK_METADATA_LOCK_GROUPS: BookMetadataLockGroup[] = [
"comicvineId",
"ranobedbId",
"lubimyczytacId",
+ "aladinId",
],
},
{
diff --git a/packages/types/src/metadata-preferences.ts b/packages/types/src/metadata-preferences.ts
index 2181010f9..276a8b5a9 100644
--- a/packages/types/src/metadata-preferences.ts
+++ b/packages/types/src/metadata-preferences.ts
@@ -115,6 +115,11 @@ export interface KoboProviderConfig {
language: string;
}
+export interface AladinProviderConfig {
+ enabled: boolean;
+ ttbKey: string;
+}
+
export interface ProviderConfigurations {
google: GoogleProviderConfig;
amazon: AmazonProviderConfig;
@@ -128,6 +133,7 @@ export interface ProviderConfigurations {
ranobedb: SimpleProviderConfig;
kobo: KoboProviderConfig;
lubimyczytac: SimpleProviderConfig;
+ aladin: AladinProviderConfig;
}
export interface ProviderStatus {
diff --git a/packages/types/src/metadata-score.ts b/packages/types/src/metadata-score.ts
index 93249995c..09a6f84c8 100644
--- a/packages/types/src/metadata-score.ts
+++ b/packages/types/src/metadata-score.ts
@@ -21,7 +21,8 @@ export type MetadataScoreField =
| "hardcoverId"
| "openLibraryId"
| "itunesId"
- | "koboId";
+ | "koboId"
+ | "aladinId";
export type MetadataScoreGroup = "core" | "publishing" | "classification" | "enrichment" | "providers";
@@ -55,6 +56,7 @@ export const METADATA_SCORE_FIELDS: Record = {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1a1b096eb..1f0ff58c2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,6 +9,7 @@ overrides:
fastify: ^5.8.5
fast-uri: ^3.1.1
fast-xml-builder: ^1.1.7
+ ws: ^8.21.0
importers:
@@ -9204,8 +9205,8 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
- ws@8.18.3:
- resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
+ ws@8.21.0:
+ resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -14438,7 +14439,7 @@ snapshots:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
engine.io-parser: 5.2.3
- ws: 8.18.3
+ ws: 8.21.0
xmlhttprequest-ssl: 2.1.2
transitivePeerDependencies:
- bufferutil
@@ -14458,7 +14459,7 @@ snapshots:
cors: 2.8.6
debug: 4.4.3
engine.io-parser: 5.2.3
- ws: 8.18.3
+ ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -17139,7 +17140,7 @@ snapshots:
socket.io-adapter@2.5.6:
dependencies:
debug: 4.4.3
- ws: 8.18.3
+ ws: 8.21.0
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -18339,7 +18340,7 @@ snapshots:
wrappy@1.0.2: {}
- ws@8.18.3: {}
+ ws@8.21.0: {}
wsl-utils@0.1.0:
dependencies:
diff --git a/server/src/db/migrations/0022_add_aladin_id.sql b/server/src/db/migrations/0022_add_aladin_id.sql
new file mode 100644
index 000000000..2d5e61766
--- /dev/null
+++ b/server/src/db/migrations/0022_add_aladin_id.sql
@@ -0,0 +1 @@
+ALTER TABLE "book_metadata" ADD COLUMN "aladin_id" varchar(20);
\ No newline at end of file
diff --git a/server/src/db/migrations/meta/0022_snapshot.json b/server/src/db/migrations/meta/0022_snapshot.json
new file mode 100644
index 000000000..9c36fc150
--- /dev/null
+++ b/server/src/db/migrations/meta/0022_snapshot.json
@@ -0,0 +1,10935 @@
+{
+ "id": "3351c184-8a15-4777-9aaa-1ca260fc20b0",
+ "prevId": "36797d38-e911-42b4-9a27-9e0fd5746e8e",
+ "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_write_audio_enabled": {
+ "name": "file_write_audio_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_audio_max_file_size_mb": {
+ "name": "file_write_audio_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 500
+ },
+ "file_rename_enabled": {
+ "name": "file_rename_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_naming_pattern": {
+ "name": "file_naming_pattern",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_fetch_preferences": {
+ "name": "metadata_fetch_preferences",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_config": {
+ "name": "book_metadata_fetch_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_last_run_at": {
+ "name": "book_metadata_fetch_last_run_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_last_queued_count": {
+ "name": "book_metadata_fetch_last_queued_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scan_mode": {
+ "name": "scan_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'auto'"
+ },
+ "poll_interval_seconds": {
+ "name": "poll_interval_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 300
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "libraries_name_lower_uidx": {
+ "name": "libraries_name_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "libraries_display_order_nonnegative_chk": {
+ "name": "libraries_display_order_nonnegative_chk",
+ "value": "\"libraries\".\"display_order\" >= 0"
+ },
+ "libraries_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"
+ },
+ "libraries_file_write_audio_max_size_chk": {
+ "name": "libraries_file_write_audio_max_size_chk",
+ "value": "\"libraries\".\"file_write_audio_max_file_size_mb\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.library_folders": {
+ "name": "library_folders",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "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
+ },
+ "primary_author_sort_name": {
+ "name": "primary_author_sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_path": {
+ "name": "folder_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'present'"
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "books_library_id_folder_path_idx": {
+ "name": "books_library_id_folder_path_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "folder_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_primary_file_id_idx": {
+ "name": "books_primary_file_id_idx",
+ "columns": [
+ {
+ "expression": "primary_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_status_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": {}
+ },
+ "books_library_visible_added_id_idx": {
+ "name": "books_library_visible_added_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"added_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_author_sort_id_idx": {
+ "name": "books_library_author_sort_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"primary_author_sort_name\" asc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_author_sort_desc_id_idx": {
+ "name": "books_library_author_sort_desc_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"primary_author_sort_name\" desc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "books_library_id_libraries_id_fk": {
+ "name": "books_library_id_libraries_id_fk",
+ "tableFrom": "books",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "books_library_folder_id_library_folders_id_fk": {
+ "name": "books_library_folder_id_library_folders_id_fk",
+ "tableFrom": "books",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "books_primary_file_id_book_files_id_fk": {
+ "name": "books_primary_file_id_book_files_id_fk",
+ "tableFrom": "books",
+ "tableTo": "book_files",
+ "columnsFrom": ["primary_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "books_library_folder_library_fk": {
+ "name": "books_library_folder_library_fk",
+ "tableFrom": "books",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id", "library_id"],
+ "columnsTo": ["id", "library_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "books_id_library_folder_id_unique": {
+ "name": "books_id_library_folder_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "library_folder_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "books_status_chk": {
+ "name": "books_status_chk",
+ "value": "\"books\".\"status\" in ('present', 'missing', 'processing')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.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.book_series": {
+ "name": "book_series",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_name": {
+ "name": "normalized_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_series_normalized_name_uidx": {
+ "name": "book_series_normalized_name_uidx",
+ "columns": [
+ {
+ "expression": "normalized_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_name_trgm_idx": {
+ "name": "book_series_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "book_series_name_lower_idx": {
+ "name": "book_series_name_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_series_memberships": {
+ "name": "book_series_memberships",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "series_id": {
+ "name": "series_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "series_index": {
+ "name": "series_index",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_series_memberships_book_display_uidx": {
+ "name": "book_series_memberships_book_display_uidx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_memberships_series_index_book_idx": {
+ "name": "book_series_memberships_series_index_book_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "series_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_memberships_book_display_idx": {
+ "name": "book_series_memberships_book_display_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_series_memberships_book_id_books_id_fk": {
+ "name": "book_series_memberships_book_id_books_id_fk",
+ "tableFrom": "book_series_memberships",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_series_memberships_series_id_book_series_id_fk": {
+ "name": "book_series_memberships_series_id_book_series_id_fk",
+ "tableFrom": "book_series_memberships",
+ "tableTo": "book_series",
+ "columnsFrom": ["series_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_series_memberships_book_id_series_id_pk": {
+ "name": "book_series_memberships_book_id_series_id_pk",
+ "columns": ["book_id", "series_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_series_memberships_display_order_nonnegative_chk": {
+ "name": "book_series_memberships_display_order_nonnegative_chk",
+ "value": "\"book_series_memberships\".\"display_order\" >= 0"
+ }
+ },
+ "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": {}
+ },
+ "book_authors_book_display_author_idx": {
+ "name": "book_authors_book_display_author_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_authors_book_id_books_id_fk": {
+ "name": "book_authors_book_id_books_id_fk",
+ "tableFrom": "book_authors",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_authors_author_id_authors_id_fk": {
+ "name": "book_authors_author_id_authors_id_fk",
+ "tableFrom": "book_authors",
+ "tableTo": "authors",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_authors_book_id_author_id_pk": {
+ "name": "book_authors_book_id_author_id_pk",
+ "columns": ["book_id", "author_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_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_id": {
+ "name": "series_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_name": {
+ "name": "series_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_index": {
+ "name": "series_index",
+ "type": "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
+ },
+ "kobo_id": {
+ "name": "kobo_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_score": {
+ "name": "metadata_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_metadata_fetch_at": {
+ "name": "last_metadata_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(256)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_written_at": {
+ "name": "last_written_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "abridged": {
+ "name": "abridged",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "audible_id": {
+ "name": "audible_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comicvine_id": {
+ "name": "comicvine_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ranobedb_id": {
+ "name": "ranobedb_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lubimyczytac_id": {
+ "name": "lubimyczytac_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aladin_id": {
+ "name": "aladin_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapters": {
+ "name": "chapters",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_fields": {
+ "name": "locked_fields",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "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_title_book_id_idx": {
+ "name": "bm_title_book_id_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_trgm_idx": {
+ "name": "bm_series_trgm_idx",
+ "columns": [
+ {
+ "expression": "series_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_series_id_idx": {
+ "name": "bm_series_id_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_id_index_book_id_idx": {
+ "name": "bm_series_id_index_book_id_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "series_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_name_lower_btrim_idx": {
+ "name": "bm_series_name_lower_btrim_idx",
+ "columns": [
+ {
+ "expression": "lower(btrim(\"series_name\"))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_publisher_trgm_idx": {
+ "name": "bm_publisher_trgm_idx",
+ "columns": [
+ {
+ "expression": "publisher",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_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"
+ },
+ "book_metadata_series_id_book_series_id_fk": {
+ "name": "book_metadata_series_id_book_series_id_fk",
+ "tableFrom": "book_metadata",
+ "tableTo": "book_series",
+ "columnsFrom": ["series_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_metadata_rating_range_chk": {
+ "name": "book_metadata_rating_range_chk",
+ "value": "\"book_metadata\".\"rating\" is null or (\"book_metadata\".\"rating\" >= 1 and \"book_metadata\".\"rating\" <= 10)"
+ },
+ "book_metadata_published_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
+ },
+ "kobo_location_source": {
+ "name": "kobo_location_source",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_type": {
+ "name": "kobo_location_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_value": {
+ "name": "kobo_location_value",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_content_source_progress_percent": {
+ "name": "kobo_content_source_progress_percent",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "koreader_progress": {
+ "name": "koreader_progress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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"
+ },
+ "reading_progress_kobo_content_source_progress_range_chk": {
+ "name": "reading_progress_kobo_content_source_progress_range_chk",
+ "value": "\"reading_progress\".\"kobo_content_source_progress_percent\" is null or (\"reading_progress\".\"kobo_content_source_progress_percent\" >= 0 and \"reading_progress\".\"kobo_content_source_progress_percent\" <= 100)"
+ }
+ },
+ "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
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'web'"
+ }
+ },
+ "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_source_chk": {
+ "name": "reading_sessions_source_chk",
+ "value": "\"reading_sessions\".\"source\" in ('web', 'kobo', 'koreader')"
+ },
+ "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_book_entitlements": {
+ "name": "kobo_book_entitlements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entitlement_id": {
+ "name": "entitlement_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cover_image_id": {
+ "name": "cover_image_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "needs_legacy_numeric_removal": {
+ "name": "needs_legacy_numeric_removal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_book_entitlements_user_id_idx": {
+ "name": "kobo_book_entitlements_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_book_entitlements_book_id_idx": {
+ "name": "kobo_book_entitlements_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_book_entitlements_user_id_users_id_fk": {
+ "name": "kobo_book_entitlements_user_id_users_id_fk",
+ "tableFrom": "kobo_book_entitlements",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_book_entitlements_book_id_books_id_fk": {
+ "name": "kobo_book_entitlements_book_id_books_id_fk",
+ "tableFrom": "kobo_book_entitlements",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_book_entitlements_user_book_unique": {
+ "name": "kobo_book_entitlements_user_book_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ },
+ "kobo_book_entitlements_user_entitlement_unique": {
+ "name": "kobo_book_entitlements_user_entitlement_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "entitlement_id"]
+ },
+ "kobo_book_entitlements_user_cover_unique": {
+ "name": "kobo_book_entitlements_user_cover_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "cover_image_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_devices": {
+ "name": "kobo_devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "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
+ },
+ "delivery_hash": {
+ "name": "delivery_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_hash": {
+ "name": "metadata_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kobo_snapshot_books_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": true
+ },
+ "force_enable_hyphenation": {
+ "name": "force_enable_hyphenation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "kepub_conversion_limit_mb": {
+ "name": "kepub_conversion_limit_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "two_way_progress_sync": {
+ "name": "two_way_progress_sync",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "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', 'devices')"
+ },
+ "achievements_rarity_chk": {
+ "name": "achievements_rarity_chk",
+ "value": "\"achievements\".\"rarity\" in ('common', 'rare', 'epic', 'legendary')"
+ },
+ "achievements_tier_chk": {
+ "name": "achievements_tier_chk",
+ "value": "\"achievements\".\"tier\" is null or (\"achievements\".\"tier\" >= 1 and \"achievements\".\"tier\" <= 4)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_achievements": {
+ "name": "user_achievements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "achievement_key": {
+ "name": "achievement_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "awarded_at": {
+ "name": "awarded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "context_json": {
+ "name": "context_json",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "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(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_error": {
+ "name": "match_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_status": {
+ "name": "last_synced_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_progress": {
+ "name": "last_synced_progress",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_rating": {
+ "name": "last_synced_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_started_at": {
+ "name": "last_synced_started_at",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_finished_at": {
+ "name": "last_synced_finished_at",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_error": {
+ "name": "sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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 aa7554360..8e5fa076e 100644
--- a/server/src/db/migrations/meta/_journal.json
+++ b/server/src/db/migrations/meta/_journal.json
@@ -155,6 +155,13 @@
"when": 1781675709588,
"tag": "0021_add_book_series_memberships",
"breakpoints": true
+ },
+ {
+ "idx": 22,
+ "version": "7",
+ "when": 1781716753204,
+ "tag": "0022_add_aladin_id",
+ "breakpoints": true
}
]
}
diff --git a/server/src/db/schema/metadata.ts b/server/src/db/schema/metadata.ts
index 870968afe..86ffe6e3e 100644
--- a/server/src/db/schema/metadata.ts
+++ b/server/src/db/schema/metadata.ts
@@ -69,6 +69,7 @@ export const bookMetadata = pgTable(
comicvineId: varchar('comicvine_id', { length: 50 }),
ranobedbId: varchar('ranobedb_id', { length: 50 }),
lubimyczytacId: text('lubimyczytac_id'),
+ aladinId: varchar('aladin_id', { length: 20 }),
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 0724453c3..fc85ecbda 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
@@ -292,6 +292,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
if (filteredProviderIds[MetadataProviderKey.COMICVINE]) scalarFields.comicvineId = filteredProviderIds[MetadataProviderKey.COMICVINE];
if (filteredProviderIds[MetadataProviderKey.RANOBEDB]) scalarFields.ranobedbId = filteredProviderIds[MetadataProviderKey.RANOBEDB];
if (filteredProviderIds[MetadataProviderKey.LUBIMYCZYTAC]) scalarFields.lubimyczytacId = filteredProviderIds[MetadataProviderKey.LUBIMYCZYTAC];
+ if (filteredProviderIds[MetadataProviderKey.ALADIN]) scalarFields.aladinId = filteredProviderIds[MetadataProviderKey.ALADIN];
const duration = this.asNullableNumber(filteredResolved.duration);
if (duration !== undefined) scalarFields.durationSeconds = duration;
@@ -382,6 +383,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
comicvineId?: string | null;
ranobedbId?: string | null;
lubimyczytacId?: string | null;
+ aladinId?: string | null;
}): Partial> {
const ids: Partial> = {};
if (meta.googleBooksId) ids[MetadataProviderKey.GOOGLE] = meta.googleBooksId;
@@ -395,6 +397,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
if (meta.comicvineId) ids[MetadataProviderKey.COMICVINE] = meta.comicvineId;
if (meta.ranobedbId) ids[MetadataProviderKey.RANOBEDB] = meta.ranobedbId;
if (meta.lubimyczytacId) ids[MetadataProviderKey.LUBIMYCZYTAC] = meta.lubimyczytacId;
+ if (meta.aladinId) ids[MetadataProviderKey.ALADIN] = meta.aladinId;
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 ed66966a7..a54612f19 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
@@ -20,6 +20,7 @@ const PROVIDER_ID_LOCK_FIELDS_BY_PROVIDER: Partial = {
@@ -248,6 +249,7 @@ export class BookMetadataLockService {
this.copyUpdateField(filteredDto, dto, 'comicvineId', 'comicvineId', lockedSet, skippedFields);
this.copyUpdateField(filteredDto, dto, 'ranobedbId', 'ranobedbId', lockedSet, skippedFields);
this.copyUpdateField(filteredDto, dto, 'lubimyczytacId', 'lubimyczytacId', lockedSet, skippedFields);
+ this.copyUpdateField(filteredDto, dto, 'aladinId', 'aladinId', lockedSet, skippedFields);
if (dto.audioMetadata) {
const filteredAudioMetadata: NonNullable = {};
@@ -378,6 +380,7 @@ export class BookMetadataLockService {
this.addFieldIfPresent(fields, dto, 'comicvineId', 'comicvineId');
this.addFieldIfPresent(fields, dto, 'ranobedbId', 'ranobedbId');
this.addFieldIfPresent(fields, dto, 'lubimyczytacId', 'lubimyczytacId');
+ this.addFieldIfPresent(fields, dto, 'aladinId', 'aladinId');
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 f95a723cb..22fd4e2c4 100644
--- a/server/src/modules/book/book.service.ts
+++ b/server/src/modules/book/book.service.ts
@@ -278,6 +278,7 @@ export class BookService {
comicvineId?: string | null;
ranobedbId?: string | null;
lubimyczytacId?: string | null;
+ aladinId?: string | null;
}): Partial> {
const providerIds: Partial> = {};
if (meta.googleBooksId) providerIds[MetadataProviderKey.GOOGLE] = meta.googleBooksId;
@@ -291,6 +292,7 @@ export class BookService {
if (meta.comicvineId) providerIds[MetadataProviderKey.COMICVINE] = meta.comicvineId;
if (meta.ranobedbId) providerIds[MetadataProviderKey.RANOBEDB] = meta.ranobedbId;
if (meta.lubimyczytacId) providerIds[MetadataProviderKey.LUBIMYCZYTAC] = meta.lubimyczytacId;
+ if (meta.aladinId) providerIds[MetadataProviderKey.ALADIN] = meta.aladinId;
return providerIds;
}
@@ -308,6 +310,7 @@ export class BookService {
| 'comicvineId'
| 'ranobedbId'
| 'lubimyczytacId'
+ | 'aladinId'
>,
providerIds: Partial>,
): void {
@@ -322,6 +325,7 @@ export class BookService {
if (providerIds[MetadataProviderKey.COMICVINE]) dto.comicvineId = providerIds[MetadataProviderKey.COMICVINE];
if (providerIds[MetadataProviderKey.RANOBEDB]) dto.ranobedbId = providerIds[MetadataProviderKey.RANOBEDB];
if (providerIds[MetadataProviderKey.LUBIMYCZYTAC]) dto.lubimyczytacId = providerIds[MetadataProviderKey.LUBIMYCZYTAC];
+ if (providerIds[MetadataProviderKey.ALADIN]) dto.aladinId = providerIds[MetadataProviderKey.ALADIN];
}
private buildMetadataRefreshPreview(
@@ -1447,6 +1451,7 @@ export class BookService {
if (dto.comicvineId !== undefined) scalarFields.comicvineId = dto.comicvineId ?? null;
if (dto.ranobedbId !== undefined) scalarFields.ranobedbId = dto.ranobedbId ?? null;
if (dto.lubimyczytacId !== undefined) scalarFields.lubimyczytacId = dto.lubimyczytacId ?? null;
+ if (dto.aladinId !== undefined) scalarFields.aladinId = dto.aladinId ?? null;
if (dto.rating !== undefined) scalarFields.rating = dto.rating ?? null;
if (dto.audioMetadata) {
if (dto.audioMetadata.durationSeconds !== undefined) scalarFields.durationSeconds = dto.audioMetadata.durationSeconds ?? null;
@@ -2689,6 +2694,7 @@ export class BookService {
[MetadataProviderKey.COMICVINE]: meta?.comicvineId ?? null,
[MetadataProviderKey.RANOBEDB]: meta?.ranobedbId ?? null,
[MetadataProviderKey.LUBIMYCZYTAC]: meta?.lubimyczytacId ?? null,
+ [MetadataProviderKey.ALADIN]: meta?.aladinId ?? null,
},
authors: authorRows,
genres: genreRows.map((g) => g.name),
@@ -2797,6 +2803,7 @@ export class BookService {
koboId: parsed.koboId,
ranobedbId: parsed.ranobedbId,
lubimyczytacId: parsed.lubimyczytacId,
+ aladinId: parsed.aladinId,
authors: parsed.authors.length > 0 ? parsed.authors.map((a) => a.name) : undefined,
genres: parsed.genres.length > 0 ? parsed.genres : undefined,
};
@@ -2828,6 +2835,7 @@ export class BookService {
koboId: parsed.koboId,
ranobedbId: parsed.ranobedbId,
lubimyczytacId: parsed.lubimyczytacId,
+ aladinId: parsed.aladinId,
authors: parsed.authors.length > 0 ? parsed.authors.map((a) => a.name) : undefined,
genres: parsed.genres.length > 0 ? parsed.genres : undefined,
};
@@ -2877,6 +2885,7 @@ export class BookService {
koboId: parsed.koboId,
ranobedbId: parsed.ranobedbId,
lubimyczytacId: parsed.lubimyczytacId,
+ aladinId: parsed.aladinId,
authors: parsed.authors.length > 0 ? parsed.authors.map((a) => a.name) : undefined,
genres: cbzGenres.length > 0 ? cbzGenres : undefined,
comicMetadata: parsed.comicMetadata ?? undefined,
diff --git a/server/src/modules/book/dto/book-dto-validation.test.ts b/server/src/modules/book/dto/book-dto-validation.test.ts
index 55e0ce959..30505b841 100644
--- a/server/src/modules/book/dto/book-dto-validation.test.ts
+++ b/server/src/modules/book/dto/book-dto-validation.test.ts
@@ -174,6 +174,12 @@ describe('Book DTO validation', () => {
expect((await errorsFor(UpdateBookMetadataDto, { isbn10: '12345678901' })).length).toBeGreaterThan(0);
});
+ it('accepts aladinId and enforces its length bound', async () => {
+ expect((await errorsFor(UpdateBookMetadataDto, { aladinId: '12345' })).length).toBe(0);
+ expect((await errorsFor(UpdateBookMetadataDto, { aladinId: null })).length).toBe(0);
+ expect((await errorsFor(UpdateBookMetadataDto, { aladinId: 'a'.repeat(21) })).length).toBeGreaterThan(0);
+ });
+
it('normalizes a zero page count to null and validates other page count values', async () => {
// A provider can return a page count of 0 (issue #329); it must normalize to null instead of failing validation.
const zeroed = plainToInstance(UpdateBookMetadataDto, { pageCount: 0 });
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 d20e0d654..1aab206de 100644
--- a/server/src/modules/book/dto/update-book-metadata.dto.ts
+++ b/server/src/modules/book/dto/update-book-metadata.dto.ts
@@ -63,5 +63,6 @@ export class UpdateBookMetadataDto {
@IsOptional() @IsString() @MaxLength(50) comicvineId?: string | null;
@IsOptional() @IsString() @MaxLength(50) ranobedbId?: string | null;
@IsOptional() @IsString() @MaxLength(512) lubimyczytacId?: string | null;
+ @IsOptional() @IsString() @MaxLength(20) aladinId?: string | null;
@IsOptional() @ValidateNested() @Type(() => ComicMetadataDto) comicMetadata?: ComicMetadataDto;
}
diff --git a/server/src/modules/file-write/file-write.constants.ts b/server/src/modules/file-write/file-write.constants.ts
index 3c5bcb934..6e693be05 100644
--- a/server/src/modules/file-write/file-write.constants.ts
+++ b/server/src/modules/file-write/file-write.constants.ts
@@ -39,6 +39,7 @@ export const COMIC_INFO_MANAGED_NOTES_KEYS = [
'ranobedbId',
'koboId',
'lubimyczytacId',
+ 'aladinId',
] as const satisfies readonly BookWritePayloadKey[];
type ComicInfoProviderKey = (typeof COMIC_INFO_PROVIDER_ID_KEYS)[number];
@@ -61,5 +62,6 @@ export const EPUB_PROVIDER_IDENTIFIER_SCHEMES = {
ranobedbId: 'RANOBEDB',
koboId: 'KOBO',
lubimyczytacId: 'LUBIMYCZYTAC',
+ aladinId: 'ALADIN',
itunesId: 'ITUNES',
} as const satisfies Partial>;
diff --git a/server/src/modules/file-write/file-write.repository.ts b/server/src/modules/file-write/file-write.repository.ts
index 281b4a76f..33b150955 100644
--- a/server/src/modules/file-write/file-write.repository.ts
+++ b/server/src/modules/file-write/file-write.repository.ts
@@ -186,6 +186,7 @@ export class FileWriteRepository {
ranobedbId: meta.ranobedbId,
koboId: meta.koboId,
lubimyczytacId: meta.lubimyczytacId,
+ aladinId: meta.aladinId,
itunesId: meta.itunesId,
audibleId: meta.audibleId,
rating: meta.rating,
diff --git a/server/src/modules/file-write/interfaces/book-write-payload.interface.ts b/server/src/modules/file-write/interfaces/book-write-payload.interface.ts
index b0f3c01c0..837cd6111 100644
--- a/server/src/modules/file-write/interfaces/book-write-payload.interface.ts
+++ b/server/src/modules/file-write/interfaces/book-write-payload.interface.ts
@@ -24,6 +24,7 @@ export interface BookWritePayload {
ranobedbId?: string | null;
koboId?: string | null;
lubimyczytacId?: string | null;
+ aladinId?: string | null;
comicIssueNumber?: string | null;
comicVolumeName?: string | null;
comicPencillers?: string[];
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 116e69dce..1002bc42d 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts
@@ -20,6 +20,7 @@ import { ComicVineProvider } from './providers/comicvine/comicvine.provider';
import { KoboProvider } from './providers/kobo/kobo.provider';
import { RanobeDbProvider } from './providers/ranobedb/ranobedb.provider';
import { LubimyczytacProvider } from './providers/lubimyczytac/lubimyczytac.provider';
+import { AladinProvider } from './providers/aladin/aladin.provider';
describe('MetadataFetchModule', () => {
it('registers provider classes and factory wiring for METADATA_PROVIDERS', () => {
@@ -39,6 +40,7 @@ describe('MetadataFetchModule', () => {
RanobeDbProvider,
KoboProvider,
LubimyczytacProvider,
+ AladinProvider,
ProviderRegistry,
MetadataFetchRepository,
MetadataFetchService,
@@ -65,6 +67,7 @@ describe('MetadataFetchModule', () => {
RanobeDbProvider,
KoboProvider,
LubimyczytacProvider,
+ AladinProvider,
]);
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 9d0e2dc99..652c50141 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.module.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.module.ts
@@ -24,6 +24,7 @@ import { KoboProvider } from './providers/kobo/kobo.provider';
import { RanobeDbClient } from './providers/ranobedb/ranobedb.client';
import { RanobeDbProvider } from './providers/ranobedb/ranobedb.provider';
import { LubimyczytacProvider } from './providers/lubimyczytac/lubimyczytac.provider';
+import { AladinProvider } from './providers/aladin/aladin.provider';
const PROVIDER_CLASSES = [
GoogleProvider,
@@ -38,6 +39,7 @@ const PROVIDER_CLASSES = [
RanobeDbProvider,
KoboProvider,
LubimyczytacProvider,
+ AladinProvider,
];
@Module({
diff --git a/server/src/modules/metadata-fetch/metadata-fetch.repository.ts b/server/src/modules/metadata-fetch/metadata-fetch.repository.ts
index ea9699e70..b980a1fc8 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.repository.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.repository.ts
@@ -21,6 +21,7 @@ export interface StoredProviderIdsRow {
comicvineId: string | null;
ranobedbId: string | null;
lubimyczytacId: string | null;
+ aladinId: string | null;
}
@Injectable()
@@ -42,6 +43,7 @@ export class MetadataFetchRepository {
comicvineId: bookMetadata.comicvineId,
ranobedbId: bookMetadata.ranobedbId,
lubimyczytacId: bookMetadata.lubimyczytacId,
+ aladinId: bookMetadata.aladinId,
})
.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 7319787c3..884eca11b 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.service.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.service.ts
@@ -73,6 +73,7 @@ export class MetadataFetchService {
[MetadataProviderKey.COMICVINE]: row.comicvineId ?? undefined,
[MetadataProviderKey.RANOBEDB]: row.ranobedbId ?? undefined,
[MetadataProviderKey.LUBIMYCZYTAC]: row.lubimyczytacId ?? undefined,
+ [MetadataProviderKey.ALADIN]: row.aladinId ?? undefined,
};
}
diff --git a/server/src/modules/metadata-fetch/providers/aladin/aladin.mapper.test.ts b/server/src/modules/metadata-fetch/providers/aladin/aladin.mapper.test.ts
new file mode 100644
index 000000000..947303f7c
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/aladin/aladin.mapper.test.ts
@@ -0,0 +1,125 @@
+import { MetadataProviderKey } from '@bookorbit/types';
+import { mapAladinItem } from './aladin.mapper';
+import { AladinItem } from './aladin.types';
+
+describe('AladinMapper', () => {
+ const baseItem: AladinItem = {
+ title: '테스트 도서',
+ link: 'https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12345',
+ author: '테스트 저자',
+ pubDate: '2024-01-15',
+ description: '테스트 설명',
+ isbn: '8912345678',
+ isbn13: '9788912345678',
+ priceSales: 15000,
+ priceStandard: 18000,
+ mallType: 'BOOK',
+ stockStatus: '',
+ mileage: 450,
+ cover: 'https://image.aladin.co.kr/product/12345/cover.jpg',
+ publisher: '테스트 출판사',
+ salesPoint: 100,
+ adult: false,
+ customerReviewRank: 8,
+ };
+
+ it('should map basic fields correctly', () => {
+ const result = mapAladinItem(baseItem);
+
+ expect(result.provider).toBe(MetadataProviderKey.ALADIN);
+ expect(result.providerId).toBe('12345');
+ expect(result.title).toBe('테스트 도서');
+ expect(result.authors).toEqual(['테스트 저자']);
+ expect(result.publisher).toBe('테스트 출판사');
+ expect(result.publishedYear).toBe(2024);
+ expect(result.isbn10).toBe('8912345678');
+ expect(result.isbn13).toBe('9788912345678');
+ expect(result.language).toBe('ko');
+ expect(result.coverUrl).toBe('https://image.aladin.co.kr/product/12345/cover.jpg');
+ expect(result.sourceUrl).toBe('https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12345');
+ });
+
+ it('should handle multiple authors', () => {
+ const item = { ...baseItem, author: '저자1, 저자2, 저자3' };
+ const result = mapAladinItem(item);
+ expect(result.authors).toEqual(['저자1', '저자2', '저자3']);
+ });
+
+ it('should handle missing author', () => {
+ const item = { ...baseItem, author: '' };
+ const result = mapAladinItem(item);
+ expect(result.authors).toBeUndefined();
+ });
+
+ it('should parse page count from subInfo', () => {
+ const item = { ...baseItem, subInfo: { itemPage: 350 } };
+ const result = mapAladinItem(item);
+ expect(result.pageCount).toBe(350);
+ });
+
+ it('should handle missing subInfo', () => {
+ const item = { ...baseItem };
+ delete (item as Partial).subInfo;
+ const result = mapAladinItem(item);
+ expect(result.pageCount).toBeUndefined();
+ });
+
+ it('should use fullDescription when available', () => {
+ const item = { ...baseItem, fullDescription: '전체 설명입니다.' };
+ const result = mapAladinItem(item);
+ expect(result.description).toBe('전체 설명입니다.');
+ });
+
+ it('should strip HTML tags from description', () => {
+ const item = { ...baseItem, description: 'HTML 설명
' };
+ const result = mapAladinItem(item);
+ expect(result.description).toBe('HTML 설명');
+ });
+
+ it('should parse genres from categoryIdList', () => {
+ const item = {
+ ...baseItem,
+ categoryIdList: [
+ { categoryId: 100, categoryName: '소설/시/희곡' },
+ { categoryId: 200, categoryName: '인문학' },
+ ],
+ };
+ const result = mapAladinItem(item);
+ expect(result.genres).toEqual(['소설/시/희곡', '인문학']);
+ });
+
+ it('should handle missing categoryIdList', () => {
+ const item = { ...baseItem };
+ delete (item as Partial).categoryIdList;
+ const result = mapAladinItem(item);
+ expect(result.genres).toBeUndefined();
+ });
+
+ it('should parse series name without fabricating an index', () => {
+ const item = { ...baseItem, seriesInfo: { seriesId: 1, seriesName: '테스트 시리즈', seriesLink: 'https://aladin.co.kr/series/1' } };
+ const result = mapAladinItem(item);
+ expect(result.seriesName).toBe('테스트 시리즈');
+ expect(result.seriesIndex).toBeUndefined();
+ });
+
+ it('should handle missing series info', () => {
+ const item = { ...baseItem };
+ delete (item as Partial).seriesInfo;
+ const result = mapAladinItem(item);
+ expect(result.seriesName).toBeUndefined();
+ expect(result.seriesIndex).toBeUndefined();
+ });
+
+ it('should use the Aladin ItemId from the link for providerId', () => {
+ const result = mapAladinItem(baseItem);
+ expect(result.providerId).toBe('12345');
+ });
+
+ it('should leave providerId empty when the link has no ItemId (ISBNs still mapped to their own fields)', () => {
+ const item = { ...baseItem, link: 'https://www.aladin.co.kr/shop/wproduct.aspx' };
+ const result = mapAladinItem(item);
+ expect(result.providerId).toBe('');
+ expect(result.isbn10).toBe('8912345678');
+ expect(result.isbn13).toBe('9788912345678');
+ });
+});
diff --git a/server/src/modules/metadata-fetch/providers/aladin/aladin.mapper.ts b/server/src/modules/metadata-fetch/providers/aladin/aladin.mapper.ts
new file mode 100644
index 000000000..7b2f66c6f
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/aladin/aladin.mapper.ts
@@ -0,0 +1,68 @@
+import { MetadataCandidate, MetadataProviderKey } from '@bookorbit/types';
+import { AladinItem } from './aladin.types';
+
+function parseAuthors(authorString: string): string[] {
+ if (!authorString) return [];
+ return authorString
+ .split(',')
+ .map((a) => a.trim())
+ .filter((a) => a.length > 0);
+}
+
+function parseYear(pubDate: string): number | undefined {
+ if (!pubDate) return undefined;
+ const match = pubDate.match(/^(\d{4})/);
+ return match ? parseInt(match[1], 10) : undefined;
+}
+
+function parsePageCount(subInfo?: AladinItem['subInfo']): number | undefined {
+ if (!subInfo?.itemPage) return undefined;
+ return subInfo.itemPage;
+}
+
+function parseDescription(description: string, fullDescription?: string): string | undefined {
+ const desc = fullDescription ?? description;
+ if (!desc) return undefined;
+ return desc.replace(/<[^>]*>/g, '').trim();
+}
+
+function parseGenres(categoryIdList?: AladinItem['categoryIdList']): string[] | undefined {
+ if (!categoryIdList?.length) return undefined;
+ return categoryIdList.map((c) => c.categoryName).filter((c) => c.length > 0);
+}
+
+function parseSeriesName(seriesInfo?: AladinItem['seriesInfo']): string | undefined {
+ return seriesInfo?.seriesName || undefined;
+}
+
+function parseItemId(link: string): string | undefined {
+ const match = link.match(/ItemId=(\d+)/i);
+ return match ? match[1] : undefined;
+}
+
+export function mapAladinItem(item: AladinItem): MetadataCandidate {
+ const authors = parseAuthors(item.author);
+ const genres = parseGenres(item.categoryIdList);
+ const publishedYear = parseYear(item.pubDate);
+ const pageCount = parsePageCount(item.subInfo);
+ const itemId = parseItemId(item.link);
+
+ return {
+ provider: MetadataProviderKey.ALADIN,
+ providerId: itemId ?? '',
+ title: item.title,
+ subtitle: undefined,
+ authors: authors.length > 0 ? authors : undefined,
+ description: parseDescription(item.description, item.fullDescription),
+ publisher: item.publisher || undefined,
+ publishedYear,
+ language: 'ko',
+ pageCount,
+ isbn10: item.isbn || undefined,
+ isbn13: item.isbn13 || undefined,
+ seriesName: parseSeriesName(item.seriesInfo),
+ genres,
+ coverUrl: item.cover || undefined,
+ sourceUrl: item.link || undefined,
+ };
+}
diff --git a/server/src/modules/metadata-fetch/providers/aladin/aladin.provider.test.ts b/server/src/modules/metadata-fetch/providers/aladin/aladin.provider.test.ts
new file mode 100644
index 000000000..bca8ef7c2
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/aladin/aladin.provider.test.ts
@@ -0,0 +1,232 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ProviderConfigurations } from '@bookorbit/types';
+
+import { ProviderConfigService } from '../../../metadata-preferences/provider-config.service';
+import { AladinProvider } from './aladin.provider';
+
+describe('AladinProvider', () => {
+ let provider: AladinProvider;
+ let providerConfig: ProviderConfigService;
+
+ const mockConfig: ProviderConfigurations = {
+ google: { enabled: true, apiKey: 'test-api-key' },
+ amazon: { enabled: true, domain: 'amazon.com', cookie: '' },
+ goodreads: { enabled: true },
+ hardcover: { enabled: false, apiKey: '' },
+ openLibrary: { enabled: true },
+ itunes: { enabled: true, coverResolution: 'high' },
+ audible: { enabled: false, domain: 'com' },
+ audnexus: { enabled: false },
+ comicvine: { enabled: false, apiKey: '' },
+ ranobedb: { enabled: false },
+ aladin: { enabled: true, ttbKey: 'test-ttb-key' },
+ };
+
+ const mockAladinItem = {
+ title: '테스트 도서',
+ link: 'https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12345',
+ author: '테스트 저자',
+ pubDate: '2024-01-15',
+ description: '테스트 설명',
+ isbn: '8912345678',
+ isbn13: '9788912345678',
+ priceSales: 15000,
+ priceStandard: 18000,
+ mallType: 'BOOK',
+ stockStatus: '',
+ mileage: 450,
+ cover: 'https://image.aladin.co.kr/product/12345/cover.jpg',
+ publisher: '테스트 출판사',
+ salesPoint: 100,
+ adult: false,
+ customerReviewRank: 8,
+ fullDescription: '전체 설명',
+ categoryIdList: [{ categoryId: 100, categoryName: '소설/시/희곡' }],
+ seriesInfo: { seriesId: 1, seriesName: '테스트 시리즈', seriesLink: 'https://aladin.co.kr/series/1' },
+ subInfo: { itemPage: 300, toc: '목차', authors: [{ name: '테스트 저자', link: 'https://aladin.co.kr/author/1' }] },
+ };
+
+ const mockAladinSearchResponse = {
+ version: 20131101,
+ title: '알라딘 검색 결과',
+ link: 'https://www.aladin.co.kr',
+ pubDate: '2024-01-15T00:00:00',
+ totalResults: 1,
+ startIndex: 1,
+ itemsPerPage: 10,
+ query: '테스트',
+ searchCategoryId: 0,
+ searchCategoryName: '',
+ item: [mockAladinItem],
+ };
+
+ const mockAladinLookupResponse = {
+ version: 20131101,
+ title: '알라딘 상품 조회',
+ link: 'https://www.aladin.co.kr',
+ pubDate: '2024-01-15T00:00:00',
+ totalResults: 1,
+ startIndex: 1,
+ itemsPerPage: 1,
+ query: '9788912345678',
+ searchCategoryId: 0,
+ searchCategoryName: '',
+ item: [mockAladinItem],
+ };
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ AladinProvider,
+ {
+ provide: ProviderConfigService,
+ useValue: {
+ getConfig: vi.fn().mockResolvedValue(mockConfig),
+ },
+ },
+ ],
+ }).compile();
+
+ provider = module.get(AladinProvider);
+ providerConfig = module.get(ProviderConfigService);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('search', () => {
+ it('should return empty array if disabled', async () => {
+ vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({
+ ...mockConfig,
+ aladin: { enabled: false, ttbKey: '' },
+ });
+
+ const result = await provider.search({ title: 'Test' });
+ expect(result).toEqual([]);
+ });
+
+ it('should return empty array if TTB key is missing', async () => {
+ global.fetch = vi.fn();
+ vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({
+ ...mockConfig,
+ aladin: { enabled: true, ttbKey: '' },
+ });
+
+ const result = await provider.search({ title: 'Test' });
+ expect(result).toEqual([]);
+ expect(global.fetch).not.toHaveBeenCalled();
+ });
+
+ it('should fetch from Aladin and return mapped items', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(mockAladinSearchResponse),
+ });
+
+ const result = await provider.search({ title: '테스트 도서' });
+
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('https://www.aladin.co.kr/ttb/api/ItemSearch.aspx'), expect.any(Object));
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('TTBKey=test-ttb-key'), expect.any(Object));
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('Query=%ED%85%8C%EC%8A%A4%ED%8A%B8+%EB%8F%84%EC%84%9C'), expect.any(Object));
+ expect(result).toHaveLength(1);
+ expect(result[0].title).toBe('테스트 도서');
+ expect(result[0].providerId).toBe('12345');
+ expect(result[0].provider).toBe('aladin');
+ expect(result[0].authors).toEqual(['테스트 저자']);
+ expect(result[0].publisher).toBe('테스트 출판사');
+ expect(result[0].publishedYear).toBe(2024);
+ expect(result[0].isbn13).toBe('9788912345678');
+ expect(result[0].language).toBe('ko');
+ expect(result[0].genres).toEqual(['소설/시/희곡']);
+ });
+
+ it('should handle search with ISBN', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({ item: [] }),
+ });
+
+ await provider.search({ isbn: '9788912345678' });
+
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('Query=9788912345678'), expect.any(Object));
+ });
+
+ it('should return empty array on fetch error', async () => {
+ global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
+
+ const result = await provider.search({ title: 'Test' });
+ expect(result).toEqual([]);
+ });
+
+ it('should return empty array if no results', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({}),
+ });
+
+ const result = await provider.search({ title: 'Test' });
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('lookupById', () => {
+ it('should return null if TTB key is missing', async () => {
+ global.fetch = vi.fn();
+ vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({
+ ...mockConfig,
+ aladin: { enabled: true, ttbKey: '' },
+ });
+
+ const result = await provider.lookupById('9788912345678');
+ expect(result).toBeNull();
+ expect(global.fetch).not.toHaveBeenCalled();
+ });
+
+ it('should look up by Aladin ItemId and return the mapped item', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(mockAladinLookupResponse),
+ });
+
+ const result = await provider.lookupById('12345');
+
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('https://www.aladin.co.kr/ttb/api/ItemLookUp.aspx'), expect.any(Object));
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('ItemIdType=ItemId'), expect.any(Object));
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('ItemId=12345'), expect.any(Object));
+ expect(result).not.toBeNull();
+ expect(result?.title).toBe('테스트 도서');
+ expect(result?.providerId).toBe('12345');
+ });
+
+ it('should pass the providerId through unchanged as the ItemId', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(mockAladinLookupResponse),
+ });
+
+ const result = await provider.lookupById('9788912345678');
+
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('ItemIdType=ItemId'), expect.any(Object));
+ expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining('ItemId=9788912345678'), expect.any(Object));
+ expect(result).not.toBeNull();
+ });
+
+ it('should return null on fetch error', async () => {
+ global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 404 });
+
+ const result = await provider.lookupById('9788912345678');
+ expect(result).toBeNull();
+ });
+
+ it('should return null if item not found', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({ item: [] }),
+ });
+
+ const result = await provider.lookupById('9788912345678');
+ expect(result).toBeNull();
+ });
+ });
+});
diff --git a/server/src/modules/metadata-fetch/providers/aladin/aladin.provider.ts b/server/src/modules/metadata-fetch/providers/aladin/aladin.provider.ts
new file mode 100644
index 000000000..df27d8d1c
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/aladin/aladin.provider.ts
@@ -0,0 +1,151 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { MetadataCandidate, MetadataProviderKey } from '@bookorbit/types';
+
+import { ProviderConfigService } from '../../../metadata-preferences/provider-config.service';
+import { sanitizeLogValue } from '../../../../common/utils/log-sanitize.utils';
+import { fetchWithThrottle } from '../../fetch-with-throttle';
+import { ProviderThrottleError } from '../../provider-throttle.error';
+import { IdentifiableProvider } from '../metadata-provider';
+import { MetadataSearchParams } from '../metadata-search-params';
+import { PROVIDER_TIMEOUT_MS } from '../provider-constants';
+import { buildRequestSignal } from '../provider-utils';
+import { mapAladinItem } from './aladin.mapper';
+import { AladinLookupResponse, AladinSearchResponse } from './aladin.types';
+
+const BASE_URL = 'https://www.aladin.co.kr/ttb/api';
+const API_VERSION = '20131101';
+
+@Injectable()
+export class AladinProvider implements IdentifiableProvider {
+ readonly key = MetadataProviderKey.ALADIN;
+ readonly label = 'Aladin';
+ readonly identifiable = true as const;
+
+ private readonly logger = new Logger(AladinProvider.name);
+
+ constructor(private readonly providerConfig: ProviderConfigService) {}
+
+ async search(params: MetadataSearchParams): Promise {
+ const { enabled, ttbKey } = await this.providerConfig.getConfig().then((c) => c.aladin);
+ if (!enabled || !ttbKey.trim()) return [];
+ const query = this.buildQuery(params);
+ if (!query) return [];
+ return this.searchItems(query, ttbKey, params.signal);
+ }
+
+ async lookupById(providerId: string, signal?: AbortSignal): Promise {
+ const { enabled, ttbKey } = await this.providerConfig.getConfig().then((c) => c.aladin);
+ if (!enabled || !ttbKey.trim()) return null;
+ return this.lookupItem(providerId, ttbKey, signal);
+ }
+
+ private buildQuery(params: MetadataSearchParams): string | null {
+ if (params.isbn) {
+ return params.isbn;
+ }
+ const parts: string[] = [];
+ if (params.title) parts.push(params.title);
+ if (params.author) parts.push(params.author);
+ return parts.length ? parts.join(' ') : null;
+ }
+
+ private async searchItems(query: string, ttbKey: string, signal?: AbortSignal): Promise {
+ const url = this.buildSearchUrl(query, ttbKey);
+ const startedAt = Date.now();
+ const safeQuery = sanitizeLogValue(query);
+ this.logger.log(`[aladin.search] [start] query="${safeQuery}"`);
+
+ try {
+ const res = await fetchWithThrottle(url, { signal: buildRequestSignal(PROVIDER_TIMEOUT_MS.DEFAULT, signal) });
+ if (!res.ok) {
+ this.logger.warn(
+ `[aladin.search] [fail] query="${safeQuery}" status=${res.status} durationMs=${Date.now() - startedAt} errorClass=HttpError error="non-ok response"`,
+ );
+ return [];
+ }
+ const body = (await res.json()) as AladinSearchResponse;
+ const items = (body.item ?? []).map(mapAladinItem);
+ this.logger.log(
+ `[aladin.search] [end] query="${safeQuery}" status=${res.status} resultCount=${items.length} durationMs=${Date.now() - startedAt}`,
+ );
+ return items;
+ } catch (err) {
+ if (err instanceof ProviderThrottleError) {
+ this.logger.warn(
+ `[aladin.search] [fail] query="${safeQuery}" durationMs=${Date.now() - startedAt} errorClass=ProviderThrottleError error="throttled"`,
+ );
+ throw err;
+ }
+ this.logger.warn(
+ `[aladin.search] [fail] query="${safeQuery}" durationMs=${Date.now() - startedAt} errorClass=${err instanceof Error ? err.name : 'UnknownError'} error="${sanitizeLogValue(err instanceof Error ? err.message : String(err))}"`,
+ );
+ throw err;
+ }
+ }
+
+ private async lookupItem(providerId: string, ttbKey: string, signal?: AbortSignal): Promise {
+ const url = this.buildLookupUrl(providerId, ttbKey);
+ const startedAt = Date.now();
+ const safeId = sanitizeLogValue(providerId);
+ this.logger.log(`[aladin.lookup] [start] providerId="${safeId}"`);
+
+ try {
+ const res = await fetchWithThrottle(url, { signal: buildRequestSignal(PROVIDER_TIMEOUT_MS.DEFAULT, signal) });
+ if (!res.ok) {
+ this.logger.warn(
+ `[aladin.lookup] [fail] providerId="${safeId}" status=${res.status} durationMs=${Date.now() - startedAt} errorClass=HttpError error="non-ok response"`,
+ );
+ return null;
+ }
+ const body = (await res.json()) as AladinLookupResponse;
+ const item = body.item?.[0];
+ if (!item) {
+ this.logger.log(`[aladin.lookup] [end] providerId="${safeId}" status=${res.status} found=false durationMs=${Date.now() - startedAt}`);
+ return null;
+ }
+ const mapped = mapAladinItem(item);
+ this.logger.log(`[aladin.lookup] [end] providerId="${safeId}" status=${res.status} found=true durationMs=${Date.now() - startedAt}`);
+ return mapped;
+ } catch (err) {
+ if (err instanceof ProviderThrottleError) {
+ this.logger.warn(
+ `[aladin.lookup] [fail] providerId="${safeId}" durationMs=${Date.now() - startedAt} errorClass=ProviderThrottleError error="throttled"`,
+ );
+ throw err;
+ }
+ this.logger.warn(
+ `[aladin.lookup] [fail] providerId="${safeId}" durationMs=${Date.now() - startedAt} errorClass=${err instanceof Error ? err.name : 'UnknownError'} error="${sanitizeLogValue(err instanceof Error ? err.message : String(err))}"`,
+ );
+ throw err;
+ }
+ }
+
+ private buildSearchUrl(query: string, ttbKey: string): string {
+ const params = new URLSearchParams({
+ TTBKey: ttbKey,
+ Query: query,
+ QueryType: 'Keyword',
+ MaxResults: '10',
+ start: '1',
+ SearchTarget: 'Book',
+ output: 'JS',
+ Version: API_VERSION,
+ Cover: 'Big',
+ OptResult: 'ebookList,usedList,reviewList,fullDescription,categoryIdList,seriesInfo,authors',
+ });
+ return `${BASE_URL}/ItemSearch.aspx?${params.toString()}`;
+ }
+
+ private buildLookupUrl(providerId: string, ttbKey: string): string {
+ const params = new URLSearchParams({
+ TTBKey: ttbKey,
+ ItemId: providerId,
+ ItemIdType: 'ItemId',
+ output: 'JS',
+ Version: API_VERSION,
+ Cover: 'Big',
+ OptResult: 'ebookList,usedList,reviewList,fullDescription,categoryIdList,seriesInfo,authors,toc,packing,subbarcode',
+ });
+ return `${BASE_URL}/ItemLookUp.aspx?${params.toString()}`;
+ }
+}
diff --git a/server/src/modules/metadata-fetch/providers/aladin/aladin.types.ts b/server/src/modules/metadata-fetch/providers/aladin/aladin.types.ts
new file mode 100644
index 000000000..8676e4c4f
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/aladin/aladin.types.ts
@@ -0,0 +1,70 @@
+export interface AladinItem {
+ title: string;
+ link: string;
+ author: string;
+ pubDate: string;
+ description: string;
+ isbn: string;
+ isbn13: string;
+ priceSales: number;
+ priceStandard: number;
+ mallType: string;
+ stockStatus: string;
+ mileage: number;
+ cover: string;
+ publisher: string;
+ salesPoint: number;
+ adult: boolean;
+ customerReviewRank: number;
+ bestRank?: number;
+ fullDescription?: string;
+ fullDescription2?: string;
+ categoryIdList?: Array<{ categoryId: number; categoryName: string }>;
+ seriesInfo?: { seriesId: number; seriesName: string; seriesLink: string };
+ subInfo?: {
+ itemPage: number;
+ toc?: string;
+ itemWeight?: number;
+ itemVolume?: string;
+ editionName?: string;
+ itemOrigin?: string;
+ translateAuthor?: string;
+ itemBindingType?: string;
+ packgeGb?: string;
+ giftYn?: string;
+ ebookList?: unknown[];
+ usedList?: unknown[];
+ reviewList?: unknown[];
+ authors?: Array<{ name: string; link: string }>;
+ phraseList?: string[];
+ tocList?: string[];
+ };
+}
+
+export interface AladinSearchResponse {
+ version: number;
+ title: string;
+ link: string;
+ pubDate: string;
+ totalResults: number;
+ startIndex: number;
+ itemsPerPage: number;
+ query: string;
+ searchCategoryId: number;
+ searchCategoryName: string;
+ item: AladinItem[];
+}
+
+export interface AladinLookupResponse {
+ version: number;
+ title: string;
+ link: string;
+ pubDate: string;
+ totalResults: number;
+ startIndex: number;
+ itemsPerPage: number;
+ query: string;
+ searchCategoryId: number;
+ searchCategoryName: string;
+ item: AladinItem[];
+}
diff --git a/server/src/modules/metadata-fetch/providers/aladin/index.ts b/server/src/modules/metadata-fetch/providers/aladin/index.ts
new file mode 100644
index 000000000..b15b79af9
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/aladin/index.ts
@@ -0,0 +1,3 @@
+export { AladinProvider } from './aladin.provider';
+export { mapAladinItem } from './aladin.mapper';
+export type { AladinItem, AladinSearchResponse, AladinLookupResponse } from './aladin.types';
diff --git a/server/src/modules/metadata-preferences/dto/update-provider-config.dto.test.ts b/server/src/modules/metadata-preferences/dto/update-provider-config.dto.test.ts
index caba2ad13..a725d7205 100644
--- a/server/src/modules/metadata-preferences/dto/update-provider-config.dto.test.ts
+++ b/server/src/modules/metadata-preferences/dto/update-provider-config.dto.test.ts
@@ -17,6 +17,7 @@ describe('UpdateProviderConfigDto', () => {
amazon: { cookie: 'session-cookie' },
hardcover: { apiKey: 'hardcover-key' },
itunes: { coverResolution: 'standard' },
+ aladin: { ttbKey: 'ttb-test-key' },
});
expect(errors).toHaveLength(0);
@@ -35,6 +36,7 @@ describe('UpdateProviderConfigDto', () => {
hardcover: { apiKey: false },
openLibrary: { enabled: 'true' },
itunes: { coverResolution: 'ultra' },
+ aladin: { ttbKey: 123 },
});
expect(errors).toEqual(
@@ -44,6 +46,7 @@ describe('UpdateProviderConfigDto', () => {
expect.objectContaining({ property: 'hardcover' }),
expect.objectContaining({ property: 'openLibrary' }),
expect.objectContaining({ property: 'itunes' }),
+ expect.objectContaining({ property: 'aladin' }),
]),
);
});
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 e371e8e3e..d3e95f060 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
@@ -43,6 +43,11 @@ export class KoboProviderConfigDto {
@IsOptional() @IsString() language?: string;
}
+export class AladinProviderConfigDto {
+ @IsOptional() @IsBoolean() enabled?: boolean;
+ @IsOptional() @IsString() ttbKey?: string;
+}
+
export class UpdateProviderConfigDto {
@IsOptional() @ValidateNested() @Type(() => GoogleProviderConfigDto) google?: GoogleProviderConfigDto;
@IsOptional() @ValidateNested() @Type(() => AmazonProviderConfigDto) amazon?: AmazonProviderConfigDto;
@@ -56,4 +61,5 @@ export class UpdateProviderConfigDto {
@IsOptional() @ValidateNested() @Type(() => SimpleProviderConfigDto) ranobedb?: SimpleProviderConfigDto;
@IsOptional() @ValidateNested() @Type(() => KoboProviderConfigDto) kobo?: KoboProviderConfigDto;
@IsOptional() @ValidateNested() @Type(() => SimpleProviderConfigDto) lubimyczytac?: SimpleProviderConfigDto;
+ @IsOptional() @ValidateNested() @Type(() => AladinProviderConfigDto) aladin?: AladinProviderConfigDto;
}
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 643c08530..856eac52d 100644
--- a/server/src/modules/metadata-preferences/provider-config.service.test.ts
+++ b/server/src/modules/metadata-preferences/provider-config.service.test.ts
@@ -75,6 +75,7 @@ describe('ProviderConfigService', () => {
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
lubimyczytac: { enabled: false },
+ aladin: { enabled: false, ttbKey: '' },
});
});
@@ -358,6 +359,7 @@ describe('ProviderConfigService', () => {
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
lubimyczytac: { enabled: false },
+ aladin: { enabled: false, ttbKey: '' },
});
expect(statuses.map((s) => s.key)).toEqual([
@@ -373,6 +375,7 @@ describe('ProviderConfigService', () => {
MetadataProviderKey.RANOBEDB,
MetadataProviderKey.KOBO,
MetadataProviderKey.LUBIMYCZYTAC,
+ MetadataProviderKey.ALADIN,
]);
expect(statuses.find((s) => s.key === MetadataProviderKey.GOOGLE)?.configured).toBe(false);
expect(statuses.find((s) => s.key === MetadataProviderKey.GOOGLE)?.hint).toContain('API key required');
@@ -398,6 +401,7 @@ describe('ProviderConfigService', () => {
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
lubimyczytac: { enabled: false },
+ aladin: { enabled: false, ttbKey: '' },
});
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 e784d6d8f..6bf8dcad8 100644
--- a/server/src/modules/metadata-preferences/provider-config.service.ts
+++ b/server/src/modules/metadata-preferences/provider-config.service.ts
@@ -45,6 +45,7 @@ const DEFAULT_CONFIG: ProviderConfigurations = {
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
lubimyczytac: { enabled: false },
+ aladin: { enabled: false, ttbKey: '' },
};
function asObject(value: unknown): Record {
@@ -126,6 +127,14 @@ function mergeKoboConfig(base: ProviderConfigurations['kobo'], value: unknown):
};
}
+function mergeAladinConfig(base: ProviderConfigurations['aladin'], value: unknown): ProviderConfigurations['aladin'] {
+ const next = asObject(value);
+ return {
+ enabled: asBoolean(next.enabled, base.enabled),
+ ttbKey: asString(next.ttbKey, base.ttbKey),
+ };
+}
+
const PROVIDER_LABELS: Record = {
[MetadataProviderKey.GOOGLE]: 'Google Books',
[MetadataProviderKey.AMAZON]: 'Amazon',
@@ -139,6 +148,7 @@ const PROVIDER_LABELS: Record = {
[MetadataProviderKey.RANOBEDB]: 'RanobeDB',
[MetadataProviderKey.KOBO]: 'Kobo',
[MetadataProviderKey.LUBIMYCZYTAC]: 'LubimyCzytac',
+ [MetadataProviderKey.ALADIN]: 'Aladin',
};
type ProviderEnableRule = {
@@ -163,6 +173,11 @@ const PROVIDER_ENABLE_RULES = {
blockedMessage: 'ComicVine requires an API key before it can be enabled',
setupHint: 'API key required',
},
+ aladin: {
+ canEnable: (config) => !!config.aladin.ttbKey.trim(),
+ blockedMessage: 'Aladin requires a TTB Key before it can be enabled',
+ setupHint: 'TTB Key required',
+ },
} satisfies Partial>;
@Injectable()
@@ -185,6 +200,7 @@ export class ProviderConfigService {
ranobedb: { ...DEFAULT_CONFIG.ranobedb },
kobo: { ...DEFAULT_CONFIG.kobo },
lubimyczytac: { ...DEFAULT_CONFIG.lubimyczytac },
+ aladin: { ...DEFAULT_CONFIG.aladin },
};
}
@@ -203,6 +219,7 @@ export class ProviderConfigService {
ranobedb: mergeSimpleConfig(base.ranobedb, next.ranobedb),
kobo: mergeKoboConfig(base.kobo, next.kobo),
lubimyczytac: mergeSimpleConfig(base.lubimyczytac, next.lubimyczytac),
+ aladin: mergeAladinConfig(base.aladin, next.aladin),
};
}
@@ -273,6 +290,10 @@ export class ProviderConfigService {
country: this.normalizeKoboPathSegment(config.kobo.country, DEFAULT_CONFIG.kobo.country),
language: this.normalizeKoboPathSegment(config.kobo.language, DEFAULT_CONFIG.kobo.language),
},
+ aladin: {
+ ...config.aladin,
+ ttbKey: config.aladin.ttbKey.trim(),
+ },
};
if (!normalized.google.enabled || normalized.google.apiKey) return normalized;
@@ -437,6 +458,13 @@ export class ProviderConfigService {
configured: true,
hint: 'Polish book catalog (lubimyczytac.pl). Scrapes public pages.',
},
+ {
+ key: MetadataProviderKey.ALADIN,
+ label: PROVIDER_LABELS[MetadataProviderKey.ALADIN],
+ enabled: cfg.aladin.enabled,
+ configured: !!this.getEnableRule('aladin')?.canEnable(cfg),
+ hint: !this.getEnableRule('aladin')?.canEnable(cfg) ? this.getEnableRule('aladin')?.setupHint : undefined,
+ },
];
}
@@ -470,6 +498,8 @@ export class ProviderConfigService {
return this.testAmazonProvider(config.amazon);
case MetadataProviderKey.HARDCOVER:
return this.testHardcoverProvider(config.hardcover.apiKey);
+ case MetadataProviderKey.ALADIN:
+ return this.testAladinProvider(config.aladin.ttbKey);
default:
throw new BadRequestException(`Provider test not supported for ${key}`);
}
@@ -581,4 +611,57 @@ export class ProviderConfigService {
message: `Connected as ${username}.`,
};
}
+
+ private async testAladinProvider(ttbKey: string): Promise {
+ const key = ttbKey.trim();
+ if (!key) {
+ return {
+ key: MetadataProviderKey.ALADIN,
+ ok: false,
+ status: 'fail',
+ message: 'Aladin TTB Key is required.',
+ };
+ }
+
+ const testUrl = `https://www.aladin.co.kr/ttb/api/ItemSearch.aspx?TTBKey=${encodeURIComponent(key)}&Query=%ED%8C%8C%EC%9D%B4%EC%8D%AC&QueryType=Keyword&MaxResults=1&start=1&SearchTarget=Book&output=JS&Version=20131101`;
+
+ try {
+ const response = await fetch(testUrl, { method: 'GET', signal: AbortSignal.timeout(PROVIDER_TEST_TIMEOUT_MS) });
+
+ if (!response.ok) {
+ return {
+ key: MetadataProviderKey.ALADIN,
+ ok: false,
+ status: 'fail',
+ message: `Aladin request failed with HTTP ${response.status}.`,
+ };
+ }
+
+ const body = (await response.json()) as { item?: unknown[]; errorCode?: number; errorMessage?: string };
+
+ if (body.errorCode) {
+ return {
+ key: MetadataProviderKey.ALADIN,
+ ok: false,
+ status: 'fail',
+ message: `Aladin API error: ${body.errorMessage ?? body.errorCode}`,
+ };
+ }
+
+ return {
+ key: MetadataProviderKey.ALADIN,
+ ok: true,
+ status: 'success',
+ message: 'Aladin API connection successful.',
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ key: MetadataProviderKey.ALADIN,
+ ok: false,
+ status: 'fail',
+ message: `Aladin connection test failed: ${message}`,
+ };
+ }
+ }
}
diff --git a/server/src/modules/metadata-score/dto/update-weights.dto.ts b/server/src/modules/metadata-score/dto/update-weights.dto.ts
index 53456985b..37f8c71e9 100644
--- a/server/src/modules/metadata-score/dto/update-weights.dto.ts
+++ b/server/src/modules/metadata-score/dto/update-weights.dto.ts
@@ -24,4 +24,5 @@ export class UpdateWeightsDto {
@IsInt() @Min(0) openLibraryId!: number;
@IsInt() @Min(0) itunesId!: number;
@IsInt() @Min(0) koboId!: number;
+ @IsInt() @Min(0) aladinId!: number;
}
diff --git a/server/src/modules/metadata-score/metadata-score.repository.ts b/server/src/modules/metadata-score/metadata-score.repository.ts
index 9b9982b33..ab6aaedb3 100644
--- a/server/src/modules/metadata-score/metadata-score.repository.ts
+++ b/server/src/modules/metadata-score/metadata-score.repository.ts
@@ -30,6 +30,7 @@ const METADATA_SCORE_BASE_SELECT = {
openLibraryId: bookMetadata.openLibraryId,
itunesId: bookMetadata.itunesId,
koboId: bookMetadata.koboId,
+ aladinId: bookMetadata.aladinId,
} as const;
export type ScorePage = {
diff --git a/server/src/modules/metadata-score/metadata-score.scorer.test.ts b/server/src/modules/metadata-score/metadata-score.scorer.test.ts
index 5201801ea..d53986a91 100644
--- a/server/src/modules/metadata-score/metadata-score.scorer.test.ts
+++ b/server/src/modules/metadata-score/metadata-score.scorer.test.ts
@@ -24,6 +24,7 @@ function makeData(overrides: Partial = {}): ScoreData {
openLibraryId: null,
itunesId: null,
koboId: null,
+ aladinId: null,
authorCount: 0,
genreCount: 0,
tagCount: 0,
@@ -81,6 +82,7 @@ describe('MetadataScoreScorer', () => {
openLibraryId: 0,
itunesId: 0,
koboId: 0,
+ aladinId: 0,
};
expect(scorer.compute(data, weights)).toBe(100);
@@ -113,6 +115,7 @@ describe('MetadataScoreScorer', () => {
openLibraryId: 0,
itunesId: 0,
koboId: 0,
+ aladinId: 0,
};
expect(scorer.compute(makeData({ title: 'Dune', coverSource: 'extracted', authorCount: 10 }), weights)).toBe(100);
diff --git a/server/src/modules/metadata-score/metadata-score.scorer.ts b/server/src/modules/metadata-score/metadata-score.scorer.ts
index 290a5cac9..db4d38311 100644
--- a/server/src/modules/metadata-score/metadata-score.scorer.ts
+++ b/server/src/modules/metadata-score/metadata-score.scorer.ts
@@ -22,6 +22,7 @@ export type ScoreData = {
openLibraryId: string | null;
itunesId: string | null;
koboId: string | null;
+ aladinId: string | null;
authorCount: number;
genreCount: number;
tagCount: number;
@@ -54,6 +55,7 @@ const SCORE_RULES: Record = {
openLibraryId: { kind: 'string', value: (data) => data.openLibraryId },
itunesId: { kind: 'string', value: (data) => data.itunesId },
koboId: { kind: 'string', value: (data) => data.koboId },
+ aladinId: { kind: 'string', value: (data) => data.aladinId },
};
@Injectable()
diff --git a/server/src/modules/metadata/extractors/comic-format.extractor.ts b/server/src/modules/metadata/extractors/comic-format.extractor.ts
index a1a05089f..eecee9f85 100644
--- a/server/src/modules/metadata/extractors/comic-format.extractor.ts
+++ b/server/src/modules/metadata/extractors/comic-format.extractor.ts
@@ -49,6 +49,7 @@ export class ComicFormatExtractor implements FormatExtractor {
ranobedbId: comicMetadata?.ranobedbId ?? null,
koboId: comicMetadata?.koboId ?? null,
lubimyczytacId: comicMetadata?.lubimyczytacId ?? null,
+ aladinId: comicMetadata?.aladinId ?? null,
itunesId: comicMetadata?.itunesId ?? null,
cover: cover ?? null,
comicMetadata: comicMetadata?.comicMetadata ?? null,
diff --git a/server/src/modules/metadata/extractors/format-extractor.interface.ts b/server/src/modules/metadata/extractors/format-extractor.interface.ts
index ca6d887af..e69512bce 100644
--- a/server/src/modules/metadata/extractors/format-extractor.interface.ts
+++ b/server/src/modules/metadata/extractors/format-extractor.interface.ts
@@ -23,6 +23,7 @@ export interface ParsedBookData {
ranobedbId?: string | null;
koboId?: string | null;
lubimyczytacId?: string | null;
+ aladinId?: string | null;
itunesId?: string | null;
audibleId?: string | null;
cover: Buffer | null;
diff --git a/server/src/modules/metadata/extractors/opf-metadata.mapper.ts b/server/src/modules/metadata/extractors/opf-metadata.mapper.ts
index 2d00021df..c8b0e5773 100644
--- a/server/src/modules/metadata/extractors/opf-metadata.mapper.ts
+++ b/server/src/modules/metadata/extractors/opf-metadata.mapper.ts
@@ -26,6 +26,7 @@ export function hasOpfMetadata(metadata: ParsedOpf): boolean {
metadata.ranobedbId !== null ||
metadata.koboId !== null ||
metadata.lubimyczytacId !== null ||
+ metadata.aladinId !== null ||
metadata.itunesId !== null
);
}
@@ -55,6 +56,7 @@ export function mapOpfMetadata(metadata: ParsedOpf, cover: Buffer | null): Parse
ranobedbId: metadata.ranobedbId,
koboId: metadata.koboId,
lubimyczytacId: metadata.lubimyczytacId,
+ aladinId: metadata.aladinId,
itunesId: metadata.itunesId,
cover,
};
diff --git a/server/src/modules/metadata/extractors/pdf-format.extractor.ts b/server/src/modules/metadata/extractors/pdf-format.extractor.ts
index 5ecbdb006..90d94b3b4 100644
--- a/server/src/modules/metadata/extractors/pdf-format.extractor.ts
+++ b/server/src/modules/metadata/extractors/pdf-format.extractor.ts
@@ -38,6 +38,7 @@ export class PdfFormatExtractor implements FormatExtractor {
ranobedbId: pdf.ranobedbId,
koboId: pdf.koboId,
lubimyczytacId: pdf.lubimyczytacId,
+ aladinId: pdf.aladinId,
itunesId: pdf.itunesId,
cover: pdf.coverBuffer,
pageCount: pdf.pageCount,
diff --git a/server/src/modules/metadata/lib/cbz-metadata.ts b/server/src/modules/metadata/lib/cbz-metadata.ts
index a32e3b2c1..14b0cdd21 100644
--- a/server/src/modules/metadata/lib/cbz-metadata.ts
+++ b/server/src/modules/metadata/lib/cbz-metadata.ts
@@ -47,6 +47,7 @@ export interface ParsedCbzMetadata {
ranobedbId: string | null;
koboId: string | null;
lubimyczytacId: string | null;
+ aladinId: string | null;
itunesId: string | null;
comicMetadata: ParsedCbzComicMetadata | null;
}
@@ -269,6 +270,7 @@ function parseComicInfoXml(xmlBuf: Buffer): ParsedCbzMetadata | null {
ranobedbId: managedNotes.get('ranobedbId') ?? null,
koboId: managedNotes.get('koboId') ?? providerIdsFromWeb.koboId ?? null,
lubimyczytacId: managedNotes.get('lubimyczytacId') ?? null,
+ aladinId: managedNotes.get('aladinId') ?? null,
itunesId: null,
comicMetadata: hasComicFields
? {
@@ -327,6 +329,7 @@ function parseComicBookInfoJson(comment: string): ParsedCbzMetadata | null {
ranobedbId: null,
koboId: null,
lubimyczytacId: null,
+ aladinId: null,
itunesId: null,
comicMetadata: null,
};
diff --git a/server/src/modules/metadata/lib/opf-parser.ts b/server/src/modules/metadata/lib/opf-parser.ts
index 24df22a73..79aa61eb2 100644
--- a/server/src/modules/metadata/lib/opf-parser.ts
+++ b/server/src/modules/metadata/lib/opf-parser.ts
@@ -24,6 +24,7 @@ export interface ParsedOpf {
ranobedbId: string | null;
koboId: string | null;
lubimyczytacId: string | null;
+ aladinId: string | null;
itunesId: string | null;
coverHref: string | null;
}
@@ -86,7 +87,7 @@ function parseBookOrbitTags(raw: string | null): string[] {
}
}
-type ProviderKey = 'google' | 'amazon' | 'goodreads' | 'hardcover' | 'openlibrary' | 'ranobedb' | 'kobo' | 'lubimyczytac' | 'itunes';
+type ProviderKey = 'google' | 'amazon' | 'goodreads' | 'hardcover' | 'openlibrary' | 'ranobedb' | 'kobo' | 'lubimyczytac' | 'aladin' | 'itunes';
// Calibre 9.x (opf3) writes provider identifiers as bare `prefix:value` text inside .
// Only these known prefixes are recognized, as a lowest-priority fallback after opf:scheme and urn:.
@@ -102,6 +103,7 @@ const PREFIX_TO_PROVIDER: Record = {
itunes: 'itunes',
lubimyczytac: 'lubimyczytac',
ranobedb: 'ranobedb',
+ aladin: 'aladin',
};
// Calibre stores custom-column values in a `calibre:user_metadata` JSON blob keyed by column name,
@@ -262,6 +264,7 @@ export function parseOpf(xml: string): ParsedOpf {
let schemeRanobedbId: string | null = null;
let schemeKoboId: string | null = null;
let schemeLubimyczytacId: string | null = null;
+ let schemeAladinId: string | null = null;
let schemeItunesId: string | null = null;
let urnGoogleBooksId: string | null = null;
@@ -272,6 +275,7 @@ export function parseOpf(xml: string): ParsedOpf {
let urnRanobedbId: string | null = null;
let urnKoboId: string | null = null;
let urnLubimyczytacId: string | null = null;
+ let urnAladinId: string | null = null;
let urnItunesId: string | null = null;
// Calibre prefix:value identifiers — lowest priority, resolved after scheme and urn.
@@ -299,6 +303,7 @@ export function parseOpf(xml: string): ParsedOpf {
if (scheme === 'ranobedb') schemeRanobedbId ??= value || null;
if (scheme === 'kobo') schemeKoboId ??= value || null;
if (scheme === 'lubimyczytac') schemeLubimyczytacId ??= value || null;
+ if (scheme === 'aladin') schemeAladinId ??= value || null;
if (scheme === 'itunes') schemeItunesId ??= value || null;
// urn:-prefixed provider identifiers (legacy / backward-compat)
@@ -310,6 +315,7 @@ export function parseOpf(xml: string): ParsedOpf {
if (value.startsWith('urn:ranobedb:')) urnRanobedbId ??= value.slice('urn:ranobedb:'.length) || null;
if (value.startsWith('urn:kobo:')) urnKoboId ??= value.slice('urn:kobo:'.length) || null;
if (value.startsWith('urn:lubimyczytac:')) urnLubimyczytacId ??= value.slice('urn:lubimyczytac:'.length) || null;
+ if (value.startsWith('urn:aladin:')) urnAladinId ??= value.slice('urn:aladin:'.length) || null;
if (value.startsWith('urn:itunes:')) urnItunesId ??= value.slice('urn:itunes:'.length) || null;
// Calibre prefix:value — only when there is no opf:scheme attribute and the value is not a urn:
@@ -334,6 +340,7 @@ export function parseOpf(xml: string): ParsedOpf {
const ranobedbId = schemeRanobedbId ?? urnRanobedbId ?? prefixIds.ranobedb ?? null;
const koboId = schemeKoboId ?? urnKoboId ?? prefixIds.kobo ?? null;
const lubimyczytacId = schemeLubimyczytacId ?? urnLubimyczytacId ?? prefixIds.lubimyczytac ?? null;
+ const aladinId = schemeAladinId ?? urnAladinId ?? prefixIds.aladin ?? null;
const itunesId = schemeItunesId ?? urnItunesId ?? prefixIds.itunes ?? null;
isbn10 ??= propertyMeta('bookorbit:isbn10') ?? namedMeta('bookorbit:isbn10');
@@ -456,6 +463,7 @@ export function parseOpf(xml: string): ParsedOpf {
ranobedbId,
koboId,
lubimyczytacId,
+ aladinId,
itunesId,
coverHref,
};
diff --git a/server/src/modules/metadata/lib/pdf-parser.ts b/server/src/modules/metadata/lib/pdf-parser.ts
index 9142fbd0c..fe88eddaa 100644
--- a/server/src/modules/metadata/lib/pdf-parser.ts
+++ b/server/src/modules/metadata/lib/pdf-parser.ts
@@ -30,6 +30,7 @@ export interface PdfParsed {
ranobedbId: string | null;
koboId: string | null;
lubimyczytacId: string | null;
+ aladinId: string | null;
itunesId: string | null;
coverBuffer: Buffer | null;
}
@@ -142,6 +143,7 @@ export async function parsePdfFile(absolutePath: string, options: PdfParseOption
ranobedbId: xmp?.ranobedbId ?? null,
koboId: xmp?.koboId ?? null,
lubimyczytacId: xmp?.lubimyczytacId ?? null,
+ aladinId: xmp?.aladinId ?? null,
itunesId: xmp?.itunesId ?? null,
coverBuffer,
};
diff --git a/server/src/modules/metadata/lib/pdf-xmp-reader.ts b/server/src/modules/metadata/lib/pdf-xmp-reader.ts
index f8b1585b5..758e56174 100644
--- a/server/src/modules/metadata/lib/pdf-xmp-reader.ts
+++ b/server/src/modules/metadata/lib/pdf-xmp-reader.ts
@@ -27,6 +27,7 @@ export interface XmpParsed {
ranobedbId: string | null;
koboId: string | null;
lubimyczytacId: string | null;
+ aladinId: string | null;
itunesId: string | null;
}
@@ -161,6 +162,7 @@ export function parseXmp(xmpXml: string): XmpParsed | null {
ranobedbId: str(merged[`${px}:ranobedbId`]),
koboId: str(merged[`${px}:koboId`]),
lubimyczytacId: str(merged[`${px}:lubimyczytacId`]),
+ aladinId: str(merged[`${px}:aladinId`]),
itunesId: str(merged[`${px}:itunesId`]),
};
}
diff --git a/server/src/modules/metadata/metadata.service.ts b/server/src/modules/metadata/metadata.service.ts
index d30a1bcea..e60417445 100644
--- a/server/src/modules/metadata/metadata.service.ts
+++ b/server/src/modules/metadata/metadata.service.ts
@@ -591,6 +591,7 @@ export class MetadataService {
ranobedbId: data.ranobedbId,
koboId: data.koboId,
lubimyczytacId: data.lubimyczytacId,
+ aladinId: data.aladinId,
itunesId: data.itunesId,
comicMetadata: data.comicMetadata ?? undefined,
});
@@ -616,6 +617,7 @@ export class MetadataService {
if (filtered.ranobedbId !== undefined) scalarFields.ranobedbId = filtered.ranobedbId;
if (filtered.koboId !== undefined) scalarFields.koboId = filtered.koboId;
if (filtered.lubimyczytacId !== undefined) scalarFields.lubimyczytacId = filtered.lubimyczytacId;
+ if (filtered.aladinId !== undefined) scalarFields.aladinId = filtered.aladinId;
if (filtered.itunesId !== undefined) scalarFields.itunesId = filtered.itunesId;
if (Object.keys(scalarFields).length > 0) {
const shouldSyncSeries =
diff --git a/server/src/modules/statistics/statistics.repository.ts b/server/src/modules/statistics/statistics.repository.ts
index 7ac7b1c74..ce76ae65d 100644
--- a/server/src/modules/statistics/statistics.repository.ts
+++ b/server/src/modules/statistics/statistics.repository.ts
@@ -738,7 +738,8 @@ export class StatisticsRepository {
${bookMetadata.koboId} is not null or
${bookMetadata.comicvineId} is not null or
${bookMetadata.ranobedbId} is not null or
- ${bookMetadata.lubimyczytacId} is not null
+ ${bookMetadata.lubimyczytacId} is not null or
+ ${bookMetadata.aladinId} is not null
)`;
const [row] = await this.db