@lde/pipeline 0.34.0 → 0.34.2
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/pipeline.d.ts +18 -9
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +27 -11
- package/dist/stage.d.ts +42 -6
- package/dist/stage.d.ts.map +1 -1
- package/dist/stage.js +49 -22
- 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 +3 -3
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
|
/**
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Dataset } from '@lde/dataset';
|
|
2
|
+
import type { Quad } from '@rdfjs/types';
|
|
2
3
|
import type { DatasetSelector } from './selector.js';
|
|
3
4
|
import { Stage } from './stage.js';
|
|
4
5
|
import type { QuadTransform } from './stage.js';
|
|
@@ -23,7 +24,7 @@ export interface BeforeStageWriteContext {
|
|
|
23
24
|
/**
|
|
24
25
|
* Context handed to a {@link PipelinePlugin.beforeDatasetWrite} transform: the
|
|
25
26
|
* `dataset` whose complete, cross-stage output is being written. Unlike
|
|
26
|
-
* {@link BeforeStageWriteContext} there is no `stage`
|
|
27
|
+
* {@link BeforeStageWriteContext} there is no `stage` – the transform sees one
|
|
27
28
|
* dataset's quads from every stage as a single stream.
|
|
28
29
|
*/
|
|
29
30
|
export interface BeforeDatasetWriteContext {
|
|
@@ -36,7 +37,7 @@ export interface PipelinePlugin {
|
|
|
36
37
|
* Transform a single stage's merged output before writing (extension point
|
|
37
38
|
* 2: per stage, post-merge). Runs once per stage per dataset, carrying the
|
|
38
39
|
* stage identity so it can mint stable per-`(dataset, stage)` IRIs. The home
|
|
39
|
-
* of stage-scoped concerns
|
|
40
|
+
* of stage-scoped concerns – provenance stamping, namespace rewriting of a
|
|
40
41
|
* stage's own quads.
|
|
41
42
|
*/
|
|
42
43
|
beforeStageWrite?: QuadTransform<BeforeStageWriteContext>;
|
|
@@ -45,7 +46,7 @@ export interface PipelinePlugin {
|
|
|
45
46
|
* dataset, cross-stage). Runs once over the concatenation of every stage's
|
|
46
47
|
* output for one dataset, after all stages complete and before the write is
|
|
47
48
|
* flushed. The home of concerns that need to reconcile quads *across* stages
|
|
48
|
-
*
|
|
49
|
+
* – deduplication, cross-stage roll-ups, or merging partition nodes that
|
|
49
50
|
* different stages contribute to.
|
|
50
51
|
*
|
|
51
52
|
* The transform is a streaming {@link QuadTransform}: the pipeline pipes each
|
|
@@ -55,11 +56,19 @@ export interface PipelinePlugin {
|
|
|
55
56
|
*/
|
|
56
57
|
beforeDatasetWrite?: QuadTransform<BeforeDatasetWriteContext>;
|
|
57
58
|
}
|
|
58
|
-
export interface PipelineOptions {
|
|
59
|
+
export interface PipelineOptions<Out = Quad> {
|
|
59
60
|
datasetSelector: DatasetSelector;
|
|
60
|
-
stages: Stage[];
|
|
61
|
-
writers: Writer | Writer[];
|
|
62
|
-
|
|
61
|
+
stages: Stage<Out>[];
|
|
62
|
+
writers: Writer<Out> | Writer<Out>[];
|
|
63
|
+
/**
|
|
64
|
+
* Quad-level plugins ({@link PipelinePlugin}). Available only to a plain quad
|
|
65
|
+
* pipeline: a projecting pipeline's write-side data is already the projected
|
|
66
|
+
* item, and both plugin points are `QuadTransform`s with no quads to act on,
|
|
67
|
+
* so the type forbids them (`never`). Reader-attached quad transforms are
|
|
68
|
+
* unaffected – they run before the projection seam. See
|
|
69
|
+
* {@link https://github.com/ldelements/lde/blob/main/docs/decisions/0013-project-inside-the-batch-per-root-type.md | ADR 13}.
|
|
70
|
+
*/
|
|
71
|
+
plugins?: [Out] extends [Quad] ? PipelinePlugin[] : never;
|
|
63
72
|
name?: string;
|
|
64
73
|
distributionResolver?: DistributionResolver;
|
|
65
74
|
chaining?: {
|
|
@@ -101,7 +110,7 @@ export interface PipelineOptions {
|
|
|
101
110
|
*/
|
|
102
111
|
timeout?: () => TimeoutPolicy;
|
|
103
112
|
}
|
|
104
|
-
export declare class Pipeline {
|
|
113
|
+
export declare class Pipeline<Out = Quad> {
|
|
105
114
|
private readonly name;
|
|
106
115
|
private readonly datasetSelector;
|
|
107
116
|
private readonly stages;
|
|
@@ -114,7 +123,7 @@ export declare class Pipeline {
|
|
|
114
123
|
private readonly timeoutFactory;
|
|
115
124
|
private readonly provenanceStore?;
|
|
116
125
|
private readonly pipelineVersion?;
|
|
117
|
-
constructor(options: PipelineOptions);
|
|
126
|
+
constructor(options: PipelineOptions<Out>);
|
|
118
127
|
run(): Promise<void>;
|
|
119
128
|
private processDataset;
|
|
120
129
|
/** Persist the processing record for a dataset, when a store is configured. */
|
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;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
|
package/dist/pipeline.js
CHANGED
|
@@ -60,7 +60,7 @@ class FanOutWriter {
|
|
|
60
60
|
// Opening a writer's run can have side effects (a cross-pod lock, a fresh
|
|
61
61
|
// collection). If one writer opens but a sibling then fails, the pipeline
|
|
62
62
|
// never gets a RunWriter to abort, so roll the opened ones back here
|
|
63
|
-
// before rethrowing
|
|
63
|
+
// before rethrowing – otherwise their locks and collections leak.
|
|
64
64
|
const results = await Promise.allSettled(this.writers.map((writer) => writer.openRun(context)));
|
|
65
65
|
const opened = results.flatMap((result) => result.status === 'fulfilled' ? [result.value] : []);
|
|
66
66
|
const failure = results.find((result) => result.status === 'rejected');
|
|
@@ -86,8 +86,8 @@ class FanOutRunWriter {
|
|
|
86
86
|
constructor(runs) {
|
|
87
87
|
this.runs = runs;
|
|
88
88
|
}
|
|
89
|
-
async write(dataset,
|
|
90
|
-
const branches = tee(
|
|
89
|
+
async write(dataset, items) {
|
|
90
|
+
const branches = tee(items, this.runs.length);
|
|
91
91
|
await Promise.all(this.runs.map((run, index) => run.write(dataset, branches[index])));
|
|
92
92
|
}
|
|
93
93
|
async flush(dataset, outcome) {
|
|
@@ -241,13 +241,17 @@ export class Pipeline {
|
|
|
241
241
|
this.writer = Array.isArray(options.writers)
|
|
242
242
|
? new FanOutWriter(options.writers)
|
|
243
243
|
: options.writers;
|
|
244
|
-
|
|
244
|
+
// Plugins are quad-typed and only reachable when `Out` is `Quad` (the
|
|
245
|
+
// `plugins` field is `never` otherwise), but inside the class body `Out` is
|
|
246
|
+
// opaque, so the conditional-typed field is read as a plain plugin array.
|
|
247
|
+
const plugins = options.plugins;
|
|
248
|
+
const transforms = plugins
|
|
245
249
|
?.map((p) => p.beforeStageWrite)
|
|
246
250
|
.filter((t) => t !== undefined);
|
|
247
251
|
this.beforeStageWrite = transforms?.length
|
|
248
252
|
? (quads, context) => transforms.reduce((q, fn) => fn(q, context), quads)
|
|
249
253
|
: undefined;
|
|
250
|
-
const datasetTransforms =
|
|
254
|
+
const datasetTransforms = plugins
|
|
251
255
|
?.map((p) => p.beforeDatasetWrite)
|
|
252
256
|
.filter((t) => t !== undefined);
|
|
253
257
|
this.beforeDatasetWrite = datasetTransforms?.length
|
|
@@ -285,7 +289,10 @@ export class Pipeline {
|
|
|
285
289
|
};
|
|
286
290
|
const openedWriter = await this.writer.openRun(context);
|
|
287
291
|
// 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.
|
|
292
|
+
// output for a dataset as one stream before it is written. The transform is
|
|
293
|
+
// quad-typed and only present when `Out` is `Quad` (a projecting pipeline
|
|
294
|
+
// has no plugins), so narrowing the run writer to `RunWriter<Quad>` here is
|
|
295
|
+
// sound – tsc cannot see that invariant.
|
|
289
296
|
const runWriter = this.beforeDatasetWrite
|
|
290
297
|
? new DatasetTransformWriter(openedWriter, this.beforeDatasetWrite)
|
|
291
298
|
: openedWriter;
|
|
@@ -495,6 +502,8 @@ export class Pipeline {
|
|
|
495
502
|
this.reporter?.datasetValidated?.(dataset, report);
|
|
496
503
|
}
|
|
497
504
|
}
|
|
505
|
+
// Walks every stage and sub-stage to collect validators; it reads only
|
|
506
|
+
// `validator`/`stages`, never output, so it is agnostic to the item type.
|
|
498
507
|
*collectStages(stages) {
|
|
499
508
|
for (const stage of stages) {
|
|
500
509
|
yield stage;
|
|
@@ -509,6 +518,9 @@ export class Pipeline {
|
|
|
509
518
|
* per-`(dataset, stage)` IRIs rather than blank nodes.
|
|
510
519
|
*/
|
|
511
520
|
stageWriter(runWriter, stage) {
|
|
521
|
+
// `beforeStageWrite` is quad-typed and only present when `Out` is `Quad` (a
|
|
522
|
+
// projecting pipeline has no plugins), so the `TransformWriter` wrap is
|
|
523
|
+
// reached only in that case; tsc cannot see the invariant, hence the casts.
|
|
512
524
|
return this.beforeStageWrite
|
|
513
525
|
? new TransformWriter(runWriter, this.beforeStageWrite, stage)
|
|
514
526
|
: runWriter;
|
|
@@ -596,10 +608,12 @@ export class Pipeline {
|
|
|
596
608
|
async runChain(dataset, distribution, stage, runWriter, context, timeout) {
|
|
597
609
|
const { stageOutputResolver, outputDir } = this.chaining;
|
|
598
610
|
const outputFiles = [];
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
611
|
+
// Chaining is a quad-only concern: every stage in a chain writes N-Triples
|
|
612
|
+
// to a scratch FileWriter (a `Writer<Quad>`), and a chained parent cannot
|
|
613
|
+
// project (a stage with `stages` forbids `project`), so it emits quads. The
|
|
614
|
+
// requirement "sub-stages imply `Out` is `Quad`" is not expressible – `stages`
|
|
615
|
+
// and `chaining` are independent options – so it is a runtime contract, and
|
|
616
|
+
// the scratch machinery treats the parent stage as `Stage<Quad>`.
|
|
603
617
|
const runScratchStage = async (chainedStage, stageDistribution) => {
|
|
604
618
|
const scratchWriter = new FileWriter({
|
|
605
619
|
outputDir: `${outputDir}/${chainedStage.name}`,
|
|
@@ -618,7 +632,7 @@ export class Pipeline {
|
|
|
618
632
|
return scratchWriter.getOutputPath(dataset);
|
|
619
633
|
};
|
|
620
634
|
try {
|
|
621
|
-
// 1. Run parent stage → scratch FileWriter.
|
|
635
|
+
// 1. Run parent stage → scratch FileWriter (quad-only, per above).
|
|
622
636
|
const parentOutput = await runScratchStage(stage, distribution);
|
|
623
637
|
outputFiles.push(parentOutput);
|
|
624
638
|
// 2. Chain through children.
|
|
@@ -632,6 +646,8 @@ export class Pipeline {
|
|
|
632
646
|
}
|
|
633
647
|
// 3. Concatenate all output files → the open run, applying the plugins'
|
|
634
648
|
// beforeStageWrite transforms once for the chain under the parent stage.
|
|
649
|
+
// The concatenation is quads (per the quad-only contract above), written
|
|
650
|
+
// to the `Out`-typed run writer – sound because chaining implies `Quad`.
|
|
635
651
|
await this.stageWriter(runWriter, stage.name).write(dataset, this.readFiles(outputFiles));
|
|
636
652
|
}
|
|
637
653
|
finally {
|
package/dist/stage.d.ts
CHANGED
|
@@ -26,6 +26,23 @@ export interface ReaderContext {
|
|
|
26
26
|
distribution: Distribution;
|
|
27
27
|
stage: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The pipeline's one type-changing seam: turn a root-complete batch of quads
|
|
31
|
+
* into a stage's output items. The only extension point whose output is not
|
|
32
|
+
* `Quad` – a quad enrichment is a {@link QuadTransform} attached to a reader
|
|
33
|
+
* (extension point 1), which runs *before* this. See
|
|
34
|
+
* {@link https://github.com/ldelements/lde/blob/main/docs/decisions/0013-project-inside-the-batch-per-root-type.md | ADR 13}.
|
|
35
|
+
*/
|
|
36
|
+
export type BatchTransform<Out> = (quads: readonly Quad[], context: BatchContext) => Iterable<Out> | AsyncIterable<Out>;
|
|
37
|
+
/**
|
|
38
|
+
* Context handed to a {@link BatchTransform}: {@link ReaderContext} plus the
|
|
39
|
+
* batch's selected roots. `bindings` are the item-selector rows the readers were
|
|
40
|
+
* given as a `VALUES` block, so the batch is root-complete by construction – the
|
|
41
|
+
* projection frames exactly these roots rather than discovering them.
|
|
42
|
+
*/
|
|
43
|
+
export interface BatchContext extends ReaderContext {
|
|
44
|
+
bindings: readonly VariableBindings[];
|
|
45
|
+
}
|
|
29
46
|
/**
|
|
30
47
|
* An {@link Reader} with zero or more {@link QuadTransform}s attached as data.
|
|
31
48
|
*
|
|
@@ -45,10 +62,27 @@ export interface AttachedReader {
|
|
|
45
62
|
}
|
|
46
63
|
/** One or more readers, each optionally carrying attached transforms. */
|
|
47
64
|
export type StageReaders = Reader | AttachedReader | (Reader | AttachedReader)[];
|
|
48
|
-
export interface StageOptions {
|
|
65
|
+
export interface StageOptions<Out = Quad> {
|
|
49
66
|
name: string;
|
|
50
67
|
readers: StageReaders;
|
|
51
68
|
itemSelector?: ItemSelector;
|
|
69
|
+
/**
|
|
70
|
+
* Turn each root-complete batch into this stage's output items – the one seam
|
|
71
|
+
* whose output type differs from `Quad` ({@link BatchTransform}). Requires
|
|
72
|
+
* {@link StageOptions.itemSelector} (a batch only exists under a selector) and
|
|
73
|
+
* forbids {@link StageOptions.stages} (a chained stage serializes to
|
|
74
|
+
* N-Triples, which a projected item cannot). Omit for a plain quad stage.
|
|
75
|
+
*/
|
|
76
|
+
project?: BatchTransform<Out>;
|
|
77
|
+
/**
|
|
78
|
+
* Capacity of the bounded queue funnelling this stage's concurrent batches
|
|
79
|
+
* into the single write. The queue applies backpressure at this many items,
|
|
80
|
+
* so it bounds memory – set it where the cost per item is large (a projected
|
|
81
|
+
* document is far heavier than a quad). Only meaningful with an item selector.
|
|
82
|
+
*
|
|
83
|
+
* @default 128
|
|
84
|
+
*/
|
|
85
|
+
queueCapacity?: number;
|
|
52
86
|
/**
|
|
53
87
|
* Maximum number of bindings per reader call.
|
|
54
88
|
*
|
|
@@ -74,7 +108,7 @@ export interface StageOptions {
|
|
|
74
108
|
* Treat a supported stage that produces no quads as a hard failure (throws),
|
|
75
109
|
* rather than a legitimately empty result.
|
|
76
110
|
*
|
|
77
|
-
* Set this for stages whose query must yield output
|
|
111
|
+
* Set this for stages whose query must yield output – typically a scalar
|
|
78
112
|
* aggregate such as `SELECT (COUNT(*) AS ?n)`, which always returns exactly
|
|
79
113
|
* one row, so zero quads can only mean the endpoint truncated or aborted the
|
|
80
114
|
* response (e.g. a timeout surfaced as an empty `HTTP 200`). The resulting
|
|
@@ -109,7 +143,7 @@ export interface SelectOptions {
|
|
|
109
143
|
/** Per-call timeout policy. */
|
|
110
144
|
timeout?: TimeoutPolicy;
|
|
111
145
|
}
|
|
112
|
-
export declare class Stage {
|
|
146
|
+
export declare class Stage<Out = Quad> {
|
|
113
147
|
readonly name: string;
|
|
114
148
|
readonly stages: readonly Stage[];
|
|
115
149
|
/** Whether an empty result is treated as a hard failure. @see {@link StageOptions.expectsOutput} */
|
|
@@ -119,13 +153,15 @@ export declare class Stage {
|
|
|
119
153
|
private readonly batchSize;
|
|
120
154
|
private readonly maxConcurrency;
|
|
121
155
|
private readonly validation?;
|
|
122
|
-
|
|
156
|
+
private readonly project?;
|
|
157
|
+
private readonly queueCapacity?;
|
|
158
|
+
constructor(options: StageOptions<Out>);
|
|
123
159
|
/** The validator for this stage, if configured. */
|
|
124
160
|
get validator(): Validator | undefined;
|
|
125
|
-
run(dataset: Dataset, distribution: Distribution, writer: DatasetWriter
|
|
161
|
+
run(dataset: Dataset, distribution: Distribution, writer: DatasetWriter<Out>, options?: RunOptions): Promise<NotSupported | void>;
|
|
126
162
|
/**
|
|
127
163
|
* Throw when {@link StageOptions.expectsOutput} is set but the stage produced
|
|
128
|
-
* no quads
|
|
164
|
+
* no quads – a supported-but-empty result that signals a truncated or aborted
|
|
129
165
|
* endpoint response rather than a legitimately empty one.
|
|
130
166
|
*/
|
|
131
167
|
private assertProduced;
|
package/dist/stage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,CAAC,GAAG,IAAI,CAC/B,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAC1B,OAAO,EAAE,GAAG,KACT,aAAa,CAAC,IAAI,CAAC,CAAC;AAEzB;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,aAAa,CAAC,aAAa,CAAC,GAAG,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;CAC3E;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,cAAc,GACd,CAAC,MAAM,GAAG,cAAc,CAAC,EAAE,CAAC;AAQhC,MAAM,WAAW,YAAY;
|
|
1
|
+
{"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
|
package/dist/stage.js
CHANGED
|
@@ -11,7 +11,15 @@ export class Stage {
|
|
|
11
11
|
batchSize;
|
|
12
12
|
maxConcurrency;
|
|
13
13
|
validation;
|
|
14
|
+
project;
|
|
15
|
+
queueCapacity;
|
|
14
16
|
constructor(options) {
|
|
17
|
+
if (options.project && !options.itemSelector) {
|
|
18
|
+
throw new Error(`Stage '${options.name}': 'project' requires an 'itemSelector' – without one there is no batch to project, only the readers' whole output.`);
|
|
19
|
+
}
|
|
20
|
+
if (options.project && (options.stages?.length ?? 0) > 0) {
|
|
21
|
+
throw new Error(`Stage '${options.name}': 'project' cannot combine with chained 'stages' – a chained stage serializes to N-Triples, which a projected item cannot.`);
|
|
22
|
+
}
|
|
15
23
|
this.name = options.name;
|
|
16
24
|
this.stages = options.stages ?? [];
|
|
17
25
|
this.readers = normalizeReaders(options.readers);
|
|
@@ -20,6 +28,8 @@ export class Stage {
|
|
|
20
28
|
this.maxConcurrency = options.maxConcurrency ?? 10;
|
|
21
29
|
this.validation = options.validation;
|
|
22
30
|
this.expectsOutput = options.expectsOutput ?? false;
|
|
31
|
+
this.project = options.project;
|
|
32
|
+
this.queueCapacity = options.queueCapacity;
|
|
23
33
|
}
|
|
24
34
|
/** The validator for this stage, if configured. */
|
|
25
35
|
get validator() {
|
|
@@ -36,6 +46,11 @@ export class Stage {
|
|
|
36
46
|
if (streams instanceof NotSupported) {
|
|
37
47
|
return streams;
|
|
38
48
|
}
|
|
49
|
+
// The non-selector path has no batch, so `project` is forbidden here (the
|
|
50
|
+
// constructor rejects `project` without an `itemSelector`). It therefore
|
|
51
|
+
// only ever writes quads, and `Out` is `Quad` at runtime – tsc cannot see
|
|
52
|
+
// that invariant, so the writer is narrowed once.
|
|
53
|
+
const quadWriter = writer;
|
|
39
54
|
// Quads the readers produced (before any validation filtering); used to
|
|
40
55
|
// enforce `expectsOutput` below.
|
|
41
56
|
let produced = 0;
|
|
@@ -50,7 +65,7 @@ export class Stage {
|
|
|
50
65
|
const onInvalid = this.validation.onInvalid ?? 'write';
|
|
51
66
|
if (onInvalid === 'write') {
|
|
52
67
|
await Promise.all([
|
|
53
|
-
|
|
68
|
+
quadWriter.write(dataset, (async function* () {
|
|
54
69
|
yield* buffer;
|
|
55
70
|
})()),
|
|
56
71
|
this.validation.validator.validate(buffer, dataset),
|
|
@@ -59,7 +74,7 @@ export class Stage {
|
|
|
59
74
|
else {
|
|
60
75
|
const accepted = await this.validateBuffer(buffer, dataset);
|
|
61
76
|
if (accepted.length > 0) {
|
|
62
|
-
await
|
|
77
|
+
await quadWriter.write(dataset, (async function* () {
|
|
63
78
|
yield* accepted;
|
|
64
79
|
})());
|
|
65
80
|
}
|
|
@@ -68,18 +83,18 @@ export class Stage {
|
|
|
68
83
|
else if (this.expectsOutput) {
|
|
69
84
|
// Only thread the per-quad counter through when the count is actually
|
|
70
85
|
// needed; the default path stays a plain streaming write with no overhead.
|
|
71
|
-
await
|
|
86
|
+
await quadWriter.write(dataset, countQuads(mergeStreams(streams), (count) => {
|
|
72
87
|
produced = count;
|
|
73
88
|
}));
|
|
74
89
|
}
|
|
75
90
|
else {
|
|
76
|
-
await
|
|
91
|
+
await quadWriter.write(dataset, mergeStreams(streams));
|
|
77
92
|
}
|
|
78
93
|
this.assertProduced(produced);
|
|
79
94
|
}
|
|
80
95
|
/**
|
|
81
96
|
* Throw when {@link StageOptions.expectsOutput} is set but the stage produced
|
|
82
|
-
* no quads
|
|
97
|
+
* no quads – a supported-but-empty result that signals a truncated or aborted
|
|
83
98
|
* endpoint response rather than a legitimately empty one.
|
|
84
99
|
*/
|
|
85
100
|
assertProduced(produced) {
|
|
@@ -107,8 +122,11 @@ export class Stage {
|
|
|
107
122
|
yield next.value;
|
|
108
123
|
}
|
|
109
124
|
})();
|
|
110
|
-
const queue = new AsyncQueue();
|
|
125
|
+
const queue = new AsyncQueue(this.queueCapacity);
|
|
111
126
|
let itemsProcessed = 0;
|
|
127
|
+
// Output items written this run. Equals quads written for a plain stage;
|
|
128
|
+
// for a projecting stage it counts projected items (documents), one or more
|
|
129
|
+
// – or fewer – per root, which is what `expectsOutput`/`onProgress` report.
|
|
112
130
|
let quadsGenerated = 0;
|
|
113
131
|
let hasResults = false;
|
|
114
132
|
const onInvalid = this.validation?.onInvalid ?? 'write';
|
|
@@ -157,25 +175,34 @@ export class Stage {
|
|
|
157
175
|
return quads;
|
|
158
176
|
}));
|
|
159
177
|
const batchQuads = readerOutputs.flat();
|
|
178
|
+
// Validation runs on the quads, before projection – validators are
|
|
179
|
+
// quad-typed. 'skip'/'halt' must resolve it before writing; 'write'
|
|
180
|
+
// validates concurrently, below, without blocking.
|
|
181
|
+
let acceptedQuads = batchQuads;
|
|
160
182
|
if (this.validation &&
|
|
161
183
|
batchQuads.length > 0 &&
|
|
162
184
|
onInvalid !== 'write') {
|
|
163
|
-
|
|
164
|
-
const accepted = await this.validateBuffer(batchQuads, dataset);
|
|
165
|
-
for (const quad of accepted) {
|
|
166
|
-
await queue.push(quad);
|
|
167
|
-
quadsGenerated++;
|
|
168
|
-
}
|
|
185
|
+
acceptedQuads = await this.validateBuffer(batchQuads, dataset);
|
|
169
186
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
// The one type-changing seam: project the root-complete batch into
|
|
188
|
+
// output items, or pass the quads through for a plain stage (`Out`
|
|
189
|
+
// is `Quad` at runtime when there is no projection).
|
|
190
|
+
const items = this.project
|
|
191
|
+
? this.project(acceptedQuads, {
|
|
192
|
+
dataset,
|
|
193
|
+
distribution,
|
|
194
|
+
stage: this.name,
|
|
195
|
+
bindings,
|
|
196
|
+
})
|
|
197
|
+
: acceptedQuads;
|
|
198
|
+
for await (const item of items) {
|
|
199
|
+
await queue.push(item);
|
|
200
|
+
quadsGenerated++;
|
|
201
|
+
}
|
|
202
|
+
if (this.validation &&
|
|
203
|
+
batchQuads.length > 0 &&
|
|
204
|
+
onInvalid === 'write') {
|
|
205
|
+
pendingValidations.push(this.validation.validator.validate(batchQuads, dataset));
|
|
179
206
|
}
|
|
180
207
|
itemsProcessed += bindings.length;
|
|
181
208
|
options?.onProgress?.(itemsProcessed, quadsGenerated);
|
|
@@ -291,7 +318,7 @@ async function* mergeStreams(streams) {
|
|
|
291
318
|
* via `onCount` once the stream is exhausted. Lets a streaming write enforce
|
|
292
319
|
* {@link StageOptions.expectsOutput} without buffering.
|
|
293
320
|
*
|
|
294
|
-
* `onCount` fires only when the consumer drains the stream
|
|
321
|
+
* `onCount` fires only when the consumer drains the stream – which the pipeline
|
|
295
322
|
* writers do. A writer that stops early would leave the count short; callers
|
|
296
323
|
* relying on it for `expectsOutput` must consume the stream fully.
|
|
297
324
|
*/
|
|
@@ -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.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
6
6
|
"directory": "packages/pipeline"
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"@rdfjs/namespace": "^2.0.1",
|
|
34
34
|
"@rdfjs/types": "^2.0.1",
|
|
35
35
|
"@tpluscode/rdf-ns-builders": "^5.0.0",
|
|
36
|
-
"@traqula/generator-sparql-1-1": "^1.1.
|
|
36
|
+
"@traqula/generator-sparql-1-1": "^1.1.8",
|
|
37
37
|
"@traqula/parser-sparql-1-1": "^1.1.5",
|
|
38
38
|
"@traqula/rules-sparql-1-1": "^1.1.0",
|
|
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"
|