📄 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