import { number, string, z } from 'zod'; import type { components } from './openapi/schema'; import { arrayCoerce, commonQuerySchema, stringArrayCoerce, stringCoerce } from './common'; export type Book = components['schemas']['BookRead']; export type BookFile = components['schemas']['FileMetadataRead']; export type BookProgress = components['schemas']['BookProgressRead']; /** * A file the library already held, so the upload did not store it again. * * `book_id` is null when the match was another file in the same upload — there is * no book to point at yet. */ export type DuplicateFile = components['schemas']['DuplicateFileRead']; export type BooksUploadResult = components['schemas']['BooksUploadResult']; /** * A stored book that may be the same book as another one. * * Weaker than `DuplicateFile`, and deliberately so: that one means the library holds * these exact bytes, this one means the metadata agrees. A second edition and a * translation both look like this, so nothing is ever refused on the strength of it. */ export type DuplicateBook = components['schemas']['DuplicateBookRead']; /** A book that was imported, together with what it might be a second copy of. */ export type PossibleDuplicate = components['schemas']['PossibleDuplicateRead']; /** Books the library holds that all look like copies of one book. */ export type DuplicateBookGroup = components['schemas']['DuplicateBookGroupRead']; /** The metadata a reader resolved while merging, or while reviewing a provider. */ export type BookMetadataUpdate = components['schemas']['BookMetadataUpdate']; /** Mirrors BookMerge in backend/src/chitai/schemas/book.py */ export const bookMergeSchema = z.object({ library_id: z.coerce.number(), survivor_id: z.coerce.number(), merged_ids: z.array(z.coerce.number()).min(1, 'Pick at least one book to fold in'), // Passed through untouched — the backend validates it as BookMetadataUpdate, and // duplicating that shape here would be two places to keep in step for no gain. metadata: z.record(z.string(), z.unknown()).optional() }); /** Mirrors DuplicateDismissal in backend/src/chitai/schemas/book.py */ export const duplicateDismissalSchema = z.object({ book_a_id: z.coerce.number(), book_b_id: z.coerce.number() }); export const bookQuerySchema = commonQuerySchema.extend({ libraries: stringArrayCoerce, authors: stringArrayCoerce, publishers: stringArrayCoerce, tags: stringArrayCoerce, shelves: stringArrayCoerce, progress: arrayCoerce(z.enum(['unread', 'in_progress', 'read'])) .nullable() .optional(), ids: stringArrayCoerce, cacheTimestamp: z.number().optional() }); const identifiersSchema = z .string() .transform((str, ctx) => { try { return JSON.parse(str); } catch { ctx.addIssue({ code: 'custom', message: 'Must be a valid JSON string' }); return z.NEVER; } }) .pipe( z.record(z.string(), z.string()) // Key-value pairs, both strings ); export const editBookMetadataSchema = z.object({ book_id: z.string(), title: string().min(1, 'Title cannot be empty'), subtitle: string().optional(), authors: z.array(z.string()).default([]), tags: z.array(z.string()).default([]), description: z.string().optional(), publisher: z.string().optional(), published_date: z .string() .optional() .transform((val) => (val ? new Date(val) : undefined)), series: z.string().optional(), series_position: z.string().optional(), identifiers: identifiersSchema, edition: z.number().optional(), pages: z.number().optional(), language: z.string().optional() }); export const bookCoverUpload = z.object({ book_id: stringCoerce, file: z.file() }); export const booksUpload = z.object({ library_id: stringCoerce, files: z.array(z.file()).min(1, 'At least one file must be uploaded') }); export const bookFilesUpload = z.object({ book_id: stringCoerce, files: z.array(z.file()).min(1, 'At least one file must be uploaded') }); export const deleteBooksSchema = z.object({ book_ids: stringArrayCoerce, // TODO Could also set minimum length of 1 delete_files: z.boolean(), library_id: stringCoerce }); export const deleteBookFilesSchema = z.object({ book_id: stringCoerce, file_ids: stringArrayCoerce, delete_files: z.boolean() }); // Mirrors BookProgressCreate in backend/src/chitai/schemas/book.py export const updateBookProgressSchema = z.object({ book_ids: stringArrayCoerce, percentage: number().min(0).max(1), completed: z.boolean().optional().default(false), epub_cfi: z.string().optional(), epub_xpointer: z.string().optional(), pdf_page: z.number().int().optional() }); export type BookQuery = z.infer; export type BookEditMetadata = z.infer; export type DeleteBook = z.infer; export type DeleteBookFiles = z.infer; export type DuplicateDismissal = z.infer; export type BookMerge = z.infer; export type UpdateBookProgress = z.infer;