@blackcatinformatics/purrdf 0.4.0 → 0.4.3

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/index.mjs CHANGED
@@ -11,9 +11,10 @@
11
11
  // wasm boundary cannot express in Rust:
12
12
  // * `ready()` — one-time async wasm instantiation (required for the `web` target).
13
13
  // * the polymorphic RDF/JS `DataFactory.literal(value, languageOrDatatype)` —
14
- // dispatching a NamedNode datatype argument to `typedLiteral` (a wasm-bindgen
15
- // exported type can't be recovered from an untyped value in Rust).
14
+ // dispatching a NamedNode datatype argument to `typedLiteral` and a
15
+ // `{ language, direction }` argument to `directionalLiteral`.
16
16
  // * `Dataset` iterability (`for (const quad of dataset)`).
17
+ // * `Dataset.from`, `Dataset#toStream`, and `DataFactory#dataset`.
17
18
  // * `datasetToStream` / `streamToDataset` — the async RDF/JS Stream/Sink primitives
18
19
  // over the synchronous `Dataset.quads()` / `Sink` engine surface.
19
20
 
@@ -21,6 +22,7 @@ import init, {
21
22
  DataFactory,
22
23
  Dataset,
23
24
  Quad,
25
+ QueryEngine,
24
26
  shaclEntail,
25
27
  shaclValidateToSarif,
26
28
  Sink,
@@ -30,6 +32,84 @@ import init, {
30
32
 
31
33
  let _ready = false;
32
34
 
35
+ function isNamedNodeTerm(value) {
36
+ return (
37
+ value != null &&
38
+ typeof value === "object" &&
39
+ value.termType === "NamedNode"
40
+ );
41
+ }
42
+
43
+ function isDirectionalLanguage(value) {
44
+ return (
45
+ value != null &&
46
+ typeof value === "object" &&
47
+ typeof value.language === "string" &&
48
+ (value.direction === "ltr" || value.direction === "rtl")
49
+ );
50
+ }
51
+
52
+ function normalizeQueryOptions(options) {
53
+ if (options == null) return { base: undefined, format: undefined };
54
+ if (typeof options !== "object") {
55
+ throw new TypeError("query options must be an object when supplied");
56
+ }
57
+ return {
58
+ base: options.base ?? undefined,
59
+ format: options.format ?? undefined,
60
+ };
61
+ }
62
+
63
+ function visualizationOptionsJson(options) {
64
+ if (options == null) return undefined;
65
+ if (typeof options !== "object" || Array.isArray(options)) {
66
+ throw new TypeError("visualization options must be an object when supplied");
67
+ }
68
+ return JSON.stringify(options);
69
+ }
70
+
71
+ function selectResultToObject(raw) {
72
+ const variables = raw.variables;
73
+ const rawRows = raw.rows;
74
+ try {
75
+ const rows = rawRows.map((row) => {
76
+ const out = Object.create(null);
77
+ for (const variable of variables) {
78
+ const value = row.get(variable);
79
+ if (value !== undefined) out[variable] = value;
80
+ }
81
+ return out;
82
+ });
83
+ return { kind: "select", variables, rows };
84
+ } finally {
85
+ for (const row of rawRows) row.free?.();
86
+ raw.free?.();
87
+ }
88
+ }
89
+
90
+ function queryResultToObject(raw) {
91
+ try {
92
+ switch (raw.kind) {
93
+ case "select": {
94
+ const select = raw.takeSelect();
95
+ if (select === undefined) throw new Error("SELECT result was already consumed");
96
+ return selectResultToObject(select);
97
+ }
98
+ case "ask":
99
+ return { kind: "ask", boolean: raw.boolean };
100
+ case "graph": {
101
+ const dataset = raw.takeDataset();
102
+ if (dataset === undefined) throw new Error("graph result was already consumed");
103
+ return { kind: "graph", dataset };
104
+ }
105
+ default:
106
+ throw new Error(`unknown SPARQL result kind ${raw.kind}`);
107
+ }
108
+ } finally {
109
+ raw.free?.();
110
+ }
111
+ }
112
+
33
113
  /**
34
114
  * Instantiate the wasm module. Idempotent. In Node the wasm bytes are read from the
35
115
  * colocated file; in a browser, pass the bytes/URL (or omit to fetch the colocated
@@ -57,6 +137,36 @@ export async function ready(wasmBytesOrUrl) {
57
137
  };
58
138
  }
59
139
 
140
+ if (!Dataset.from) {
141
+ Dataset.from = function (quads = []) {
142
+ const dataset = new Dataset();
143
+ for (const quad of quads ?? []) dataset.add(quad);
144
+ return dataset;
145
+ };
146
+ }
147
+
148
+ if (!Dataset.prototype.toStream) {
149
+ Dataset.prototype.toStream = function () {
150
+ return datasetToStream(this);
151
+ };
152
+ }
153
+
154
+ if (!Dataset.prototype.__purrdfVisualizationApi) {
155
+ const visualModelJson = Dataset.prototype.visualModelJson;
156
+ const visualExportJson = Dataset.prototype.visualExportJson;
157
+ const visualSvgJson = Dataset.prototype.visualSvgJson;
158
+ Dataset.prototype.visualModel = function (options) {
159
+ return JSON.parse(visualModelJson.call(this, visualizationOptionsJson(options)));
160
+ };
161
+ Dataset.prototype.visualExport = function (options) {
162
+ return JSON.parse(visualExportJson.call(this, visualizationOptionsJson(options)));
163
+ };
164
+ Dataset.prototype.visualSvg = function (options) {
165
+ return JSON.parse(visualSvgJson.call(this, visualizationOptionsJson(options)));
166
+ };
167
+ Dataset.prototype.__purrdfVisualizationApi = true;
168
+ }
169
+
60
170
  // RDF/JS DatasetCore.add(quad)/delete(quad) MUST return the dataset instance so calls
61
171
  // chain (`ds.add(q1).add(q2)`). The wasm methods return a bool ("did the effective set
62
172
  // change?"); the spec surface returns `this` (the changed-bit stays observable via
@@ -91,21 +201,66 @@ export async function ready(wasmBytesOrUrl) {
91
201
 
92
202
  // Present the RDF/JS-spec polymorphic literal(value, languageOrDatatype). The wasm
93
203
  // method takes `(value, language?)`; a NamedNode second argument is a datatype.
204
+ // PurRDF also accepts `{ language, direction }` for RDF 1.2 dirLangString literals.
94
205
  if (!DataFactory.prototype.__purrdfPolymorphicLiteral) {
95
206
  const wasmLiteral = DataFactory.prototype.literal;
96
207
  DataFactory.prototype.literal = function (value, languageOrDatatype) {
97
- if (
98
- languageOrDatatype != null &&
99
- typeof languageOrDatatype === "object" &&
100
- languageOrDatatype.termType === "NamedNode"
101
- ) {
208
+ if (isNamedNodeTerm(languageOrDatatype)) {
102
209
  return this.typedLiteral(value, languageOrDatatype);
103
210
  }
211
+ if (isDirectionalLanguage(languageOrDatatype)) return this.directionalLiteral(value, languageOrDatatype.language, languageOrDatatype.direction);
104
212
  return wasmLiteral.call(this, value, languageOrDatatype ?? undefined);
105
213
  };
106
214
  DataFactory.prototype.__purrdfPolymorphicLiteral = true;
107
215
  }
108
216
 
217
+ if (!DataFactory.prototype.dataset) {
218
+ DataFactory.prototype.dataset = function (quads = []) {
219
+ return Dataset.from(quads ?? []);
220
+ };
221
+ }
222
+
223
+ if (!QueryEngine.prototype.__purrdfPackageRootApi) {
224
+ const wasmQuery = QueryEngine.prototype.query;
225
+ const wasmSelect = QueryEngine.prototype.select;
226
+ const wasmAsk = QueryEngine.prototype.ask;
227
+ const wasmConstruct = QueryEngine.prototype.construct;
228
+ const wasmDescribe = QueryEngine.prototype.describe;
229
+ const wasmUpdate = QueryEngine.prototype.update;
230
+ const wasmQueryRaw = QueryEngine.prototype.queryRaw;
231
+
232
+ QueryEngine.prototype.query = function (dataset, sparql, options) {
233
+ const { base } = normalizeQueryOptions(options);
234
+ return queryResultToObject(wasmQuery.call(this, dataset, sparql, base));
235
+ };
236
+ QueryEngine.prototype.select = function (dataset, sparql, options) {
237
+ const { base } = normalizeQueryOptions(options);
238
+ return selectResultToObject(wasmSelect.call(this, dataset, sparql, base));
239
+ };
240
+ QueryEngine.prototype.ask = function (dataset, sparql, options) {
241
+ const { base } = normalizeQueryOptions(options);
242
+ return wasmAsk.call(this, dataset, sparql, base);
243
+ };
244
+ QueryEngine.prototype.construct = function (dataset, sparql, options) {
245
+ const { base } = normalizeQueryOptions(options);
246
+ return wasmConstruct.call(this, dataset, sparql, base);
247
+ };
248
+ QueryEngine.prototype.describe = function (dataset, sparql, options) {
249
+ const { base } = normalizeQueryOptions(options);
250
+ return wasmDescribe.call(this, dataset, sparql, base);
251
+ };
252
+ QueryEngine.prototype.update = function (dataset, sparql, options) {
253
+ const { base } = normalizeQueryOptions(options);
254
+ wasmUpdate.call(this, dataset, sparql, base);
255
+ return dataset;
256
+ };
257
+ QueryEngine.prototype.queryRaw = function (dataset, sparql, options) {
258
+ const { base, format } = normalizeQueryOptions(options);
259
+ return wasmQueryRaw.call(this, dataset, sparql, base, format);
260
+ };
261
+ QueryEngine.prototype.__purrdfPackageRootApi = true;
262
+ }
263
+
109
264
  _ready = true;
110
265
  }
111
266
 
@@ -134,6 +289,7 @@ export {
134
289
  DataFactory,
135
290
  Dataset,
136
291
  Quad,
292
+ QueryEngine,
137
293
  shaclEntail,
138
294
  shaclValidateToSarif,
139
295
  Sink,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackcatinformatics/purrdf",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "A wasm32, in-memory RDF 1.2 engine with an idiomatic RDF/JS (DataFactory/Dataset/Stream) API. Quoted-triple terms and directional literals included.",
5
5
  "type": "module",
6
6
  "license": "MIT OR Apache-2.0",
@@ -37,7 +37,13 @@
37
37
  "pkg/purrdf_wasm_bg.wasm.d.ts"
38
38
  ],
39
39
  "scripts": {
40
- "test": "node --test tests/*.test.mjs"
40
+ "check": "npm run typecheck && npm test && npm run smoke:pack",
41
+ "smoke:pack": "node tests/pack-smoke.mjs",
42
+ "test": "node --test tests/*.test.mjs",
43
+ "typecheck": "tsc -p tsconfig.types.json"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "7.0.2"
41
47
  },
42
48
  "engines": {
43
49
  "node": ">=18"
@@ -149,6 +149,18 @@ export class Dataset {
149
149
  * the other text formats).
150
150
  */
151
151
  serialize(format: string): string;
152
+ /**
153
+ * `visualExportJson(optionsJson?)` -> model, scene, geometry, and index as JSON.
154
+ */
155
+ visualExportJson(options_json?: string | null): string;
156
+ /**
157
+ * `visualModelJson(optionsJson?)` -> the renderer-neutral RDF 1.2 model as JSON.
158
+ */
159
+ visualModelJson(options_json?: string | null): string;
160
+ /**
161
+ * `visualSvgJson(optionsJson?)` -> deterministic SVG and its complete export.
162
+ */
163
+ visualSvgJson(options_json?: string | null): string;
152
164
  /**
153
165
  * `size` — the number of effective quads.
154
166
  */
@@ -198,6 +210,109 @@ export class Quad {
198
210
  readonly value: string;
199
211
  }
200
212
 
213
+ /**
214
+ * A reusable SPARQL engine that keeps the native plan cache alive across calls.
215
+ */
216
+ export class QueryEngine {
217
+ free(): void;
218
+ [Symbol.dispose](): void;
219
+ /**
220
+ * Run an ASK query and return the boolean result.
221
+ */
222
+ ask(dataset: Dataset, sparql: string, base?: string | null): boolean;
223
+ /**
224
+ * Run a CONSTRUCT query and return its result dataset.
225
+ */
226
+ construct(dataset: Dataset, sparql: string, base?: string | null): Dataset;
227
+ /**
228
+ * Run a DESCRIBE query and return its result dataset.
229
+ */
230
+ describe(dataset: Dataset, sparql: string, base?: string | null): Dataset;
231
+ /**
232
+ * Create a reusable offline SPARQL engine.
233
+ */
234
+ constructor();
235
+ /**
236
+ * Run any SPARQL query and return a typed raw wasm result wrapper.
237
+ */
238
+ query(dataset: Dataset, sparql: string, base?: string | null): QueryResult;
239
+ /**
240
+ * Run any SPARQL query and serialize its raw result.
241
+ */
242
+ queryRaw(dataset: Dataset, sparql: string, base?: string | null, format?: string | null): string;
243
+ /**
244
+ * Run a SELECT query and return typed rows.
245
+ */
246
+ select(dataset: Dataset, sparql: string, base?: string | null): SelectResult;
247
+ /**
248
+ * Apply a SPARQL UPDATE atomically to the supplied dataset.
249
+ */
250
+ update(dataset: Dataset, sparql: string, base?: string | null): void;
251
+ }
252
+
253
+ /**
254
+ * A typed SPARQL result returned by the raw wasm binding.
255
+ */
256
+ export class QueryResult {
257
+ private constructor();
258
+ free(): void;
259
+ [Symbol.dispose](): void;
260
+ /**
261
+ * Move the graph dataset out of this wrapper.
262
+ */
263
+ takeDataset(): Dataset | undefined;
264
+ /**
265
+ * Move the SELECT result out of this wrapper.
266
+ */
267
+ takeSelect(): SelectResult | undefined;
268
+ /**
269
+ * The ASK boolean when `kind === "ask"`, otherwise `undefined`.
270
+ */
271
+ readonly boolean: boolean | undefined;
272
+ /**
273
+ * The result discriminator: `"select"`, `"ask"`, or `"graph"`.
274
+ */
275
+ readonly kind: string;
276
+ }
277
+
278
+ /**
279
+ * A typed SELECT result returned by the raw wasm binding.
280
+ */
281
+ export class SelectResult {
282
+ private constructor();
283
+ free(): void;
284
+ [Symbol.dispose](): void;
285
+ /**
286
+ * The result discriminator.
287
+ */
288
+ readonly kind: string;
289
+ /**
290
+ * SELECT rows, one object-like row per solution.
291
+ */
292
+ readonly rows: SelectRow[];
293
+ /**
294
+ * Projected variables, in SELECT projection order.
295
+ */
296
+ readonly variables: string[];
297
+ }
298
+
299
+ /**
300
+ * One SELECT binding row.
301
+ */
302
+ export class SelectRow {
303
+ private constructor();
304
+ free(): void;
305
+ [Symbol.dispose](): void;
306
+ /**
307
+ * Return the bound term for a variable name, or `undefined` for unbound/absent.
308
+ */
309
+ get(variable: string): Term | undefined;
310
+ /**
311
+ * Variables projected by this row, in SELECT projection order.
312
+ */
313
+ readonly variables: string[];
314
+ }
315
+
201
316
  /**
202
317
  * An RDF/JS `Sink` — a streaming consumer that interns pushed quads through the
203
318
  * `purrdf-events` protocol and freezes them at `finish()`.
@@ -306,6 +421,10 @@ export interface InitOutput {
306
421
  readonly __wbg_datafactory_free: (a: number, b: number) => void;
307
422
  readonly __wbg_dataset_free: (a: number, b: number) => void;
308
423
  readonly __wbg_quad_free: (a: number, b: number) => void;
424
+ readonly __wbg_queryengine_free: (a: number, b: number) => void;
425
+ readonly __wbg_queryresult_free: (a: number, b: number) => void;
426
+ readonly __wbg_selectresult_free: (a: number, b: number) => void;
427
+ readonly __wbg_selectrow_free: (a: number, b: number) => void;
309
428
  readonly __wbg_sink_free: (a: number, b: number) => void;
310
429
  readonly __wbg_term_free: (a: number, b: number) => void;
311
430
  readonly datafactory_blankNode: (a: number, b: number, c: number) => number;
@@ -332,6 +451,9 @@ export interface InitOutput {
332
451
  readonly dataset_query: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
333
452
  readonly dataset_serialize: (a: number, b: number, c: number, d: number) => void;
334
453
  readonly dataset_size: (a: number) => number;
454
+ readonly dataset_visualExportJson: (a: number, b: number, c: number, d: number) => void;
455
+ readonly dataset_visualModelJson: (a: number, b: number, c: number, d: number) => void;
456
+ readonly dataset_visualSvgJson: (a: number, b: number, c: number, d: number) => void;
335
457
  readonly quad_asTerm: (a: number, b: number) => void;
336
458
  readonly quad_equals: (a: number, b: number) => number;
337
459
  readonly quad_graph: (a: number) => number;
@@ -340,6 +462,22 @@ export interface InitOutput {
340
462
  readonly quad_subject: (a: number) => number;
341
463
  readonly quad_term_type: (a: number, b: number) => void;
342
464
  readonly quad_value: (a: number, b: number) => void;
465
+ readonly queryengine_ask: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
466
+ readonly queryengine_construct: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
467
+ readonly queryengine_new: () => number;
468
+ readonly queryengine_query: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
469
+ readonly queryengine_queryRaw: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
470
+ readonly queryengine_select: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
471
+ readonly queryengine_update: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
472
+ readonly queryresult_boolean: (a: number) => number;
473
+ readonly queryresult_kind: (a: number, b: number) => void;
474
+ readonly queryresult_takeDataset: (a: number) => number;
475
+ readonly queryresult_takeSelect: (a: number) => number;
476
+ readonly selectresult_kind: (a: number, b: number) => void;
477
+ readonly selectresult_rows: (a: number, b: number) => void;
478
+ readonly selectresult_variables: (a: number, b: number) => void;
479
+ readonly selectrow_get: (a: number, b: number, c: number) => number;
480
+ readonly selectrow_variables: (a: number, b: number) => void;
343
481
  readonly shaclEntail: (a: number, b: number, c: number, d: number, e: number) => void;
344
482
  readonly shaclValidateToSarif: (a: number, b: number, c: number, d: number, e: number) => void;
345
483
  readonly sink_finish: (a: number, b: number) => void;
@@ -356,6 +494,7 @@ export interface InitOutput {
356
494
  readonly term_term_type: (a: number, b: number) => void;
357
495
  readonly term_value: (a: number, b: number) => void;
358
496
  readonly version: (a: number) => void;
497
+ readonly queryengine_describe: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
359
498
  readonly __wbindgen_export: (a: number, b: number) => number;
360
499
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
361
500
  readonly __wbindgen_add_to_stack_pointer: (a: number) => number;