mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(reader): add settings system with per-book delta and defaults
This commit is contained in:
@@ -29,7 +29,7 @@ export default tseslint.config(
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
'prettier/prettier': ['error', { endOfLine: 'auto' }],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import { KoboModule } from './modules/kobo/kobo.module';
|
||||
import { LibraryModule } from './modules/library/library.module';
|
||||
import { PathModule } from './modules/path/path.module';
|
||||
import { MetadataModule } from './modules/metadata/metadata.module';
|
||||
import { ReaderPreferencesModule } from './modules/reader-preferences/reader-preferences.module';
|
||||
import { RoleModule } from './modules/role/role.module';
|
||||
import { ScannerModule } from './modules/scanner/scanner.module';
|
||||
import { SeedModule } from './modules/seed/seed.module';
|
||||
@@ -47,6 +48,7 @@ import { UserModule } from './modules/user/user.module';
|
||||
BookmarkModule,
|
||||
AnnotationModule,
|
||||
CbzModule,
|
||||
ReaderPreferencesModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||
|
||||
@@ -215,6 +215,22 @@ CREATE TABLE "bookmarks" (
|
||||
"created_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "reader_default_preferences" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"user_id" integer NOT NULL,
|
||||
"format_group" varchar(10) NOT NULL,
|
||||
"settings" jsonb NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "reader_preferences" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"user_id" integer NOT NULL,
|
||||
"book_file_id" integer NOT NULL,
|
||||
"settings" jsonb NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "reading_progress" (
|
||||
"book_file_id" integer NOT NULL,
|
||||
"user_id" integer NOT NULL,
|
||||
@@ -267,6 +283,9 @@ ALTER TABLE "annotations" ADD CONSTRAINT "annotations_user_id_users_id_fk" FOREI
|
||||
ALTER TABLE "annotations" ADD CONSTRAINT "annotations_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "bookmarks" ADD CONSTRAINT "bookmarks_book_id_books_id_fk" FOREIGN KEY ("book_id") REFERENCES "public"."books"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "reader_default_preferences" ADD CONSTRAINT "reader_default_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "reader_preferences" ADD CONSTRAINT "reader_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "reader_preferences" ADD CONSTRAINT "reader_preferences_book_file_id_book_files_id_fk" FOREIGN KEY ("book_file_id") REFERENCES "public"."book_files"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "reading_progress" ADD CONSTRAINT "reading_progress_book_file_id_book_files_id_fk" FOREIGN KEY ("book_file_id") REFERENCES "public"."book_files"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "reading_progress" ADD CONSTRAINT "reading_progress_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "oidc_group_mappings" ADD CONSTRAINT "oidc_group_mappings_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
@@ -275,6 +294,8 @@ CREATE INDEX "password_reset_tokens_user_id_idx" ON "password_reset_tokens" USIN
|
||||
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "annotations_user_id_idx" ON "annotations" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "bookmarks_user_id_idx" ON "bookmarks" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "rdp_user_format_idx" ON "reader_default_preferences" USING btree ("user_id","format_group");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "rp_user_file_idx" ON "reader_preferences" USING btree ("user_id","book_file_id");--> statement-breakpoint
|
||||
CREATE INDEX "reading_progress_user_id_idx" ON "reading_progress" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "oidc_sessions_user_id_idx" ON "oidc_sessions" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "oidc_sessions_subject_issuer_idx" ON "oidc_sessions" USING btree ("oidc_subject","oidc_issuer");--> statement-breakpoint
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "05501c4f-0a30-4fac-961f-d84a298725a2",
|
||||
"id": "93dc2d98-8ab8-41bc-8cc1-8cbbe6f34df1",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@@ -41,9 +41,7 @@
|
||||
"app_settings_key_unique": {
|
||||
"name": "app_settings_key_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"key"
|
||||
]
|
||||
"columns": ["key"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -114,12 +112,8 @@
|
||||
"name": "password_reset_tokens_user_id_users_id_fk",
|
||||
"tableFrom": "password_reset_tokens",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -129,9 +123,7 @@
|
||||
"password_reset_tokens_token_hash_unique": {
|
||||
"name": "password_reset_tokens_token_hash_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token_hash"
|
||||
]
|
||||
"columns": ["token_hash"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -182,9 +174,7 @@
|
||||
"permissions_name_unique": {
|
||||
"name": "permissions_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -255,12 +245,8 @@
|
||||
"name": "refresh_tokens_user_id_users_id_fk",
|
||||
"tableFrom": "refresh_tokens",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -270,9 +256,7 @@
|
||||
"refresh_tokens_token_hash_unique": {
|
||||
"name": "refresh_tokens_token_hash_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token_hash"
|
||||
]
|
||||
"columns": ["token_hash"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -302,12 +286,8 @@
|
||||
"name": "role_permissions_role_id_roles_id_fk",
|
||||
"tableFrom": "role_permissions",
|
||||
"tableTo": "roles",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["role_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -315,12 +295,8 @@
|
||||
"name": "role_permissions_permission_id_permissions_id_fk",
|
||||
"tableFrom": "role_permissions",
|
||||
"tableTo": "permissions",
|
||||
"columnsFrom": [
|
||||
"permission_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["permission_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -328,10 +304,7 @@
|
||||
"compositePrimaryKeys": {
|
||||
"role_permissions_role_id_permission_id_pk": {
|
||||
"name": "role_permissions_role_id_permission_id_pk",
|
||||
"columns": [
|
||||
"role_id",
|
||||
"permission_id"
|
||||
]
|
||||
"columns": ["role_id", "permission_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
@@ -390,9 +363,7 @@
|
||||
"roles_name_unique": {
|
||||
"name": "roles_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -430,12 +401,8 @@
|
||||
"name": "user_library_access_user_id_users_id_fk",
|
||||
"tableFrom": "user_library_access",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -443,12 +410,8 @@
|
||||
"name": "user_library_access_library_id_libraries_id_fk",
|
||||
"tableFrom": "user_library_access",
|
||||
"tableTo": "libraries",
|
||||
"columnsFrom": [
|
||||
"library_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["library_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -456,10 +419,7 @@
|
||||
"compositePrimaryKeys": {
|
||||
"user_library_access_user_id_library_id_pk": {
|
||||
"name": "user_library_access_user_id_library_id_pk",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"library_id"
|
||||
]
|
||||
"columns": ["user_id", "library_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
@@ -490,12 +450,8 @@
|
||||
"name": "user_roles_user_id_users_id_fk",
|
||||
"tableFrom": "user_roles",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -503,12 +459,8 @@
|
||||
"name": "user_roles_role_id_roles_id_fk",
|
||||
"tableFrom": "user_roles",
|
||||
"tableTo": "roles",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["role_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -516,10 +468,7 @@
|
||||
"compositePrimaryKeys": {
|
||||
"user_roles_user_id_role_id_pk": {
|
||||
"name": "user_roles_user_id_role_id_pk",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"role_id"
|
||||
]
|
||||
"columns": ["user_id", "role_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
@@ -636,16 +585,12 @@
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
"columns": ["username"]
|
||||
},
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -744,12 +689,8 @@
|
||||
"name": "book_files_book_id_books_id_fk",
|
||||
"tableFrom": "book_files",
|
||||
"tableTo": "books",
|
||||
"columnsFrom": [
|
||||
"book_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["book_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -757,12 +698,8 @@
|
||||
"name": "book_files_library_folder_id_library_folders_id_fk",
|
||||
"tableFrom": "book_files",
|
||||
"tableTo": "library_folders",
|
||||
"columnsFrom": [
|
||||
"library_folder_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["library_folder_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -829,12 +766,8 @@
|
||||
"name": "books_library_id_libraries_id_fk",
|
||||
"tableFrom": "books",
|
||||
"tableTo": "libraries",
|
||||
"columnsFrom": [
|
||||
"library_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["library_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -842,12 +775,8 @@
|
||||
"name": "books_library_folder_id_library_folders_id_fk",
|
||||
"tableFrom": "books",
|
||||
"tableTo": "library_folders",
|
||||
"columnsFrom": [
|
||||
"library_folder_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["library_folder_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1020,12 +949,8 @@
|
||||
"name": "library_folders_library_id_libraries_id_fk",
|
||||
"tableFrom": "library_folders",
|
||||
"tableTo": "libraries",
|
||||
"columnsFrom": [
|
||||
"library_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["library_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1097,12 +1022,8 @@
|
||||
"name": "book_authors_book_id_books_id_fk",
|
||||
"tableFrom": "book_authors",
|
||||
"tableTo": "books",
|
||||
"columnsFrom": [
|
||||
"book_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["book_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1110,12 +1031,8 @@
|
||||
"name": "book_authors_author_id_authors_id_fk",
|
||||
"tableFrom": "book_authors",
|
||||
"tableTo": "authors",
|
||||
"columnsFrom": [
|
||||
"author_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["author_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1123,10 +1040,7 @@
|
||||
"compositePrimaryKeys": {
|
||||
"book_authors_book_id_author_id_pk": {
|
||||
"name": "book_authors_book_id_author_id_pk",
|
||||
"columns": [
|
||||
"book_id",
|
||||
"author_id"
|
||||
]
|
||||
"columns": ["book_id", "author_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
@@ -1224,12 +1138,8 @@
|
||||
"name": "book_metadata_book_id_books_id_fk",
|
||||
"tableFrom": "book_metadata",
|
||||
"tableTo": "books",
|
||||
"columnsFrom": [
|
||||
"book_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["book_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1263,12 +1173,8 @@
|
||||
"name": "book_tags_book_id_books_id_fk",
|
||||
"tableFrom": "book_tags",
|
||||
"tableTo": "books",
|
||||
"columnsFrom": [
|
||||
"book_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["book_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1276,12 +1182,8 @@
|
||||
"name": "book_tags_tag_id_tags_id_fk",
|
||||
"tableFrom": "book_tags",
|
||||
"tableTo": "tags",
|
||||
"columnsFrom": [
|
||||
"tag_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["tag_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1289,10 +1191,7 @@
|
||||
"compositePrimaryKeys": {
|
||||
"book_tags_book_id_tag_id_pk": {
|
||||
"name": "book_tags_book_id_tag_id_pk",
|
||||
"columns": [
|
||||
"book_id",
|
||||
"tag_id"
|
||||
]
|
||||
"columns": ["book_id", "tag_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
@@ -1324,9 +1223,7 @@
|
||||
"tags_name_unique": {
|
||||
"name": "tags_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -1409,12 +1306,8 @@
|
||||
"name": "scan_jobs_library_id_libraries_id_fk",
|
||||
"tableFrom": "scan_jobs",
|
||||
"tableTo": "libraries",
|
||||
"columnsFrom": [
|
||||
"library_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["library_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1522,12 +1415,8 @@
|
||||
"name": "annotations_user_id_users_id_fk",
|
||||
"tableFrom": "annotations",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1535,12 +1424,8 @@
|
||||
"name": "annotations_book_id_books_id_fk",
|
||||
"tableFrom": "annotations",
|
||||
"tableTo": "books",
|
||||
"columnsFrom": [
|
||||
"book_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["book_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1615,12 +1500,8 @@
|
||||
"name": "bookmarks_user_id_users_id_fk",
|
||||
"tableFrom": "bookmarks",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1628,12 +1509,169 @@
|
||||
"name": "bookmarks_book_id_books_id_fk",
|
||||
"tableFrom": "bookmarks",
|
||||
"tableTo": "books",
|
||||
"columnsFrom": [
|
||||
"book_id"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
"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": {},
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
@@ -1709,12 +1747,8 @@
|
||||
"name": "reading_progress_book_file_id_book_files_id_fk",
|
||||
"tableFrom": "reading_progress",
|
||||
"tableTo": "book_files",
|
||||
"columnsFrom": [
|
||||
"book_file_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["book_file_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1722,12 +1756,8 @@
|
||||
"name": "reading_progress_user_id_users_id_fk",
|
||||
"tableFrom": "reading_progress",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1735,10 +1765,7 @@
|
||||
"compositePrimaryKeys": {
|
||||
"reading_progress_book_file_id_user_id_pk": {
|
||||
"name": "reading_progress_book_file_id_user_id_pk",
|
||||
"columns": [
|
||||
"book_file_id",
|
||||
"user_id"
|
||||
]
|
||||
"columns": ["book_file_id", "user_id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
@@ -1782,12 +1809,8 @@
|
||||
"name": "oidc_group_mappings_role_id_roles_id_fk",
|
||||
"tableFrom": "oidc_group_mappings",
|
||||
"tableTo": "roles",
|
||||
"columnsFrom": [
|
||||
"role_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["role_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1797,9 +1820,7 @@
|
||||
"oidc_group_mappings_oidc_group_claim_unique": {
|
||||
"name": "oidc_group_mappings_oidc_group_claim_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"oidc_group_claim"
|
||||
]
|
||||
"columns": ["oidc_group_claim"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -1919,12 +1940,8 @@
|
||||
"name": "oidc_sessions_user_id_users_id_fk",
|
||||
"tableFrom": "oidc_sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1940,11 +1957,7 @@
|
||||
"public.library_access_level": {
|
||||
"name": "library_access_level",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"viewer",
|
||||
"editor",
|
||||
"owner"
|
||||
]
|
||||
"values": ["viewer", "editor", "owner"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
@@ -1957,4 +1970,4 @@
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1768523691867,
|
||||
"when": 1768596999296,
|
||||
"tag": "0000_baseline",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { index, integer, pgTable, primaryKey, real, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
|
||||
import { index, integer, jsonb, pgTable, primaryKey, real, serial, text, timestamp, uniqueIndex, varchar } from 'drizzle-orm/pg-core';
|
||||
|
||||
import { bookFiles, books } from './books';
|
||||
import { users } from './auth';
|
||||
@@ -69,3 +69,39 @@ export const annotations = pgTable(
|
||||
|
||||
export type AnnotationRow = typeof annotations.$inferSelect;
|
||||
export type NewAnnotation = typeof annotations.$inferInsert;
|
||||
|
||||
export const readerDefaultPreferences = pgTable(
|
||||
'reader_default_preferences',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
formatGroup: varchar('format_group', { length: 10 }).notNull(),
|
||||
settings: jsonb('settings').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(t) => [uniqueIndex('rdp_user_format_idx').on(t.userId, t.formatGroup)],
|
||||
);
|
||||
|
||||
export type ReaderDefaultPreference = typeof readerDefaultPreferences.$inferSelect;
|
||||
export type NewReaderDefaultPreference = typeof readerDefaultPreferences.$inferInsert;
|
||||
|
||||
export const readerPreferences = pgTable(
|
||||
'reader_preferences',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
bookFileId: integer('book_file_id')
|
||||
.notNull()
|
||||
.references(() => bookFiles.id, { onDelete: 'cascade' }),
|
||||
settings: jsonb('settings').notNull(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(t) => [uniqueIndex('rp_user_file_idx').on(t.userId, t.bookFileId)],
|
||||
);
|
||||
|
||||
export type ReaderPreference = typeof readerPreferences.$inferSelect;
|
||||
export type NewReaderPreference = typeof readerPreferences.$inferInsert;
|
||||
|
||||
@@ -12,10 +12,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([
|
||||
ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
(req: any) => req?.cookies?.access_token ?? null,
|
||||
]),
|
||||
jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), (req: any) => req?.cookies?.access_token ?? null]),
|
||||
secretOrKey: config.get<string>('auth.jwtSecret')!,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { IsObject } from 'class-validator';
|
||||
|
||||
export class UpsertPreferenceDto {
|
||||
@IsObject()
|
||||
settings: Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, ParseIntPipe, Put } from '@nestjs/common';
|
||||
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import type { RequestUser } from '../../common/types/request-user';
|
||||
import { UpsertPreferenceDto } from './dto/upsert-preference.dto';
|
||||
import { ReaderPreferencesService } from './reader-preferences.service';
|
||||
|
||||
@Controller('reader')
|
||||
export class ReaderPreferencesController {
|
||||
constructor(private readonly readerPreferencesService: ReaderPreferencesService) {}
|
||||
|
||||
@Get('preferences/:bookFileId')
|
||||
async getPreference(@Param('bookFileId', ParseIntPipe) bookFileId: number, @CurrentUser() user: RequestUser) {
|
||||
const pref = await this.readerPreferencesService.getPreference(user.id, bookFileId);
|
||||
return { settings: pref?.settings ?? null, isCustomized: !!pref };
|
||||
}
|
||||
|
||||
@Put('preferences/:bookFileId')
|
||||
@HttpCode(204)
|
||||
async upsertPreference(@Param('bookFileId', ParseIntPipe) bookFileId: number, @Body() dto: UpsertPreferenceDto, @CurrentUser() user: RequestUser) {
|
||||
await this.readerPreferencesService.upsertPreference(user.id, bookFileId, dto.settings);
|
||||
}
|
||||
|
||||
@Delete('preferences/:bookFileId')
|
||||
@HttpCode(204)
|
||||
async deletePreference(@Param('bookFileId', ParseIntPipe) bookFileId: number, @CurrentUser() user: RequestUser) {
|
||||
await this.readerPreferencesService.deletePreference(user.id, bookFileId);
|
||||
}
|
||||
|
||||
@Get('defaults')
|
||||
async getAllDefaults(@CurrentUser() user: RequestUser) {
|
||||
const rows = await this.readerPreferencesService.getAllDefaults(user.id);
|
||||
return Object.fromEntries(rows.map((r) => [r.formatGroup, r.settings]));
|
||||
}
|
||||
|
||||
@Put('defaults/:formatGroup')
|
||||
@HttpCode(204)
|
||||
async upsertDefault(@Param('formatGroup') formatGroup: string, @Body() dto: UpsertPreferenceDto, @CurrentUser() user: RequestUser) {
|
||||
await this.readerPreferencesService.upsertDefault(user.id, formatGroup, dto.settings);
|
||||
}
|
||||
|
||||
@Delete('defaults/:formatGroup')
|
||||
@HttpCode(204)
|
||||
async deleteDefault(@Param('formatGroup') formatGroup: string, @CurrentUser() user: RequestUser) {
|
||||
await this.readerPreferencesService.deleteDefault(user.id, formatGroup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ReaderPreferencesController } from './reader-preferences.controller';
|
||||
import { ReaderPreferencesRepository } from './reader-preferences.repository';
|
||||
import { ReaderPreferencesService } from './reader-preferences.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ReaderPreferencesController],
|
||||
providers: [ReaderPreferencesRepository, ReaderPreferencesService],
|
||||
})
|
||||
export class ReaderPreferencesModule {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
|
||||
import { DB } from '../../db';
|
||||
import * as schema from '../../db/schema';
|
||||
|
||||
type Db = NodePgDatabase<typeof schema>;
|
||||
|
||||
@Injectable()
|
||||
export class ReaderPreferencesRepository {
|
||||
constructor(@Inject(DB) private readonly db: Db) {}
|
||||
|
||||
async findPreference(userId: number, bookFileId: number) {
|
||||
return this.db.query.readerPreferences.findFirst({
|
||||
where: and(eq(schema.readerPreferences.userId, userId), eq(schema.readerPreferences.bookFileId, bookFileId)),
|
||||
});
|
||||
}
|
||||
|
||||
async upsertPreference(userId: number, bookFileId: number, settings: Record<string, unknown>) {
|
||||
await this.db
|
||||
.insert(schema.readerPreferences)
|
||||
.values({ userId, bookFileId, settings, updatedAt: new Date() })
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.readerPreferences.userId, schema.readerPreferences.bookFileId],
|
||||
set: { settings, updatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async deletePreference(userId: number, bookFileId: number) {
|
||||
await this.db
|
||||
.delete(schema.readerPreferences)
|
||||
.where(and(eq(schema.readerPreferences.userId, userId), eq(schema.readerPreferences.bookFileId, bookFileId)));
|
||||
}
|
||||
|
||||
async findAllDefaults(userId: number) {
|
||||
return this.db.query.readerDefaultPreferences.findMany({
|
||||
where: eq(schema.readerDefaultPreferences.userId, userId),
|
||||
});
|
||||
}
|
||||
|
||||
async upsertDefault(userId: number, formatGroup: string, settings: Record<string, unknown>) {
|
||||
await this.db
|
||||
.insert(schema.readerDefaultPreferences)
|
||||
.values({ userId, formatGroup, settings, updatedAt: new Date() })
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.readerDefaultPreferences.userId, schema.readerDefaultPreferences.formatGroup],
|
||||
set: { settings, updatedAt: new Date() },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDefault(userId: number, formatGroup: string) {
|
||||
await this.db
|
||||
.delete(schema.readerDefaultPreferences)
|
||||
.where(and(eq(schema.readerDefaultPreferences.userId, userId), eq(schema.readerDefaultPreferences.formatGroup, formatGroup)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { ReaderPreferencesRepository } from './reader-preferences.repository';
|
||||
|
||||
const VALID_FORMAT_GROUPS = ['epub', 'pdf', 'cbx'] as const;
|
||||
type FormatGroup = (typeof VALID_FORMAT_GROUPS)[number];
|
||||
|
||||
function assertValidFormatGroup(formatGroup: string): asserts formatGroup is FormatGroup {
|
||||
if (!VALID_FORMAT_GROUPS.includes(formatGroup as FormatGroup)) {
|
||||
throw new BadRequestException(`Invalid format group "${formatGroup}". Must be one of: ${VALID_FORMAT_GROUPS.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReaderPreferencesService {
|
||||
constructor(private readonly repo: ReaderPreferencesRepository) {}
|
||||
|
||||
async getPreference(userId: number, bookFileId: number) {
|
||||
return this.repo.findPreference(userId, bookFileId);
|
||||
}
|
||||
|
||||
async upsertPreference(userId: number, bookFileId: number, settings: Record<string, unknown>) {
|
||||
await this.repo.upsertPreference(userId, bookFileId, settings);
|
||||
}
|
||||
|
||||
async deletePreference(userId: number, bookFileId: number) {
|
||||
await this.repo.deletePreference(userId, bookFileId);
|
||||
}
|
||||
|
||||
async getAllDefaults(userId: number) {
|
||||
return this.repo.findAllDefaults(userId);
|
||||
}
|
||||
|
||||
async upsertDefault(userId: number, formatGroup: string, settings: Record<string, unknown>) {
|
||||
assertValidFormatGroup(formatGroup);
|
||||
await this.repo.upsertDefault(userId, formatGroup, settings);
|
||||
}
|
||||
|
||||
async deleteDefault(userId: number, formatGroup: string) {
|
||||
assertValidFormatGroup(formatGroup);
|
||||
await this.repo.deleteDefault(userId, formatGroup);
|
||||
}
|
||||
}
|
||||
@@ -158,7 +158,13 @@ export class UserRepository {
|
||||
}
|
||||
|
||||
async update(id: number, data: Partial<Pick<typeof schema.users.$inferInsert, 'name' | 'email' | 'active' | 'settings'>>) {
|
||||
const [user] = await this.db.update(schema.users).set(data).where(eq(schema.users.id, id)).returning({
|
||||
const { settings, ...rest } = data;
|
||||
const setData: Record<string, unknown> = { ...rest };
|
||||
if (settings !== undefined) {
|
||||
// Merge into existing settings rather than replacing the whole object
|
||||
setData.settings = sql`${schema.users.settings} || ${JSON.stringify(settings)}::jsonb`;
|
||||
}
|
||||
const [user] = await this.db.update(schema.users).set(setData).where(eq(schema.users.id, id)).returning({
|
||||
id: schema.users.id,
|
||||
username: schema.users.username,
|
||||
name: schema.users.name,
|
||||
|
||||
@@ -17,9 +17,6 @@ describe('AppController (e2e)', () => {
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user