A workbench with the folded records in a rail, the survivor's fields live and editable beside them, and per-field actions chosen by what kind of field it is. Fields the records agree on stay out of the way. Reachable from a duplicate group and from the selection toolbar, which is the only way to merge a pair the detector never proposed.
233 lines
6.4 KiB
TypeScript
233 lines
6.4 KiB
TypeScript
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<Book> => {
|
|
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<PaginatedResponse<Book>> => {
|
|
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<Book> => {
|
|
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<BooksUploadResult> => {
|
|
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<DuplicateBookGroup[]> => {
|
|
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<Book> => {
|
|
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);
|
|
}
|
|
}
|
|
);
|