/** * Alexandria API Type Exports * * These types can be imported by other services (e.g., bendv3) for type-safe * integration with the Alexandria API. * * @example * ```typescript * import type { SearchQuery, SearchResult, BookResult } from '@ooheynerds/alexandria-worker'; * ``` */ import { z } from 'zod'; // ================================================================================= // Request Schemas (Zod) // ================================================================================= export const SearchQuerySchema = z.object({ isbn: z.string() .transform((val) => { if (!val) return undefined; const clean = val.replace(/[^0-9X]/gi, '').toUpperCase(); if (clean.length !== 10 && clean.length !== 13) { throw new Error("Invalid ISBN format. Must be 10 or 13 characters (digits and 'X' for ISBN-10 check digit)"); } return val; }) .optional(), title: z.string().optional(), author: z.string().optional(), limit: z.string().optional().transform((val) => { const parsed = val ? parseInt(val, 10) : 10; return Math.max(1, Math.min(100, parsed)); }), offset: z.string().optional().transform((val) => { const parsed = val ? parseInt(val, 10) : 0; return Math.max(0, parsed); }), }); export const CombinedSearchQuerySchema = z.object({ q: z.string().min(1, 'Query parameter "q" is required'), limit: z.string().optional().transform((val) => { const parsed = val ? parseInt(val, 10) : 10; return Math.max(1, Math.min(100, parsed)); }), offset: z.string().optional().transform((val) => { const parsed = val ? parseInt(val, 10) : 0; return Math.max(0, parsed); }), }); export const CoverBatchSchema = z.object({ isbns: z.array(z.string()).min(1).max(10), }); export const ProcessCoverSchema = z.object({ work_key: z.string(), provider_url: z.string().url(), isbn: z.string().optional(), }); export const EnrichEditionSchema = z.object({ isbn: z.string(), title: z.string().optional(), subtitle: z.string().optional(), publisher: z.string().optional(), publication_date: z.string().optional(), page_count: z.number().optional(), format: z.string().optional(), language: z.string().optional(), primary_provider: z.enum(['isbndb', 'google-books', 'openlibrary', 'user-correction']), cover_urls: z.object({ large: z.string().optional(), medium: z.string().optional(), small: z.string().optional(), }).optional(), cover_source: z.string().optional(), work_key: z.string().optional(), openlibrary_edition_id: z.string().optional(), amazon_asins: z.array(z.string()).optional(), google_books_volume_ids: z.array(z.string()).optional(), goodreads_edition_ids: z.array(z.string()).optional(), alternate_isbns: z.array(z.string()).optional(), }); export const EnrichWorkSchema = z.object({ work_key: z.string(), title: z.string(), subtitle: z.string().optional(), description: z.string().optional(), original_language: z.string().optional(), first_publication_year: z.number().optional(), subject_tags: z.array(z.string()).optional(), primary_provider: z.enum(['isbndb', 'google-books', 'openlibrary']), cover_urls: z.object({ large: z.string().optional(), medium: z.string().optional(), small: z.string().optional(), }).optional(), cover_source: z.string().optional(), openlibrary_work_id: z.string().optional(), goodreads_work_ids: z.array(z.string()).optional(), amazon_asins: z.array(z.string()).optional(), google_books_volume_ids: z.array(z.string()).optional(), }); export const EnrichAuthorSchema = z.object({ author_key: z.string(), name: z.string(), gender: z.string().optional(), gender_qid: z.string().optional(), // Wikidata Q-ID for gender (e.g., Q6581097 = male) nationality: z.string().optional(), citizenship_qid: z.string().optional(), // Wikidata Q-ID for citizenship birth_year: z.number().optional(), death_year: z.number().optional(), birth_place: z.string().optional(), // City/town name birth_place_qid: z.string().optional(), // Wikidata Q-ID for birth place birth_country: z.string().optional(), // Country name (e.g., "United States") birth_country_qid: z.string().optional(), // Wikidata Q-ID for country (e.g., Q30) death_place: z.string().optional(), death_place_qid: z.string().optional(), bio: z.string().optional(), bio_source: z.string().optional(), author_photo_url: z.string().optional(), primary_provider: z.enum(['isbndb', 'openlibrary', 'wikidata']), openlibrary_author_id: z.string().optional(), goodreads_author_ids: z.array(z.string()).optional(), wikidata_id: z.string().optional(), }); export const QueueEnrichmentSchema = z.object({ entity_type: z.enum(['work', 'edition', 'author']), entity_key: z.string(), providers_to_try: z.array(z.string()), priority: z.number().min(1).max(10).default(5), }); // External ID Resolution Schemas (Issue #155) export const ExternalIdQuerySchema = z.object({ entity_type: z.enum(['edition', 'work', 'author']), key: z.string(), }); export const ResolveExternalIdSchema = z.object({ provider: z.string(), id: z.string(), type: z.enum(['edition', 'work', 'author']).default('edition').optional(), }); // Recommendation Schemas (Issue #164) export const SubjectsQuerySchema = z.object({ ids: z.string() .min(1, 'At least one ID required') .transform((val) => val.split(',').map(id => id.trim()).filter(Boolean)) .describe('Comma-separated ISBNs or work_keys'), limit: z.string() .optional() .default('1') .transform((val) => parseInt(val, 10)) .pipe(z.number().int().min(1).max(10)) .describe('Max results per ID'), nocache: z.string() .optional() .transform((val) => val === 'true') .describe('Bypass cache'), }); export const SimilarBooksQuerySchema = z.object({ subjects: z.string() .min(1, 'At least one subject required') .transform((val) => val.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)) .describe('Comma-separated subject tags'), exclude: z.string() .optional() .transform((val) => val ? val.split(',').map(id => id.trim()).filter(Boolean) : []) .describe('Work keys to exclude'), limit: z.string() .optional() .default('100') .transform((val) => parseInt(val, 10)) .pipe(z.number().int().min(1).max(500)) .describe('Max results'), min_overlap: z.string() .optional() .default('1') .transform((val) => parseInt(val, 10)) .pipe(z.number().int().min(1)) .describe('Minimum subject overlap required'), nocache: z.string() .optional() .transform((val) => val === 'true') .describe('Bypass cache'), }); // ================================================================================= // Inferred TypeScript Types from Zod Schemas // ================================================================================= export type SearchQuery = z.infer; export type CombinedSearchQuery = z.infer; export type CoverBatch = z.infer; export type ProcessCover = z.infer; export type EnrichEdition = z.infer; export type EnrichWork = z.infer; export type EnrichAuthor = z.infer; export type QueueEnrichment = z.infer; export type ExternalIdQuery = z.infer; export type ResolveExternalId = z.infer; export type SubjectsQuery = z.infer; export type SimilarBooksQuery = z.infer; // ================================================================================= // Response Types // ================================================================================= /** * Author reference in search results (enriched with metadata from enriched_authors table) * @since 2.2.3 - Added enriched author metadata fields */ export interface AuthorReference { name: string; key: string; // e.g., "/authors/OL7234434A" openlibrary: string; // e.g., "https://openlibrary.org/authors/OL7234434A" // Enriched metadata (from enriched_authors table) bio?: string | null; // Author biography gender?: string | null; // e.g., "male", "female", "Unknown" nationality?: string | null; // e.g., "United States", "British" birth_year?: number | null; // Birth year death_year?: number | null; // Death year wikidata_id?: string | null; // Wikidata identifier (e.g., "Q35064") image?: string | null; // Author photo URL (from author_photo_url column) } export interface BookResult { type?: 'edition' | 'work' | 'author'; // Present in combined search title: string; authors: AuthorReference[]; // Array of author objects with name, key, openlibrary URL isbn: string | null; // Cover images (dual format for backward compatibility) coverUrl: string | null; // Legacy: Single cover URL (typically large) coverUrls?: { // Modern: Multiple sizes via /covers/:isbn/:size endpoint large: string; medium: string; small: string; } | null; coverSource: 'r2' | 'external' | 'external-fallback' | 'enriched-cached' | null; publish_date: string | null; publishers: string | null; // Publisher name as string pages: number | null; // Page count as number work_title: string | null; openlibrary_edition: string | null; openlibrary_work: string | null; } export interface PaginationMetadata { limit: number; offset: number; total: number; hasMore: boolean; returnedCount: number; totalEstimated?: boolean; // For combined text searches } export interface SearchResult { query: { isbn?: string; title?: string; author?: string; }; query_duration_ms: number; count?: number; // Deprecated - use pagination.total results: BookResult[]; pagination: PaginationMetadata; cache_hit?: boolean; // Whether result was served from cache } export interface CombinedSearchResult { query: string; search_type: 'isbn' | 'text'; query_duration_ms: number; results: BookResult[]; pagination: PaginationMetadata; cache_hit?: boolean; // Whether result was served from cache } export interface HealthCheck { status: 'ok' | 'error'; database: 'connected' | 'disconnected'; r2_covers: 'bound' | 'not_configured'; hyperdrive_latency_ms?: number; timestamp: string; message?: string; } export interface DatabaseStats { editions: number; isbns: number; works: number; authors: number; query_duration_ms: number; } export interface CoverMetadata { format: string; size: number; uploaded: string; provider?: string; isbn?: string; } export interface CoverStatus { exists: boolean; isbn: string; format?: string; size?: number; uploaded?: string; provider?: string; urls?: { original: string; large: string; medium: string; small: string; }; } export interface CoverProcessResult { status: 'processed' | 'already_exists' | 'no_cover' | 'error'; isbn: string; provider?: string; metadata?: CoverMetadata; message?: string; error?: string; } export interface BatchCoverResult { total: number; successful: number; failed: number; results: Array<{ isbn: string; status: 'success' | 'error'; message?: string; }>; } export interface EnrichmentResult { status: 'created' | 'updated'; entity_type: 'edition' | 'work' | 'author'; entity_key: string; message?: string; } export interface EnrichmentQueueResult { status: 'queued'; job_id: string; entity_type: 'work' | 'edition' | 'author'; entity_key: string; priority: number; message: string; } export interface EnrichmentJobStatus { job_id: string; status: 'pending' | 'processing' | 'completed' | 'failed'; entity_type: 'work' | 'edition' | 'author'; entity_key: string; created_at: string; updated_at: string; result?: unknown; error?: string; } export interface ErrorResponse { error: string; message?: string; details?: unknown; } /** * Full author details including Wikidata diversity data * Returned by GET /api/authors/:key */ export interface AuthorDetails { author_key: string; // e.g., "/authors/OL7234434A" name: string; // Diversity fields (from Wikidata) gender: string | null; // e.g., "male", "female" gender_qid: string | null; // e.g., "Q6581097" (male) nationality: string | null; // e.g., "United States" citizenship_qid: string | null; // e.g., "Q30" // Birth/Death birth_year: number | null; death_year: number | null; birth_place: string | null; // City/town name birth_place_qid: string | null; birth_country: string | null; // Country name birth_country_qid: string | null; death_place: string | null; death_place_qid: string | null; // Biography bio: string | null; bio_source: string | null; // External IDs wikidata_id: string | null; // e.g., "Q18590295" openlibrary_author_id: string | null; goodreads_author_ids: string[] | null; // Metadata author_photo_url: string | null; book_count: number; wikidata_enriched_at: string | null; // ISO timestamp } /** * External ID mapping from crosswalk table * @since 2.3.0 - External ID Resolution (Issue #155) */ export interface ExternalId { provider: string; // e.g., "amazon", "goodreads", "google-books" provider_id: string; // External ID from provider confidence: number; // Confidence score (0-100) created_at?: string; // ISO timestamp } /** * External ID lookup response * @since 2.3.0 - External ID Resolution (Issue #155) */ export interface ExternalIdResult { success: boolean; data: ExternalId[]; meta: { source: 'crosswalk' | 'array_backfill'; backfilled: boolean; latency_ms?: number; }; } /** * Resolved entity from external ID * @since 2.3.0 - External ID Resolution (Issue #155) */ export interface ResolvedEntity { key: string; // Our internal key (ISBN, work_key, author_key) entity_type: 'edition' | 'work' | 'author'; confidence: number; // Confidence score of mapping } /** * Resolve external ID response * @since 2.3.0 - External ID Resolution (Issue #155) */ export interface ResolveExternalIdResult { success: boolean; data: ResolvedEntity; } /** * Single book subjects result * @since 2.4.0 - Recommendation System (Issue #164) */ export interface BookSubjects { id: string; // Original ID from request type: 'isbn' | 'work'; // Detected identifier type work_key: string | null; // OpenLibrary work key title: string | null; // Book title subjects: string[]; // Array of normalized subject tags match_source: 'enriched_works' | 'works_fallback' | 'not_found'; } /** * Subjects endpoint response data * @since 2.4.0 - Recommendation System (Issue #164) */ export interface SubjectsData { results: BookSubjects[]; total: number; // Number of IDs with subject data missing: string[]; // IDs without subject data query: { ids_count: number; limit: number; }; } /** * Similar book result (full metadata) * @since 2.4.0 - Recommendation System (Issue #164) */ export interface SimilarBook { work_key: string; title: string; isbn: string | null; subjects: string[]; // Array of normalized subject tags subject_match_count: number; // Number of matching subjects authors: AuthorReference[]; publish_date: string | null; publishers: string | null; pages: number | null; cover_url: string | null; cover_source: 'r2' | 'external' | 'external-fallback' | 'enriched-cached' | null; openlibrary_work: string; openlibrary_edition: string | null; } /** * Similar books endpoint response data * @since 2.4.0 - Recommendation System (Issue #164) */ export interface SimilarBooksData { results: SimilarBook[]; total: number; // Total matching works query: { subjects: string[]; // Normalized subjects from request excluded_count: number; // Number of excluded works min_overlap: number; }; } // ================================================================================= // API Client Types (for consumers like bendv3) // ================================================================================= /** * Alexandria API client configuration */ export interface AlexandriaClientConfig { baseUrl: string; timeout?: number; headers?: Record; } /** * Type-safe API endpoint paths */ export const ENDPOINTS = { HEALTH: '/health', STATS: '/api/stats', SEARCH: '/api/search', SEARCH_COMBINED: '/api/search/combined', AUTHOR_DETAILS: '/api/authors/:key', // Author diversity data ENRICH_EDITION: '/api/enrich/edition', ENRICH_WORK: '/api/enrich/work', ENRICH_AUTHOR: '/api/enrich/author', ENRICH_QUEUE: '/api/enrich/queue', ENRICH_STATUS: '/api/enrich/status', COVER_PROCESS: '/api/covers/process', COVER_SERVE: '/api/covers', COVER_ISBN_STATUS: '/covers/:isbn/status', COVER_ISBN_PROCESS: '/covers/:isbn/process', COVER_ISBN_SERVE: '/covers/:isbn/:size', COVER_BATCH: '/covers/batch', EXTERNAL_IDS: '/api/external-ids/:entity_type/:key', // External ID lookup (Issue #155) RESOLVE_EXTERNAL_ID: '/api/resolve/:provider/:id', // Reverse lookup (Issue #155) RECOMMENDATIONS_SUBJECTS: '/api/recommendations/subjects', // Recommendation subjects (Issue #164) RECOMMENDATIONS_SIMILAR: '/api/recommendations/similar', // Similar books (Issue #164) } as const; /** * HTTP methods used by Alexandria API */ export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; /** * API route definitions with types */ export interface APIRoute { method: HTTPMethod; path: string; requestSchema?: z.ZodSchema; responseType?: TResponse; } /** * Complete API surface area for type-safe integration */ export const API_ROUTES = { search: { method: 'GET', path: ENDPOINTS.SEARCH, requestSchema: SearchQuerySchema, } as APIRoute, searchCombined: { method: 'GET', path: ENDPOINTS.SEARCH_COMBINED, requestSchema: CombinedSearchQuerySchema, } as APIRoute, authorDetails: { method: 'GET', path: ENDPOINTS.AUTHOR_DETAILS, } as APIRoute<{ key: string }, AuthorDetails>, health: { method: 'GET', path: ENDPOINTS.HEALTH, } as APIRoute, stats: { method: 'GET', path: ENDPOINTS.STATS, } as APIRoute, enrichEdition: { method: 'POST', path: ENDPOINTS.ENRICH_EDITION, requestSchema: EnrichEditionSchema, } as APIRoute, enrichWork: { method: 'POST', path: ENDPOINTS.ENRICH_WORK, requestSchema: EnrichWorkSchema, } as APIRoute, enrichAuthor: { method: 'POST', path: ENDPOINTS.ENRICH_AUTHOR, requestSchema: EnrichAuthorSchema, } as APIRoute, queueEnrichment: { method: 'POST', path: ENDPOINTS.ENRICH_QUEUE, requestSchema: QueueEnrichmentSchema, } as APIRoute, processCover: { method: 'POST', path: ENDPOINTS.COVER_PROCESS, requestSchema: ProcessCoverSchema, } as APIRoute, batchCovers: { method: 'POST', path: ENDPOINTS.COVER_BATCH, requestSchema: CoverBatchSchema, } as APIRoute, externalIds: { method: 'GET', path: ENDPOINTS.EXTERNAL_IDS, requestSchema: ExternalIdQuerySchema, } as APIRoute, resolveExternalId: { method: 'GET', path: ENDPOINTS.RESOLVE_EXTERNAL_ID, requestSchema: ResolveExternalIdSchema, } as APIRoute, recommendationSubjects: { method: 'GET', path: ENDPOINTS.RECOMMENDATIONS_SUBJECTS, requestSchema: SubjectsQuerySchema, } as APIRoute, recommendationSimilar: { method: 'GET', path: ENDPOINTS.RECOMMENDATIONS_SIMILAR, requestSchema: SimilarBooksQuerySchema, } as APIRoute, } as const; // ================================================================================= // Re-export Zod for consumers who want to use schemas directly // ================================================================================= export { z };