@netwerk-digitaal-erfgoed/network-of-terms-query 6.2.9 → 6.2.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netwerk-digitaal-erfgoed/network-of-terms-query",
3
- "version": "6.2.9",
3
+ "version": "6.2.10",
4
4
  "description": "Engine for querying sources in the Network of Terms",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/netwerk-digitaal-erfgoed/network-of-terms#readme",
@@ -30,19 +30,19 @@
30
30
  "main": "./dist/index.js",
31
31
  "types": "./dist/index.d.ts",
32
32
  "dependencies": {
33
- "@comunica/query-sparql": "^4.3.0",
34
- "@comunica/types": "^4.2.0",
35
- "@comunica/utils-bindings-factory": "^4.2.0",
33
+ "@comunica/query-sparql": "^5.1.2",
34
+ "@comunica/types": "^5.1.0",
35
+ "@comunica/utils-bindings-factory": "^5.1.0",
36
36
  "@hapi/hoek": "^11.0.7",
37
37
  "@opentelemetry/api": "^1.9.0",
38
- "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0",
38
+ "@opentelemetry/exporter-metrics-otlp-proto": "0.208.0",
39
39
  "@opentelemetry/resources": "2.2.0",
40
40
  "@opentelemetry/sdk-metrics": "2.2.0",
41
- "@opentelemetry/semantic-conventions": "1.37.0",
41
+ "@opentelemetry/semantic-conventions": "1.38.0",
42
42
  "@rdfjs/types": "^2.0.1",
43
43
  "env-schema": "^6.0.1",
44
44
  "jest-dev-server": "11.0.0",
45
- "joi": "^17.13.3",
45
+ "joi": "^18.0.2",
46
46
  "nock": "^14.0.10",
47
47
  "pino": "^9.7.0",
48
48
  "pretty-ms": "^9.2.0",
@@ -51,7 +51,7 @@
51
51
  "tslib": "^2.3.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@comunica/query-sparql-file": "^4.3.0",
54
+ "@comunica/query-sparql-file": "^5.1.2",
55
55
  "asynciterator": "^3.9.0"
56
56
  }
57
57
  }
package/src/query.ts CHANGED
@@ -4,7 +4,6 @@ import { LoggerPino } from './helpers/logger-pino.js';
4
4
  import Pino from 'pino';
5
5
  import PrettyMilliseconds from 'pretty-ms';
6
6
  import * as RDF from '@rdfjs/types';
7
- import { Bindings } from '@rdfjs/types';
8
7
  import { Term, TermsTransformer } from './terms.js';
9
8
  import { QueryMode, queryVariants } from './search/query-mode.js';
10
9
  import { Dataset, Distribution, IRI } from './catalog.js';
@@ -14,6 +13,50 @@ import { DataFactory } from 'rdf-data-factory';
14
13
  import { sourceQueriesHistogram } from './instrumentation.js';
15
14
  import { config } from './config.js';
16
15
 
16
+ /**
17
+ * Check if a query requires string substitution instead of initialBindings.
18
+ * Workaround for Comunica v5 traqula bug that crashes with:
19
+ * - SERVICE clauses
20
+ * - VALUES combination
21
+ */
22
+ function requiresStringSubstitution(query: string): boolean {
23
+ const hasService = /\bSERVICE\b/i.test(query);
24
+ const hasValues = /\bVALUES\b/i.test(query);
25
+ return hasService || hasValues;
26
+ }
27
+
28
+ /**
29
+ * Substitute bindings directly into a SPARQL query string.
30
+ * This is a workaround for Comunica v5's initialBindings bug with SERVICE clauses.
31
+ */
32
+ function substituteBindings(
33
+ query: string,
34
+ bindings: Record<string, RDF.Term>,
35
+ ): string {
36
+ let result = query;
37
+ for (const [name, term] of Object.entries(bindings)) {
38
+ const pattern = new RegExp(`\\?${name}\\b`, 'g');
39
+ if (term.termType === 'NamedNode') {
40
+ result = result.replace(pattern, `<${term.value}>`);
41
+ } else if (term.termType === 'Literal') {
42
+ const literal = term as RDF.Literal;
43
+ const datatype = literal.datatype?.value;
44
+ if (
45
+ datatype &&
46
+ datatype !== 'http://www.w3.org/2001/XMLSchema#string' &&
47
+ datatype !== 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString'
48
+ ) {
49
+ result = result.replace(pattern, `"${term.value}"^^<${datatype}>`);
50
+ } else if (literal.language) {
51
+ result = result.replace(pattern, `"${term.value}"@${literal.language}`);
52
+ } else {
53
+ result = result.replace(pattern, `"${term.value}"`);
54
+ }
55
+ }
56
+ }
57
+ return result;
58
+ }
59
+
17
60
  export type TermsResult = Terms | TimeoutError | ServerError;
