@lde/pipeline 0.33.3 → 0.34.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/README.md CHANGED
@@ -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
- Generic plugin that rewrites namespace prefixes in `void:class` and `void:property` quad objects. Accepts `from` and `to` options specifying the source and target namespace URI prefixes. `void:vocabulary` quads are left unchanged so consumers can see which namespace the source dataset actually uses.
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 ‘@lde/pipeline’;
409
+ import { namespaceNormalizationPlugin } from '@lde/pipeline';
401
410
 
402
411
  new Pipeline({
403
412
  // ...
404
413
  plugins: [
405
414
  namespaceNormalizationPlugin({
406
- from: ‘http://example.org/’,
407
- to: ‘https://example.org/’,
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
- Normalizes Schema.org namespace prefixes in `void:class` and `void:property` quad objects. By default, rewrites `http://schema.org/` to `https://schema.org/`. Pass `{ reverse: true }` to normalize in the opposite direction (`https://` to `http://`). `void:vocabulary` quads are left unchanged so consumers can see which namespace the source dataset actually uses.
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 ‘@lde/pipeline’;
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
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './validator.js';
2
+ export * from './namespaceAlias.js';
2
3
  export * from './asyncQueue.js';
3
4
  export * from './batch.js';
4
5
  export * from './pipeline.js';
@@ -11,7 +12,7 @@ export * from './sparql/index.js';
11
12
  export * from './distribution/index.js';
12
13
  export * from './provenance/index.js';
13
14
  export * from './writer/index.js';
14
- export * from './plugin/namespaceNormalization.js';
15
15
  export * from './plugin/provenance.js';
16
+ export * from './plugin/namespaceNormalization.js';
16
17
  export * from './plugin/schemaOrgNormalization.js';
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,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,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,oCAAoC,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
@@ -1,4 +1,5 @@
1
1
  export * from './validator.js';
2
+ export * from './namespaceAlias.js';
2
3
  export * from './asyncQueue.js';
3
4
  export * from './batch.js';
4
5
  export * from './pipeline.js';
@@ -11,6 +12,6 @@ export * from './sparql/index.js';
11
12
  export * from './distribution/index.js';
12
13
  export * from './provenance/index.js';
13
14
  export * from './writer/index.js';
14
- export * from './plugin/namespaceNormalization.js';
15
15
  export * from './plugin/provenance.js';
16
+ export * from './plugin/namespaceNormalization.js';
16
17
  export * from './plugin/schemaOrgNormalization.js';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Declares that two namespaces should be treated as equivalent, working
3
+ * around vocabularies that publish under both HTTP and HTTPS variants of the
4
+ * same IRI (notably schema.org).
5
+ */
6
+ export interface NamespaceAlias {
7
+ /**
8
+ * The namespace IRIs should be normalized to (e.g. `https://schema.org/`).
9
+ */
10
+ canonical: string;
11
+ /**
12
+ * The equivalent namespace that may appear in source data (e.g.
13
+ * `http://schema.org/`).
14
+ */
15
+ alias: string;
16
+ }
17
+ /**
18
+ * Rewrite an IRI in an alias namespace to its canonical form. IRIs outside
19
+ * every alias namespace are returned unchanged.
20
+ */
21
+ export declare function canonicalizeIri(iri: string, namespaceAliases: readonly NamespaceAlias[]): string;
22
+ /**
23
+ * All equivalent forms of an IRI under the given aliases: the IRI itself
24
+ * plus, when it falls in a canonical or alias namespace, the counterpart in
25
+ * the other namespace.
26
+ */
27
+ export declare function aliasVariants(iri: string, namespaceAliases: readonly NamespaceAlias[]): string[];
28
+ //# sourceMappingURL=namespaceAlias.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"namespaceAlias.d.ts","sourceRoot":"","sources":["../src/namespaceAlias.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,SAAS,cAAc,EAAE,GAC1C,MAAM,CAOR;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,gBAAgB,EAAE,SAAS,cAAc,EAAE,GAC1C,MAAM,EAAE,CAUV"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Rewrite an IRI in an alias namespace to its canonical form. IRIs outside
3
+ * every alias namespace are returned unchanged.
4
+ */
5
+ export function canonicalizeIri(iri, namespaceAliases) {
6
+ for (const { canonical, alias } of namespaceAliases) {
7
+ if (iri.startsWith(alias)) {
8
+ return canonical + iri.slice(alias.length);
9
+ }
10
+ }
11
+ return iri;
12
+ }
13
+ /**
14
+ * All equivalent forms of an IRI under the given aliases: the IRI itself
15
+ * plus, when it falls in a canonical or alias namespace, the counterpart in
16
+ * the other namespace.
17
+ */
18
+ export function aliasVariants(iri, namespaceAliases) {
19
+ for (const { canonical, alias } of namespaceAliases) {
20
+ if (iri.startsWith(canonical)) {
21
+ return [iri, alias + iri.slice(canonical.length)];
22
+ }
23
+ if (iri.startsWith(alias)) {
24
+ return [iri, canonical + iri.slice(alias.length)];
25
+ }
26
+ }
27
+ return [iri];
28
+ }
@@ -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 the merged, post-stage quad stream before writing (extension
28
- * point 2: pipeline-wide, post-merge). The home of cross-cutting concerns
29
- * – provenance, namespace normalisation – that apply regardless of which
30
- * reader produced a quad.
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?;
@@ -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;AAChD,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,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;CAC3D;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;AAqID,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,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;IA+C9B,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;YAyCZ,cAAc;IAqN5B,+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"}
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 runWriter = await this.writer.openRun(context);
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
- await runWriter.flush?.(dataset, stageFailed ? 'failed' : 'success');
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 namespace prefixes in `void:class` and
12
- * `void:property` quad objects from {@link NamespaceNormalizationOptions.from}
13
- * to {@link NamespaceNormalizationOptions.to}.
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
- * `void:vocabulary` quads are left unchanged so consumers can see which
16
- * namespace the source dataset actually uses.
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
- * Pipeline plugin that normalizes namespace prefixes in `void:class` and
23
- * `void:property` quad objects.
24
- *
25
- * `void:vocabulary` quads are left unchanged so consumers can see which
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":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAO5C,MAAM,WAAW,6BAA6B;IAC5C,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,6BAA6B,GACrC,aAAa,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAErC;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B,GACrC,cAAc,CAKhB"}
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 namespace prefixes in `void:class` and
6
- * `void:property` quad objects from {@link NamespaceNormalizationOptions.from}
7
- * to {@link NamespaceNormalizationOptions.to}.
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
- * `void:vocabulary` quads are left unchanged so consumers can see which
10
- * namespace the source dataset actually uses.
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
- return (quads) => normalizeNamespace(quads, options);
19
+ const namespaceAliases = [
20
+ { canonical: options.to, alias: options.from },
21
+ ];
22
+ return (quads) => normalize(quads, namespaceAliases);
14
23
  }
15
24
  /**
16
- * Pipeline plugin that normalizes namespace prefixes in `void:class` and
17
- * `void:property` quad objects.
18
- *
19
- * `void:vocabulary` quads are left unchanged so consumers can see which
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
- async function* normalizeNamespace(quads, { from, to }) {
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
- if ((q.predicate.equals(_void.class) || q.predicate.equals(_void.property)) &&
31
- q.object.termType === 'NamedNode' &&
32
- q.object.value.startsWith(from)) {
33
- yield quad(q.subject, q.predicate, namedNode(to + q.object.value.slice(from.length)), q.graph);
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
- * Pipeline plugin that normalizes Schema.org namespace prefixes in `void:class`
14
- * and `void:property` quad objects.
15
- *
16
- * By default, rewrites `http://schema.org/` to `https://schema.org/`. Pass
17
- * `{ reverse: true }` to normalize in the opposite direction.
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
- * `void:vocabulary` quads are left unchanged so consumers can see which
20
- * namespace the source dataset actually uses.
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,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAS5C,MAAM,WAAW,6BAA6B;IAC5C,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,+HAA+H;AAC/H,eAAO,MAAM,+BAA+B,EAAE,aAAa,CAAC;IAC1D,OAAO,EAAE,OAAO,CAAC;CAClB,CAGC,CAAC;AAEH;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,CAAC,EAAE,6BAA6B,GACtC,cAAc,CAOhB"}
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
- /** QuadTransform that normalizes `http://schema.org/` to `https://schema.org/` in `void:class` and `void:property` objects. */
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
- * Pipeline plugin that normalizes Schema.org namespace prefixes in `void:class`
11
- * and `void:property` quad objects.
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
- * `void:vocabulary` quads are left unchanged so consumers can see which
17
- * namespace the source dataset actually uses.
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lde/pipeline",
3
- "version": "0.33.3",
3
+ "version": "0.34.0",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/ldelements/lde.git",
6
6
  "directory": "packages/pipeline"