import { command, form, getRequestEvent, query } from '$app/server'; import { error } from '@sveltejs/kit'; import { bookCoverUpload, booksUpload, bookQuerySchema, deleteBookFilesSchema, deleteBooksSchema, editBookMetadataSchema, updateBookProgressSchema, duplicateDismissalSchema, bookMergeSchema, type Book, type BooksUploadResult, type DuplicateBookGroup, bookFilesUpload } from '$lib/schema/index'; import { stringCoerce, type PaginatedResponse } from '$lib/schema/common'; import { createQueryParams } from '$lib/utils'; /** * The backend's own message for a failed response, rather than its JSON envelope. * * A refused duplicate answers 409 with a `detail` worth reading and the offending * files in `extra`; passing the body through whole puts JSON in front of the reader. */ function detailOf(body: string): string { try { const parsed = JSON.parse(body); return typeof parsed?.detail === 'string' ? parsed.detail : body; } catch { return body; } } export const getBook = query(stringCoerce, async (id): Promise => { const { locals } = getRequestEvent(); const response = await locals.api.get(`/books/${id}`); if (!response.ok) { if (response.status == 404) error(404, 'The book does not exist'); error(500, 'An unkown error occurred'); } return await response.json(); }); export const listBooks = query(bookQuerySchema, async (data): Promise> => { const { locals } = getRequestEvent(); const params = createQueryParams(data); const response = await locals.api.get(`/books?${params.toString()}`); if (!response.ok) error(500, 'An unkown error occurred'); return await response.json(); }); export const updateBookMetadata = form(editBookMetadataSchema, async (data): Promise => { const { locals } = getRequestEvent(); const response = await locals.api.patch(`/books/${data.book_id}`, data); if (!response.ok) { const message = await response.text(); error(response.status, message); } return await response.json(); }); export const updateBookCover = form(bookCoverUpload, async ({ book_id, file }) => { const { locals } = getRequestEvent(); const formData = new FormData(); formData.append('file', file); const response = await locals.api.putMultipart(`/books/${book_id}/cover`, formData); if (!response.ok) { const message = await response.text(); error(response.status, message); } return await response.json(); }); export const uploadBooks = form( booksUpload, async ({ library_id, files }): Promise => { const { locals } = getRequestEvent(); const formData = new FormData(); files.forEach((file) => { formData.append('files', file); }); const response = await locals.api.postMultipart( `/books/fromFiles?library_id=${library_id}`, formData ); if (!response.ok) { const message = await response.text(); error(response.status, message); } return await response.json(); } ); export const uploadBookFiles = form(bookFilesUpload, async ({ book_id, files }) => { const { locals } = getRequestEvent(); const formData = new FormData(); files.forEach((file) => { formData.append('files', file); }); const response = await locals.api.postMultipart(`/books/${book_id}/files`, formData); if (!response.ok) { // 409 here means the file is already stored under a different book, which is // something the reader can act on — so the message has to survive the trip. error(response.status, detailOf(await response.text())); } return await response.json(); }); export const deleteBooks = command(deleteBooksSchema, async (data) => { const { locals } = getRequestEvent(); const params = createQueryParams(data); const response = await locals.api.delete(`/books?${params.toString()}`); if (!response.ok) { const message = await response.text(); error(response.status, message); } }); export const deleteBookFiles = command(deleteBookFilesSchema, async ({ book_id, ...data }) => { const { locals } = getRequestEvent(); const params = createQueryParams(data); const response = await locals.api.delete(`/books/${book_id}/files?${params.toString()}`); if (!response.ok) { const message = await response.text(); error(response.status, message); } }); /** * Books already in the library that look like copies of one another. * * Metadata only, so every group is a question rather than a verdict — which is why * the screen it feeds offers a way to disagree. */ export const listDuplicateBooks = query( stringCoerce, async (libraryId): Promise => { const { locals } = getRequestEvent(); const response = await locals.api.get(`/books/duplicate-books?library_id=${libraryId}`); if (!response.ok) error(response.status, detailOf(await response.text())); return await response.json(); } ); /** * Fold several books into one and delete the records folded in. * * Irreversible, so the caller is expected to have shown what is about to happen. */ export const mergeBooks = command( bookMergeSchema, async ({ library_id, ...data }): Promise => { const { locals } = getRequestEvent(); const response = await locals.api.post(`/books/merge?library_id=${library_id}`, data); if (!response.ok) error(response.status, detailOf(await response.text())); return await response.json(); } ); /** Record that two books are not the same book, so the pair stops being proposed. */ export const dismissDuplicateBooks = command(duplicateDismissalSchema, async (data) => { const { locals } = getRequestEvent(); const response = await locals.api.post('/books/duplicate-books/dismissals', data); if (!response.ok) error(response.status, detailOf(await response.text())); }); /** Undo a dismissal, so the pair is proposed again. */ export const restoreDuplicateBooks = command(duplicateDismissalSchema, async (data) => { const { locals } = getRequestEvent(); const params = createQueryParams(data); const response = await locals.api.delete( `/books/duplicate-books/dismissals?${params.toString()}` ); if (!response.ok) error(response.status, detailOf(await response.text())); }); export const updateBookProgress = command( updateBookProgressSchema, async ({ book_ids, ...data }) => { const { locals } = getRequestEvent(); const params = createQueryParams({ book_ids: book_ids }); const response = await locals.api.post(`/books/progress?${params.toString()}`, { ...data }); if (!response.ok) { const message = await response.text(); error(response.status, message); } } );