/** * A tiny in-memory "database" for the demo. * * Catalog books use `@x12i/api-simulator/store`. Active checkouts stay on a Map * keyed by book+member (composite key), which is a natural fit alongside createStore. */ import { createStore } from '@x12i/api-simulator/store'; import { books, type Book } from '../data/books.js'; export interface CheckoutRecord { bookId: string; memberId: string; checkedOutAt: string; dueAt: string; } const CHECKOUT_DAYS = 14; const catalog = createStore(books.map((book) => ({ ...book }))); const activeCheckouts = new Map(); const checkoutKey = (bookId: string, memberId: string): string => `${bookId}::${memberId}`; export class LibraryError extends Error { readonly status: number; readonly code: string; constructor(status: number, code: string, message: string) { super(message); this.name = 'LibraryError'; this.status = status; this.code = code; } } export const findBook = (bookId: string): Book | undefined => catalog.get(bookId); export const listBooks = (): Book[] => catalog.all(); export const checkoutBook = (bookId: string, memberId: string): CheckoutRecord => { const book = catalog.get(bookId); if (!book) throw new LibraryError(404, 'BOOK_NOT_FOUND', `No book with id ${bookId}.`); if (book.copiesAvailable < 1) { throw new LibraryError(409, 'BOOK_UNAVAILABLE', `${book.title} has no available copies.`); } catalog.update(bookId, { copiesAvailable: book.copiesAvailable - 1 }); const checkedOutAt = new Date(); const dueAt = new Date(checkedOutAt.getTime() + CHECKOUT_DAYS * 24 * 60 * 60 * 1000); const record: CheckoutRecord = { bookId, memberId, checkedOutAt: checkedOutAt.toISOString(), dueAt: dueAt.toISOString() }; activeCheckouts.set(checkoutKey(bookId, memberId), record); return record; }; export const returnBook = (bookId: string, memberId: string): CheckoutRecord => { const key = checkoutKey(bookId, memberId); const record = activeCheckouts.get(key); if (!record) { throw new LibraryError( 409, 'NO_ACTIVE_CHECKOUT', `Member ${memberId} has no active checkout for book ${bookId}.` ); } const book = catalog.get(bookId); if (book) { catalog.update(bookId, { copiesAvailable: Math.min(book.copiesTotal, book.copiesAvailable + 1) }); } activeCheckouts.delete(key); return record; }; export const checkoutsForMember = (memberId: string): CheckoutRecord[] => [...activeCheckouts.values()].filter((record) => record.memberId === memberId); export const resetLibraryState = (): void => { catalog.reset(); activeCheckouts.clear(); };