@factoidal/core 0.18.0 → 0.20.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 +27 -0
- package/README.md +37 -14
- package/authz/server.mjs +5 -0
- package/bin/factoidal.mjs +47 -8
- package/browser.d.ts +13 -0
- package/browser.js +14 -0
- package/index.d.ts +224 -21
- package/index.mjs +14 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +251 -23
- package/lib/dataset.mjs +96 -12
- package/lib/lean-entry.mjs +30 -12
- package/lib/nodes.mjs +92 -14
- package/lib/sbm11-lws.mjs +4 -2
- package/lib/sbm11.mjs +16 -6
- package/lib/version.mjs +1 -1
- package/package.json +2 -2
- package/rdfjs.mjs +10 -1
package/lib/api.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { dataFactory, termFromSrj, termToJson, quadToJson, _setTextEngine } from
|
|
|
26
26
|
import { Dataset, INTERNAL, checkOptions, makeRegistry, WRITER_TAGS } from './dataset.mjs';
|
|
27
27
|
import { version as packageVersion } from './version.mjs';
|
|
28
28
|
import { openGeneration, readRegion, manifestKeys, hexOfBytes } from './store.mjs';
|
|
29
|
-
import { fullEngineMessage } from './lean-entry.mjs';
|
|
29
|
+
import { fullEngineMessage, encodeJsValue } from './lean-entry.mjs';
|
|
30
30
|
import { memoryDeltaLogStore } from './delta-log-hosts.mjs';
|
|
31
31
|
import { FactoidalError, errorFromEnvelope, envelopeResult } from './errors.mjs';
|
|
32
32
|
import { buildNodes } from './nodes.mjs';
|
|
@@ -735,16 +735,33 @@ function buildApi(driver) {
|
|
|
735
735
|
* skipped statement in `dataset.diagnostics`
|
|
736
736
|
* (https://github.com/danbri/factoidal/issues/716); `maxErrors: n` stops
|
|
737
737
|
* after n diagnostics. With no `format` the engine infers one
|
|
738
|
-
* (`dataset.format`, `dataset.formatInferred`).
|
|
738
|
+
* (`dataset.format`, `dataset.formatInferred`). `{rdfstar:true}` reads
|
|
739
|
+
* JSON-LD as JSON-LD-star (`@triple`, `@reifies`, `@annotation`), as
|
|
740
|
+
* jsonldToRdf() does; it applies to format 'jsonld' only.
|
|
739
741
|
* @param {string} text
|
|
740
742
|
* @param {{format?: string, baseIRI?: string, lenient?: boolean,
|
|
741
|
-
* maxErrors?: number}} [options]
|
|
743
|
+
* maxErrors?: number, rdfstar?: boolean}} [options]
|
|
742
744
|
* @returns {Promise<Dataset>}
|
|
743
745
|
*/
|
|
744
746
|
async function parse(text, options) {
|
|
745
747
|
if (typeof text !== 'string') throw new TypeError('parse: text must be a string');
|
|
746
748
|
const opts = await checkedOptions('parse', optionsOf('parse', options));
|
|
747
749
|
const c = await ctx();
|
|
750
|
+
if (opts.rdfstar) {
|
|
751
|
+
const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
|
|
752
|
+
if (tag !== 'jsonld') {
|
|
753
|
+
throw new FactoidalError(
|
|
754
|
+
`parse: rdfstar applies to format 'jsonld' only (format '${tag}'${inferred ? ', inferred' : ''})`,
|
|
755
|
+
{ code: 'option.conflict' });
|
|
756
|
+
}
|
|
757
|
+
requireEntryFn(c.e, 'datasetFromJsonLd', 'parse {rdfstar:true} (datasetFromJsonLd)');
|
|
758
|
+
const optionsJson = JSON.stringify({
|
|
759
|
+
...(opts.baseIRI ? { base: opts.baseIRI } : {}),
|
|
760
|
+
rdfstar: true,
|
|
761
|
+
});
|
|
762
|
+
return c.wrap(entryResult(c.e.datasetFromJsonLd(text, optionsJson), 'parse'), {},
|
|
763
|
+
{ format: tag, formatInferred: inferred });
|
|
764
|
+
}
|
|
748
765
|
if (opts.lenient) {
|
|
749
766
|
const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
|
|
750
767
|
requireEntryFn(c.e, 'parseDocument', 'parse {lenient:true} (parseDocument)');
|
|
@@ -885,30 +902,43 @@ function buildApi(driver) {
|
|
|
885
902
|
}
|
|
886
903
|
|
|
887
904
|
/**
|
|
888
|
-
* The RDFC-1.0 canonical hash of
|
|
889
|
-
* hexadecimal digits, of the canonical N-Quads document (
|
|
890
|
-
* Canonicalization 1.0, section 4.4 note 2)
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* in
|
|
894
|
-
*
|
|
905
|
+
* The RDFC-1.0 canonical hash of a dataset: the SHA-256, as 64 lowercase
|
|
906
|
+
* hexadecimal digits, of the canonical N-Quads document that canonicalize()
|
|
907
|
+
* answers (RDF Dataset Canonicalization 1.0, section 4.4 note 2), graph
|
|
908
|
+
* names included. Op datasetCanonicalHash computes the canonical form and
|
|
909
|
+
* the digest in Lean. Since 0.20.0; 0.17.0 to 0.19.x dropped every graph
|
|
910
|
+
* name first, so datasets that differ only in their graph names got the
|
|
911
|
+
* same hash. To hash a graph whatever its name, use
|
|
912
|
+
* canonicalHash(asDefaultGraph(g)).
|
|
913
|
+
* @param {Dataset} dataset
|
|
895
914
|
* @returns {Promise<string>}
|
|
896
915
|
*/
|
|
897
|
-
async function canonicalHash(
|
|
898
|
-
if (!(
|
|
899
|
-
throw new TypeError('canonicalHash: expected a Dataset
|
|
916
|
+
async function canonicalHash(dataset) {
|
|
917
|
+
if (!(dataset instanceof Dataset)) {
|
|
918
|
+
throw new TypeError('canonicalHash: expected a Dataset');
|
|
900
919
|
}
|
|
901
|
-
|
|
902
|
-
const c =
|
|
903
|
-
requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
|
|
920
|
+
dataset.size; // throws the disposed error
|
|
921
|
+
const c = dataset._ctx;
|
|
904
922
|
requireEntryFn(c.e, 'datasetCanonicalHash', 'canonicalHash');
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
923
|
+
return entryResult(c.e.datasetCanonicalHash(dataset.handle), 'canonicalHash').hash;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* A new Dataset that holds the triples of every graph of `dataset`, all in
|
|
928
|
+
* the default graph (op datasetGraphAsDefault with the union of the
|
|
929
|
+
* graphs). With one entry of graphs(), it is that graph without its name.
|
|
930
|
+
* @param {Dataset} dataset
|
|
931
|
+
* @returns {Dataset}
|
|
932
|
+
*/
|
|
933
|
+
function asDefaultGraph(dataset) {
|
|
934
|
+
if (!(dataset instanceof Dataset)) {
|
|
935
|
+
throw new TypeError('asDefaultGraph: expected a Dataset');
|
|
911
936
|
}
|
|
937
|
+
dataset.size; // throws the disposed error
|
|
938
|
+
const c = dataset._ctx;
|
|
939
|
+
requireEntryFn(c.e, 'datasetGraphAsDefault', 'asDefaultGraph');
|
|
940
|
+
return c.wrap(entryResult(c.e.datasetGraphAsDefault(dataset.handle,
|
|
941
|
+
JSON.stringify({ type: 'union' })), 'asDefaultGraph'), {});
|
|
912
942
|
}
|
|
913
943
|
|
|
914
944
|
/**
|
|
@@ -949,6 +979,95 @@ function buildApi(driver) {
|
|
|
949
979
|
await ctx();
|
|
950
980
|
}
|
|
951
981
|
|
|
982
|
+
// The synchronous term functions (sameValue, syntaxFor, splitIri, ...) call
|
|
983
|
+
// the loaded engine: `await ready()`, or any other call, loads it first.
|
|
984
|
+
function loadedCtx(who, op = who) {
|
|
985
|
+
if (!ctxCache) {
|
|
986
|
+
throw new FactoidalError(
|
|
987
|
+
`${who}: the engine is not loaded yet. Await any @factoidal/core call first ` +
|
|
988
|
+
'(for example `await ready()`).', { code: 'op.unknown' });
|
|
989
|
+
}
|
|
990
|
+
requireEntryFn(ctxCache.e, op, who);
|
|
991
|
+
return ctxCache;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* SPARQL 1.1 `=` on two RDF terms (sections 17.3 and 17.4.1.7), decided by
|
|
996
|
+
* the engine (L4Factoidal.SPARQL.sameValue;
|
|
997
|
+
* https://github.com/danbri/factoidal/issues/1052).
|
|
998
|
+
* @param {object|string} a an RDF/JS term or an N-Triples term string
|
|
999
|
+
* @param {object|string} b
|
|
1000
|
+
* @returns {boolean|null} true: the same value; false: different values;
|
|
1001
|
+
* null: incomparable (the operator's type error)
|
|
1002
|
+
*/
|
|
1003
|
+
function sameValue(a, b) {
|
|
1004
|
+
const c = loadedCtx('sameValue');
|
|
1005
|
+
const r = entryResult(c.e.sameValue(JSON.stringify(termToJson(a)),
|
|
1006
|
+
JSON.stringify(termToJson(b))), 'sameValue');
|
|
1007
|
+
return r.sameValue === 'true' ? true : (r.sameValue === 'false' ? false : null);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const iriText = (iri) => (typeof iri === 'string' ? iri : (iri && iri.value) || '');
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* The syntax of a document by its file name (or IRI path) and its media
|
|
1014
|
+
* type, from the engine's table of W3C registrations
|
|
1015
|
+
* (L4Factoidal.RDF.syntaxOfNames; https://github.com/danbri/factoidal/issues/1053).
|
|
1016
|
+
* The media type wins when it is given; an unknown media type does not fall
|
|
1017
|
+
* back to the name.
|
|
1018
|
+
* @param {string|{fileName?: string, mediaType?: string}} what a file name or IRI,
|
|
1019
|
+
* or both names
|
|
1020
|
+
* @returns {string|null} a syntax name of formats(), or null
|
|
1021
|
+
*/
|
|
1022
|
+
function syntaxFor(what) {
|
|
1023
|
+
const c = loadedCtx('syntaxFor');
|
|
1024
|
+
const o = (typeof what === 'string') ? { fileName: what } : (what || {});
|
|
1025
|
+
const r = entryResult(c.e.syntaxFor(String(o.fileName ?? ''), String(o.mediaType ?? '')), 'syntaxFor');
|
|
1026
|
+
return r.syntax ?? null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* An IRI as a namespace and a local name: the local name is the longest
|
|
1031
|
+
* suffix that is an XML NCName, '' when there is none
|
|
1032
|
+
* (L4Factoidal.RDF.splitIriXml; https://github.com/danbri/factoidal/issues/1051 item 4).
|
|
1033
|
+
* @param {string|{value: string}} iri
|
|
1034
|
+
* @returns {{namespace: string, localName: string}}
|
|
1035
|
+
*/
|
|
1036
|
+
function splitIri(iri) {
|
|
1037
|
+
const c = loadedCtx('splitIri');
|
|
1038
|
+
const r = entryResult(c.e.splitIri(iriText(iri)), 'splitIri');
|
|
1039
|
+
return { namespace: r.namespace, localName: r.localName };
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/** Is the text an XML NCName (Namespaces in XML 1.0, production [4])? */
|
|
1043
|
+
function isNCName(text) {
|
|
1044
|
+
const c = loadedCtx('isNCName');
|
|
1045
|
+
return entryResult(c.e.isNCName(String(text)), 'isNCName').ncName === true;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* The engine's vocabulary table: prefix, namespace and term local names of
|
|
1050
|
+
* rdf, rdfs, xsd and owl, and the namespaces of foaf, dcterms, dc and schema
|
|
1051
|
+
* (L4Factoidal.RDF.vocabularyTable; https://github.com/danbri/factoidal/issues/1051 item 5).
|
|
1052
|
+
* @returns {Array<{prefix: string, namespace: string, terms: string[]}>}
|
|
1053
|
+
*/
|
|
1054
|
+
function vocabularies() {
|
|
1055
|
+
const c = loadedCtx('vocabularies');
|
|
1056
|
+
return entryResult(c.e.vocabularies(), 'vocabularies').vocabularies;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** The container membership property rdf:_n, n >= 1, as a NamedNode. */
|
|
1060
|
+
function memberIri(n) {
|
|
1061
|
+
const c = loadedCtx('memberIri');
|
|
1062
|
+
return dataFactory.namedNode(entryResult(c.e.memberIri(String(n)), 'memberIri').iri);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** The n of rdf:_n, or null for any other IRI. */
|
|
1066
|
+
function memberOrdinal(iri) {
|
|
1067
|
+
const c = loadedCtx('memberOrdinal');
|
|
1068
|
+
return entryResult(c.e.memberOrdinal(iriText(iri)), 'memberOrdinal').ordinal ?? null;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
952
1071
|
/** The engine's name tables (formats, regimes, query outputs, pack syntaxes). */
|
|
953
1072
|
async function engineTables() {
|
|
954
1073
|
return (await ctx()).tables();
|
|
@@ -2198,13 +2317,110 @@ function buildApi(driver) {
|
|
|
2198
2317
|
turtlePrefixes: has('serializeTurtleWith'),
|
|
2199
2318
|
store: has('storeOpen') && has('storeHandleQuery'),
|
|
2200
2319
|
deltaLog: has('deltaLogEncode') && has('deltaLogDecode') && has('deltaLogMerge'),
|
|
2320
|
+
literalValues: has('literalValue') && has('literalFromValue'),
|
|
2201
2321
|
};
|
|
2202
2322
|
}
|
|
2203
2323
|
|
|
2324
|
+
// ---------------------------------------------------------------
|
|
2325
|
+
// Literal values (https://github.com/danbri/factoidal/issues/1051 items 1-3).
|
|
2326
|
+
// The ops literalValue and literalFromValue (formal/lean4/Wasm/Ops/
|
|
2327
|
+
// LiteralValue.lean, over L4Factoidal.RDF.LiteralValue) make every
|
|
2328
|
+
// decision: the XSD lexical-to-value mapping, validity, the canonical form,
|
|
2329
|
+
// the datatype of a JavaScript value. These functions are SYNCHRONOUS, so a
|
|
2330
|
+
// library-style API can call them in a getter; the engine must be loaded
|
|
2331
|
+
// first (await ready(), or any other call).
|
|
2332
|
+
// ---------------------------------------------------------------
|
|
2333
|
+
function loadedEntry(who) {
|
|
2334
|
+
if (!ctxCache) {
|
|
2335
|
+
throw new FactoidalError(
|
|
2336
|
+
`${who}: the engine is not loaded yet. Await any @factoidal/core call ` +
|
|
2337
|
+
'first (for example `await ready()`); after that this function is synchronous.',
|
|
2338
|
+
{ code: 'op.unknown' });
|
|
2339
|
+
}
|
|
2340
|
+
return ctxCache.e;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function literalAnswer(term, who) {
|
|
2344
|
+
if (!term || typeof term !== 'object' || term.termType !== 'Literal') {
|
|
2345
|
+
throw new TypeError(`${who}: expected an RDF/JS Literal term`);
|
|
2346
|
+
}
|
|
2347
|
+
const e = loadedEntry(who);
|
|
2348
|
+
requireEntryFn(e, 'literalValue', who);
|
|
2349
|
+
return entryResult(e.literalValue(JSON.stringify(termToJson(term))), who);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// NaN and the infinities have no JSON number; the engine names them in
|
|
2353
|
+
// `special`, and this copies the matching JavaScript number in.
|
|
2354
|
+
const SPECIAL_NUMBERS = { NaN: NaN, INF: Infinity, '-INF': -Infinity };
|
|
2355
|
+
function valueOut(v) {
|
|
2356
|
+
if (v && typeof v.special === 'string' && v.special in SPECIAL_NUMBERS) {
|
|
2357
|
+
return { ...v, number: SPECIAL_NUMBERS[v.special] };
|
|
2358
|
+
}
|
|
2359
|
+
// A JSON text -0 may reach this layer as 0; negativeZero is the engine's
|
|
2360
|
+
// statement of the sign.
|
|
2361
|
+
if (v && v.negativeZero === true) return { ...v, number: -0 };
|
|
2362
|
+
return v;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
/**
|
|
2366
|
+
* The value of a literal: {datatype, recognized, valid, value?, canonical?}.
|
|
2367
|
+
* `recognized` false: the engine has no lexical space for the datatype, and
|
|
2368
|
+
* `valid` is null. `value` and `canonical` are present for a valid literal
|
|
2369
|
+
* of a datatype with a value model (the shapes are in API.md).
|
|
2370
|
+
*/
|
|
2371
|
+
function literalValue(term) {
|
|
2372
|
+
const r = literalAnswer(term, 'literalValue');
|
|
2373
|
+
const out = { datatype: r.datatype, recognized: r.recognized,
|
|
2374
|
+
valid: r.recognized ? r.valid : null };
|
|
2375
|
+
if (r.value !== undefined) out.value = valueOut(r.value);
|
|
2376
|
+
if (r.canonical !== undefined) out.canonical = r.canonical;
|
|
2377
|
+
return out;
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
/** true or false: the lexical form is in the datatype's lexical space;
|
|
2381
|
+
* null: the engine has no lexical space for the datatype. */
|
|
2382
|
+
function isValidLiteral(term) {
|
|
2383
|
+
const r = literalAnswer(term, 'isValidLiteral');
|
|
2384
|
+
return r.recognized ? r.valid : null;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/** The literal with the canonical lexical form of its value (XSD 1.1 Part 2
|
|
2388
|
+
* section 2.3.1), the same datatype and language; null when the literal is
|
|
2389
|
+
* not valid or the engine has no canonical mapping for its datatype. */
|
|
2390
|
+
function canonicalLiteral(term) {
|
|
2391
|
+
const r = literalAnswer(term, 'canonicalLiteral');
|
|
2392
|
+
if (r.canonical === undefined) return null;
|
|
2393
|
+
if (term.language) return dataFactory.literal(r.canonical, term.direction
|
|
2394
|
+
? { language: term.language, direction: term.direction } : term.language);
|
|
2395
|
+
return dataFactory.literal(r.canonical, dataFactory.namedNode(r.datatype));
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/**
|
|
2399
|
+
* The literal of a JavaScript value. The engine chooses the datatype (table
|
|
2400
|
+
* in API.md) unless `options.datatype` names one (an IRI, a NamedNode, or
|
|
2401
|
+
* an XSD built-in local name such as 'int'); the value must then be in that
|
|
2402
|
+
* datatype's value space, else a FactoidalError with code literal.value.
|
|
2403
|
+
*/
|
|
2404
|
+
function literalFromValue(value, options) {
|
|
2405
|
+
const who = 'literalFromValue';
|
|
2406
|
+
const tagged = encodeJsValue(value);
|
|
2407
|
+
if (!tagged) {
|
|
2408
|
+
throw new TypeError(`${who}: expected a boolean, number, bigint, string or Date`);
|
|
2409
|
+
}
|
|
2410
|
+
const dt = options && options.datatype;
|
|
2411
|
+
const hint = dt === undefined || dt === null ? ''
|
|
2412
|
+
: (typeof dt === 'string' ? dt : (dt && typeof dt.value === 'string' ? dt.value : null));
|
|
2413
|
+
if (hint === null) throw new TypeError(`${who}: options.datatype must be a string or a NamedNode`);
|
|
2414
|
+
const e = loadedEntry(who);
|
|
2415
|
+
requireEntryFn(e, 'literalFromValue', who);
|
|
2416
|
+
const r = entryResult(e.literalFromValue(JSON.stringify(tagged), hint), who);
|
|
2417
|
+
return dataFactory.literal(r.value, dataFactory.namedNode(r.datatype));
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2204
2420
|
// The node-composition API (lib/nodes.mjs, issue 1002 step 2).
|
|
2205
2421
|
const nodeApi = buildNodes({
|
|
2206
2422
|
entry, entryResult, requireEntryFn, Dataset,
|
|
2207
|
-
parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf,
|
|
2423
|
+
parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
|
|
2208
2424
|
});
|
|
2209
2425
|
|
|
2210
2426
|
return {
|
|
@@ -2224,6 +2440,7 @@ function buildApi(driver) {
|
|
|
2224
2440
|
canonicalize,
|
|
2225
2441
|
graphs,
|
|
2226
2442
|
canonicalHash,
|
|
2443
|
+
asDefaultGraph,
|
|
2227
2444
|
shaclValidate,
|
|
2228
2445
|
shexValidate,
|
|
2229
2446
|
owlClosure,
|
|
@@ -2287,8 +2504,19 @@ function buildApi(driver) {
|
|
|
2287
2504
|
queryRaw,
|
|
2288
2505
|
isomorphic,
|
|
2289
2506
|
fromQuads,
|
|
2507
|
+
literalValue,
|
|
2508
|
+
isValidLiteral,
|
|
2509
|
+
canonicalLiteral,
|
|
2510
|
+
literalFromValue,
|
|
2290
2511
|
ready,
|
|
2291
2512
|
engineTables,
|
|
2513
|
+
sameValue,
|
|
2514
|
+
syntaxFor,
|
|
2515
|
+
splitIri,
|
|
2516
|
+
isNCName,
|
|
2517
|
+
vocabularies,
|
|
2518
|
+
memberIri,
|
|
2519
|
+
memberOrdinal,
|
|
2292
2520
|
Dataset,
|
|
2293
2521
|
Store,
|
|
2294
2522
|
FactoidalError,
|
package/lib/dataset.mjs
CHANGED
|
@@ -405,22 +405,44 @@ class Dataset {
|
|
|
405
405
|
termToJson(graph)]);
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
// The options argument of has, match and delete: `{match: 'term'|'value'}`,
|
|
409
|
+
// checked by the engine (op apiCheck). '' for the term match, which every
|
|
410
|
+
// engine serves with the two-argument ops.
|
|
411
|
+
_matchMode(who, options) {
|
|
412
|
+
if (options === undefined || options === null) return '';
|
|
413
|
+
checkOptions(this._ctx.e, `Dataset.${who}`, options);
|
|
414
|
+
if (options.match === undefined || options.match === null) return '';
|
|
415
|
+
const mode = String(options.match).toLowerCase();
|
|
416
|
+
return mode === 'term' ? '' : JSON.stringify({ match: mode });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* RDF/JS DatasetCore.has: one indexed lookup in the engine. With
|
|
421
|
+
* `{match: 'value'}` a literal object matches every object literal of the
|
|
422
|
+
* same value (sameValue(); https://github.com/danbri/factoidal/issues/1052).
|
|
423
|
+
*/
|
|
424
|
+
has(quad, options) {
|
|
410
425
|
this._open('has');
|
|
411
|
-
const
|
|
412
|
-
|
|
426
|
+
const mode = this._matchMode('has', options);
|
|
427
|
+
const pattern = this._pattern(quad.subject, quad.predicate, quad.object, quad.graph || { termType: 'DefaultGraph' });
|
|
428
|
+
const r = envelope(mode
|
|
429
|
+
? this._ctx.e.datasetMatchCount(this._handle, pattern, mode)
|
|
430
|
+
: this._ctx.e.datasetMatchCount(this._handle, pattern), 'has');
|
|
413
431
|
return r.count > 0;
|
|
414
432
|
}
|
|
415
433
|
|
|
416
434
|
/**
|
|
417
435
|
* RDF/JS DatasetCore.match: the matching quads as a new Dataset, found
|
|
418
|
-
* through the engine's indexes. `null`/`undefined` matches any term.
|
|
436
|
+
* through the engine's indexes. `null`/`undefined` matches any term. With
|
|
437
|
+
* `{match: 'value'}` a literal object matches by value (as has()).
|
|
419
438
|
*/
|
|
420
|
-
match(subject, predicate, object, graph) {
|
|
439
|
+
match(subject, predicate, object, graph, options) {
|
|
421
440
|
this._open('match');
|
|
422
|
-
const
|
|
423
|
-
|
|
441
|
+
const mode = this._matchMode('match', options);
|
|
442
|
+
const pattern = this._pattern(subject, predicate, object, graph);
|
|
443
|
+
const r = envelope(mode
|
|
444
|
+
? this._ctx.e.datasetMatch(this._handle, pattern, mode)
|
|
445
|
+
: this._ctx.e.datasetMatch(this._handle, pattern), 'match');
|
|
424
446
|
return this._ctx.wrap(r, this._prefixes);
|
|
425
447
|
}
|
|
426
448
|
|
|
@@ -438,15 +460,77 @@ class Dataset {
|
|
|
438
460
|
return this;
|
|
439
461
|
}
|
|
440
462
|
|
|
441
|
-
/**
|
|
442
|
-
|
|
463
|
+
/**
|
|
464
|
+
* RDF/JS DatasetCore.delete: remove one quad. With `{match: 'value'}` a
|
|
465
|
+
* literal object removes every quad of the same subject, predicate and
|
|
466
|
+
* graph whose object has the same value (as has()).
|
|
467
|
+
*/
|
|
468
|
+
delete(quad, options) {
|
|
443
469
|
this._open('delete');
|
|
444
|
-
const
|
|
445
|
-
|
|
470
|
+
const mode = this._matchMode('delete', options);
|
|
471
|
+
const quads = JSON.stringify([quadToJson(quad)]);
|
|
472
|
+
const r = envelope(mode
|
|
473
|
+
? this._ctx.e.datasetDeleteQuadsJson(this._handle, quads, mode)
|
|
474
|
+
: this._ctx.e.datasetDeleteQuadsJson(this._handle, quads), 'delete');
|
|
446
475
|
this._changed(r.count);
|
|
447
476
|
return this;
|
|
448
477
|
}
|
|
449
478
|
|
|
479
|
+
// RDF lists and containers (https://github.com/danbri/factoidal/issues/1051
|
|
480
|
+
// item 6). The engine walks and builds them (L4Factoidal.RDF.Collections).
|
|
481
|
+
// `graph`: null/undefined or the DefaultGraph for the default graph, else
|
|
482
|
+
// the graph name.
|
|
483
|
+
_collectionOp(op, who) {
|
|
484
|
+
this._open(who);
|
|
485
|
+
if (typeof this._ctx.e[op] !== 'function') {
|
|
486
|
+
throw new FactoidalError(`Dataset.${who}: the loaded engine has no op '${op}'; it is older than this package`,
|
|
487
|
+
{ code: 'op.unknown' });
|
|
488
|
+
}
|
|
489
|
+
return this._ctx.e[op].bind(this._ctx.e);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
_graphArg(graph) {
|
|
493
|
+
return graph === undefined || graph === null ? '' : JSON.stringify(termToJson(graph));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The items of the RDF list (rdf:first / rdf:rest) at `head`, in order.
|
|
498
|
+
* rdf:nil is the empty list. A list that is not well formed (a node with no
|
|
499
|
+
* or two rdf:first or rdf:rest, or a cycle) is a FactoidalError (op.failed)
|
|
500
|
+
* whose message names the reason and the node.
|
|
501
|
+
*/
|
|
502
|
+
listItems(head, graph) {
|
|
503
|
+
const op = this._collectionOp('datasetListItems', 'listItems');
|
|
504
|
+
const r = envelope(op(this._handle, JSON.stringify(termToJson(head)), this._graphArg(graph)), 'listItems');
|
|
505
|
+
return r.items.map(termFromSrj);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* The members rdf:_1, rdf:_2, ... of `node`, sorted by n, and its kind
|
|
510
|
+
* (rdf:Bag, rdf:Seq or rdf:Alt from its rdf:type, or null).
|
|
511
|
+
* @returns {{kind: NamedNode|null, items: Array<{ordinal: number, term: Term}>}}
|
|
512
|
+
*/
|
|
513
|
+
containerItems(node, graph) {
|
|
514
|
+
const op = this._collectionOp('datasetContainerItems', 'containerItems');
|
|
515
|
+
const r = envelope(op(this._handle, JSON.stringify(termToJson(node)), this._graphArg(graph)), 'containerItems');
|
|
516
|
+
return {
|
|
517
|
+
kind: r.kind ? termFromSrj(r.kind) : null,
|
|
518
|
+
items: r.items.map((m) => ({ ordinal: m.ordinal, term: termFromSrj(m.term) })),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Add a new RDF list of `items` (new blank nodes, fresh in this Dataset)
|
|
524
|
+
* and answer its head; the empty list is rdf:nil and adds nothing.
|
|
525
|
+
*/
|
|
526
|
+
addList(items, graph) {
|
|
527
|
+
const op = this._collectionOp('datasetAddList', 'addList');
|
|
528
|
+
const r = envelope(op(this._handle, JSON.stringify([...items].map((t) => termToJson(t))),
|
|
529
|
+
this._graphArg(graph)), 'addList');
|
|
530
|
+
this._changed(r.count);
|
|
531
|
+
return termFromSrj(r.head);
|
|
532
|
+
}
|
|
533
|
+
|
|
450
534
|
/** Release the engine copy. Idempotent. */
|
|
451
535
|
dispose() {
|
|
452
536
|
if (this._disposed) return;
|
package/lib/lean-entry.mjs
CHANGED
|
@@ -63,22 +63,40 @@ function failEnvelope(err) {
|
|
|
63
63
|
return JSON.stringify({ ok: false, code: 'op.failed', error: String((err && err.message) || err) });
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* A JavaScript value tagged with its JavaScript type, {js, v}, for the engine
|
|
68
|
+
* (Wasm/Ops/ExtFns.lean, Wasm/Ops/LiteralValue.lean). This copies the value
|
|
69
|
+
* as text; the engine chooses the datatype and the lexical form
|
|
70
|
+
* (L4Factoidal.XSD.JsValue.jsLiteral, issue 894 item 3, issue 1051 item 1).
|
|
71
|
+
* A number is its Number::toString text ('-0' for negative zero); a Date is
|
|
72
|
+
* the text of getTime() ('NaN' for an invalid Date). Any other value: null.
|
|
73
|
+
* @param {unknown} value
|
|
74
|
+
* @returns {{js: string, v: string|boolean} | null}
|
|
75
|
+
*/
|
|
76
|
+
export function encodeJsValue(value) {
|
|
77
|
+
switch (typeof value) {
|
|
78
|
+
case 'boolean': return { js: 'boolean', v: value };
|
|
79
|
+
case 'number': return { js: 'number', v: Object.is(value, -0) ? '-0' : String(value) };
|
|
80
|
+
case 'bigint': return { js: 'bigint', v: value.toString() };
|
|
81
|
+
case 'string': return { js: 'string', v: value };
|
|
82
|
+
case 'object':
|
|
83
|
+
if (value instanceof Date) return { js: 'date', v: String(value.getTime()) };
|
|
84
|
+
return null;
|
|
85
|
+
default: return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
// One answer for Wasm/Ops/ExtFns.lean: the JavaScript value tagged with its
|
|
67
|
-
// JavaScript type (
|
|
68
|
-
//
|
|
69
|
-
// (L4Factoidal.XSD.JsValue.jsLiteral, issue 894 item 3). '' means "no
|
|
70
|
-
// value" (the section 17.6 error).
|
|
90
|
+
// JavaScript type (encodeJsValue), or a term object. '' means "no value" (the
|
|
91
|
+
// section 17.6 error).
|
|
71
92
|
function encodeExtAnswer(out) {
|
|
72
93
|
if (out === null || out === undefined || out === EXT_PENDING_MARKER) return '';
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
case 'string': return JSON.stringify({ js: 'string', v: out });
|
|
78
|
-
case 'object':
|
|
79
|
-
try { return JSON.stringify(appTermToJson(out)); } catch (_error) { return ''; }
|
|
80
|
-
default: return '';
|
|
94
|
+
const tagged = encodeJsValue(out);
|
|
95
|
+
if (tagged) return JSON.stringify(tagged);
|
|
96
|
+
if (typeof out === 'object') {
|
|
97
|
+
try { return JSON.stringify(appTermToJson(out)); } catch (_error) { return ''; }
|
|
81
98
|
}
|
|
99
|
+
return '';
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
// The Lean module asks its host through ONE global,
|