📄 src/issn/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 type { IssnPortalResult } from './types';
18
19interface ISSNProps extends Record<string, string | null> { issn: string | null; }
20
21type ISSNApis = {
22 'issn-portal': StandardApiAdapter<ISSNProps, IssnPortalResult>;
23};
24
25function validateISSNMod11_2(issn: string): ValidationResult {
26 const clean = issn.replace('-', '');
27 if (clean.length !== 8) return { valid: false, error: `Expected 8 chars, got ${clean.length}` };
28 const weights = [8, 7, 6, 5, 4, 3, 2];
29 let sum = 0;
30 for (let i = 0; i < 7; i++) sum += parseInt(clean[i], 10) * weights[i];
31 const remainder = sum % 11;
32 const checkVal = remainder === 0 ? 0 : 11 - remainder;
33 const checkChar = checkVal === 10 ? 'X' : String(checkVal);
34 if (clean[7].toUpperCase() !== checkChar)
35 return { valid: false, error: `Check digit mismatch: expected ${checkChar}, got ${clean[7]}` };
36 return { valid: true };
37}
38
39export const ISSNAdapter: StandardAdapter<ISSNProps, ISSNApis> = {
40 standardId: 'issn',
41 uriScheme: 'ISSN',
42
43 normalize(value: string): string {
44 const digits = value.replace(/[\s-]/g, '');
45 if (digits.length === 8) return `${digits.slice(0, 4)}-${digits.slice(4)}`;
46 return value.trim();
47 },
48
49 display(value: string): string {
50 return ISSNAdapter.normalize(value);
51 },
52
53 validate(value: string | null | undefined): ValidationResult {
54 if (!value) return { valid: false, error: 'ISSN is absent' };
55 const normalized = ISSNAdapter.normalize(value);
56 if (!/^[0-9]{4}-[0-9]{3}[0-9X]$/.test(normalized))
57 return { valid: false, error: `ISSN does not match pattern: ${normalized}` };
58 return validateISSNMod11_2(normalized);
59 },
60
61 toLinkedDataUri(value: string): string {
62 return `https://portal.issn.org/resource/ISSN/${ISSNAdapter.normalize(value)}`;
63 },
64
65 apis: {
66 'issn-portal': {
67 apiId: 'issn-portal',
68 name: 'ISSN Portal',
69 async fetch({ issn }): Promise<IssnPortalResult> {
70 if (!issn) return {};
71 const res = await fetch(`https://portal.issn.org/resource/ISSN/${issn}?format=json`);
72 if (!res.ok) return {};
73 const data = await res.json() as Record<string, unknown>;
74 return {
75 title: data['dc:title'] as string | undefined,
76 medium: data['dc:format'] as string | undefined,
77 country: data['dc:publisher'] as string | undefined,
78 issn,
79 };
80 },
81 },
82 },
83};
84