@cyanheads/zenodo-mcp-server 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/AGENTS.md +435 -0
  2. package/CLAUDE.md +435 -0
  3. package/Dockerfile +140 -0
  4. package/LICENSE +201 -0
  5. package/README.md +292 -0
  6. package/changelog/0.1.x/0.1.1.md +21 -0
  7. package/changelog/template.md +151 -0
  8. package/dist/config/server-config.d.ts +13 -0
  9. package/dist/config/server-config.d.ts.map +1 -0
  10. package/dist/config/server-config.js +19 -0
  11. package/dist/config/server-config.js.map +1 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +21 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/mcp-server/tools/definitions/get-record.tool.d.ts +186 -0
  17. package/dist/mcp-server/tools/definitions/get-record.tool.d.ts.map +1 -0
  18. package/dist/mcp-server/tools/definitions/get-record.tool.js +568 -0
  19. package/dist/mcp-server/tools/definitions/get-record.tool.js.map +1 -0
  20. package/dist/mcp-server/tools/definitions/index.d.ts +721 -0
  21. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
  22. package/dist/mcp-server/tools/definitions/index.js +21 -0
  23. package/dist/mcp-server/tools/definitions/index.js.map +1 -0
  24. package/dist/mcp-server/tools/definitions/list-files.tool.d.ts +108 -0
  25. package/dist/mcp-server/tools/definitions/list-files.tool.d.ts.map +1 -0
  26. package/dist/mcp-server/tools/definitions/list-files.tool.js +348 -0
  27. package/dist/mcp-server/tools/definitions/list-files.tool.js.map +1 -0
  28. package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts +92 -0
  29. package/dist/mcp-server/tools/definitions/list-versions.tool.d.ts.map +1 -0
  30. package/dist/mcp-server/tools/definitions/list-versions.tool.js +268 -0
  31. package/dist/mcp-server/tools/definitions/list-versions.tool.js.map +1 -0
  32. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts +99 -0
  33. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.d.ts.map +1 -0
  34. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js +324 -0
  35. package/dist/mcp-server/tools/definitions/lookup-vocabulary.tool.js.map +1 -0
  36. package/dist/mcp-server/tools/definitions/read-file.tool.d.ts +104 -0
  37. package/dist/mcp-server/tools/definitions/read-file.tool.d.ts.map +1 -0
  38. package/dist/mcp-server/tools/definitions/read-file.tool.js +370 -0
  39. package/dist/mcp-server/tools/definitions/read-file.tool.js.map +1 -0
  40. package/dist/mcp-server/tools/definitions/search-records.tool.d.ts +181 -0
  41. package/dist/mcp-server/tools/definitions/search-records.tool.d.ts.map +1 -0
  42. package/dist/mcp-server/tools/definitions/search-records.tool.js +531 -0
  43. package/dist/mcp-server/tools/definitions/search-records.tool.js.map +1 -0
  44. package/dist/mcp-server/tools/record-miss.d.ts +37 -0
  45. package/dist/mcp-server/tools/record-miss.d.ts.map +1 -0
  46. package/dist/mcp-server/tools/record-miss.js +51 -0
  47. package/dist/mcp-server/tools/record-miss.js.map +1 -0
  48. package/dist/mcp-server/tools/render.d.ts +31 -0
  49. package/dist/mcp-server/tools/render.d.ts.map +1 -0
  50. package/dist/mcp-server/tools/render.js +53 -0
  51. package/dist/mcp-server/tools/render.js.map +1 -0
  52. package/dist/mcp-server/tools/schema-helpers.d.ts +20 -0
  53. package/dist/mcp-server/tools/schema-helpers.d.ts.map +1 -0
  54. package/dist/mcp-server/tools/schema-helpers.js +44 -0
  55. package/dist/mcp-server/tools/schema-helpers.js.map +1 -0
  56. package/dist/services/zenodo/cache.d.ts +25 -0
  57. package/dist/services/zenodo/cache.d.ts.map +1 -0
  58. package/dist/services/zenodo/cache.js +58 -0
  59. package/dist/services/zenodo/cache.js.map +1 -0
  60. package/dist/services/zenodo/html-to-text.d.ts +23 -0
  61. package/dist/services/zenodo/html-to-text.d.ts.map +1 -0
  62. package/dist/services/zenodo/html-to-text.js +148 -0
  63. package/dist/services/zenodo/html-to-text.js.map +1 -0
  64. package/dist/services/zenodo/http.d.ts +73 -0
  65. package/dist/services/zenodo/http.d.ts.map +1 -0
  66. package/dist/services/zenodo/http.js +378 -0
  67. package/dist/services/zenodo/http.js.map +1 -0
  68. package/dist/services/zenodo/identifiers.d.ts +104 -0
  69. package/dist/services/zenodo/identifiers.d.ts.map +1 -0
  70. package/dist/services/zenodo/identifiers.js +261 -0
  71. package/dist/services/zenodo/identifiers.js.map +1 -0
  72. package/dist/services/zenodo/normalize.d.ts +29 -0
  73. package/dist/services/zenodo/normalize.d.ts.map +1 -0
  74. package/dist/services/zenodo/normalize.js +324 -0
  75. package/dist/services/zenodo/normalize.js.map +1 -0
  76. package/dist/services/zenodo/query-builder.d.ts +87 -0
  77. package/dist/services/zenodo/query-builder.d.ts.map +1 -0
  78. package/dist/services/zenodo/query-builder.js +221 -0
  79. package/dist/services/zenodo/query-builder.js.map +1 -0
  80. package/dist/services/zenodo/resource-types.d.ts +34 -0
  81. package/dist/services/zenodo/resource-types.d.ts.map +1 -0
  82. package/dist/services/zenodo/resource-types.js +97 -0
  83. package/dist/services/zenodo/resource-types.js.map +1 -0
  84. package/dist/services/zenodo/text-preview.d.ts +56 -0
  85. package/dist/services/zenodo/text-preview.d.ts.map +1 -0
  86. package/dist/services/zenodo/text-preview.js +151 -0
  87. package/dist/services/zenodo/text-preview.js.map +1 -0
  88. package/dist/services/zenodo/types.d.ts +517 -0
  89. package/dist/services/zenodo/types.d.ts.map +1 -0
  90. package/dist/services/zenodo/types.js +8 -0
  91. package/dist/services/zenodo/types.js.map +1 -0
  92. package/dist/services/zenodo/zenodo-service.d.ts +98 -0
  93. package/dist/services/zenodo/zenodo-service.d.ts.map +1 -0
  94. package/dist/services/zenodo/zenodo-service.js +392 -0
  95. package/dist/services/zenodo/zenodo-service.js.map +1 -0
  96. package/package.json +110 -0
  97. package/server.json +113 -0
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @fileoverview Pure parsers for the identifier forms Zenodo tools accept: record
3
+ * references (record ids, Zenodo and external DOIs, zenodo.org and doi.org URLs),
4
+ * funder, award, ORCID, and community references, plus file-key path encoding.
5
+ * Nothing here touches the network; URLs are parsed locally and never fetched.
6
+ * @module services/zenodo/identifiers
7
+ */
8
+ const ZENODO_HOSTS = new Set(['zenodo.org', 'www.zenodo.org']);
9
+ const DOI_HOSTS = new Set(['doi.org', 'dx.doi.org', 'www.doi.org']);
10
+ const DOI_PATTERN = /^10\.\d{4,9}\/\S+$/;
11
+ const ZENODO_DOI_PATTERN = /^10\.5281\/zenodo\.(\d+)$/i;
12
+ const ZENODO_SUFFIX_PATTERN = /^zenodo\.(\d+)$/i;
13
+ const QUOTE_CHARS = new Set(['"', "'", '“', '”', '‘', '’']);
14
+ const SPACE = /\s/;
15
+ /**
16
+ * Index just past `s[start, end)` once every trailing character in `chars` is
17
+ * dropped. A backward scan rather than a `[…]+$` regex, which backtracks
18
+ * quadratically when a long run of those characters is followed by anything else.
19
+ */
20
+ function trimEndOf(s, start, end, matches) {
21
+ let e = end;
22
+ while (e > start && matches(s[e - 1]))
23
+ e--;
24
+ return e;
25
+ }
26
+ const isSpace = (ch) => SPACE.test(ch);
27
+ const isTrailingPunctuation = (ch) => ch === '.' || ch === ',' || ch === ';';
28
+ /** `s` without trailing `.`/`,`/`;`. */
29
+ function stripTrailingPunctuation(s) {
30
+ return s.slice(0, trimEndOf(s, 0, s.length, isTrailingPunctuation));
31
+ }
32
+ /** `s` without trailing `/`. */
33
+ function stripTrailingSlashes(s) {
34
+ return s.slice(0, trimEndOf(s, 0, s.length, (ch) => ch === '/'));
35
+ }
36
+ /**
37
+ * Removes wrapping `<…>`, `"…"`, `'…'`, and surrounding whitespace, repeatedly, in
38
+ * any nesting order. Trailing `.`/`,`/`;` after a closing wrapper — an id copied
39
+ * out of a sentence such as `…available at <https://doi.org/…>.` — sits outside the
40
+ * value and is dropped with it; punctuation inside the wrapper is left to the
41
+ * per-form rules. Walks two indices inward, so deep nesting stays linear.
42
+ */
43
+ function stripWrapping(raw) {
44
+ let start = 0;
45
+ let end = raw.length;
46
+ for (;;) {
47
+ while (start < end && isSpace(raw[start]))
48
+ start++;
49
+ end = trimEndOf(raw, start, end, isSpace);
50
+ const inner = trimEndOf(raw, start, trimEndOf(raw, start, end, isTrailingPunctuation), isSpace);
51
+ const first = raw[start];
52
+ const last = raw[inner - 1];
53
+ const wrapped = inner - start >= 2 &&
54
+ ((first === '<' && last === '>') || (QUOTE_CHARS.has(first) && QUOTE_CHARS.has(last)));
55
+ if (!wrapped)
56
+ return raw.slice(start, end);
57
+ start++;
58
+ end = inner - 1;
59
+ }
60
+ }
61
+ /** Classifies a bare DOI string (no `doi:` prefix, no URL). */
62
+ function classifyDoi(doi, inputKind) {
63
+ const stripped = stripTrailingPunctuation(doi);
64
+ const zenodo = ZENODO_DOI_PATTERN.exec(stripped);
65
+ if (zenodo?.[1]) {
66
+ return {
67
+ kind: 'recid',
68
+ recid: zenodo[1],
69
+ inputKind: inputKind === 'url' ? 'url' : 'zenodo_doi',
70
+ };
71
+ }
72
+ if (!DOI_PATTERN.test(doi)) {
73
+ return {
74
+ kind: 'invalid',
75
+ message: `"${doi}" is not a well-formed DOI (expected 10.NNNN/suffix).`,
76
+ };
77
+ }
78
+ return {
79
+ kind: 'external_doi',
80
+ doi,
81
+ inputKind: inputKind === 'url' ? 'url' : 'external_doi',
82
+ ...(stripped !== doi && DOI_PATTERN.test(stripped) ? { strippedDoi: stripped } : {}),
83
+ };
84
+ }
85
+ function safeDecode(s) {
86
+ try {
87
+ return decodeURIComponent(s);
88
+ }
89
+ catch {
90
+ return s;
91
+ }
92
+ }
93
+ /** Parses a zenodo.org or doi.org URL. Other hosts are rejected. */
94
+ function parseUrl(raw) {
95
+ let url;
96
+ try {
97
+ url = new URL(raw);
98
+ }
99
+ catch {
100
+ return { kind: 'invalid', message: `"${raw}" is not a valid URL.` };
101
+ }
102
+ const host = url.hostname.toLowerCase();
103
+ if (DOI_HOSTS.has(host)) {
104
+ const doi = safeDecode(url.pathname.replace(/^\/+/, ''));
105
+ return classifyDoi(doi, 'url');
106
+ }
107
+ if (!ZENODO_HOSTS.has(host)) {
108
+ const note = host.endsWith('zenodo.org')
109
+ ? `${host} is a different Zenodo instance; only zenodo.org records are served.`
110
+ : `${host} is not zenodo.org or doi.org.`;
111
+ return { kind: 'invalid', message: note };
112
+ }
113
+ const path = stripTrailingPunctuation(url.pathname);
114
+ if (/^\/badge\/latestdoi\//i.test(path)) {
115
+ return {
116
+ kind: 'invalid',
117
+ message: 'A zenodo.org/badge/latestdoi URL carries a GitHub repository id, not a Zenodo record id.',
118
+ };
119
+ }
120
+ const record = /^\/(?:api\/)?records?\/(\d+)(?:\/|$)/i.exec(path);
121
+ if (record?.[1])
122
+ return { kind: 'recid', recid: record[1], inputKind: 'url' };
123
+ const doiPath = /^\/doi\/(.+)$/i.exec(path);
124
+ if (doiPath?.[1])
125
+ return classifyDoi(safeDecode(doiPath[1]), 'url');
126
+ const badge = /^\/badge\/doi\/(.+?)(?:\.svg)?$/i.exec(path);
127
+ if (badge?.[1])
128
+ return classifyDoi(safeDecode(badge[1]), 'url');
129
+ return {
130
+ kind: 'invalid',
131
+ message: `The zenodo.org path ${url.pathname} does not name a record.`,
132
+ };
133
+ }
134
+ /**
135
+ * Classifies a record reference. Accepts a record id, `zenodo.N`, a Zenodo DOI
136
+ * (any case, optionally `doi:`-prefixed), another DOI, a doi.org URL, or a
137
+ * zenodo.org record/DOI/badge URL. Wrapping `<>` / quotes (with any punctuation
138
+ * after them) and trailing punctuation after a record id or Zenodo DOI are
139
+ * stripped, in whichever order they were combined. Query strings and
140
+ * fragments are ignored. Never fetches anything.
141
+ */
142
+ export function parseRecordRef(raw) {
143
+ const s = stripWrapping(raw);
144
+ if (!s)
145
+ return { kind: 'invalid', message: 'The id is empty.' };
146
+ const withScheme = /^(?:www\.)?(?:zenodo|doi|dx\.doi|sandbox\.zenodo)\.org\//i.test(s)
147
+ ? `https://${s}`
148
+ : s;
149
+ if (/^https?:\/\//i.test(withScheme))
150
+ return parseUrl(withScheme);
151
+ const bare = s.replace(/^doi:\s*/i, '');
152
+ const stripped = stripTrailingPunctuation(bare);
153
+ if (bare === s && /^\d+$/.test(stripped)) {
154
+ return { kind: 'recid', recid: stripped, inputKind: 'record_id' };
155
+ }
156
+ const suffix = ZENODO_SUFFIX_PATTERN.exec(stripped);
157
+ if (suffix?.[1])
158
+ return { kind: 'recid', recid: suffix[1], inputKind: 'zenodo_doi' };
159
+ if (/^10\./.test(bare))
160
+ return classifyDoi(bare, 'external_doi');
161
+ return {
162
+ kind: 'invalid',
163
+ message: `"${raw.trim().slice(0, 120)}" is not a Zenodo record id, DOI, or zenodo.org/doi.org URL.`,
164
+ };
165
+ }
166
+ /** True when `raw` parses as a single DOI or URL record reference (not a bare number). */
167
+ export function isWholeIdentifier(raw) {
168
+ const s = raw.trim();
169
+ if (/^[a-z_.]+:/i.test(s) && !/^https?:/i.test(s))
170
+ return false;
171
+ const ref = parseRecordRef(s);
172
+ return ref.kind !== 'invalid' && ref.inputKind !== 'record_id' && !ZENODO_SUFFIX_PATTERN.test(s);
173
+ }
174
+ const ROR_ID = /^0[a-z0-9]{6}\d{2}$/;
175
+ /**
176
+ * Parses a funder reference: a ROR id (`01cwqze88`), a ROR URL, or a Crossref
177
+ * Funder DOI (`10.13039/100000002`, bare, `doi:`-prefixed, or as a doi.org URL).
178
+ * Returns `undefined` for anything else — funder names are never matched.
179
+ */
180
+ export function parseFunderRef(raw) {
181
+ const s = stripTrailingSlashes(stripWrapping(raw));
182
+ const ror = /^(?:https?:\/\/)?(?:www\.)?ror\.org\/(.+)$/i.exec(s)?.[1] ?? s;
183
+ if (ROR_ID.test(ror.toLowerCase()))
184
+ return { kind: 'ror', ror: ror.toLowerCase() };
185
+ const doi = s.replace(/^(?:https?:\/\/)?(?:dx\.)?doi\.org\//i, '').replace(/^doi:\s*/i, '');
186
+ if (/^10\.13039\/\S+$/i.test(doi))
187
+ return { kind: 'funder_doi', doi: doi.toLowerCase() };
188
+ return;
189
+ }
190
+ /** European Commission ROR id — the funder behind CORDIS award DOIs (`10.3030/N`). */
191
+ export const EUROPEAN_COMMISSION_ROR = '00k4n6c32';
192
+ /**
193
+ * Parses an award reference: `<funder-ror>::<number>`, a CORDIS award DOI
194
+ * (`10.3030/<n>` → European Commission), or a bare grant number.
195
+ */
196
+ export function parseAwardRef(raw) {
197
+ const s = stripWrapping(raw);
198
+ const id = /^(0[a-z0-9]{6}\d{2})::(.+)$/i.exec(s);
199
+ if (id?.[1] && id[2])
200
+ return { field: 'id', value: `${id[1].toLowerCase()}::${id[2]}` };
201
+ const cordis = /^(?:(?:https?:\/\/)?(?:dx\.)?doi\.org\/|doi:\s*)?10\.3030\/(\S+)$/i.exec(s);
202
+ if (cordis?.[1])
203
+ return { field: 'id', value: `${EUROPEAN_COMMISSION_ROR}::${cordis[1]}` };
204
+ return { field: 'number', value: s };
205
+ }
206
+ /**
207
+ * Normalizes an ORCID input: strips an `orcid.org/` URL prefix and uppercases a
208
+ * trailing check character `x`. Validation is separate ({@link isValidOrcid}).
209
+ */
210
+ export function normalizeOrcid(raw) {
211
+ return stripTrailingSlashes(stripWrapping(raw).replace(/^(?:https?:\/\/)?(?:www\.)?orcid\.org\//i, '')).replace(/x$/, 'X');
212
+ }
213
+ /** The `0000-0000-0000-000X` shape of an ORCID iD. */
214
+ export const ORCID_PATTERN = /^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$/;
215
+ /** True when `orcid` has the {@link ORCID_PATTERN} shape and a valid ISO 7064 mod 11-2 check digit. */
216
+ export function isValidOrcid(orcid) {
217
+ if (!ORCID_PATTERN.test(orcid))
218
+ return false;
219
+ const digits = orcid.replaceAll('-', '');
220
+ let total = 0;
221
+ for (const ch of digits.slice(0, 15))
222
+ total = (total + Number(ch)) * 2;
223
+ const result = (12 - (total % 11)) % 11;
224
+ return digits.at(-1) === (result === 10 ? 'X' : String(result));
225
+ }
226
+ /**
227
+ * Parses a community reference: a slug, a UUID, or a `zenodo.org/communities/<slug>`
228
+ * URL (scheme optional). Returns `undefined` when the value cannot be a
229
+ * community identifier, so it is never interpolated into a request path.
230
+ */
231
+ export function parseCommunityRef(raw) {
232
+ const s = stripWrapping(raw);
233
+ const fromUrl = /^(?:https?:\/\/)?(?:www\.)?zenodo\.org\/communities\/([^/?#]+)/i.exec(s)?.[1] ?? s;
234
+ const value = stripTrailingSlashes(safeDecode(fromUrl));
235
+ return /^[A-Za-z0-9][A-Za-z0-9_.-]{0,199}$/.test(value) ? value : undefined;
236
+ }
237
+ /**
238
+ * True when a `/`-separated key or member path has a `.` or `..` segment.
239
+ * `encodeURIComponent` leaves those literal and the URL parser resolves them, so
240
+ * such a path would reach a different zenodo.org endpoint instead of naming a file.
241
+ */
242
+ export function hasDotSegment(key) {
243
+ return key.split('/').some((segment) => segment === '.' || segment === '..');
244
+ }
245
+ /**
246
+ * Encodes a file key (or ZIP member path) for a URL path: each `/`-separated
247
+ * segment is percent-encoded. Request paths must reject {@link hasDotSegment}
248
+ * keys first, since `.` and `..` survive encoding.
249
+ */
250
+ export function encodeKeySegments(key) {
251
+ return key.split('/').map(encodeURIComponent).join('/');
252
+ }
253
+ /** The public download URL for a top-level file. */
254
+ export function downloadUrl(recid, key) {
255
+ return `https://zenodo.org/api/records/${recid}/files/${encodeKeySegments(key)}/content`;
256
+ }
257
+ /** The public landing page for a record. */
258
+ export function recordUrl(recid) {
259
+ return `https://zenodo.org/records/${recid}`;
260
+ }
261
+ //# sourceMappingURL=identifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identifiers.js","sourceRoot":"","sources":["../../../src/services/zenodo/identifiers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @fileoverview Raw InvenioRDM JSON → domain types. Strips embedded file manifests
3
+ * from search and version hits, converts HTML descriptions to plain text, and keeps
4
+ * absent upstream fields absent instead of coercing them to `0`, `''`, or `false`.
5
+ * @module services/zenodo/normalize
6
+ */
7
+ import type { AwardEntry, CommunityEntry, ContainerListing, FunderEntry, LicenseEntry, RawAward, RawCommunity, RawContainer, RawFunder, RawLicense, RawRecord, RawSearchResponse, RawTombstone, RecordSummary, SearchFacets, Tombstone, VersionSummary, ZenodoRecord } from './types.js';
8
+ /**
9
+ * `{ [key]: value }` when the value is present, `{}` otherwise — for building
10
+ * objects under `exactOptionalPropertyTypes`. `null` and `''` count as absent.
11
+ */
12
+ export declare function opt<K extends string, V>(key: K, value: V | null | undefined): Partial<Record<K, V>>;
13
+ /** Normalizes a full RDM record (from a record GET or a search hit). */
14
+ export declare function normalizeRecord(raw: RawRecord): ZenodoRecord;
15
+ /** Normalizes a 410 tombstone body. */
16
+ export declare function normalizeTombstone(raw: RawTombstone | undefined): Tombstone;
17
+ /** Normalizes a search hit to a summary, dropping its embedded file manifest. */
18
+ export declare function normalizeSearchHit(raw: RawRecord): RecordSummary;
19
+ /** Facet counts from search aggregations. */
20
+ export declare function normalizeFacets(aggs: RawSearchResponse['aggregations']): SearchFacets;
21
+ /** Normalizes a version hit (manifest stripped; this-version usage counts). */
22
+ export declare function normalizeVersionHit(raw: RawRecord): VersionSummary;
23
+ /** Normalizes a `/container` listing. */
24
+ export declare function normalizeContainer(raw: RawContainer): ContainerListing;
25
+ export declare function normalizeCommunity(raw: RawCommunity): CommunityEntry;
26
+ export declare function normalizeFunder(raw: RawFunder): FunderEntry;
27
+ export declare function normalizeAward(raw: RawAward): AwardEntry;
28
+ export declare function normalizeLicense(raw: RawLicense): LicenseEntry;
29
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/services/zenodo/normalize.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,gBAAgB,EAGhB,WAAW,EACX,YAAY,EAGZ,QAAQ,EACR,YAAY,EACZ,YAAY,EAGZ,SAAS,EAGT,UAAU,EACV,SAAS,EACT,iBAAiB,EAEjB,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,SAAS,EAET,cAAc,EACd,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EACrC,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,GAC1B,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAIvB;AA4DD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,YAAY,CAuF5D;AAED,uCAAuC;AACvC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,SAAS,CAO3E;AAUD,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,SAAS,GAAG,aAAa,CAyChE;AAOD,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,iBAAiB,CAAC,cAAc,CAAC,GAAG,YAAY,CAmBrF;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,GAAG,cAAc,CAelE;AAED,yCAAyC;AACzC,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,CActE;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,cAAc,CAYpE;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,WAAW,CAS3D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,GAAG,UAAU,CAcxD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,YAAY,CAS9D"}
@@ -0,0 +1,324 @@
1
+ /**
2
+ * @fileoverview Raw InvenioRDM JSON → domain types. Strips embedded file manifests
3
+ * from search and version hits, converts HTML descriptions to plain text, and keeps
4
+ * absent upstream fields absent instead of coercing them to `0`, `''`, or `false`.
5
+ * @module services/zenodo/normalize
6
+ */
7
+ import { htmlToText } from './html-to-text.js';
8
+ import { downloadUrl, recordUrl } from './identifiers.js';
9
+ /**
10
+ * `{ [key]: value }` when the value is present, `{}` otherwise — for building
11
+ * objects under `exactOptionalPropertyTypes`. `null` and `''` count as absent.
12
+ */
13
+ export function opt(key, value) {
14
+ return value === undefined || value === null || value === ''
15
+ ? {}
16
+ : { [key]: value };
17
+ }
18
+ /** English value of a multilingual map, else the first non-empty value. */
19
+ function i18n(map) {
20
+ if (!map)
21
+ return;
22
+ return map.en || Object.values(map).find((v) => v) || undefined;
23
+ }
24
+ function identifierOf(ids, scheme) {
25
+ return ids?.find((i) => i.scheme?.toLowerCase() === scheme)?.identifier || undefined;
26
+ }
27
+ function counts(block) {
28
+ return { ...opt('views', block?.unique_views), ...opt('downloads', block?.unique_downloads) };
29
+ }
30
+ function stripMd5(checksum) {
31
+ if (!checksum)
32
+ return;
33
+ return checksum.startsWith('md5:') ? checksum.slice(4) : undefined;
34
+ }
35
+ function normalizePerson(raw) {
36
+ const p = raw.person_or_org ?? {};
37
+ const name = p.name || [p.given_name, p.family_name].filter(Boolean).join(' ') || p.family_name || '';
38
+ return {
39
+ name,
40
+ ...opt('type', p.type),
41
+ ...opt('orcid', identifierOf(p.identifiers, 'orcid')),
42
+ ...opt('role', raw.role ? (i18n(raw.role.title) ?? raw.role.id) : undefined),
43
+ affiliations: (raw.affiliations ?? [])
44
+ .filter((a) => a.name || a.id)
45
+ .map((a) => ({ ...opt('name', a.name), ...opt('ror', a.id) })),
46
+ };
47
+ }
48
+ /** The complete manifest in upstream order (`files.order` when non-empty, else object order). */
49
+ function normalizeManifest(recid, files) {
50
+ const entries = files?.entries ?? {};
51
+ const keys = files?.order?.length
52
+ ? files.order.filter((k) => k in entries)
53
+ : Object.keys(entries);
54
+ return keys.map((mapKey) => {
55
+ const e = entries[mapKey] ?? {};
56
+ const key = e.key ?? mapKey;
57
+ return {
58
+ key,
59
+ ...opt('size', e.size),
60
+ ...opt('mimetype', e.mimetype),
61
+ ...opt('md5', stripMd5(e.checksum)),
62
+ download_url: downloadUrl(recid, key),
63
+ };
64
+ });
65
+ }
66
+ function embargoUntil(raw) {
67
+ const embargo = raw.access?.embargo;
68
+ return embargo?.active ? embargo.until : undefined;
69
+ }
70
+ /** Normalizes a full RDM record (from a record GET or a search hit). */
71
+ export function normalizeRecord(raw) {
72
+ const m = raw.metadata ?? {};
73
+ const recid = String(raw.id);
74
+ const description = m.description ? htmlToText(m.description) : undefined;
75
+ const embargo = raw.access?.embargo;
76
+ const codeRepository = raw.custom_fields?.['code:codeRepository'];
77
+ return {
78
+ recid,
79
+ ...opt('concept_recid', raw.parent?.id),
80
+ ...opt('doi', raw.pids?.doi?.identifier),
81
+ ...opt('doi_provider', raw.pids?.doi?.provider),
82
+ ...opt('concept_doi', raw.parent?.pids?.doi?.identifier),
83
+ ...opt('oai_id', raw.pids?.oai?.identifier),
84
+ title: m.title ?? '',
85
+ ...opt('publication_date', m.publication_date),
86
+ ...opt('version', m.version),
87
+ ...opt('publisher', m.publisher),
88
+ ...(m.resource_type?.id
89
+ ? { resource_type: { id: m.resource_type.id, ...opt('title', i18n(m.resource_type.title)) } }
90
+ : {}),
91
+ ...opt('description', description),
92
+ additional_descriptions: (m.additional_descriptions ?? [])
93
+ .filter((d) => d.description)
94
+ .map((d) => ({
95
+ ...opt('type', d.type ? (i18n(d.type.title) ?? d.type.id) : undefined),
96
+ text: htmlToText(d.description ?? ''),
97
+ })),
98
+ creators: (m.creators ?? []).map(normalizePerson),
99
+ contributors: (m.contributors ?? []).map(normalizePerson),
100
+ keywords: (m.subjects ?? []).map((s) => s.subject).filter((s) => !!s),
101
+ rights: (m.rights ?? []).map((r) => ({
102
+ ...opt('id', r.id),
103
+ title: i18n(r.title) ?? r.id ?? '',
104
+ ...opt('url', r.props?.url),
105
+ })),
106
+ access: {
107
+ status: raw.access?.status ?? 'unknown',
108
+ ...opt('record', raw.access?.record),
109
+ ...opt('files', raw.access?.files),
110
+ embargo_active: embargo?.active === true,
111
+ ...opt('embargo_until', embargoUntil(raw)),
112
+ ...opt('embargo_reason', embargo?.active ? embargo.reason : undefined),
113
+ },
114
+ funding: (m.funding ?? []).map((f) => ({
115
+ ...opt('funder_id', f.funder?.id),
116
+ ...opt('funder_name', f.funder?.name),
117
+ ...opt('award_id', f.award?.id),
118
+ ...opt('award_number', f.award?.number),
119
+ ...opt('award_acronym', f.award?.acronym),
120
+ ...opt('award_title', i18n(f.award?.title)),
121
+ ...opt('award_program', f.award?.program),
122
+ ...opt('award_doi', identifierOf(f.award?.identifiers, 'doi')),
123
+ ...opt('award_url', identifierOf(f.award?.identifiers, 'url')),
124
+ })),
125
+ related_identifiers: (m.related_identifiers ?? [])
126
+ .filter((r) => r.identifier)
127
+ .map((r) => ({
128
+ identifier: r.identifier ?? '',
129
+ ...opt('scheme', r.scheme),
130
+ ...opt('relation', r.relation_type?.id),
131
+ ...opt('resource_type', r.resource_type?.id),
132
+ })),
133
+ ...opt('code_repository', typeof codeRepository === 'string' ? codeRepository : undefined),
134
+ communities: (raw.parent?.communities?.entries ?? [])
135
+ .filter((c) => c.slug)
136
+ .map((c) => ({ slug: c.slug ?? '', ...opt('title', c.metadata?.title) })),
137
+ versions: {
138
+ ...opt('index', raw.versions?.index),
139
+ ...opt('is_latest', raw.versions?.is_latest),
140
+ },
141
+ ...(raw.stats
142
+ ? {
143
+ stats: {
144
+ this_version: counts(raw.stats.this_version),
145
+ all_versions: counts(raw.stats.all_versions),
146
+ },
147
+ }
148
+ : {}),
149
+ files: {
150
+ enabled: raw.files?.enabled === true,
151
+ ...opt('count', raw.files?.count),
152
+ ...opt('total_bytes', raw.files?.total_bytes),
153
+ entries: normalizeManifest(recid, raw.files),
154
+ },
155
+ ...opt('revision', raw.revision_id),
156
+ };
157
+ }
158
+ /** Normalizes a 410 tombstone body. */
159
+ export function normalizeTombstone(raw) {
160
+ return {
161
+ ...opt('removal_date', raw?.removal_date),
162
+ ...opt('removal_reason', raw?.removal_reason?.id),
163
+ ...opt('note', raw?.note),
164
+ ...opt('citation_text', raw?.citation_text),
165
+ };
166
+ }
167
+ /** First `max` characters at a word boundary, with `…` when cut. */
168
+ function snippet(text, max) {
169
+ if (text.length <= max)
170
+ return text;
171
+ const cut = text.slice(0, max);
172
+ const space = cut.lastIndexOf(' ');
173
+ return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}…`;
174
+ }
175
+ /** Normalizes a search hit to a summary, dropping its embedded file manifest. */
176
+ export function normalizeSearchHit(raw) {
177
+ const m = raw.metadata ?? {};
178
+ const recid = String(raw.id);
179
+ const creators = m.creators ?? [];
180
+ const description = m.description ? htmlToText(m.description) : undefined;
181
+ return {
182
+ recid,
183
+ ...opt('doi', raw.pids?.doi?.identifier),
184
+ ...opt('doi_provider', raw.pids?.doi?.provider),
185
+ ...opt('concept_recid', raw.parent?.id),
186
+ ...opt('concept_doi', raw.parent?.pids?.doi?.identifier),
187
+ title: m.title ?? '',
188
+ ...opt('publication_date', m.publication_date),
189
+ ...(m.resource_type?.id
190
+ ? { resource_type: { id: m.resource_type.id, ...opt('title', i18n(m.resource_type.title)) } }
191
+ : {}),
192
+ ...opt('version', m.version),
193
+ ...opt('is_latest', raw.versions?.is_latest),
194
+ ...opt('version_index', raw.versions?.index),
195
+ creators: creators.slice(0, 5).map((c) => {
196
+ const p = normalizePerson(c);
197
+ return { name: p.name, ...opt('orcid', p.orcid) };
198
+ }),
199
+ creator_count: creators.length,
200
+ license_ids: (m.rights ?? []).map((r) => r.id).filter((id) => !!id),
201
+ access: {
202
+ status: raw.access?.status ?? 'unknown',
203
+ ...opt('files', raw.access?.files),
204
+ ...opt('embargo_until', embargoUntil(raw)),
205
+ },
206
+ communities: (raw.parent?.communities?.entries ?? [])
207
+ .map((c) => c.slug)
208
+ .filter((s) => !!s)
209
+ .slice(0, 5),
210
+ ...opt('file_count', raw.files?.count),
211
+ ...opt('total_bytes', raw.files?.total_bytes),
212
+ ...opt('views', raw.stats?.all_versions?.unique_views),
213
+ ...opt('downloads', raw.stats?.all_versions?.unique_downloads),
214
+ ...opt('description_snippet', description ? snippet(description, 280) : undefined),
215
+ zenodo_url: recordUrl(recid),
216
+ };
217
+ }
218
+ function bucket(b) {
219
+ const id = String(b.key ?? '');
220
+ return { id, label: b.label ?? id, count: b.doc_count ?? 0 };
221
+ }
222
+ /** Facet counts from search aggregations. */
223
+ export function normalizeFacets(aggs) {
224
+ const buckets = (name) => aggs?.[name]?.buckets ?? [];
225
+ return {
226
+ resource_type: buckets('resource_type')
227
+ .slice(0, 10)
228
+ .map((b) => {
229
+ const subtypes = (b.inner?.buckets ?? []).slice(0, 5).map(bucket);
230
+ return { ...bucket(b), ...(subtypes.length ? { subtypes } : {}) };
231
+ }),
232
+ access_status: buckets('access_status').map(bucket),
233
+ file_type: buckets('file_type').slice(0, 10).map(bucket),
234
+ subject: buckets('subject')
235
+ .slice(0, 10)
236
+ .map((b) => ({ label: b.label ?? String(b.key ?? ''), count: b.doc_count ?? 0 })),
237
+ publication_year: buckets('publication_date')
238
+ .map((b) => ({ year: String(b.key ?? ''), count: b.doc_count ?? 0 }))
239
+ .sort((a, b) => b.year.localeCompare(a.year))
240
+ .slice(0, 10),
241
+ };
242
+ }
243
+ /** Normalizes a version hit (manifest stripped; this-version usage counts). */
244
+ export function normalizeVersionHit(raw) {
245
+ const m = raw.metadata ?? {};
246
+ return {
247
+ recid: String(raw.id),
248
+ ...opt('doi', raw.pids?.doi?.identifier),
249
+ ...opt('version', m.version),
250
+ title: m.title ?? '',
251
+ ...opt('publication_date', m.publication_date),
252
+ ...opt('index', raw.versions?.index),
253
+ ...opt('is_latest', raw.versions?.is_latest),
254
+ ...opt('file_count', raw.files?.count),
255
+ ...opt('total_bytes', raw.files?.total_bytes),
256
+ ...opt('views', raw.stats?.this_version?.unique_views),
257
+ ...opt('downloads', raw.stats?.this_version?.unique_downloads),
258
+ };
259
+ }
260
+ /** Normalizes a `/container` listing. */
261
+ export function normalizeContainer(raw) {
262
+ return {
263
+ members: (raw.entries ?? [])
264
+ .filter((e) => e.key)
265
+ .map((e) => ({
266
+ path: e.key ?? '',
267
+ ...opt('size', e.size),
268
+ ...opt('compressed_size', e.compressed_size),
269
+ ...opt('mimetype', e.mimetype),
270
+ })),
271
+ ...opt('total', raw.total),
272
+ upstream_truncated: raw.truncated === true,
273
+ directory_count: raw.directories?.length ?? 0,
274
+ };
275
+ }
276
+ export function normalizeCommunity(raw) {
277
+ const m = raw.metadata ?? {};
278
+ return {
279
+ slug: raw.slug ?? '',
280
+ uuid: raw.id ?? '',
281
+ ...opt('title', m.title),
282
+ ...opt('community_type', m.type ? (i18n(m.type.title) ?? m.type.id) : undefined),
283
+ ...opt('website', m.website),
284
+ organizations: (m.organizations ?? [])
285
+ .map((o) => o.name || (o.id ? `ROR ${o.id}` : ''))
286
+ .filter(Boolean),
287
+ };
288
+ }
289
+ export function normalizeFunder(raw) {
290
+ return {
291
+ ror_id: raw.id ?? '',
292
+ ...opt('name', raw.name ?? i18n(raw.title)),
293
+ ...opt('funder_doi', identifierOf(raw.identifiers, 'doi')),
294
+ ...opt('acronym', raw.acronym),
295
+ ...opt('country', raw.country),
296
+ ...opt('country_name', raw.country_name),
297
+ };
298
+ }
299
+ export function normalizeAward(raw) {
300
+ return {
301
+ id: raw.id ?? '',
302
+ ...opt('number', raw.number),
303
+ ...opt('acronym', raw.acronym),
304
+ ...opt('title', i18n(raw.title)),
305
+ ...opt('program', raw.program),
306
+ ...opt('funder_id', raw.funder?.id),
307
+ ...opt('funder_name', raw.funder?.name),
308
+ ...opt('award_doi', identifierOf(raw.identifiers, 'doi')),
309
+ ...opt('award_url', identifierOf(raw.identifiers, 'url')),
310
+ ...opt('start_date', raw.start_date),
311
+ ...opt('end_date', raw.end_date),
312
+ };
313
+ }
314
+ export function normalizeLicense(raw) {
315
+ const osi = raw.props?.osi_approved;
316
+ return {
317
+ id: raw.id ?? '',
318
+ ...opt('title', i18n(raw.title)),
319
+ ...opt('url', raw.props?.url),
320
+ ...(osi === 'y' || osi === 'n' ? { osi_approved: osi === 'y' } : {}),
321
+ tags: raw.tags ?? [],
322
+ };
323
+ }
324
+ //# sourceMappingURL=normalize.js.map