@lde/distribution-probe 0.1.4 → 0.1.5
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/dist/probe.d.ts +8 -2
- package/dist/probe.d.ts.map +1 -1
- package/dist/probe.js +65 -14
- package/package.json +1 -1
package/dist/probe.d.ts
CHANGED
|
@@ -46,8 +46,14 @@ declare abstract class ProbeResult {
|
|
|
46
46
|
* Result of probing a SPARQL endpoint.
|
|
47
47
|
*/
|
|
48
48
|
export declare class SparqlProbeResult extends ProbeResult {
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Content types the probe was prepared to accept as a valid answer. A SELECT or
|
|
51
|
+
* ASK query may be answered with SPARQL results in JSON or XML; the endpoint
|
|
52
|
+
* chooses, so success is not tied to a single serialization. A single string is
|
|
53
|
+
* accepted and normalized to a one-element list for backwards compatibility.
|
|
54
|
+
*/
|
|
55
|
+
readonly acceptedContentTypes: readonly string[];
|
|
56
|
+
constructor(url: string, response: Response, responseTimeMs: number, acceptedContentTypes: string | readonly string[], failureReason?: string | null);
|
|
51
57
|
isSuccess(): boolean;
|
|
52
58
|
}
|
|
53
59
|
/**
|
package/dist/probe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAGnE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAKD;;GAEG;AACH,qBAAa,YAAY;aAEL,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;aACf,cAAc,EAAE,MAAM;gBAFtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM;CAEzC;AAED;;GAEG;AACH,uBAAe,WAAW;aAUN,GAAG,EAAE,MAAM;IAT7B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAQ;IACjD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAgB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,SAAgB,QAAQ,EAAE,MAAM,EAAE,CAAM;IACxC,SAAgB,cAAc,EAAE,MAAM,CAAC;gBAGrB,GAAG,EAAE,MAAM,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;IAa9B,SAAS,IAAI,OAAO;CAO5B;
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../src/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AAGnE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAKD;;GAEG;AACH,qBAAa,YAAY;aAEL,GAAG,EAAE,MAAM;aACX,OAAO,EAAE,MAAM;aACf,cAAc,EAAE,MAAM;gBAFtB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM;CAEzC;AAED;;GAEG;AACH,uBAAe,WAAW;aAUN,GAAG,EAAE,MAAM;IAT7B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,YAAY,EAAE,IAAI,GAAG,IAAI,CAAQ;IACjD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAgB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,SAAgB,QAAQ,EAAE,MAAM,EAAE,CAAM;IACxC,SAAgB,cAAc,EAAE,MAAM,CAAC;gBAGrB,GAAG,EAAE,MAAM,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;IAa9B,SAAS,IAAI,OAAO;CAO5B;AAMD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD;;;;;OAKG;IACH,SAAgB,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;gBAGtD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,oBAAoB,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChD,aAAa,GAAE,MAAM,GAAG,IAAW;IAS5B,SAAS,IAAI,OAAO;CAQ9B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,WAAW;IAClD,SAAgB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAGhD,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,EACtB,aAAa,GAAE,MAAM,GAAG,IAAW;CAQtC;AAED,MAAM,MAAM,eAAe,GACvB,iBAAiB,GACjB,mBAAmB,GACnB,YAAY,CAAC;AAIjB;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CACzB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,eAAe,CAAC,CAkC1B"}
|
package/dist/probe.js
CHANGED
|
@@ -46,19 +46,29 @@ class ProbeResult {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
const SPARQL_RESULTS_JSON = 'application/sparql-results+json';
|
|
49
|
+
const SPARQL_RESULTS_XML = 'application/sparql-results+xml';
|
|
49
50
|
const SPARQL_RDF_RESULTS = 'application/n-triples';
|
|
50
51
|
/**
|
|
51
52
|
* Result of probing a SPARQL endpoint.
|
|
52
53
|
*/
|
|
53
54
|
export class SparqlProbeResult extends ProbeResult {
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Content types the probe was prepared to accept as a valid answer. A SELECT or
|
|
57
|
+
* ASK query may be answered with SPARQL results in JSON or XML; the endpoint
|
|
58
|
+
* chooses, so success is not tied to a single serialization. A single string is
|
|
59
|
+
* accepted and normalized to a one-element list for backwards compatibility.
|
|
60
|
+
*/
|
|
61
|
+
acceptedContentTypes;
|
|
62
|
+
constructor(url, response, responseTimeMs, acceptedContentTypes, failureReason = null) {
|
|
56
63
|
super(url, response, responseTimeMs, failureReason);
|
|
57
|
-
this.
|
|
64
|
+
this.acceptedContentTypes =
|
|
65
|
+
typeof acceptedContentTypes === 'string'
|
|
66
|
+
? [acceptedContentTypes]
|
|
67
|
+
: acceptedContentTypes;
|
|
58
68
|
}
|
|
59
69
|
isSuccess() {
|
|
60
70
|
return (super.isSuccess() &&
|
|
61
|
-
(this.contentType?.startsWith(
|
|
71
|
+
this.acceptedContentTypes.some((type) => this.contentType?.startsWith(type) ?? false));
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
/**
|
|
@@ -137,18 +147,33 @@ function detectSparqlQueryType(query) {
|
|
|
137
147
|
const match = /\b(ASK|SELECT|CONSTRUCT|DESCRIBE)\b/i.exec(withoutComments);
|
|
138
148
|
return (match?.[1].toUpperCase() ?? 'SELECT');
|
|
139
149
|
}
|
|
140
|
-
|
|
150
|
+
/**
|
|
151
|
+
* Content types a SPARQL endpoint may legitimately answer with, in preference
|
|
152
|
+
* order, for the given query type. SELECT and ASK return a results document
|
|
153
|
+
* (JSON or XML – the endpoint chooses); CONSTRUCT and DESCRIBE return RDF.
|
|
154
|
+
*/
|
|
155
|
+
function acceptableContentTypes(queryType) {
|
|
141
156
|
if (queryType === 'ASK' || queryType === 'SELECT') {
|
|
142
|
-
return SPARQL_RESULTS_JSON;
|
|
157
|
+
return [SPARQL_RESULTS_JSON, SPARQL_RESULTS_XML];
|
|
143
158
|
}
|
|
144
|
-
return SPARQL_RDF_RESULTS;
|
|
159
|
+
return [SPARQL_RDF_RESULTS];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Build an `Accept` header that prefers the first content type but still accepts
|
|
163
|
+
* the rest at a lower q-value, so an endpoint that only serves a later type is
|
|
164
|
+
* not rejected with a 406.
|
|
165
|
+
*/
|
|
166
|
+
function acceptHeader(contentTypes) {
|
|
167
|
+
return contentTypes
|
|
168
|
+
.map((type, index) => (index === 0 ? type : `${type};q=0.9`))
|
|
169
|
+
.join(', ');
|
|
145
170
|
}
|
|
146
171
|
async function probeSparqlEndpoint(url, _distribution, options, authHeaders, start) {
|
|
147
172
|
const queryType = detectSparqlQueryType(options.sparqlQuery);
|
|
148
|
-
const
|
|
173
|
+
const acceptedContentTypes = acceptableContentTypes(queryType);
|
|
149
174
|
const headers = new Headers({
|
|
150
175
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
151
|
-
Accept:
|
|
176
|
+
Accept: acceptHeader(acceptedContentTypes),
|
|
152
177
|
});
|
|
153
178
|
for (const [key, value] of authHeaders) {
|
|
154
179
|
headers.set(key, value);
|
|
@@ -160,19 +185,19 @@ async function probeSparqlEndpoint(url, _distribution, options, authHeaders, sta
|
|
|
160
185
|
body: `query=${encodeURIComponent(options.sparqlQuery)}`,
|
|
161
186
|
});
|
|
162
187
|
const actualContentType = response.headers.get('Content-Type');
|
|
163
|
-
const
|
|
188
|
+
const matchedContentType = acceptedContentTypes.find((type) => actualContentType?.startsWith(type) ?? false);
|
|
164
189
|
let failureReason = null;
|
|
165
|
-
if (response.ok &&
|
|
166
|
-
failureReason = await validateSparqlResponse(response, queryType);
|
|
190
|
+
if (response.ok && matchedContentType !== undefined) {
|
|
191
|
+
failureReason = await validateSparqlResponse(response, queryType, matchedContentType);
|
|
167
192
|
}
|
|
168
193
|
else {
|
|
169
194
|
// Drain unconsumed body to release the underlying connection.
|
|
170
195
|
await response.body?.cancel();
|
|
171
196
|
}
|
|
172
197
|
const responseTimeMs = Math.round(performance.now() - start);
|
|
173
|
-
return new SparqlProbeResult(url, response, responseTimeMs,
|
|
198
|
+
return new SparqlProbeResult(url, response, responseTimeMs, acceptedContentTypes, failureReason);
|
|
174
199
|
}
|
|
175
|
-
async function validateSparqlResponse(response, queryType) {
|
|
200
|
+
async function validateSparqlResponse(response, queryType, contentType) {
|
|
176
201
|
const body = await response.text();
|
|
177
202
|
if (body.length === 0) {
|
|
178
203
|
return 'SPARQL endpoint returned an empty response';
|
|
@@ -182,6 +207,11 @@ async function validateSparqlResponse(response, queryType) {
|
|
|
182
207
|
// endpoint answered. Deep parse validation is the data-dump path’s job.
|
|
183
208
|
return null;
|
|
184
209
|
}
|
|
210
|
+
return contentType.startsWith(SPARQL_RESULTS_XML)
|
|
211
|
+
? validateSparqlXmlResults(body, queryType)
|
|
212
|
+
: validateSparqlJsonResults(body, queryType);
|
|
213
|
+
}
|
|
214
|
+
function validateSparqlJsonResults(body, queryType) {
|
|
185
215
|
let json;
|
|
186
216
|
try {
|
|
187
217
|
json = JSON.parse(body);
|
|
@@ -201,6 +231,27 @@ async function validateSparqlResponse(response, queryType) {
|
|
|
201
231
|
}
|
|
202
232
|
return null;
|
|
203
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Lightweight structural check on a SPARQL Query Results XML document. Mirrors
|
|
236
|
+
* the JSON path’s intent – confirm the endpoint answered with the expected shape
|
|
237
|
+
* – without pulling in a full XML parser.
|
|
238
|
+
*/
|
|
239
|
+
function validateSparqlXmlResults(body, queryType) {
|
|
240
|
+
if (!/<sparql[\s>]/i.test(body)) {
|
|
241
|
+
return 'SPARQL endpoint returned invalid XML';
|
|
242
|
+
}
|
|
243
|
+
if (queryType === 'ASK') {
|
|
244
|
+
if (!/<boolean>\s*(true|false)\s*<\/boolean>/i.test(body)) {
|
|
245
|
+
return 'SPARQL endpoint did not return a valid ASK result';
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
// SELECT
|
|
250
|
+
if (!/<results[\s/>]/i.test(body)) {
|
|
251
|
+
return 'SPARQL endpoint did not return a valid results object';
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
204
255
|
async function probeDataDump(url, distribution, options, authHeaders, start) {
|
|
205
256
|
// Express a preference for the declared media type, but accept anything as a
|
|
206
257
|
// fallback. Servers that implement RFC 9110 §12.5.1 content negotiation will
|