Files
chitai/frontend/src/lib/schema/book.ts
T
patrick 3a29294f96 chore: clear the mechanical lint and type findings
Dead imports and locals removed, each blocks keyed, `any` narrowed to unknown.
Two context setters kept their calls and lost only the unused binding; the
settings redirect no longer awaits a parent whose data it discards. A leading
underscore now marks a binding that only holds a position.
2026-08-17 17:55:21 -04:00

146 lines
4.9 KiB
TypeScript

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<typeof bookQuerySchema>;
export type BookEditMetadata = z.infer<typeof editBookMetadataSchema>;
export type DeleteBook = z.infer<typeof deleteBooksSchema>;
export type DeleteBookFiles = z.infer<typeof deleteBookFilesSchema>;
export type DuplicateDismissal = z.infer<typeof duplicateDismissalSchema>;
export type BookMerge = z.infer<typeof bookMergeSchema>;
export type UpdateBookProgress = z.infer<typeof updateBookProgressSchema>;