@lde/pipeline 0.34.0 → 0.34.1
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/README.md +1 -1
- package/dist/guard/blankNodes.d.ts +7 -0
- package/dist/guard/blankNodes.d.ts.map +1 -1
- package/dist/guard/blankNodes.js +7 -0
- package/dist/writer/fileWriter.d.ts.map +1 -1
- package/dist/writer/fileWriter.js +17 -0
- package/dist/writer/skolemizeBlankNodes.d.ts +27 -0
- package/dist/writer/skolemizeBlankNodes.d.ts.map +1 -0
- package/dist/writer/skolemizeBlankNodes.js +53 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -298,7 +298,7 @@ After all stages for a dataset have run, the pipeline calls `validator.report(da
|
|
|
298
298
|
Writes generated quads to a destination. A `Writer` is transactional: each pipeline run opens one run on it (`openRun(context)`), writes every dataset through the resulting `RunWriter`, and ends with exactly one `commit()` (on success) or `abort(error)` (on failure). The run lifecycle is the home of destination-level concerns such as atomic swaps, deletion sweeps and cross-pod locks; the pipeline drives `openRun → write* → commit/abort` uniformly and never branches on the writer’s update mode. After a dataset’s stages complete, its `flush(dataset, outcome)` says whether the dataset succeeded, so a writer can gate destructive finalization (e.g. an In-place stale-document sweep) on `'success'`.
|
|
299
299
|
|
|
300
300
|
- `SparqlUpdateWriter` — writes to a SPARQL endpoint via UPDATE queries; writes are visible as they land, so `commit`/`abort` are no-ops
|
|
301
|
-
- `FileWriter` — writes to local files; each file is streamed to a sibling temp file and atomically renamed on flush, `commit` finalizes any files still open, and `abort` discards temp output
|
|
301
|
+
- `FileWriter` — writes to local files; each file is streamed to a sibling temp file and atomically renamed on flush, `commit` finalizes any files still open, and `abort` discards temp output. In `n-quads` format it skolemises blank nodes to dataset-scoped IRIs, because such files are typically `cat`-indexed into one served graph where document-scoped blank-node labels from different datasets would otherwise fuse (see `blankNodes`/`failOnBlankNodes` and ldelements/lde#474); Turtle/N-Triples output is left unchanged
|
|
302
302
|
|
|
303
303
|
The `RunContext` handed to `openRun` carries the run’s identity (`runId`, `startedAt`), the full selection (`selectedSources()`, complete by commit time – including datasets skipped as unchanged) and the pipeline’s provenance store, when configured.
|
|
304
304
|
|
|
@@ -16,6 +16,13 @@ import type { QuadTransform } from '../stage.js';
|
|
|
16
16
|
* NO blank nodes. Mint stable (skolem) IRIs instead — see `skolemIri` in
|
|
17
17
|
* `@lde/dataset`. These helpers make that invariant testable and enforceable.
|
|
18
18
|
*
|
|
19
|
+
* `FileWriter` skolemises blank nodes automatically for `n-quads` output as a
|
|
20
|
+
* safety net, so a producer that slips a blank node through cannot fuse it into
|
|
21
|
+
* another dataset there. These helpers still matter for producers that must hold
|
|
22
|
+
* the invariant regardless of writer – e.g. output bound for a `SparqlUpdateWriter`
|
|
23
|
+
* or a `cat`-indexed Turtle sink, which do not skolemise – and for turning a slip
|
|
24
|
+
* into a loud failure rather than a silently rewritten IRI.
|
|
25
|
+
*
|
|
19
26
|
* See ldelements/lde#474 and netwerk-digitaal-erfgoed/dataset-knowledge-graph#352.
|
|
20
27
|
*/
|
|
21
28
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blankNodes.d.ts","sourceRoot":"","sources":["../../src/guard/blankNodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"blankNodes.d.ts","sourceRoot":"","sources":["../../src/guard/blankNodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,EAAE,CAU1D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAU9D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,KAAK,aAAa,CAAC,OAAO,CAAC,CAelE"}
|
package/dist/guard/blankNodes.js
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* NO blank nodes. Mint stable (skolem) IRIs instead — see `skolemIri` in
|
|
15
15
|
* `@lde/dataset`. These helpers make that invariant testable and enforceable.
|
|
16
16
|
*
|
|
17
|
+
* `FileWriter` skolemises blank nodes automatically for `n-quads` output as a
|
|
18
|
+
* safety net, so a producer that slips a blank node through cannot fuse it into
|
|
19
|
+
* another dataset there. These helpers still matter for producers that must hold
|
|
20
|
+
* the invariant regardless of writer – e.g. output bound for a `SparqlUpdateWriter`
|
|
21
|
+
* or a `cat`-indexed Turtle sink, which do not skolemise – and for turning a slip
|
|
22
|
+
* into a loud failure rather than a silently rewritten IRI.
|
|
23
|
+
*
|
|
17
24
|
* See ldelements/lde#474 and netwerk-digitaal-erfgoed/dataset-knowledge-graph#352.
|
|
18
25
|
*/
|
|
19
26
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileWriter.d.ts","sourceRoot":"","sources":["../../src/writer/fileWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAOvC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"fileWriter.d.ts","sourceRoot":"","sources":["../../src/writer/fileWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAOvC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG5E,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,GAAG,CAAC;CACtC;AAgBD;;;;;GAKG;AACH,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED;;;;;;;;;;GAUG;AACH,qBAAa,UAAW,YAAW,MAAM;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IACpD,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA4B;gBAE1C,OAAO,EAAE,iBAAiB;IAQhC,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC;YAwB9C,UAAU;YAsDV,SAAS;YAOT,SAAS;YAgCT,WAAW;IAczB;;;;OAIG;YACW,cAAc;IAQ5B,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAIvC,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM;IAQrC,OAAO,CAAC,WAAW;YAIL,eAAe;IA6B7B,OAAO,CAAC,YAAY;CAUrB"}
|
|
@@ -3,6 +3,7 @@ import { mkdir, rename, rm } from 'node:fs/promises';
|
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import filenamifyUrl from 'filenamify-url';
|
|
5
5
|
import { DataFactory, Writer as N3Writer } from 'n3';
|
|
6
|
+
import { skolemizeBlankNodes } from './skolemizeBlankNodes.js';
|
|
6
7
|
const formatMap = {
|
|
7
8
|
turtle: 'Turtle',
|
|
8
9
|
'n-triples': 'N-Triples',
|
|
@@ -71,6 +72,22 @@ export class FileWriter {
|
|
|
71
72
|
const addQuad = (quad) => n3Writer.addQuad(graphNode
|
|
72
73
|
? DataFactory.quad(quad.subject, quad.predicate, quad.object, graphNode)
|
|
73
74
|
: quad);
|
|
75
|
+
// n-quads files are cat-ed into one served graph, where document-scoped
|
|
76
|
+
// blank-node labels from different datasets collapse into one node
|
|
77
|
+
// (ldelements/lde#478, dataset-knowledge-graph#352/#420). Skolemise blank
|
|
78
|
+
// nodes to dataset-scoped IRIs so they stay distinct. Hashing the write to
|
|
79
|
+
// scope the IRIs needs it buffered, so only n-quads pays that cost – Turtle
|
|
80
|
+
// and N-Triples (single-document, offline) stream through unchanged.
|
|
81
|
+
if (this.format === 'n-quads') {
|
|
82
|
+
const batch = [first.value];
|
|
83
|
+
for await (const quad of { [Symbol.asyncIterator]: () => iterator }) {
|
|
84
|
+
batch.push(quad);
|
|
85
|
+
}
|
|
86
|
+
for (const quad of skolemizeBlankNodes(batch, dataset.iri.toString())) {
|
|
87
|
+
addQuad(quad);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
74
91
|
addQuad(first.value);
|
|
75
92
|
for await (const quad of { [Symbol.asyncIterator]: () => iterator }) {
|
|
76
93
|
addQuad(quad);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Quad } from '@rdfjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Rewrite every blank node in `quads` to a deterministic, dataset-scoped IRI,
|
|
4
|
+
* leaving the quads otherwise unchanged.
|
|
5
|
+
*
|
|
6
|
+
* A served store such as the Dataset Knowledge Graph assembles one graph per
|
|
7
|
+
* dataset by concatenating every per-dataset n-quads file into a single index
|
|
8
|
+
* (`qlever index` over the `cat` of all files). Document-scoped blank-node
|
|
9
|
+
* labels are not preserved across that concatenation: two datasets' `_:b0`
|
|
10
|
+
* collapse into one node, silently fusing unrelated measurements, activities
|
|
11
|
+
* and linksets across datasets (see ldelements/lde#478 and
|
|
12
|
+
* netwerk-digitaal-erfgoed/dataset-knowledge-graph#352 / #420). IRIs are never
|
|
13
|
+
* relabelled, so skolemising each blank node keeps distinct nodes distinct.
|
|
14
|
+
*
|
|
15
|
+
* Each blank node becomes `<dataset>/.well-known/skolem#<batch>-<label>`. The
|
|
16
|
+
* dataset IRI rules out fusion across datasets; `<batch>`, a content hash of
|
|
17
|
+
* this write's quads, rules out fusion across the separate writes that land in
|
|
18
|
+
* one dataset's file – their labels both restart at `b0`, but writes carrying
|
|
19
|
+
* different content hash differently. Two byte-identical writes collapse onto
|
|
20
|
+
* the same IRIs, which is correct: they carry the same nodes.
|
|
21
|
+
*
|
|
22
|
+
* @param quads - The quads to rewrite, possibly containing blank nodes.
|
|
23
|
+
* @param datasetIri - The dataset the quads belong to; scopes every minted IRI.
|
|
24
|
+
* @returns The same quads with every blank node replaced by a skolem IRI.
|
|
25
|
+
*/
|
|
26
|
+
export declare function skolemizeBlankNodes(quads: Quad[], datasetIri: string): Quad[];
|
|
27
|
+
//# sourceMappingURL=skolemizeBlankNodes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skolemizeBlankNodes.d.ts","sourceRoot":"","sources":["../../src/writer/skolemizeBlankNodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAKL,MAAM,cAAc,CAAC;AAMtB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,EAAE,CAgB7E"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DataFactory } from 'n3';
|
|
2
|
+
import { hashSuffix, skolemIri } from '@lde/dataset';
|
|
3
|
+
const { namedNode, quad: makeQuad } = DataFactory;
|
|
4
|
+
/**
|
|
5
|
+
* Rewrite every blank node in `quads` to a deterministic, dataset-scoped IRI,
|
|
6
|
+
* leaving the quads otherwise unchanged.
|
|
7
|
+
*
|
|
8
|
+
* A served store such as the Dataset Knowledge Graph assembles one graph per
|
|
9
|
+
* dataset by concatenating every per-dataset n-quads file into a single index
|
|
10
|
+
* (`qlever index` over the `cat` of all files). Document-scoped blank-node
|
|
11
|
+
* labels are not preserved across that concatenation: two datasets' `_:b0`
|
|
12
|
+
* collapse into one node, silently fusing unrelated measurements, activities
|
|
13
|
+
* and linksets across datasets (see ldelements/lde#478 and
|
|
14
|
+
* netwerk-digitaal-erfgoed/dataset-knowledge-graph#352 / #420). IRIs are never
|
|
15
|
+
* relabelled, so skolemising each blank node keeps distinct nodes distinct.
|
|
16
|
+
*
|
|
17
|
+
* Each blank node becomes `<dataset>/.well-known/skolem#<batch>-<label>`. The
|
|
18
|
+
* dataset IRI rules out fusion across datasets; `<batch>`, a content hash of
|
|
19
|
+
* this write's quads, rules out fusion across the separate writes that land in
|
|
20
|
+
* one dataset's file – their labels both restart at `b0`, but writes carrying
|
|
21
|
+
* different content hash differently. Two byte-identical writes collapse onto
|
|
22
|
+
* the same IRIs, which is correct: they carry the same nodes.
|
|
23
|
+
*
|
|
24
|
+
* @param quads - The quads to rewrite, possibly containing blank nodes.
|
|
25
|
+
* @param datasetIri - The dataset the quads belong to; scopes every minted IRI.
|
|
26
|
+
* @returns The same quads with every blank node replaced by a skolem IRI.
|
|
27
|
+
*/
|
|
28
|
+
export function skolemizeBlankNodes(quads, datasetIri) {
|
|
29
|
+
const base = `${datasetIri}/.well-known/skolem#${hashSuffix(fingerprint(quads))}`;
|
|
30
|
+
const skolemize = (term) => term.termType === 'BlankNode'
|
|
31
|
+
? namedNode(skolemIri(base, term.value))
|
|
32
|
+
: term;
|
|
33
|
+
// A skolemised blank node is a NamedNode, valid in every term position, so the
|
|
34
|
+
// casts hold: skolemize only rewrites blank nodes and leaves other terms as-is.
|
|
35
|
+
return quads.map((quad) => makeQuad(skolemize(quad.subject), quad.predicate, skolemize(quad.object), skolemize(quad.graph)));
|
|
36
|
+
}
|
|
37
|
+
/** A deterministic string identifying a write's quads, for a per-write IRI segment. */
|
|
38
|
+
function fingerprint(quads) {
|
|
39
|
+
return quads
|
|
40
|
+
.map((quad) => `${termKey(quad.subject)} ${quad.predicate.value} ${termKey(quad.object)}`)
|
|
41
|
+
.sort()
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
44
|
+
function termKey(term) {
|
|
45
|
+
switch (term.termType) {
|
|
46
|
+
case 'BlankNode':
|
|
47
|
+
return `_:${term.value}`;
|
|
48
|
+
case 'Literal':
|
|
49
|
+
return `"${term.value}"^^<${term.datatype.value}>@${term.language}`;
|
|
50
|
+
default:
|
|
51
|
+
return `<${term.value}>`;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/pipeline",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
6
6
|
"directory": "packages/pipeline"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"fetch-sparql-endpoint": "^7.1.1",
|
|
40
40
|
"filenamify-url": "^4.0.0",
|
|
41
41
|
"is-network-error": "^1.3.2",
|
|
42
|
-
"n3": "^2.1.
|
|
42
|
+
"n3": "^2.1.1",
|
|
43
43
|
"p-retry": "^8.0.0",
|
|
44
44
|
"rdf-string": "^2.0.1",
|
|
45
45
|
"tslib": "^2.3.0"
|