@blackcatinformatics/purrdf 0.4.0 → 0.5.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/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,126 @@ 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 length = raw.rowCount;
74
+ let closed = false;
75
+
76
+ const close = () => {
77
+ if (closed) return;
78
+ closed = true;
79
+ raw.free?.();
80
+ };
81
+ if (length === 0) close();
82
+ const materialize = (row) => {
83
+ try {
84
+ const out = Object.create(null);
85
+ for (let index = 0; index < variables.length; index += 1) {
86
+ const variable = variables[index];
87
+ const value = row.takeValue(index);
88
+ if (value !== undefined) out[variable] = value;
89
+ }
90
+ return out;
91
+ } finally {
92
+ row.free?.();
93
+ if (raw.remaining === 0) close();
94
+ }
95
+ };
96
+ const rows = {
97
+ get length() {
98
+ return length;
99
+ },
100
+ get remaining() {
101
+ return closed ? 0 : raw.remaining;
102
+ },
103
+ take(index) {
104
+ if (closed) return undefined;
105
+ const row = raw.takeRow(index);
106
+ return row === undefined ? undefined : materialize(row);
107
+ },
108
+ next() {
109
+ if (closed) return { done: true, value: undefined };
110
+ const row = raw.nextRow();
111
+ if (row === undefined) {
112
+ close();
113
+ return { done: true, value: undefined };
114
+ }
115
+ return { done: false, value: materialize(row) };
116
+ },
117
+ return() {
118
+ close();
119
+ return { done: true, value: undefined };
120
+ },
121
+ toArray() {
122
+ return Array.from(this);
123
+ },
124
+ free: close,
125
+ [Symbol.iterator]() {
126
+ return this;
127
+ },
128
+ };
129
+ return { kind: "select", variables, rowCount: length, rows, free: close };
130
+ }
131
+
132
+ function queryResultToObject(raw) {
133
+ try {
134
+ switch (raw.kind) {
135
+ case "select": {
136
+ const select = raw.takeSelect();
137
+ if (select === undefined) throw new Error("SELECT result was already consumed");
138
+ return selectResultToObject(select);
139
+ }
140
+ case "ask":
141
+ return { kind: "ask", boolean: raw.boolean };
142
+ case "graph": {
143
+ const dataset = raw.takeDataset();
144
+ if (dataset === undefined) throw new Error("graph result was already consumed");
145
+ return { kind: "graph", dataset };
146
+ }
147
+ default:
148
+ throw new Error(`unknown SPARQL result kind ${raw.kind}`);
149
+ }
150
+ } finally {
151
+ raw.free?.();
152
+ }
153
+ }
154
+
33
155
  /**
34
156
  * Instantiate the wasm module. Idempotent. In Node the wasm bytes are read from the
35
157
  * colocated file; in a browser, pass the bytes/URL (or omit to fetch the colocated
@@ -57,6 +179,36 @@ export async function ready(wasmBytesOrUrl) {
57
179
  };
58
180
  }
59
181
 
182
+ if (!Dataset.from) {
183
+ Dataset.from = function (quads = []) {
184
+ const dataset = new Dataset();
185
+ for (const quad of quads ?? []) dataset.add(quad);
186
+ return dataset;
187
+ };
188
+ }
189
+
190
+ if (!Dataset.prototype.toStream) {
191
+ Dataset.prototype.toStream = function () {
192
+ return datasetToStream(this);
193
+ };
194
+ }
195
+
196
+ if (!Dataset.prototype.__purrdfVisualizationApi) {
197
+ const visualModelJson = Dataset.prototype.visualModelJson;
198
+ const visualExportJson = Dataset.prototype.visualExportJson;
199
+ const visualSvgJson = Dataset.prototype.visualSvgJson;
200
+ Dataset.prototype.visualModel = function (options) {
201
+ return JSON.parse(visualModelJson.call(this, visualizationOptionsJson(options)));
202
+ };
203
+ Dataset.prototype.visualExport = function (options) {
204
+ return JSON.parse(visualExportJson.call(this, visualizationOptionsJson(options)));
205
+ };
206
+ Dataset.prototype.visualSvg = function (options) {
207
+ return JSON.parse(visualSvgJson.call(this, visualizationOptionsJson(options)));
208
+ };
209
+ Dataset.prototype.__purrdfVisualizationApi = true;
210
+ }
211
+
60
212
  // RDF/JS DatasetCore.add(quad)/delete(quad) MUST return the dataset instance so calls
61
213
  // chain (`ds.add(q1).add(q2)`). The wasm methods return a bool ("did the effective set
62
214
  // change?"); the spec surface returns `this` (the changed-bit stays observable via
@@ -91,21 +243,66 @@ export async function ready(wasmBytesOrUrl) {
91
243
 
92
244
  // Present the RDF/JS-spec polymorphic literal(value, languageOrDatatype). The wasm
93
245
  // method takes `(value, language?)`; a NamedNode second argument is a datatype.
246
+ // PurRDF also accepts `{ language, direction }` for RDF 1.2 dirLangString literals.
94
247
  if (!DataFactory.prototype.__purrdfPolymorphicLiteral) {
95
248
  const wasmLiteral = DataFactory.prototype.literal;
96
249
  DataFactory.prototype.literal = function (value, languageOrDatatype) {
97
- if (
98
- languageOrDatatype != null &&
99
- typeof languageOrDatatype === "object" &&
100
- languageOrDatatype.termType === "NamedNode"
101
- ) {
250
+ if (isNamedNodeTerm(languageOrDatatype)) {
102
251
  return this.typedLiteral(value, languageOrDatatype);
103
252
  }
253
+ if (isDirectionalLanguage(languageOrDatatype)) return this.directionalLiteral(value, languageOrDatatype.language, languageOrDatatype.direction);
104
254
  return wasmLiteral.call(this, value, languageOrDatatype ?? undefined);
105
255
  };
106
256
  DataFactory.prototype.__purrdfPolymorphicLiteral = true;
107
257
  }
108
258
 
259
+ if (!DataFactory.prototype.dataset) {
260
+ DataFactory.prototype.dataset = function (quads = []) {
261
+ return Dataset.from(quads ?? []);
262
+ };
263
+ }
264
+
265
+ if (!QueryEngine.prototype.__purrdfPackageRootApi) {
266
+ const wasmQuery = QueryEngine.prototype.query;
267
+ const wasmSelect = QueryEngine.prototype.select;
268
+ const wasmAsk = QueryEngine.prototype.ask;
269
+ const wasmConstruct = QueryEngine.prototype.construct;
270
+ const wasmDescribe = QueryEngine.prototype.describe;
271
+ const wasmUpdate = QueryEngine.prototype.update;
272
+ const wasmQueryRaw = QueryEngine.prototype.queryRaw;
273
+
274
+ QueryEngine.prototype.query = function (dataset, sparql, options) {
275
+ const { base } = normalizeQueryOptions(options);
276
+ return queryResultToObject(wasmQuery.call(this, dataset, sparql, base));
277
+ };
278
+ QueryEngine.prototype.select = function (dataset, sparql, options) {
279
+ const { base } = normalizeQueryOptions(options);
280
+ return selectResultToObject(wasmSelect.call(this, dataset, sparql, base));
281
+ };
282
+ QueryEngine.prototype.ask = function (dataset, sparql, options) {
283
+ const { base } = normalizeQueryOptions(options);
284
+ return wasmAsk.call(this, dataset, sparql, base);
285
+ };
286
+ QueryEngine.prototype.construct = function (dataset, sparql, options) {
287
+ const { base } = normalizeQueryOptions(options);
288
+ return wasmConstruct.call(this, dataset, sparql, base);
289
+ };
290
+ QueryEngine.prototype.describe = function (dataset, sparql, options) {
291
+ const { base } = normalizeQueryOptions(options);
292
+ return wasmDescribe.call(this, dataset, sparql, base);
293
+ };
294
+ QueryEngine.prototype.update = function (dataset, sparql, options) {
295
+ const { base } = normalizeQueryOptions(options);
296
+ wasmUpdate.call(this, dataset, sparql, base);
297
+ return dataset;
298
+ };
299
+ QueryEngine.prototype.queryRaw = function (dataset, sparql, options) {
300
+ const { base, format } = normalizeQueryOptions(options);
301
+ return wasmQueryRaw.call(this, dataset, sparql, base, format);
302
+ };
303
+ QueryEngine.prototype.__purrdfPackageRootApi = true;
304
+ }
305
+
109
306
  _ready = true;
110
307
  }
111
308
 
@@ -134,6 +331,7 @@ export {
134
331
  DataFactory,
135
332
  Dataset,
136
333
  Quad,
334
+ QueryEngine,
137
335
  shaclEntail,
138
336
  shaclValidateToSarif,
139
337
  Sink,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackcatinformatics/purrdf",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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,126 @@ 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
+ * Move the next unconsumed row out of the result.
287
+ */
288
+ nextRow(): SelectRow | undefined;
289
+ /**
290
+ * Move a row out by result index. Each row can be consumed once.
291
+ */
292
+ takeRow(index: number): SelectRow | undefined;
293
+ /**
294
+ * The result discriminator.
295
+ */
296
+ readonly kind: string;
297
+ /**
298
+ * Number of rows that have not yet been consumed.
299
+ */
300
+ readonly remaining: number;
301
+ /**
302
+ * Total number of SELECT rows, including rows already consumed.
303
+ */
304
+ readonly rowCount: number;
305
+ /**
306
+ * Projected variables, in SELECT projection order.
307
+ */
308
+ readonly variables: string[];
309
+ }
310
+
311
+ /**
312
+ * One SELECT binding row.
313
+ */
314
+ export class SelectRow {
315
+ private constructor();
316
+ free(): void;
317
+ [Symbol.dispose](): void;
318
+ /**
319
+ * Return the bound term for a variable name, or `undefined` for unbound/absent.
320
+ */
321
+ get(variable: string): Term | undefined;
322
+ /**
323
+ * Move one value out by projection index, or return `undefined` when the
324
+ * cell is unbound, absent, or was already consumed.
325
+ */
326
+ takeValue(index: number): Term | undefined;
327
+ /**
328
+ * Variables projected by this row, in SELECT projection order.
329
+ */
330
+ readonly variables: string[];
331
+ }
332
+
201
333
  /**
202
334
  * An RDF/JS `Sink` — a streaming consumer that interns pushed quads through the
203
335
  * `purrdf-events` protocol and freezes them at `finish()`.
@@ -306,6 +438,10 @@ export interface InitOutput {
306
438
  readonly __wbg_datafactory_free: (a: number, b: number) => void;
307
439
  readonly __wbg_dataset_free: (a: number, b: number) => void;
308
440
  readonly __wbg_quad_free: (a: number, b: number) => void;
441
+ readonly __wbg_queryengine_free: (a: number, b: number) => void;
442
+ readonly __wbg_queryresult_free: (a: number, b: number) => void;
443
+ readonly __wbg_selectresult_free: (a: number, b: number) => void;
444
+ readonly __wbg_selectrow_free: (a: number, b: number) => void;
309
445
  readonly __wbg_sink_free: (a: number, b: number) => void;
310
446
  readonly __wbg_term_free: (a: number, b: number) => void;
311
447
  readonly datafactory_blankNode: (a: number, b: number, c: number) => number;
@@ -332,6 +468,9 @@ export interface InitOutput {
332
468
  readonly dataset_query: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
333
469
  readonly dataset_serialize: (a: number, b: number, c: number, d: number) => void;
334
470
  readonly dataset_size: (a: number) => number;
471
+ readonly dataset_visualExportJson: (a: number, b: number, c: number, d: number) => void;
472
+ readonly dataset_visualModelJson: (a: number, b: number, c: number, d: number) => void;
473
+ readonly dataset_visualSvgJson: (a: number, b: number, c: number, d: number) => void;
335
474
  readonly quad_asTerm: (a: number, b: number) => void;
336
475
  readonly quad_equals: (a: number, b: number) => number;
337
476
  readonly quad_graph: (a: number) => number;
@@ -340,6 +479,26 @@ export interface InitOutput {
340
479
  readonly quad_subject: (a: number) => number;
341
480
  readonly quad_term_type: (a: number, b: number) => void;
342
481
  readonly quad_value: (a: number, b: number) => void;
482
+ readonly queryengine_ask: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
483
+ readonly queryengine_construct: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
484
+ readonly queryengine_new: () => number;
485
+ readonly queryengine_query: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
486
+ readonly queryengine_queryRaw: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
487
+ readonly queryengine_select: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
488
+ readonly queryengine_update: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
489
+ readonly queryresult_boolean: (a: number) => number;
490
+ readonly queryresult_kind: (a: number, b: number) => void;
491
+ readonly queryresult_takeDataset: (a: number) => number;
492
+ readonly queryresult_takeSelect: (a: number) => number;
493
+ readonly selectresult_kind: (a: number, b: number) => void;
494
+ readonly selectresult_nextRow: (a: number) => number;
495
+ readonly selectresult_remaining: (a: number) => number;
496
+ readonly selectresult_row_count: (a: number) => number;
497
+ readonly selectresult_takeRow: (a: number, b: number) => number;
498
+ readonly selectresult_variables: (a: number, b: number) => void;
499
+ readonly selectrow_get: (a: number, b: number, c: number) => number;
500
+ readonly selectrow_takeValue: (a: number, b: number) => number;
501
+ readonly selectrow_variables: (a: number, b: number) => void;
343
502
  readonly shaclEntail: (a: number, b: number, c: number, d: number, e: number) => void;
344
503
  readonly shaclValidateToSarif: (a: number, b: number, c: number, d: number, e: number) => void;
345
504
  readonly sink_finish: (a: number, b: number) => void;
@@ -356,6 +515,7 @@ export interface InitOutput {
356
515
  readonly term_term_type: (a: number, b: number) => void;
357
516
  readonly term_value: (a: number, b: number) => void;
358
517
  readonly version: (a: number) => void;
518
+ readonly queryengine_describe: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
359
519
  readonly __wbindgen_export: (a: number, b: number) => number;
360
520
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
361
521
  readonly __wbindgen_add_to_stack_pointer: (a: number) => number;