@lde/pipeline 0.33.4 → 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 +19 -20
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/pipeline.d.ts +29 -4
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +113 -2
- package/dist/plugin/namespaceNormalization.d.ts +18 -16
- package/dist/plugin/namespaceNormalization.d.ts.map +1 -1
- package/dist/plugin/namespaceNormalization.js +36 -18
- package/dist/plugin/schemaOrgNormalization.d.ts +14 -13
- package/dist/plugin/schemaOrgNormalization.d.ts.map +1 -1
- package/dist/plugin/schemaOrgNormalization.js +12 -8
- 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
|
|
|
@@ -390,51 +390,50 @@ Otherwise it imports (if needed), runs the stages, and writes an updated record.
|
|
|
390
390
|
|
|
391
391
|
### Plugins
|
|
392
392
|
|
|
393
|
-
Plugins hook into the pipeline lifecycle via the `PipelinePlugin` interface. Register them in the `plugins` array when constructing a `Pipeline`.
|
|
393
|
+
Plugins hook into the pipeline lifecycle via the `PipelinePlugin` interface. Register them in the `plugins` array when constructing a `Pipeline`. A plugin can implement either or both of two hooks, each a `QuadTransform`:
|
|
394
|
+
|
|
395
|
+
- **`beforeStageWrite`** runs over each stage’s output as it is written – scoped to one stage’s quads for one dataset.
|
|
396
|
+
- **`beforeDatasetWrite`** runs once over a dataset’s combined cross-stage output, just before it is written – scoped to the whole dataset, so it can reconcile quads that different stages produced. The output is streamed through a single queue rather than materialized, so memory stays bounded by whatever the transform itself buffers; it honors the writer’s run lifecycle (`flush`/`reset`/`commit`/`abort`), and a failure is isolated to the dataset being written.
|
|
397
|
+
|
|
398
|
+
Both hooks are domain-agnostic – dedup, roll-ups, provenance stamping, or namespace rewriting can use them. This package ships `provenancePlugin()`, `namespaceNormalizationPlugin()`, and `schemaOrgNormalizationPlugin()` (all `beforeStageWrite` plugins). Plugins that must understand a particular RDF shape live with that vocabulary: for example, [@lde/pipeline-void](../pipeline-void) ships `schemaOrgPartitionMergePlugin()`, a `beforeDatasetWrite` plugin that merges the VoID partition _nodes_ that Schema.org `http`/`https` variants produce.
|
|
399
|
+
|
|
400
|
+
#### `provenancePlugin()`
|
|
401
|
+
|
|
402
|
+
Appends [PROV-O](https://www.w3.org/TR/prov-o/) provenance quads (`prov:Entity`, `prov:Activity`, `prov:startedAtTime`, `prov:endedAtTime`) to every stage’s output. The `prov:Activity` is a stable IRI keyed on `(dataset, stage)`, not a blank node, so activities stay distinct – and a re-run stays idempotent – when per-dataset outputs are merged into one graph (blank-node labels are not unique across separately serialised documents and would fuse unrelated activities).
|
|
394
403
|
|
|
395
404
|
#### `namespaceNormalizationPlugin(options)`
|
|
396
405
|
|
|
397
|
-
|
|
406
|
+
Rewrites every IRI in the `from` namespace to the `to` namespace, in subject, predicate and object position alike. A blanket, vocabulary-agnostic rewrite for standardizing a namespace across a dataset’s own quads – for example when mapping instance data to an application profile.
|
|
398
407
|
|
|
399
408
|
```typescript
|
|
400
|
-
import { namespaceNormalizationPlugin } from
|
|
409
|
+
import { namespaceNormalizationPlugin } from '@lde/pipeline';
|
|
401
410
|
|
|
402
411
|
new Pipeline({
|
|
403
412
|
// ...
|
|
404
413
|
plugins: [
|
|
405
414
|
namespaceNormalizationPlugin({
|
|
406
|
-
from:
|
|
407
|
-
to:
|
|
415
|
+
from: 'http://example.org/',
|
|
416
|
+
to: 'https://example.org/',
|
|
408
417
|
}),
|
|
409
418
|
],
|
|
410
419
|
});
|
|
411
420
|
```
|
|
412
421
|
|
|
413
|
-
#### `provenancePlugin()`
|
|
414
|
-
|
|
415
|
-
Appends [PROV-O](https://www.w3.org/TR/prov-o/) provenance quads (`prov:Entity`, `prov:Activity`, `prov:startedAtTime`, `prov:endedAtTime`) to every stage’s output. The `prov:Activity` is a stable IRI keyed on `(dataset, stage)`, not a blank node, so activities stay distinct – and a re-run stays idempotent – when per-dataset outputs are merged into one graph (blank-node labels are not unique across separately serialised documents and would fuse unrelated activities).
|
|
416
|
-
|
|
417
422
|
#### `schemaOrgNormalizationPlugin(options?)`
|
|
418
423
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
This is a convenience wrapper around `namespaceNormalizationPlugin`.
|
|
424
|
+
Convenience wrapper around `namespaceNormalizationPlugin` for the common case: rewrites `http://schema.org/` to `https://schema.org/` (pass `{ reverse: true }` for the opposite direction).
|
|
422
425
|
|
|
423
426
|
```typescript
|
|
424
|
-
import { schemaOrgNormalizationPlugin, provenancePlugin } from
|
|
427
|
+
import { schemaOrgNormalizationPlugin, provenancePlugin } from '@lde/pipeline';
|
|
425
428
|
|
|
426
429
|
new Pipeline({
|
|
427
430
|
// ...
|
|
428
431
|
plugins: [schemaOrgNormalizationPlugin(), provenancePlugin()],
|
|
429
432
|
});
|
|
430
|
-
|
|
431
|
-
// Or reverse: normalize https to http
|
|
432
|
-
new Pipeline({
|
|
433
|
-
// ...
|
|
434
|
-
plugins: [schemaOrgNormalizationPlugin({reverse: true})],
|
|
435
|
-
});
|
|
436
433
|
```
|
|
437
434
|
|
|
435
|
+
This is a plain rewrite and knows nothing about VoID. If your pipeline emits VoID partitions and a dataset mixes both schema.org namespaces, rewriting the `void:class` objects alone still leaves two `void:classPartition` nodes for one class – use [`schemaOrgPartitionMergePlugin`](../pipeline-void) from `@lde/pipeline-void`, which also merges those nodes.
|
|
436
|
+
|
|
438
437
|
## Usage
|
|
439
438
|
|
|
440
439
|
```typescript
|
|
@@ -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
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export * from './sparql/index.js';
|
|
|
12
12
|
export * from './distribution/index.js';
|
|
13
13
|
export * from './provenance/index.js';
|
|
14
14
|
export * from './writer/index.js';
|
|
15
|
-
export * from './plugin/namespaceNormalization.js';
|
|
16
15
|
export * from './plugin/provenance.js';
|
|
16
|
+
export * from './plugin/namespaceNormalization.js';
|
|
17
17
|
export * from './plugin/schemaOrgNormalization.js';
|
|
18
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oCAAoC,CAAC;AACnD,cAAc,oCAAoC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,6 @@ export * from './sparql/index.js';
|
|
|
12
12
|
export * from './distribution/index.js';
|
|
13
13
|
export * from './provenance/index.js';
|
|
14
14
|
export * from './writer/index.js';
|
|
15
|
-
export * from './plugin/namespaceNormalization.js';
|
|
16
15
|
export * from './plugin/provenance.js';
|
|
16
|
+
export * from './plugin/namespaceNormalization.js';
|
|
17
17
|
export * from './plugin/schemaOrgNormalization.js';
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -20,16 +20,40 @@ export interface BeforeStageWriteContext {
|
|
|
20
20
|
dataset: Dataset;
|
|
21
21
|
stage: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Context handed to a {@link PipelinePlugin.beforeDatasetWrite} transform: the
|
|
25
|
+
* `dataset` whose complete, cross-stage output is being written. Unlike
|
|
26
|
+
* {@link BeforeStageWriteContext} there is no `stage` — the transform sees one
|
|
27
|
+
* dataset's quads from every stage as a single stream.
|
|
28
|
+
*/
|
|
29
|
+
export interface BeforeDatasetWriteContext {
|
|
30
|
+
dataset: Dataset;
|
|
31
|
+
}
|
|
23
32
|
/** Plugin that hooks into pipeline lifecycle events. */
|
|
24
33
|
export interface PipelinePlugin {
|
|
25
34
|
name: string;
|
|
26
35
|
/**
|
|
27
|
-
* Transform
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
36
|
+
* Transform a single stage's merged output before writing (extension point
|
|
37
|
+
* 2: per stage, post-merge). Runs once per stage per dataset, carrying the
|
|
38
|
+
* stage identity so it can mint stable per-`(dataset, stage)` IRIs. The home
|
|
39
|
+
* of stage-scoped concerns — provenance stamping, namespace rewriting of a
|
|
40
|
+
* stage's own quads.
|
|
31
41
|
*/
|
|
32
42
|
beforeStageWrite?: QuadTransform<BeforeStageWriteContext>;
|
|
43
|
+
/**
|
|
44
|
+
* Transform a whole dataset's output before writing (extension point 3: per
|
|
45
|
+
* dataset, cross-stage). Runs once over the concatenation of every stage's
|
|
46
|
+
* output for one dataset, after all stages complete and before the write is
|
|
47
|
+
* flushed. The home of concerns that need to reconcile quads *across* stages
|
|
48
|
+
* — deduplication, cross-stage roll-ups, or merging partition nodes that
|
|
49
|
+
* different stages contribute to.
|
|
50
|
+
*
|
|
51
|
+
* The transform is a streaming {@link QuadTransform}: the pipeline pipes each
|
|
52
|
+
* stage's output through it via a bounded queue and never materializes the
|
|
53
|
+
* dataset, so a pass-through or streaming transform stays O(1). A transform
|
|
54
|
+
* that genuinely needs cross-stage state buffers only its own working set.
|
|
55
|
+
*/
|
|
56
|
+
beforeDatasetWrite?: QuadTransform<BeforeDatasetWriteContext>;
|
|
33
57
|
}
|
|
34
58
|
export interface PipelineOptions {
|
|
35
59
|
datasetSelector: DatasetSelector;
|
|
@@ -83,6 +107,7 @@ export declare class Pipeline {
|
|
|
83
107
|
private readonly stages;
|
|
84
108
|
private readonly writer;
|
|
85
109
|
private readonly beforeStageWrite?;
|
|
110
|
+
private readonly beforeDatasetWrite?;
|
|
86
111
|
private readonly distributionResolver;
|
|
87
112
|
private readonly chaining?;
|
|
88
113
|
private readonly reporter?;
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAGrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAGrD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,KAAK,EAKV,MAAM,EACP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,KAAK,oBAAoB,EAI1B,MAAM,4BAA4B,CAAC;AAKpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAY7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,2BAA2B,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IAC1D;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,EAAE,aAAa,CAAC,yBAAyB,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,eAAe,CAAC;IACjC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,EAAE;QACT,mBAAmB,EAAE,mBAAmB,CAAC;QACzC,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,gBAAgB,GAAG,SAAS,gBAAgB,EAAE,CAAC;IAC1D;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,MAAM,aAAa,CAAC;CAC/B;AA0OD,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAU;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAyC;IAC3E,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAA2C;IAC/E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAuB;IAC5D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAA8B;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAsB;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAS;gBAE9B,OAAO,EAAE,eAAe;IAyD9B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YA8CZ,cAAc;IAgO5B,+EAA+E;YACjE,aAAa;YAmBb,gBAAgB;IAW9B,OAAO,CAAE,aAAa;IAOtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAMnB;;;;;;;;OAQG;IACH,OAAO,CAAC,0BAA0B;IA4BlC;;;;;OAKG;YACW,SAAS;IAuCvB;;;OAGG;YACW,QAAQ;IA0CtB,2EAA2E;YAC7D,eAAe;YAqBf,QAAQ;YAuEP,SAAS;CAczB"}
|
package/dist/pipeline.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { StreamParser } from 'n3';
|
|
4
|
+
import { AsyncQueue } from './asyncQueue.js';
|
|
4
5
|
import { FileWriter } from './writer/fileWriter.js';
|
|
5
6
|
import { NoDistributionAvailable, ResolvedDistribution, } from './distribution/resolver.js';
|
|
6
7
|
import { SparqlDistributionResolver } from './distribution/index.js';
|
|
@@ -121,12 +122,102 @@ class TransformWriter {
|
|
|
121
122
|
await this.inner.write(dataset, this.transform(quads, { dataset, stage: this.stage }));
|
|
122
123
|
}
|
|
123
124
|
}
|
|
125
|
+
/** Discards a dataset's queued output; used to unblock the transform on reset. */
|
|
126
|
+
class DatasetReset extends Error {
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A {@link RunWriter} decorator that applies a {@link PipelinePlugin.beforeDatasetWrite}
|
|
130
|
+
* transform once over a dataset's complete, cross-stage output.
|
|
131
|
+
*
|
|
132
|
+
* Every stage's {@link write} for a dataset feeds a single {@link AsyncQueue};
|
|
133
|
+
* the transform consumes that queue as one stream and its output goes to the
|
|
134
|
+
* inner writer. The queue is created on the first write and closed on
|
|
135
|
+
* {@link flush}, so stage output streams through the transform (bounded by the
|
|
136
|
+
* queue's backpressure) rather than being materialized. Datasets are processed
|
|
137
|
+
* one at a time, so a single in-flight queue suffices.
|
|
138
|
+
*/
|
|
139
|
+
class DatasetTransformWriter {
|
|
140
|
+
inner;
|
|
141
|
+
transform;
|
|
142
|
+
current;
|
|
143
|
+
constructor(inner, transform) {
|
|
144
|
+
this.inner = inner;
|
|
145
|
+
this.transform = transform;
|
|
146
|
+
}
|
|
147
|
+
async write(dataset, quads) {
|
|
148
|
+
const { queue } = this.open(dataset);
|
|
149
|
+
for await (const quad of quads) {
|
|
150
|
+
await queue.push(quad);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async flush(dataset, outcome) {
|
|
154
|
+
let writeError;
|
|
155
|
+
if (this.current?.iri === dataset.iri.toString()) {
|
|
156
|
+
const { queue, innerWrite } = this.current;
|
|
157
|
+
this.current = undefined;
|
|
158
|
+
queue.close();
|
|
159
|
+
// Capture rather than propagate immediately, so the inner writer still
|
|
160
|
+
// gets its flush (as 'failed') to finalize/clean up this dataset.
|
|
161
|
+
try {
|
|
162
|
+
await innerWrite;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
writeError = error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
await this.inner.flush?.(dataset, writeError ? 'failed' : outcome);
|
|
169
|
+
if (writeError)
|
|
170
|
+
throw writeError;
|
|
171
|
+
}
|
|
172
|
+
async reset(dataset) {
|
|
173
|
+
await this.discardCurrent(dataset, new DatasetReset());
|
|
174
|
+
await this.inner.reset?.(dataset);
|
|
175
|
+
}
|
|
176
|
+
async commit() {
|
|
177
|
+
await this.inner.commit();
|
|
178
|
+
}
|
|
179
|
+
async abort(error) {
|
|
180
|
+
await this.discardCurrent(undefined, error);
|
|
181
|
+
await this.inner.abort(error);
|
|
182
|
+
}
|
|
183
|
+
open(dataset) {
|
|
184
|
+
const iri = dataset.iri.toString();
|
|
185
|
+
if (this.current?.iri === iri)
|
|
186
|
+
return this.current;
|
|
187
|
+
const queue = new AsyncQueue();
|
|
188
|
+
const innerWrite = this.inner.write(dataset, this.transform(queue, { dataset }));
|
|
189
|
+
// If the inner write (or the transform) fails, abort the queue so a
|
|
190
|
+
// producer blocked on push() is released and surfaces the error, instead
|
|
191
|
+
// of hanging forever waiting for a consumer that has stopped pulling. The
|
|
192
|
+
// rejection is re-observed (and re-thrown) by flush()/discardCurrent via
|
|
193
|
+
// `innerWrite`, so attaching this handler does not swallow it.
|
|
194
|
+
void innerWrite.catch((error) => queue.abort(error));
|
|
195
|
+
this.current = { iri, queue, innerWrite };
|
|
196
|
+
return this.current;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Abandon the in-flight dataset (the given one, or any when `dataset` is
|
|
200
|
+
* undefined): abort the queue so the transform and inner write terminate,
|
|
201
|
+
* then await them, swallowing the abort so it does not mask the caller's flow.
|
|
202
|
+
*/
|
|
203
|
+
async discardCurrent(dataset, error) {
|
|
204
|
+
if (!this.current)
|
|
205
|
+
return;
|
|
206
|
+
if (dataset && this.current.iri !== dataset.iri.toString())
|
|
207
|
+
return;
|
|
208
|
+
const { queue, innerWrite } = this.current;
|
|
209
|
+
this.current = undefined;
|
|
210
|
+
queue.abort(error);
|
|
211
|
+
await innerWrite.catch(() => undefined);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
124
214
|
export class Pipeline {
|
|
125
215
|
name;
|
|
126
216
|
datasetSelector;
|
|
127
217
|
stages;
|
|
128
218
|
writer;
|
|
129
219
|
beforeStageWrite;
|
|
220
|
+
beforeDatasetWrite;
|
|
130
221
|
distributionResolver;
|
|
131
222
|
chaining;
|
|
132
223
|
reporter;
|
|
@@ -156,6 +247,12 @@ export class Pipeline {
|
|
|
156
247
|
this.beforeStageWrite = transforms?.length
|
|
157
248
|
? (quads, context) => transforms.reduce((q, fn) => fn(q, context), quads)
|
|
158
249
|
: undefined;
|
|
250
|
+
const datasetTransforms = options.plugins
|
|
251
|
+
?.map((p) => p.beforeDatasetWrite)
|
|
252
|
+
.filter((t) => t !== undefined);
|
|
253
|
+
this.beforeDatasetWrite = datasetTransforms?.length
|
|
254
|
+
? (quads, context) => datasetTransforms.reduce((q, fn) => fn(q, context), quads)
|
|
255
|
+
: undefined;
|
|
159
256
|
this.distributionResolver =
|
|
160
257
|
options.distributionResolver ?? new SparqlDistributionResolver();
|
|
161
258
|
this.chaining = options.chaining;
|
|
@@ -186,7 +283,12 @@ export class Pipeline {
|
|
|
186
283
|
selectedSources: () => selectedSources,
|
|
187
284
|
provenance: this.provenanceStore,
|
|
188
285
|
};
|
|
189
|
-
const
|
|
286
|
+
const openedWriter = await this.writer.openRun(context);
|
|
287
|
+
// Wrap the run writer so a whole-dataset transform sees every stage's
|
|
288
|
+
// output for a dataset as one stream before it is written.
|
|
289
|
+
const runWriter = this.beforeDatasetWrite
|
|
290
|
+
? new DatasetTransformWriter(openedWriter, this.beforeDatasetWrite)
|
|
291
|
+
: openedWriter;
|
|
190
292
|
try {
|
|
191
293
|
for await (const dataset of datasets) {
|
|
192
294
|
selectedSources.push(dataset.iri.toString());
|
|
@@ -345,7 +447,16 @@ export class Pipeline {
|
|
|
345
447
|
await this.distributionResolver.cleanup?.();
|
|
346
448
|
unsubscribe?.();
|
|
347
449
|
}
|
|
348
|
-
|
|
450
|
+
try {
|
|
451
|
+
await runWriter.flush?.(dataset, stageFailed ? 'failed' : 'success');
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
// A per-dataset writer failure must not abort the whole run: isolate it
|
|
455
|
+
// like a stage failure so the remaining datasets still process. The
|
|
456
|
+
// dataset is recorded 'failed' below and retried on the next run.
|
|
457
|
+
this.reporter?.stageFailed?.('write', error instanceof Error ? error : new Error(String(error)));
|
|
458
|
+
stageFailed = true;
|
|
459
|
+
}
|
|
349
460
|
await this.reportValidators(dataset);
|
|
350
461
|
// A dataset whose stages threw produced incomplete output; record it as
|
|
351
462
|
// ‘failed’ rather than freezing a broken result under a ‘success’ record.
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import type { QuadTransform } from '../stage.js';
|
|
2
|
-
import type { PipelinePlugin } from '../pipeline.js';
|
|
3
|
-
import type { Dataset } from '@lde/dataset';
|
|
2
|
+
import type { BeforeStageWriteContext, PipelinePlugin } from '../pipeline.js';
|
|
4
3
|
export interface NamespaceNormalizationOptions {
|
|
5
4
|
/** Namespace URI prefix to match (e.g. `http://schema.org/`). */
|
|
6
5
|
from: string;
|
|
7
|
-
/** Namespace URI prefix to replace with (e.g. `https://schema.org/`). */
|
|
6
|
+
/** Namespace URI prefix to replace it with (e.g. `https://schema.org/`). */
|
|
8
7
|
to: string;
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
|
-
* Creates a QuadTransform that rewrites
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Creates a {@link QuadTransform} that rewrites every IRI in the
|
|
11
|
+
* {@link NamespaceNormalizationOptions.from} namespace to
|
|
12
|
+
* {@link NamespaceNormalizationOptions.to} — in subject, predicate and object
|
|
13
|
+
* position alike.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* This is a blanket, vocabulary-agnostic rewrite: it normalizes the namespace
|
|
16
|
+
* wherever it appears, with no knowledge of VoID or any other RDF shape. Use it
|
|
17
|
+
* to standardize a namespace across a dataset's own quads (for example
|
|
18
|
+
* `http://schema.org/` → `https://schema.org/` when mapping instance data to an
|
|
19
|
+
* application profile). Merging VoID partition _nodes_ that two namespace
|
|
20
|
+
* variants produced is a separate, shape-aware concern handled by
|
|
21
|
+
* `@lde/pipeline-void`.
|
|
17
22
|
*/
|
|
18
|
-
export declare function namespaceNormalizationTransform(options: NamespaceNormalizationOptions): QuadTransform<
|
|
19
|
-
dataset: Dataset;
|
|
20
|
-
}>;
|
|
23
|
+
export declare function namespaceNormalizationTransform(options: NamespaceNormalizationOptions): QuadTransform<BeforeStageWriteContext>;
|
|
21
24
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* namespace the source dataset actually uses.
|
|
25
|
+
* A generic {@link PipelinePlugin} that normalizes a namespace prefix across a
|
|
26
|
+
* stage's output via {@link PipelinePlugin.beforeStageWrite}. Rewrites every
|
|
27
|
+
* matching IRI, in any term position; see
|
|
28
|
+
* {@link namespaceNormalizationTransform}.
|
|
27
29
|
*/
|
|
28
30
|
export declare function namespaceNormalizationPlugin(options: NamespaceNormalizationOptions): PipelinePlugin;
|
|
29
31
|
//# sourceMappingURL=namespaceNormalization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"namespaceNormalization.d.ts","sourceRoot":"","sources":["../../src/plugin/namespaceNormalization.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"namespaceNormalization.d.ts","sourceRoot":"","sources":["../../src/plugin/namespaceNormalization.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAM9E,MAAM,WAAW,6BAA6B;IAC5C,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,6BAA6B,GACrC,aAAa,CAAC,uBAAuB,CAAC,CAKxC;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,CAKhB"}
|
|
@@ -1,23 +1,31 @@
|
|
|
1
|
+
import { canonicalizeIri } from '../namespaceAlias.js';
|
|
1
2
|
import { DataFactory } from 'n3';
|
|
2
|
-
import { _void } from '@tpluscode/rdf-ns-builders';
|
|
3
3
|
const { namedNode, quad } = DataFactory;
|
|
4
4
|
/**
|
|
5
|
-
* Creates a QuadTransform that rewrites
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Creates a {@link QuadTransform} that rewrites every IRI in the
|
|
6
|
+
* {@link NamespaceNormalizationOptions.from} namespace to
|
|
7
|
+
* {@link NamespaceNormalizationOptions.to} — in subject, predicate and object
|
|
8
|
+
* position alike.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* This is a blanket, vocabulary-agnostic rewrite: it normalizes the namespace
|
|
11
|
+
* wherever it appears, with no knowledge of VoID or any other RDF shape. Use it
|
|
12
|
+
* to standardize a namespace across a dataset's own quads (for example
|
|
13
|
+
* `http://schema.org/` → `https://schema.org/` when mapping instance data to an
|
|
14
|
+
* application profile). Merging VoID partition _nodes_ that two namespace
|
|
15
|
+
* variants produced is a separate, shape-aware concern handled by
|
|
16
|
+
* `@lde/pipeline-void`.
|
|
11
17
|
*/
|
|
12
18
|
export function namespaceNormalizationTransform(options) {
|
|
13
|
-
|
|
19
|
+
const namespaceAliases = [
|
|
20
|
+
{ canonical: options.to, alias: options.from },
|
|
21
|
+
];
|
|
22
|
+
return (quads) => normalize(quads, namespaceAliases);
|
|
14
23
|
}
|
|
15
24
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* namespace the source dataset actually uses.
|
|
25
|
+
* A generic {@link PipelinePlugin} that normalizes a namespace prefix across a
|
|
26
|
+
* stage's output via {@link PipelinePlugin.beforeStageWrite}. Rewrites every
|
|
27
|
+
* matching IRI, in any term position; see
|
|
28
|
+
* {@link namespaceNormalizationTransform}.
|
|
21
29
|
*/
|
|
22
30
|
export function namespaceNormalizationPlugin(options) {
|
|
23
31
|
return {
|
|
@@ -25,15 +33,25 @@ export function namespaceNormalizationPlugin(options) {
|
|
|
25
33
|
beforeStageWrite: namespaceNormalizationTransform(options),
|
|
26
34
|
};
|
|
27
35
|
}
|
|
28
|
-
|
|
36
|
+
function normalizeTerm(term, namespaceAliases) {
|
|
37
|
+
if (term.termType !== 'NamedNode') {
|
|
38
|
+
return term;
|
|
39
|
+
}
|
|
40
|
+
const canonical = canonicalizeIri(term.value, namespaceAliases);
|
|
41
|
+
return (canonical === term.value ? term : namedNode(canonical));
|
|
42
|
+
}
|
|
43
|
+
async function* normalize(quads, namespaceAliases) {
|
|
29
44
|
for await (const q of quads) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
const subject = normalizeTerm(q.subject, namespaceAliases);
|
|
46
|
+
const predicate = normalizeTerm(q.predicate, namespaceAliases);
|
|
47
|
+
const object = normalizeTerm(q.object, namespaceAliases);
|
|
48
|
+
if (subject === q.subject &&
|
|
49
|
+
predicate === q.predicate &&
|
|
50
|
+
object === q.object) {
|
|
51
|
+
yield q;
|
|
34
52
|
}
|
|
35
53
|
else {
|
|
36
|
-
yield q;
|
|
54
|
+
yield quad(subject, predicate, object, q.graph);
|
|
37
55
|
}
|
|
38
56
|
}
|
|
39
57
|
}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import type { QuadTransform } from '../stage.js';
|
|
2
|
-
import type { PipelinePlugin } from '../pipeline.js';
|
|
3
|
-
import type { Dataset } from '@lde/dataset';
|
|
2
|
+
import type { BeforeStageWriteContext, PipelinePlugin } from '../pipeline.js';
|
|
4
3
|
export interface SchemaOrgNormalizationOptions {
|
|
5
4
|
/** When true, normalizes `https://schema.org/` to `http://schema.org/` instead. */
|
|
6
5
|
reverse?: boolean;
|
|
7
6
|
}
|
|
8
|
-
/** QuadTransform that normalizes `http://schema.org/` to `https://schema.org/` in `void:class` and `void:property` objects. */
|
|
9
|
-
export declare const schemaOrgNormalizationTransform: QuadTransform<{
|
|
10
|
-
dataset: Dataset;
|
|
11
|
-
}>;
|
|
12
7
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
* A {@link QuadTransform} that normalizes `http://schema.org/` to
|
|
9
|
+
* `https://schema.org/` in every term position. See
|
|
10
|
+
* {@link namespaceNormalizationTransform}.
|
|
11
|
+
*/
|
|
12
|
+
export declare const schemaOrgNormalizationTransform: QuadTransform<BeforeStageWriteContext>;
|
|
13
|
+
/**
|
|
14
|
+
* A generic {@link PipelinePlugin} that normalizes the Schema.org namespace
|
|
15
|
+
* across a stage's output.
|
|
18
16
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* By default rewrites `http://schema.org/` to `https://schema.org/`; pass
|
|
18
|
+
* `{ reverse: true }` to normalize the other way. It is a blanket rewrite over
|
|
19
|
+
* every matching IRI and knows nothing about VoID — to merge the VoID partition
|
|
20
|
+
* nodes that mixed `http`/`https` variants produce, use
|
|
21
|
+
* `schemaOrgPartitionMergePlugin` from `@lde/pipeline-void` instead.
|
|
21
22
|
*/
|
|
22
23
|
export declare function schemaOrgNormalizationPlugin(options?: SchemaOrgNormalizationOptions): PipelinePlugin;
|
|
23
24
|
//# sourceMappingURL=schemaOrgNormalization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemaOrgNormalization.d.ts","sourceRoot":"","sources":["../../src/plugin/schemaOrgNormalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"schemaOrgNormalization.d.ts","sourceRoot":"","sources":["../../src/plugin/schemaOrgNormalization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAS9E,MAAM,WAAW,6BAA6B;IAC5C,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,aAAa,CAAC,uBAAuB,CAI/E,CAAC;AAEL;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,CAAC,EAAE,6BAA6B,GACtC,cAAc,CAOhB"}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { namespaceNormalizationPlugin, namespaceNormalizationTransform, } from './namespaceNormalization.js';
|
|
2
2
|
const HTTP_SCHEMA_ORG = 'http://schema.org/';
|
|
3
3
|
const HTTPS_SCHEMA_ORG = 'https://schema.org/';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* A {@link QuadTransform} that normalizes `http://schema.org/` to
|
|
6
|
+
* `https://schema.org/` in every term position. See
|
|
7
|
+
* {@link namespaceNormalizationTransform}.
|
|
8
|
+
*/
|
|
5
9
|
export const schemaOrgNormalizationTransform = namespaceNormalizationTransform({
|
|
6
10
|
from: HTTP_SCHEMA_ORG,
|
|
7
11
|
to: HTTPS_SCHEMA_ORG,
|
|
8
12
|
});
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* By default, rewrites `http://schema.org/` to `https://schema.org/`. Pass
|
|
14
|
-
* `{ reverse: true }` to normalize in the opposite direction.
|
|
14
|
+
* A generic {@link PipelinePlugin} that normalizes the Schema.org namespace
|
|
15
|
+
* across a stage's output.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* By default rewrites `http://schema.org/` to `https://schema.org/`; pass
|
|
18
|
+
* `{ reverse: true }` to normalize the other way. It is a blanket rewrite over
|
|
19
|
+
* every matching IRI and knows nothing about VoID — to merge the VoID partition
|
|
20
|
+
* nodes that mixed `http`/`https` variants produce, use
|
|
21
|
+
* `schemaOrgPartitionMergePlugin` from `@lde/pipeline-void` instead.
|
|
18
22
|
*/
|
|
19
23
|
export function schemaOrgNormalizationPlugin(options) {
|
|
20
24
|
const from = options?.reverse ? HTTPS_SCHEMA_ORG : HTTP_SCHEMA_ORG;
|
|
@@ -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.
|
|
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"
|