18
61
 
19
62
  export class TermsResponse {
@@ -141,10 +184,21 @@ export class QueryTermsService {
141
184
  const logger = new LoggerPino({ logger: this.logger });
142
185
  // Extract HTTP credentials if the distribution URL contains any.
143
186
  const url = new URL(distribution.endpoint.toString());
144
- this.logger.info(`Querying "${url}" with "${query}"...`);
145
- const quadStream = await this.engine.queryQuads(query, {
187
+
188
+ // Workaround for https://github.com/comunica/comunica/issues/1655, so use
189
+ // string substitution instead of initialBindings for:
190
+ // - SERVICE clauses crash with initialBindings
191
+ // - VALUES crashes in some combinations
192
+ const useStringSubstitution = requiresStringSubstitution(query);
193
+ const finalQuery = useStringSubstitution
194
+ ? substituteBindings(query, bindings)
195
+ : query;
196
+
197
+ this.logger.info(`Querying "${url}" with "${finalQuery}"...`);
198
+ const quadStream = await this.engine.queryQuads(finalQuery, {
146
199
  log: logger,
147
- httpAuth: url.username === '' ? '' : url.username + ':' + url.password,
200
+ httpAuth:
201
+ url.username === '' ? undefined : url.username + ':' + url.password,
148
202
  httpTimeout: timeoutMs,
149
203
  noCache: true,
150
204
  sources: [
@@ -153,9 +207,10 @@ export class QueryTermsService {
153
207
  value: url.origin + url.pathname,
154
208
  },
155
209
  ],
156
- initialBindings: bindingsFactory.fromRecord(
157
- bindings,
158
- ) as unknown as Bindings,
210
+ // Only pass initialBindings when NOT using string substitution
211
+ ...(useStringSubstitution
212
+ ? {}
213
+ : { initialBindings: bindingsFactory.fromRecord(bindings) }),
159
214
  });
160
215
 
161
216
  return new Promise((resolve) => {
package/src/test-utils.ts CHANGED
@@ -187,7 +187,7 @@ export const testCatalog = (port: number) =>
187
187
  new SparqlDistribution(
188
188
  'https://data.beeldengeluid.nl/id/datadownload/0026',
189
189
  'https://username:password@gtaa.apis.beeldengeluid.nl/sparql',
190
- 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
190
+ 'CONSTRUCT { ?s ?p ?o } WHERE { ?s skos:inScheme ?datasetUri ; ?p ?o }',
191
191
  'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
192
192
  ),
193
193
  ],
@@ -21,11 +21,12 @@ describe('Query', () => {
21
21
  vi.clearAllMocks();
22
22
  });
23
23
  it('passes dataset IRI query parameter to Comunica', async () => {
24
+ // Use GTAA which doesn't have VALUES, so uses initialBindings (not string substitution)
24
25
  const config = await query(
25
- 'https://data.netwerkdigitaalerfgoed.nl/rkd/rkdartists/sparql',
26
+ 'https://data.beeldengeluid.nl/id/datadownload/0026',
26
27
  );
27
28
  expect(config.initialBindings.get('datasetUri')?.value).toEqual(
28
- 'https://data.rkd.nl/rkdartists',
29
+ 'http://data.beeldengeluid.nl/gtaa/Persoonsnamen',
29
30
  );
30
31
  });
31
32
 
package/vite.config.ts CHANGED
@@ -16,11 +16,11 @@ export default defineConfig(() => ({
16
16
  provider: 'v8' as const,
17
17
  thresholds: {
18
18
  autoUpdate: true,
19
- lines: 58.24,
20
- functions: 40,
21
- branches: 96.36,
22
- statements: 58.24,
19
+ lines: 56.81,
20
+ functions: 40.25,
21
+ branches: 91.22,
22
+ statements: 56.81,
23
23
  },
24
24
  },
25
25
  },
26
- }));
26
+ }));