📄 src/isbn/index.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 *
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 *
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import type { StandardAdapter, StandardApiAdapter, ValidationResult }
16 from '@the_library/canonical-schemas/types/StandardAdapter';
17import { apiKeyDictionary } from '../api-keys';
18import type { OpenLibraryBookResult, GoogleBooksVolumeResult } from './types';
19
20interface ISBNProps extends Record<string, string | null> { isbn: string | null; }
21
22type ISBNApis = {
23 openlibrary: StandardApiAdapter<ISBNProps, OpenLibraryBookResult>;
24 googlebooks: StandardApiAdapter<ISBNProps, GoogleBooksVolumeResult>;
25};
26
27function validateISBN13Luhn(isbn13: string): ValidationResult {
28 if (isbn13.length !== 13) return { valid: false, error: `Expected 13 digits, got ${isbn13.length}` };
29 let sum = 0;
30 for (let i = 0; i < 12; i++) sum += parseInt(isbn13[i], 10) * (i % 2 === 0 ? 1 : 3);
31 const expected = (10 - (sum % 10)) % 10;
32 if (expected !== parseInt(isbn13[12], 10))
33 return { valid: false, error: `Check digit mismatch: expected ${expected}, got ${isbn13[12]}` };
34 return { valid: true };
35}
36
37export const ISBNAdapter: StandardAdapter<ISBNProps, ISBNApis> = {
38 standardId: 'isbn',
39 uriScheme: 'ISBN',
40
41 normalize(value: string): string {
42 const digits = value.replace(/[\s-]/g, '');
43 if (digits.length === 10) {
44 const base = '978' + digits.slice(0, 9);
45 let sum = 0;
46 for (let i = 0; i < 12; i++) sum += parseInt(base[i], 10) * (i % 2 === 0 ? 1 : 3);
47 return base + (10 - (sum % 10)) % 10;
48 }
49 return digits;
50 },
51
52 display(value: string): string {
53 const n = ISBNAdapter.normalize(value);
54 if (n.length === 13) return `${n.slice(0, 3)}-${n[3]}-${n.slice(4, 7)}-${n.slice(7, 12)}-${n[12]}`;
55 return n;
56 },
57
58 validate(value: string | null | undefined): ValidationResult {
59 if (!value) return { valid: false, error: 'ISBN is absent' };
60 const normalized = ISBNAdapter.normalize(value);
61 if (!/^97[89][0-9]{10}$/.test(normalized))
62 return { valid: false, error: `ISBN does not match pattern: ${normalized}` };
63 return validateISBN13Luhn(normalized);
64 },
65
66 toLinkedDataUri(value: string): string {
67 return `urn:isbn:${ISBNAdapter.normalize(value)}`;
68 },
69
70 apis: {
71 openlibrary: {
72 apiId: 'openlibrary',
73 name: 'Open Library Books API',
74 async fetch({ isbn }): Promise<OpenLibraryBookResult> {
75 if (!isbn) return {};
76 const res = await fetch(
77 `https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`
78 );
79 if (!res.ok) return {};
80 const data = await res.json() as Record<string, unknown>;
81 const book = (data[`ISBN:${isbn}`] ?? {}) as Record<string, unknown>;
82 return {
83 title: book.title as string | undefined,
84 authors: (book.authors as { name: string }[] | undefined)?.map(a => a.name),
85 publishDate: book.publish_date as string | undefined,
86 pageCount: book.number_of_pages as number | undefined,
87 coverUrl: (book.cover as { medium?: string } | undefined)?.medium,
88 description: (book.excerpts as { text: string }[] | undefined)?.[0]?.text,
89 };
90 },
91 },
92 googlebooks: {
93 apiId: 'googlebooks',
94 name: 'Google Books API',
95 async fetch({ isbn }): Promise<GoogleBooksVolumeResult> {
96 if (!isbn) return {};
97 const key = apiKeyDictionary.require('googlebooks');
98 const res = await fetch(
99 `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&key=${key}`
100 );
101 if (!res.ok) return {};
102 const data = await res.json() as { items?: { volumeInfo: Record<string, unknown> }[] };
103 const info = data.items?.[0]?.volumeInfo ?? {};
104 return {
105 title: info.title as string | undefined,
106 authors: info.authors as string[] | undefined,
107 publishedDate: info.publishedDate as string | undefined,
108 pageCount: info.pageCount as number | undefined,
109 language: info.language as string | undefined,
110 description: info.description as string | undefined,
111 thumbnail: (info.imageLinks as { thumbnail?: string } | undefined)?.thumbnail,
112 };
113 },
114 },
115 },
116};
117