mcp-databnf 1.1.1 → 2.1.0
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.
- package/CHANGELOG.md +22 -0
- package/PRIVACY.md +152 -0
- package/README.md +548 -290
- package/dist/bnf/client.d.ts +15 -34
- package/dist/bnf/client.js +271 -192
- package/dist/bnf/client.js.map +1 -1
- package/dist/index.js +551 -307
- package/dist/index.js.map +1 -1
- package/package.json +23 -11
- package/server.json +73 -0
package/dist/bnf/client.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import process from "process";
|
|
3
|
+
|
|
1
4
|
// src/version.ts
|
|
2
|
-
var PKG_VERSION = "
|
|
5
|
+
var PKG_VERSION = "2.1.0";
|
|
3
6
|
var REPO_URL = "https://github.com/smeet666/mcp-databnf";
|
|
4
7
|
|
|
5
8
|
// src/config.ts
|
|
@@ -12,7 +15,9 @@ var DEFAULT_USER_AGENT = `mcp-databnf/${PKG_VERSION} (+${REPO_URL})`;
|
|
|
12
15
|
function createLogger(level) {
|
|
13
16
|
const rank = LOG_LEVELS.indexOf(level);
|
|
14
17
|
const write = (at, message) => {
|
|
15
|
-
if (rank === 0 || LOG_LEVELS.indexOf(at) > rank)
|
|
18
|
+
if (rank === 0 || LOG_LEVELS.indexOf(at) > rank) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
16
21
|
process.stderr.write(`[mcp-databnf] ${at}: ${message}
|
|
17
22
|
`);
|
|
18
23
|
};
|
|
@@ -27,9 +32,11 @@ function createLogger(level) {
|
|
|
27
32
|
}
|
|
28
33
|
function readInteger(env, name, fallback, min, max) {
|
|
29
34
|
const raw = env[name];
|
|
30
|
-
if (raw === void 0 || raw.trim() === "")
|
|
35
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
31
38
|
const value = Number(raw);
|
|
32
|
-
if (!Number.isFinite(value)
|
|
39
|
+
if (!(Number.isFinite(value) && Number.isInteger(value))) {
|
|
33
40
|
process.stderr.write(
|
|
34
41
|
`[mcp-databnf] error: ${name}="${raw}" is not a whole number; using ${fallback}.
|
|
35
42
|
`
|
|
@@ -80,7 +87,7 @@ var BnfError = class extends Error {
|
|
|
80
87
|
code;
|
|
81
88
|
details;
|
|
82
89
|
constructor(code, message, details = {}) {
|
|
83
|
-
super(message);
|
|
90
|
+
super(message, details.cause === void 0 ? void 0 : { cause: details.cause });
|
|
84
91
|
this.name = "BnfError";
|
|
85
92
|
this.code = code;
|
|
86
93
|
this.details = details;
|
|
@@ -110,7 +117,9 @@ var Cache = class {
|
|
|
110
117
|
entries = /* @__PURE__ */ new Map();
|
|
111
118
|
get(key) {
|
|
112
119
|
const entry = this.entries.get(key);
|
|
113
|
-
if (!entry)
|
|
120
|
+
if (!entry) {
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
114
123
|
if (entry.expiresAt <= Date.now()) {
|
|
115
124
|
this.entries.delete(key);
|
|
116
125
|
return void 0;
|
|
@@ -120,12 +129,16 @@ var Cache = class {
|
|
|
120
129
|
return { value: entry.value, retrievedAt: entry.retrievedAt };
|
|
121
130
|
}
|
|
122
131
|
set(key, value, retrievedAt) {
|
|
123
|
-
if (this.ttlMs <= 0)
|
|
132
|
+
if (this.ttlMs <= 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
124
135
|
this.entries.delete(key);
|
|
125
136
|
this.entries.set(key, { value, expiresAt: Date.now() + this.ttlMs, retrievedAt });
|
|
126
137
|
while (this.entries.size > this.maxEntries) {
|
|
127
138
|
const oldest = this.entries.keys().next();
|
|
128
|
-
if (oldest.done)
|
|
139
|
+
if (oldest.done) {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
129
142
|
this.entries.delete(oldest.value);
|
|
130
143
|
}
|
|
131
144
|
}
|
|
@@ -135,6 +148,9 @@ var Cache = class {
|
|
|
135
148
|
};
|
|
136
149
|
|
|
137
150
|
// src/bnf/endpoint.ts
|
|
151
|
+
var TRAILING_DOT = /\.$/;
|
|
152
|
+
var FRAGMENT = /#.*$/;
|
|
153
|
+
var HTTP_SCHEME = /^http:/;
|
|
138
154
|
var SPARQL_ENDPOINT = "https://data.bnf.fr/sparql";
|
|
139
155
|
var ALLOWED_HOST = "data.bnf.fr";
|
|
140
156
|
var GALLICA_HOST = "gallica.bnf.fr";
|
|
@@ -142,22 +158,23 @@ function assertRequestable(url) {
|
|
|
142
158
|
let parsed;
|
|
143
159
|
try {
|
|
144
160
|
parsed = new URL(url);
|
|
145
|
-
} catch {
|
|
146
|
-
throw refusal(`mcp-databnf refuses to request "${url}": it is not an address
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
throw refusal(`mcp-databnf refuses to request "${url}": it is not an address.`, cause);
|
|
147
163
|
}
|
|
148
|
-
const host = parsed.hostname.toLowerCase().replace(
|
|
164
|
+
const host = parsed.hostname.toLowerCase().replace(TRAILING_DOT, "");
|
|
149
165
|
if (host !== ALLOWED_HOST) {
|
|
150
166
|
throw refusal(
|
|
151
167
|
`mcp-databnf requests ${ALLOWED_HOST} and nothing else; "${host}" was refused. Gallica in particular is described from the catalogue and never read.`
|
|
152
168
|
);
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
|
-
var refusal = (message) => new BnfError("network_error", message, {
|
|
156
|
-
hint: "This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host."
|
|
171
|
+
var refusal = (message, cause) => new BnfError("network_error", message, {
|
|
172
|
+
hint: "This server reads the BnF catalogue at data.bnf.fr. It holds no licence to read the digitised contents, so it does not follow an address that leaves that host.",
|
|
173
|
+
cause
|
|
157
174
|
});
|
|
158
175
|
function isGallicaAddress(address) {
|
|
159
176
|
try {
|
|
160
|
-
const host = new URL(address).hostname.toLowerCase().replace(
|
|
177
|
+
const host = new URL(address).hostname.toLowerCase().replace(TRAILING_DOT, "");
|
|
161
178
|
return host === GALLICA_HOST || host.endsWith(`.${GALLICA_HOST}`);
|
|
162
179
|
} catch {
|
|
163
180
|
return false;
|
|
@@ -184,21 +201,28 @@ var PREFIXES = {
|
|
|
184
201
|
rdafrbr: "http://rdvocab.info/uri/schema/FRBRentitiesRDA/"
|
|
185
202
|
};
|
|
186
203
|
var PREFIX_BLOCK = Object.entries(PREFIXES).map(([name, iri]) => `PREFIX ${name}: <${iri}>`).join("\n");
|
|
187
|
-
var publicPageFor = (entityIri) => entityIri.replace(
|
|
204
|
+
var publicPageFor = (entityIri) => entityIri.replace(FRAGMENT, "").replace(HTTP_SCHEME, "https:");
|
|
188
205
|
|
|
189
206
|
// src/bnf/http.ts
|
|
207
|
+
var VIRTUOSO_ERROR = /^Virtuoso\s+(\w+)\s+Error\s+([\s\S]*)$/;
|
|
190
208
|
var RETRYABLE = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
191
209
|
var PUSH_BACK = /* @__PURE__ */ new Set([429, 503]);
|
|
192
210
|
var LONGEST_WAIT_MS = 3e4;
|
|
193
211
|
var RETRIES_AFTER_SILENCE = 1;
|
|
194
212
|
var MAX_REDIRECTS = 2;
|
|
195
213
|
function parseRetryAfter(value, now = Date.now()) {
|
|
196
|
-
if (!value)
|
|
214
|
+
if (!value) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
197
217
|
const trimmed = value.trim();
|
|
198
218
|
const seconds = Number(trimmed);
|
|
199
|
-
if (Number.isFinite(seconds) && seconds >= 0)
|
|
219
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
220
|
+
return Math.round(seconds * 1e3);
|
|
221
|
+
}
|
|
200
222
|
const at = Date.parse(trimmed);
|
|
201
|
-
if (Number.isNaN(at))
|
|
223
|
+
if (Number.isNaN(at)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
202
226
|
return Math.max(0, at - now);
|
|
203
227
|
}
|
|
204
228
|
function backoffMs(attempt) {
|
|
@@ -206,8 +230,10 @@ function backoffMs(attempt) {
|
|
|
206
230
|
return base + Math.floor(Math.random() * 500);
|
|
207
231
|
}
|
|
208
232
|
function readEngineError(body) {
|
|
209
|
-
const match =
|
|
210
|
-
if (!match)
|
|
233
|
+
const match = VIRTUOSO_ERROR.exec(body.trim());
|
|
234
|
+
if (!match) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
211
237
|
const state = match[1] ?? "";
|
|
212
238
|
const text2 = (match[2] ?? "").split("\n")[0]?.trim() ?? "";
|
|
213
239
|
return { kind: state === "37000" ? "compile" : "runtime", text: text2 };
|
|
@@ -249,6 +275,78 @@ async function postOnce(options, signal) {
|
|
|
249
275
|
url: SPARQL_ENDPOINT
|
|
250
276
|
});
|
|
251
277
|
}
|
|
278
|
+
function readRefusal(response, attempt, maxRetries) {
|
|
279
|
+
const pushBack = RETRYABLE.has(response.status);
|
|
280
|
+
if (PUSH_BACK.has(response.status)) {
|
|
281
|
+
const asked = parseRetryAfter(response.headers.get("retry-after"));
|
|
282
|
+
if (asked !== null && asked > LONGEST_WAIT_MS) {
|
|
283
|
+
return {
|
|
284
|
+
kind: "refused",
|
|
285
|
+
pushBack,
|
|
286
|
+
error: rateLimited(
|
|
287
|
+
`data.bnf.fr asked this client to wait ${Math.round(asked / 1e3)} seconds (HTTP ${response.status}).`,
|
|
288
|
+
{ url: SPARQL_ENDPOINT, status: response.status }
|
|
289
|
+
)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (attempt >= maxRetries) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "refused",
|
|
295
|
+
pushBack,
|
|
296
|
+
error: rateLimited(
|
|
297
|
+
`data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,
|
|
298
|
+
{
|
|
299
|
+
url: SPARQL_ENDPOINT,
|
|
300
|
+
status: response.status
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return { kind: "again", pushBack, waitMs: asked ?? backoffMs(attempt) };
|
|
306
|
+
}
|
|
307
|
+
if (RETRYABLE.has(response.status) && attempt < maxRetries) {
|
|
308
|
+
return { kind: "again", pushBack, waitMs: backoffMs(attempt) };
|
|
309
|
+
}
|
|
310
|
+
if (response.status === 400 || response.status === 422) {
|
|
311
|
+
const engine = readEngineError(response.body);
|
|
312
|
+
return {
|
|
313
|
+
kind: "refused",
|
|
314
|
+
pushBack,
|
|
315
|
+
error: invalidInput(
|
|
316
|
+
`data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : "."}`,
|
|
317
|
+
"Try fewer or plainer words. If this keeps happening with ordinary input, please report it."
|
|
318
|
+
)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
kind: "refused",
|
|
323
|
+
pushBack,
|
|
324
|
+
error: networkError(`data.bnf.fr answered HTTP ${response.status}.`, {
|
|
325
|
+
url: SPARQL_ENDPOINT,
|
|
326
|
+
status: response.status
|
|
327
|
+
})
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function readFailure(error, attempts) {
|
|
331
|
+
const { attempt, maxRetries, timeoutMs } = attempts;
|
|
332
|
+
if (error instanceof Error && error.name === "BnfError") {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
336
|
+
if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {
|
|
337
|
+
throw timeout(
|
|
338
|
+
`No answer from data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,
|
|
339
|
+
{ url: SPARQL_ENDPOINT }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return error;
|
|
343
|
+
}
|
|
344
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
345
|
+
if (attempt >= maxRetries) {
|
|
346
|
+
throw networkError(`Could not reach data.bnf.fr: ${failure.message}`, { url: SPARQL_ENDPOINT });
|
|
347
|
+
}
|
|
348
|
+
return failure;
|
|
349
|
+
}
|
|
252
350
|
async function runQuery(options) {
|
|
253
351
|
const { timeoutMs, maxRetries, limiter, logger } = options;
|
|
254
352
|
let lastError = null;
|
|
@@ -256,7 +354,8 @@ async function runQuery(options) {
|
|
|
256
354
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
257
355
|
if (askedWaitMs > 0) {
|
|
258
356
|
logger.debug(`waiting ${askedWaitMs}ms, as asked`);
|
|
259
|
-
|
|
357
|
+
const asked = askedWaitMs;
|
|
358
|
+
await new Promise((resolve) => setTimeout(resolve, asked));
|
|
260
359
|
askedWaitMs = 0;
|
|
261
360
|
}
|
|
262
361
|
await limiter.beforeRequest();
|
|
@@ -276,66 +375,19 @@ async function runQuery(options) {
|
|
|
276
375
|
limiter.succeeded();
|
|
277
376
|
return parsed;
|
|
278
377
|
}
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
if (asked !== null && asked > LONGEST_WAIT_MS) {
|
|
283
|
-
throw rateLimited(
|
|
284
|
-
`data.bnf.fr asked this client to wait ${Math.round(asked / 1e3)} seconds (HTTP ${response.status}).`,
|
|
285
|
-
{ url: SPARQL_ENDPOINT, status: response.status }
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
if (attempt >= maxRetries) {
|
|
289
|
-
throw rateLimited(
|
|
290
|
-
`data.bnf.fr asked this client to slow down (HTTP ${response.status}).`,
|
|
291
|
-
{
|
|
292
|
-
url: SPARQL_ENDPOINT,
|
|
293
|
-
status: response.status
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
askedWaitMs = asked ?? backoffMs(attempt);
|
|
298
|
-
lastError = new Error(`HTTP ${response.status}`);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (RETRYABLE.has(response.status) && attempt < maxRetries) {
|
|
302
|
-
lastError = new Error(`HTTP ${response.status}`);
|
|
303
|
-
askedWaitMs = backoffMs(attempt);
|
|
304
|
-
continue;
|
|
378
|
+
const verdict = readRefusal(response, attempt, maxRetries);
|
|
379
|
+
if (verdict.pushBack) {
|
|
380
|
+
limiter.pushBack();
|
|
305
381
|
}
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
throw invalidInput(
|
|
309
|
-
`data.bnf.fr would not run this query${engine?.text ? `: ${engine.text}` : "."}`,
|
|
310
|
-
"Try fewer or plainer words. If this keeps happening with ordinary input, please report it."
|
|
311
|
-
);
|
|
382
|
+
if (verdict.kind === "refused") {
|
|
383
|
+
throw verdict.error;
|
|
312
384
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
status: response.status
|
|
316
|
-
});
|
|
385
|
+
askedWaitMs = verdict.waitMs;
|
|
386
|
+
lastError = new Error(`HTTP ${response.status}`);
|
|
317
387
|
} catch (error) {
|
|
318
388
|
clearTimeout(deadline);
|
|
319
|
-
|
|
320
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
321
|
-
lastError = error;
|
|
322
|
-
if (attempt >= Math.min(maxRetries, RETRIES_AFTER_SILENCE)) {
|
|
323
|
-
throw timeout(
|
|
324
|
-
`No answer from data.bnf.fr within ${timeoutMs}ms. A query spanning the whole catalogue can take longer than the service will spend on it.`,
|
|
325
|
-
{ url: SPARQL_ENDPOINT }
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
askedWaitMs = backoffMs(attempt);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
332
|
-
if (attempt >= maxRetries) {
|
|
333
|
-
throw networkError(`Could not reach data.bnf.fr: ${lastError.message}`, {
|
|
334
|
-
url: SPARQL_ENDPOINT
|
|
335
|
-
});
|
|
336
|
-
}
|
|
389
|
+
lastError = readFailure(error, { attempt, maxRetries, timeoutMs });
|
|
337
390
|
askedWaitMs = backoffMs(attempt);
|
|
338
|
-
continue;
|
|
339
391
|
} finally {
|
|
340
392
|
clearTimeout(deadline);
|
|
341
393
|
}
|
|
@@ -369,9 +421,10 @@ function parseResults(body) {
|
|
|
369
421
|
let payload;
|
|
370
422
|
try {
|
|
371
423
|
payload = JSON.parse(body);
|
|
372
|
-
} catch {
|
|
424
|
+
} catch (cause) {
|
|
373
425
|
throw parseFailure("data.bnf.fr answered with something that is not JSON.", {
|
|
374
|
-
url: SPARQL_ENDPOINT
|
|
426
|
+
url: SPARQL_ENDPOINT,
|
|
427
|
+
cause
|
|
375
428
|
});
|
|
376
429
|
}
|
|
377
430
|
if (typeof payload !== "object" || payload === null || !("results" in payload) || typeof payload.results !== "object" || !Array.isArray(payload.results?.bindings)) {
|
|
@@ -383,6 +436,13 @@ function parseResults(body) {
|
|
|
383
436
|
}
|
|
384
437
|
|
|
385
438
|
// src/bnf/sparql.ts
|
|
439
|
+
var LEADING_JOINERS = /^['-]+/u;
|
|
440
|
+
var TRAILING_JOINERS = /['-]+$/u;
|
|
441
|
+
var ARK_IN_INPUT = /(?:^|\/)ark:\/12148\/(c[bc][0-9a-z]+)/i;
|
|
442
|
+
var LEADING_HASH = /^#/;
|
|
443
|
+
var FRAGMENT_OR_QUERY = /[#?]/;
|
|
444
|
+
var TEMP_WORK_IN_INPUT = /temp-work\/([0-9a-f]{32})/i;
|
|
445
|
+
var GALLICA_ARK = /ark:\/12148\/([0-9a-z]+)/i;
|
|
386
446
|
function escapeLiteral(value) {
|
|
387
447
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "");
|
|
388
448
|
}
|
|
@@ -391,49 +451,20 @@ var INVISIBLE = /[\u0000-\u0008\u000e-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200
|
|
|
391
451
|
function withoutInvisibles(input) {
|
|
392
452
|
return input.replace(INVISIBLE, "");
|
|
393
453
|
}
|
|
394
|
-
function invisibleCharacters(input) {
|
|
395
|
-
const found = [];
|
|
396
|
-
for (const character of input.match(INVISIBLE) ?? []) {
|
|
397
|
-
if (!found.includes(character)) found.push(character);
|
|
398
|
-
}
|
|
399
|
-
return found;
|
|
400
|
-
}
|
|
401
|
-
function charactersSetAside(input) {
|
|
402
|
-
const found = [];
|
|
403
|
-
for (const character of withoutInvisibles(input).normalize("NFC")) {
|
|
404
|
-
if (/[\p{L}\p{N}'\-\s]/u.test(character) || /[‘’ʼ]/u.test(character)) continue;
|
|
405
|
-
if (!found.includes(character)) found.push(character);
|
|
406
|
-
}
|
|
407
|
-
return found;
|
|
408
|
-
}
|
|
409
|
-
function readSearchText(input) {
|
|
410
|
-
const words = toSearchWords(input);
|
|
411
|
-
return {
|
|
412
|
-
words,
|
|
413
|
-
terms: toIndexTerms(words),
|
|
414
|
-
invisible: invisibleCharacters(input),
|
|
415
|
-
setAside: charactersSetAside(input)
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
454
|
function toSearchWords(input) {
|
|
419
455
|
const cleaned = withoutInvisibles(input).normalize("NFC").replace(/[‘’ʼ]/gu, "'").replace(/[^\p{L}\p{N}'-]+/gu, " ");
|
|
420
456
|
const words = [];
|
|
421
457
|
for (const raw of cleaned.split(" ")) {
|
|
422
|
-
const word = raw.replace(
|
|
423
|
-
if (word === "")
|
|
458
|
+
const word = raw.replace(LEADING_JOINERS, "").replace(TRAILING_JOINERS, "");
|
|
459
|
+
if (word === "") {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
424
462
|
words.push(word.length > 60 ? word.slice(0, 60) : word);
|
|
425
|
-
if (words.length >= 12)
|
|
426
|
-
|
|
427
|
-
return words;
|
|
428
|
-
}
|
|
429
|
-
function toIndexTerms(words) {
|
|
430
|
-
const terms = [];
|
|
431
|
-
for (const word of words) {
|
|
432
|
-
for (const piece of word.split(/['-]+/u)) {
|
|
433
|
-
if (piece !== "" && !terms.includes(piece)) terms.push(piece);
|
|
463
|
+
if (words.length >= 12) {
|
|
464
|
+
break;
|
|
434
465
|
}
|
|
435
466
|
}
|
|
436
|
-
return
|
|
467
|
+
return words;
|
|
437
468
|
}
|
|
438
469
|
function containsAllWords(words) {
|
|
439
470
|
if (words.length === 0) {
|
|
@@ -452,8 +483,8 @@ function parseEntityId(input) {
|
|
|
452
483
|
if (trimmed === "") {
|
|
453
484
|
throw invalidInput("An identifier is required.");
|
|
454
485
|
}
|
|
455
|
-
const arkMatch =
|
|
456
|
-
const bare = arkMatch ? arkMatch[1] : trimmed.replace(
|
|
486
|
+
const arkMatch = ARK_IN_INPUT.exec(trimmed);
|
|
487
|
+
const bare = arkMatch ? arkMatch[1] : trimmed.replace(LEADING_HASH, "").split(FRAGMENT_OR_QUERY)[0];
|
|
457
488
|
const candidate = (bare ?? "").toLowerCase();
|
|
458
489
|
if (ARK_ID.test(candidate)) {
|
|
459
490
|
return {
|
|
@@ -463,7 +494,7 @@ function parseEntityId(input) {
|
|
|
463
494
|
pageUrl: `https://data.bnf.fr/ark:/12148/${candidate}`
|
|
464
495
|
};
|
|
465
496
|
}
|
|
466
|
-
const tempMatch =
|
|
497
|
+
const tempMatch = TEMP_WORK_IN_INPUT.exec(trimmed);
|
|
467
498
|
const digest = (tempMatch?.[1] ?? candidate).toLowerCase();
|
|
468
499
|
if (TEMP_WORK_ID.test(digest)) {
|
|
469
500
|
return {
|
|
@@ -484,7 +515,7 @@ function hintFor(input) {
|
|
|
484
515
|
const name = (page2[2] ?? "").replace(/_/g, " ");
|
|
485
516
|
return `That is the readable address of a data.bnf.fr page, and ${page2[1]} in it is the record's FRBNF number rather than the identifier these tools take. Search for "${name}" with search_authors or search_works, and use the identifier the row carries.`;
|
|
486
517
|
}
|
|
487
|
-
const gallicaArk =
|
|
518
|
+
const gallicaArk = GALLICA_ARK.exec(input)?.[1] ?? input;
|
|
488
519
|
if (GALLICA_DOCUMENT.test(gallicaArk)) {
|
|
489
520
|
return "That is a Gallica identifier: it names a digitised document rather than a record in the catalogue, and these tools read the catalogue. Search for the work with search_works, then list_editions will give you the Gallica link alongside the edition it belongs to.";
|
|
490
521
|
}
|
|
@@ -492,7 +523,7 @@ function hintFor(input) {
|
|
|
492
523
|
}
|
|
493
524
|
var iriOf = (id) => `<${id.iri}>`;
|
|
494
525
|
function boundedInteger(value, min, max, name) {
|
|
495
|
-
if (!Number.isFinite(value)
|
|
526
|
+
if (!(Number.isFinite(value) && Number.isInteger(value))) {
|
|
496
527
|
throw invalidInput(`'${name}' has to be a whole number.`);
|
|
497
528
|
}
|
|
498
529
|
if (value < min || value > max) {
|
|
@@ -713,6 +744,14 @@ LIMIT 20`);
|
|
|
713
744
|
}
|
|
714
745
|
|
|
715
746
|
// src/bnf/parse.ts
|
|
747
|
+
var ARK_IN_IRI = /ark:\/12148\/(c[bc][0-9a-z]+)/i;
|
|
748
|
+
var TEMP_WORK_IN_IRI = /temp-work\/([0-9a-f]{32})/i;
|
|
749
|
+
var TRAILING_SEPARATOR = /[/#]$/;
|
|
750
|
+
var PATH_SEPARATOR = /[/#]/;
|
|
751
|
+
var ARK_SEGMENT = /ark:\/12148\/([^/?#]+)/;
|
|
752
|
+
var QUERY_START = /[?#]/;
|
|
753
|
+
var LEADING_DOTS = /^[./]+/;
|
|
754
|
+
var TRAILING_SLASHES = /\/+$/;
|
|
716
755
|
var P = {
|
|
717
756
|
type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
|
718
757
|
label: "http://www.w3.org/2000/01/rdf-schema#label",
|
|
@@ -768,36 +807,50 @@ var ALIGNMENT_HOSTS = [
|
|
|
768
807
|
{ host: "catalogue.bnf.fr", name: "catalogue" }
|
|
769
808
|
];
|
|
770
809
|
var text = (term) => {
|
|
771
|
-
if (!term)
|
|
810
|
+
if (!term) {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
772
813
|
const value = term.value.trim();
|
|
773
814
|
return value === "" ? null : value;
|
|
774
815
|
};
|
|
775
816
|
var integer = (term) => {
|
|
776
817
|
const raw = text(term);
|
|
777
|
-
if (raw === null)
|
|
818
|
+
if (raw === null) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
778
821
|
const value = Number(raw);
|
|
779
822
|
return Number.isInteger(value) ? value : null;
|
|
780
823
|
};
|
|
781
824
|
function idFromIri(iri) {
|
|
782
|
-
const ark =
|
|
783
|
-
if (ark?.[1])
|
|
784
|
-
|
|
785
|
-
|
|
825
|
+
const ark = ARK_IN_IRI.exec(iri);
|
|
826
|
+
if (ark?.[1]) {
|
|
827
|
+
return ark[1].toLowerCase();
|
|
828
|
+
}
|
|
829
|
+
const temp = TEMP_WORK_IN_IRI.exec(iri);
|
|
830
|
+
if (temp?.[1]) {
|
|
831
|
+
return `temp-work/${temp[1].toLowerCase()}`;
|
|
832
|
+
}
|
|
786
833
|
return null;
|
|
787
834
|
}
|
|
788
|
-
var vocabularyTerm = (iri) => decodeURIComponent(iri.replace(
|
|
835
|
+
var vocabularyTerm = (iri) => decodeURIComponent(iri.replace(TRAILING_SEPARATOR, "").split(PATH_SEPARATOR).pop() ?? iri);
|
|
789
836
|
function toDigitisedLink(address, role, fromId, fromTitle) {
|
|
790
|
-
if (!address
|
|
791
|
-
|
|
792
|
-
|
|
837
|
+
if (!(address && isGallicaAddress(address))) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
const segment = ARK_SEGMENT.exec(address)?.[1] ?? null;
|
|
841
|
+
if (!segment) {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
793
844
|
const ark = segment.split(".")[0] ?? segment;
|
|
794
|
-
if (ark === "")
|
|
845
|
+
if (ark === "") {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
795
848
|
return { ark, url: address, rendering: renderingOf(address, ark), role, fromId, fromTitle };
|
|
796
849
|
}
|
|
797
850
|
function renderingOf(address, ark) {
|
|
798
|
-
const path = address.split(
|
|
851
|
+
const path = address.split(QUERY_START)[0] ?? address;
|
|
799
852
|
const after = path.slice(path.indexOf(`ark:/12148/${ark}`) + `ark:/12148/${ark}`.length);
|
|
800
|
-
const rendering = after.replace(
|
|
853
|
+
const rendering = after.replace(LEADING_DOTS, "").replace(TRAILING_SLASHES, "");
|
|
801
854
|
return rendering === "" ? null : rendering;
|
|
802
855
|
}
|
|
803
856
|
function page(rows, limit, skipped) {
|
|
@@ -807,9 +860,13 @@ function page(rows, limit, skipped) {
|
|
|
807
860
|
function readWindowOccupancy(results) {
|
|
808
861
|
for (const row of results.results.bindings) {
|
|
809
862
|
const raw = text(row.windowRows);
|
|
810
|
-
if (raw === null)
|
|
863
|
+
if (raw === null) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
811
866
|
const count = Number(raw);
|
|
812
|
-
if (Number.isInteger(count))
|
|
867
|
+
if (Number.isInteger(count)) {
|
|
868
|
+
return count >= TEXT_WINDOW;
|
|
869
|
+
}
|
|
813
870
|
}
|
|
814
871
|
return null;
|
|
815
872
|
}
|
|
@@ -818,10 +875,15 @@ function byPredicate(results) {
|
|
|
818
875
|
const index = /* @__PURE__ */ new Map();
|
|
819
876
|
for (const row of results.results.bindings) {
|
|
820
877
|
const predicate = text(row.p);
|
|
821
|
-
if (predicate === null)
|
|
878
|
+
if (predicate === null) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
822
881
|
const bucket = index.get(predicate);
|
|
823
|
-
if (bucket)
|
|
824
|
-
|
|
882
|
+
if (bucket) {
|
|
883
|
+
bucket.push(row);
|
|
884
|
+
} else {
|
|
885
|
+
index.set(predicate, [row]);
|
|
886
|
+
}
|
|
825
887
|
}
|
|
826
888
|
return index;
|
|
827
889
|
}
|
|
@@ -830,7 +892,9 @@ var allValues = (index, predicate) => {
|
|
|
830
892
|
const seen = /* @__PURE__ */ new Set();
|
|
831
893
|
for (const row of index.get(predicate) ?? []) {
|
|
832
894
|
const value = text(row.o);
|
|
833
|
-
if (value !== null)
|
|
895
|
+
if (value !== null) {
|
|
896
|
+
seen.add(value);
|
|
897
|
+
}
|
|
834
898
|
}
|
|
835
899
|
return [...seen];
|
|
836
900
|
};
|
|
@@ -841,8 +905,12 @@ function alignments(index, selfId) {
|
|
|
841
905
|
const found = {};
|
|
842
906
|
for (const predicate of [P.sameAs, P.exactMatch, P.closeMatch, P.seeAlso]) {
|
|
843
907
|
for (const address of allValues(index, predicate)) {
|
|
844
|
-
if (!address.startsWith("http"))
|
|
845
|
-
|
|
908
|
+
if (!address.startsWith("http")) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (selfId && address.includes(selfId)) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
846
914
|
let host;
|
|
847
915
|
try {
|
|
848
916
|
host = new URL(address).hostname.toLowerCase();
|
|
@@ -852,8 +920,12 @@ function alignments(index, selfId) {
|
|
|
852
920
|
const known = ALIGNMENT_HOSTS.find(
|
|
853
921
|
(entry) => host === entry.host || host.endsWith(`.${entry.host}`)
|
|
854
922
|
);
|
|
855
|
-
if (!known)
|
|
856
|
-
|
|
923
|
+
if (!known) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const addresses = found[known.name] ?? /* @__PURE__ */ new Set();
|
|
927
|
+
found[known.name] = addresses;
|
|
928
|
+
addresses.add(address);
|
|
857
929
|
}
|
|
858
930
|
}
|
|
859
931
|
return Object.fromEntries(Object.entries(found).map(([name, set]) => [name, [...set]]));
|
|
@@ -868,14 +940,18 @@ function toAuthorSummaries(results, limit) {
|
|
|
868
940
|
const seen = /* @__PURE__ */ new Set();
|
|
869
941
|
let skipped = 0;
|
|
870
942
|
for (const row of results.results.bindings) {
|
|
871
|
-
if (isOccupancyRow(row, "person"))
|
|
943
|
+
if (isOccupancyRow(row, "person")) {
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
872
946
|
const iri = text(row.person);
|
|
873
947
|
const id = iri === null ? null : idFromIri(iri);
|
|
874
948
|
if (iri === null || id === null) {
|
|
875
949
|
skipped += 1;
|
|
876
950
|
continue;
|
|
877
951
|
}
|
|
878
|
-
if (seen.has(id))
|
|
952
|
+
if (seen.has(id)) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
879
955
|
seen.add(id);
|
|
880
956
|
rows.push({
|
|
881
957
|
id,
|
|
@@ -896,18 +972,24 @@ function toAuthorDetail(results, id, pageUrl) {
|
|
|
896
972
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
897
973
|
for (const row of index.get(P.altLabel) ?? []) {
|
|
898
974
|
const label = text(row.o);
|
|
899
|
-
if (label === null || seenLabels.has(label))
|
|
975
|
+
if (label === null || seenLabels.has(label)) {
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
900
978
|
seenLabels.add(label);
|
|
901
979
|
otherNames.push({ label, language: text(row.lang) });
|
|
902
980
|
}
|
|
903
981
|
const sameAs = alignments(index, id);
|
|
904
982
|
const isni = isniAddresses(index);
|
|
905
|
-
if (isni.length > 0)
|
|
983
|
+
if (isni.length > 0) {
|
|
984
|
+
sameAs.isni = [.../* @__PURE__ */ new Set([...sameAs.isni ?? [], ...isni])];
|
|
985
|
+
}
|
|
906
986
|
const catalogueUrl = allValues(index, P.seeAlso).find((address) => address.includes("catalogue.bnf.fr")) ?? null;
|
|
907
987
|
const depictions = [];
|
|
908
988
|
for (const address of allValues(index, P.depiction)) {
|
|
909
989
|
const link = toDigitisedLink(address, "depiction", id, null);
|
|
910
|
-
if (link)
|
|
990
|
+
if (link) {
|
|
991
|
+
depictions.push(link);
|
|
992
|
+
}
|
|
911
993
|
}
|
|
912
994
|
return {
|
|
913
995
|
id,
|
|
@@ -941,8 +1023,12 @@ function toAuthorDetail(results, id, pageUrl) {
|
|
|
941
1023
|
function readStatus(statement, iri) {
|
|
942
1024
|
if (statement !== null) {
|
|
943
1025
|
const said = statement.toLowerCase();
|
|
944
|
-
if (said.includes("provisional"))
|
|
945
|
-
|
|
1026
|
+
if (said.includes("provisional")) {
|
|
1027
|
+
return "provisional";
|
|
1028
|
+
}
|
|
1029
|
+
if (said.includes("established")) {
|
|
1030
|
+
return "established";
|
|
1031
|
+
}
|
|
946
1032
|
}
|
|
947
1033
|
return iri.includes("/temp-work/") ? "provisional" : "established";
|
|
948
1034
|
}
|
|
@@ -951,7 +1037,9 @@ function toWorkSummaries(results, limit) {
|
|
|
951
1037
|
const byId = /* @__PURE__ */ new Map();
|
|
952
1038
|
let skipped = 0;
|
|
953
1039
|
for (const row of results.results.bindings) {
|
|
954
|
-
if (isOccupancyRow(row, "work"))
|
|
1040
|
+
if (isOccupancyRow(row, "work")) {
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
955
1043
|
const iri = text(row.work);
|
|
956
1044
|
const id = iri === null ? null : idFromIri(iri);
|
|
957
1045
|
if (iri === null || id === null) {
|
|
@@ -979,7 +1067,7 @@ function toWorkSummaries(results, limit) {
|
|
|
979
1067
|
}
|
|
980
1068
|
return {
|
|
981
1069
|
...page(
|
|
982
|
-
order.map((id) => byId.get(id)),
|
|
1070
|
+
order.map((id) => byId.get(id)).filter((one) => one !== void 0),
|
|
983
1071
|
limit,
|
|
984
1072
|
skipped
|
|
985
1073
|
),
|
|
@@ -1012,13 +1100,15 @@ function toAuthoredWorks(results, limit) {
|
|
|
1012
1100
|
order.push(id);
|
|
1013
1101
|
}
|
|
1014
1102
|
const form = text(row.form);
|
|
1015
|
-
if (form
|
|
1103
|
+
if (form?.startsWith("http")) {
|
|
1016
1104
|
const term = vocabularyTerm(form);
|
|
1017
|
-
if (!work.forms.includes(term))
|
|
1105
|
+
if (!work.forms.includes(term)) {
|
|
1106
|
+
work.forms.push(term);
|
|
1107
|
+
}
|
|
1018
1108
|
}
|
|
1019
1109
|
}
|
|
1020
1110
|
return page(
|
|
1021
|
-
order.map((id) => byId.get(id)),
|
|
1111
|
+
order.map((id) => byId.get(id)).filter((one) => one !== void 0),
|
|
1022
1112
|
limit,
|
|
1023
1113
|
skipped
|
|
1024
1114
|
);
|
|
@@ -1031,13 +1121,17 @@ function toWorkDetail(results, id, iri, pageUrl) {
|
|
|
1031
1121
|
for (const row of index.get(P.creator) ?? []) {
|
|
1032
1122
|
const creatorIri = text(row.o);
|
|
1033
1123
|
const creatorId = creatorIri ? idFromIri(creatorIri) : null;
|
|
1034
|
-
if (!creatorId || creators.some((c) => c.id === creatorId))
|
|
1124
|
+
if (!creatorId || creators.some((c) => c.id === creatorId)) {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1035
1127
|
creators.push({ id: creatorId, name: text(row.name) });
|
|
1036
1128
|
}
|
|
1037
1129
|
const depictions = [];
|
|
1038
1130
|
for (const address of allValues(index, P.depiction)) {
|
|
1039
1131
|
const link = toDigitisedLink(address, "depiction", id, null);
|
|
1040
|
-
if (link)
|
|
1132
|
+
if (link) {
|
|
1133
|
+
depictions.push(link);
|
|
1134
|
+
}
|
|
1041
1135
|
}
|
|
1042
1136
|
const statusStatement = firstValue(index, P.statusOfIdentification);
|
|
1043
1137
|
const catalogueUrl = allValues(index, P.seeAlso).find((address) => address.includes("catalogue.bnf.fr")) ?? null;
|
|
@@ -1107,7 +1201,7 @@ function toEditions(results, limit) {
|
|
|
1107
1201
|
}
|
|
1108
1202
|
}
|
|
1109
1203
|
return page(
|
|
1110
|
-
order.map((id) => byId.get(id)),
|
|
1204
|
+
order.map((id) => byId.get(id)).filter((one) => one !== void 0),
|
|
1111
1205
|
limit,
|
|
1112
1206
|
skipped
|
|
1113
1207
|
);
|
|
@@ -1126,7 +1220,9 @@ function toDigitisedLinks(results, limit) {
|
|
|
1126
1220
|
["depiction", "depiction"]
|
|
1127
1221
|
]) {
|
|
1128
1222
|
const link = toDigitisedLink(text(row[column]), role, fromId, title);
|
|
1129
|
-
if (!link || seen.has(link.url))
|
|
1223
|
+
if (!link || seen.has(link.url)) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1130
1226
|
seen.add(link.url);
|
|
1131
1227
|
links.push(link);
|
|
1132
1228
|
}
|
|
@@ -1137,7 +1233,9 @@ function toTypes(results) {
|
|
|
1137
1233
|
const types = /* @__PURE__ */ new Set();
|
|
1138
1234
|
for (const row of results.results.bindings) {
|
|
1139
1235
|
const iri = text(row.type);
|
|
1140
|
-
if (iri !== null)
|
|
1236
|
+
if (iri !== null) {
|
|
1237
|
+
types.add(iri);
|
|
1238
|
+
}
|
|
1141
1239
|
}
|
|
1142
1240
|
return [...types];
|
|
1143
1241
|
}
|
|
@@ -1178,7 +1276,9 @@ var RateLimiter = class {
|
|
|
1178
1276
|
async beforeRequest() {
|
|
1179
1277
|
const elapsed = Date.now() - this.lastStartedAt;
|
|
1180
1278
|
const wait = Math.min(this.intervalMs, this.intervalMs - elapsed);
|
|
1181
|
-
if (wait > 0)
|
|
1279
|
+
if (wait > 0) {
|
|
1280
|
+
await sleep(wait);
|
|
1281
|
+
}
|
|
1182
1282
|
this.lastStartedAt = Date.now();
|
|
1183
1283
|
}
|
|
1184
1284
|
/** Called when the service asks for room: double the gap, up to the ceiling. */
|
|
@@ -1191,9 +1291,13 @@ var RateLimiter = class {
|
|
|
1191
1291
|
* response after a rough patch does not undo the caution that earned it.
|
|
1192
1292
|
*/
|
|
1193
1293
|
succeeded() {
|
|
1194
|
-
if (this.intervalMs === this.baseIntervalMs)
|
|
1294
|
+
if (this.intervalMs === this.baseIntervalMs) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1195
1297
|
this.calmStreak += 1;
|
|
1196
|
-
if (this.calmStreak < 3)
|
|
1298
|
+
if (this.calmStreak < 3) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1197
1301
|
this.calmStreak = 0;
|
|
1198
1302
|
this.intervalMs = Math.max(this.baseIntervalMs, Math.round(this.intervalMs / 2));
|
|
1199
1303
|
}
|
|
@@ -1212,7 +1316,9 @@ function withGuarantees(config) {
|
|
|
1212
1316
|
const defaults = loadConfig({});
|
|
1213
1317
|
const bounded = (value, fallback, min, max) => {
|
|
1214
1318
|
const parsed = Number(value);
|
|
1215
|
-
if (!Number.isFinite(parsed))
|
|
1319
|
+
if (!Number.isFinite(parsed)) {
|
|
1320
|
+
return fallback;
|
|
1321
|
+
}
|
|
1216
1322
|
return Math.min(max, Math.max(min, parsed));
|
|
1217
1323
|
};
|
|
1218
1324
|
const claimed = typeof config.userAgent === "string" ? config.userAgent.trim() : "";
|
|
@@ -1263,9 +1369,6 @@ var BnfClient = class {
|
|
|
1263
1369
|
return this.config.timeoutMs;
|
|
1264
1370
|
}
|
|
1265
1371
|
/** The one address this client sends anything to. */
|
|
1266
|
-
get endpoint() {
|
|
1267
|
-
return SPARQL_ENDPOINT;
|
|
1268
|
-
}
|
|
1269
1372
|
/**
|
|
1270
1373
|
* `cacheKey` is stated rather than taken from the query, because two reads of
|
|
1271
1374
|
* one query can produce different values: a list cut to five rows is not the
|
|
@@ -1309,32 +1412,7 @@ var BnfClient = class {
|
|
|
1309
1412
|
return { data, cached: false, retrievedAt: new Date(retrievedAt).toISOString() };
|
|
1310
1413
|
}
|
|
1311
1414
|
/** Read an identifier a caller wrote, refusing anything this dataset cannot address. */
|
|
1312
|
-
identify(input) {
|
|
1313
|
-
return parseEntityId(input);
|
|
1314
|
-
}
|
|
1315
1415
|
/** The words a search will actually look for, which a tool reports back. */
|
|
1316
|
-
words(input) {
|
|
1317
|
-
return toSearchWords(input);
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* The terms the index requires, which is what a row's presence rests on.
|
|
1321
|
-
*
|
|
1322
|
-
* The index splits inside a word at an apostrophe and at a hyphen, so a word
|
|
1323
|
-
* a caller wrote as one can reach it as two, each required on its own.
|
|
1324
|
-
*/
|
|
1325
|
-
indexTerms(words) {
|
|
1326
|
-
return toIndexTerms(words);
|
|
1327
|
-
}
|
|
1328
|
-
/**
|
|
1329
|
-
* How a piece of caller text became the terms the search was run with.
|
|
1330
|
-
*
|
|
1331
|
-
* A tool reports the reading back, so the same reading has to be the one the
|
|
1332
|
-
* query was built from: the words, the terms the index makes of them, and
|
|
1333
|
-
* the characters that reached neither.
|
|
1334
|
-
*/
|
|
1335
|
-
searchReading(input) {
|
|
1336
|
-
return readSearchText(input);
|
|
1337
|
-
}
|
|
1338
1416
|
searchAuthors(name, limit, offset) {
|
|
1339
1417
|
const words = toSearchWords(name);
|
|
1340
1418
|
const query = searchAuthorsQuery(words, limit, offset);
|
|
@@ -1443,6 +1521,7 @@ var BnfClient = class {
|
|
|
1443
1521
|
}
|
|
1444
1522
|
};
|
|
1445
1523
|
export {
|
|
1446
|
-
BnfClient
|
|
1524
|
+
BnfClient,
|
|
1525
|
+
parseEntityId
|
|
1447
1526
|
};
|
|
1448
1527
|
//# sourceMappingURL=client.js.map
|