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.
146 lines
4.9 KiB
TypeScript
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>;
|