@lde/pipeline 0.35.3 → 0.35.5
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 +9 -445
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +11 -3
- package/dist/provenance/fileProvenanceStore.d.ts +7 -0
- package/dist/provenance/fileProvenanceStore.d.ts.map +1 -1
- package/dist/provenance/fileProvenanceStore.js +18 -1
- package/dist/provenance/store.d.ts +8 -0
- package/dist/provenance/store.d.ts.map +1 -1
- package/dist/sparql/graph.d.ts +13 -3
- package/dist/sparql/graph.d.ts.map +1 -1
- package/dist/sparql/graph.js +14 -2
- package/dist/sparql/selector.d.ts +8 -4
- package/dist/sparql/selector.d.ts.map +1 -1
- package/dist/sparql/selector.js +15 -4
- package/dist/stage.d.ts +34 -1
- package/dist/stage.d.ts.map +1 -1
- package/dist/stage.js +20 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,453 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lde/pipeline
|
|
2
2
|
|
|
3
|
-
A framework for transforming large RDF datasets, primarily using [SPARQL](https://www.w3.org/TR/sparql11-query/) queries with TypeScript for the parts that are hard to express in SPARQL alone.
|
|
3
|
+
A framework for transforming large RDF datasets, primarily using [SPARQL](https://www.w3.org/TR/sparql11-query/) queries with TypeScript for the parts that are hard to express in SPARQL alone. Transformations are plain SPARQL query files – portable, transparent, testable and version-controlled – and memory stays bounded no matter how large the dataset.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Composable.** Executors are an interface: wrap a SPARQL reader with custom TypeScript to handle edge cases like date parsing or string normalisation (see [Reader](#reader)).
|
|
7
|
-
- **Extensible.** A plugin system lets packages like [@lde/pipeline-void](../pipeline-void) (or your own plugins) hook into the pipeline lifecycle.
|
|
5
|
+
## Installation
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
A **Pipeline** consists of:
|
|
12
|
-
|
|
13
|
-
- a **Dataset Selector** that selects which datasets to process
|
|
14
|
-
- a **Distribution Resolver** that resolves each dataset to a usable SPARQL endpoint
|
|
15
|
-
- one or more **Stages**, each consisting of:
|
|
16
|
-
- an optional **Item Selector** that selects resources (as variable bindings) for fan-out
|
|
17
|
-
- one or more **Executors** that generate triples
|
|
18
|
-
|
|
19
|
-
### Dataset Selector
|
|
20
|
-
|
|
21
|
-
Selects datasets, either manually or by querying a DCAT Dataset Registry:
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
// From a registry
|
|
25
|
-
const selector = new RegistrySelector({
|
|
26
|
-
registry: new Client(new URL('https://example.com/sparql')),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Manual
|
|
30
|
-
const selector = new ManualDatasetSelection([dataset]);
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Distribution Resolver
|
|
34
|
-
|
|
35
|
-
Resolves each dataset to a usable SPARQL endpoint. `SparqlDistributionResolver` probes a dataset’s own endpoint; wrap it in `ImportResolver` to add the ability to import a data dump into a local SPARQL server.
|
|
36
|
-
|
|
37
|
-
`ImportResolver`’s `strategy` controls how the source is chosen, ordered by how eagerly a dump is imported:
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
const resolver = new ImportResolver(new SparqlDistributionResolver(), {
|
|
41
|
-
importer,
|
|
42
|
-
server,
|
|
43
|
-
strategy: 'sparqlWithImportFallback',
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
- `'sparql'` (default) – use the dataset’s own SPARQL endpoint when one is available; import a data dump only when no endpoint responds.
|
|
48
|
-
- `'sparqlWithImportFallback'` – like `'sparql'`, but also fall back to the data dump when the endpoint passes probing yet a stage fails against it at runtime. The pipeline discards the endpoint-sourced partial output and re-runs all stages against the import. Use this when endpoints are present but unreliable for heavy aggregate queries.
|
|
49
|
-
- `'import'` – always import the data dump, even when a working endpoint is advertised.
|
|
50
|
-
|
|
51
|
-
### Stage
|
|
52
|
-
|
|
53
|
-
A stage groups an item selector, one or more readers, and configuration:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
new Stage({
|
|
57
|
-
name: 'per-class',
|
|
58
|
-
itemSelector: new SparqlItemSelector({
|
|
59
|
-
query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
|
|
60
|
-
}),
|
|
61
|
-
readers: reader,
|
|
62
|
-
batchSize: 100,
|
|
63
|
-
maxConcurrency: 5,
|
|
64
|
-
});
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
#### Batch size
|
|
68
|
-
|
|
69
|
-
`batchSize` (default: 10) controls how many variable bindings are passed to each reader call as a `VALUES` clause. It also sets the page size for the item selector's SPARQL requests, so that each paginated request fills exactly one reader batch.
|
|
70
|
-
|
|
71
|
-
Some SPARQL endpoints enforce different result limits for SELECT and CONSTRUCT queries. Since the selector uses SELECT and the reader uses CONSTRUCT, a `LIMIT` clause in the selector query overrides `batchSize` as the page size. Use this when the endpoint caps SELECT results below your desired batch size:
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
// Endpoint caps SELECT results at 500, but each CONSTRUCT can handle 1000 bindings.
|
|
75
|
-
new Stage({
|
|
76
|
-
name: 'per-class',
|
|
77
|
-
itemSelector: new SparqlItemSelector({
|
|
78
|
-
query: 'SELECT DISTINCT ?class WHERE { ?s a ?class } LIMIT 500',
|
|
79
|
-
}),
|
|
80
|
-
readers: reader,
|
|
81
|
-
batchSize: 1000, // Two SELECT pages fill one CONSTRUCT batch.
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
#### Concurrency
|
|
86
|
-
|
|
87
|
-
`maxConcurrency` (default: 10) limits the total number of concurrent SPARQL queries. Within each batch, all readers run in parallel; the number of concurrent batches is automatically reduced to `⌊maxConcurrency / executorCount⌋` so the total query pressure stays within the limit. For example, with `maxConcurrency: 10` and two readers per stage, up to 5 batches run concurrently (10 SPARQL queries total).
|
|
88
|
-
|
|
89
|
-
#### Expecting output
|
|
90
|
-
|
|
91
|
-
`expectsOutput` (default: `false`) marks a stage whose query must yield at least one quad. A supported stage that produces none is then treated as a hard failure rather than a legitimately empty result.
|
|
92
|
-
|
|
93
|
-
Set it for scalar aggregates such as `SELECT (COUNT(*) AS ?n)`, which always return exactly one row – so zero output can only mean the endpoint truncated or aborted the response (e.g. a timeout surfaced as an empty `HTTP 200`). The failure flows through like any other hard stage failure, triggering the [reactive dump fallback](#distribution-resolver) when `strategy: 'sparqlWithImportFallback'` is configured. Leave it `false` for stages that may legitimately be empty, such as class or property partitions of a dataset that lacks that structure.
|
|
94
|
-
|
|
95
|
-
### Item Selector
|
|
96
|
-
|
|
97
|
-
Selects resources from the distribution and fans out reader calls per batch of results. Implements the `ItemSelector` interface:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
interface ItemSelector {
|
|
101
|
-
select(
|
|
102
|
-
distribution: Distribution,
|
|
103
|
-
batchSize?: number,
|
|
104
|
-
): AsyncIterable<VariableBindings>;
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
The distribution is received at run time, so selectors don't need the endpoint URL at construction time. The `batchSize` parameter is set by the stage. Use `SparqlItemSelector` for SPARQL-based selection with automatic pagination:
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
new SparqlItemSelector({
|
|
112
|
-
query: 'SELECT DISTINCT ?class WHERE { ?s a ?class }',
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
A row is yielded only when **every** projected variable binds a `NamedNode` – binding values double as stable item identities downstream, which a blank-node label or literal cannot provide. Other rows are dropped client-side but still count toward pagination, so they are fetched first; to skip them at the endpoint, filter in the query itself (e.g. `FILTER(isIRI(?s))`, as `selectByClass` in `@lde/search-pipeline` does with `FILTER(!isBlank(?root))`).
|
|
117
|
-
|
|
118
|
-
#### Capping total results with `maxResults`
|
|
119
|
-
|
|
120
|
-
By default, `SparqlItemSelector` paginates through **all** matching rows: any `LIMIT` clause in the query is interpreted as the page size, then it walks pages with `OFFSET` until the source is exhausted. To cap the total bindings yielded across all pages – for sampling, testing, prototyping, or just safety – set `maxResults`:
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
new SparqlItemSelector({
|
|
124
|
-
query: 'SELECT DISTINCT ?s WHERE { ?s a <http://example.com/Class> }',
|
|
125
|
-
maxResults: 50,
|
|
126
|
-
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
When `maxResults` is set:
|
|
130
|
-
|
|
131
|
-
- Pagination stops as soon as `maxResults` bindings have been yielded – no wasted page request after the cap is hit.
|
|
132
|
-
- As long as no row has been dropped, the last (partial) page's `LIMIT` is shrunk to the remaining cap so the endpoint doesn't over-fetch on the remainder (e.g. with `maxResults: 85` and `pageSize: 10`, the 9th page request is `LIMIT 5`, not `LIMIT 10`). Once rows have been dropped, pages stay at full size – shrinking to the yielded remainder would crawl a dropped-row region one row per request.
|
|
133
|
-
- The first page uses the configured page size as-is; `maxResults` and page size stay orthogonal. If `maxResults < pageSize`, the first page may return a few rows that aren't yielded.
|
|
134
|
-
- `maxResults: 0` is a valid no-op; the selector yields nothing without issuing any SPARQL request.
|
|
135
|
-
- `maxResults` is independent of any `LIMIT` clause in the query, which still controls page size when the cap is larger than one page.
|
|
136
|
-
|
|
137
|
-
For dynamic queries that depend on the distribution, implement `ItemSelector` directly:
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
const itemSelector: ItemSelector = {
|
|
141
|
-
select: (distribution, batchSize) => {
|
|
142
|
-
const query = buildQuery(distribution);
|
|
143
|
-
return new SparqlItemSelector({ query }).select(distribution, batchSize);
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Reader
|
|
149
|
-
|
|
150
|
-
Generates RDF triples. The built-in `SparqlConstructReader` runs a SPARQL CONSTRUCT query with template substitution and variable bindings:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
const reader = new SparqlConstructReader({
|
|
154
|
-
query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
|
|
155
|
-
});
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
When querying endpoints that return line-oriented formats like N-Triples (e.g. QLever), enable `lineBuffer` to work around an [N3.js chunk-splitting bug](https://github.com/rdfjs/N3.js/issues/578) that causes intermittent parse errors on large responses:
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
const reader = new SparqlConstructReader({
|
|
162
|
-
query: 'CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }',
|
|
163
|
-
lineBuffer: true,
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
SPARQL CONSTRUCT queries can produce duplicate triples – for example, constant triples (like `?dataset a edm:ProvidedCHO`) are emitted for every solution row. Enable `deduplicate` to remove duplicates inline on the stream using a string-based identity set (inspired by [Comunica's `distinctConstruct`](https://comunica.dev/docs/query/advanced/context/#14--distinct-construct)):
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
const reader = new SparqlConstructReader({
|
|
171
|
-
query: 'CONSTRUCT { ?s a edm:ProvidedCHO . ?s ?p ?o } WHERE { ?s ?p ?o }',
|
|
172
|
-
deduplicate: true,
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
The dedup set is scoped to each `read()` call, so memory stays bounded to the number of unique quads per batch. A standalone `deduplicateQuads()` function is also exported for use outside the reader.
|
|
177
|
-
|
|
178
|
-
### Extending a stage with a quad transform
|
|
179
|
-
|
|
180
|
-
Some logic is hard to express in pure SPARQL – cleaning up messy date notations, converting locale-specific dates to ISO 8601, or sampling a reader’s output and firing follow-up queries. Rather than subclass `Reader`, attach a `QuadTransform` to it as data: a plain function `(quads, context) => quads` that post-processes one reader’s output before the stage merges it with its siblings. This is extension point 1 of [ADR 2](../../docs/decisions/0002-unify-pipeline-extension-on-quad-transforms.md).
|
|
181
|
-
|
|
182
|
-
A transform receives an `ReaderContext` – the `dataset`, the `distribution` (so it can fire its own SPARQL queries), and the `stage` name. It runs once per reader call, so **write it to accept being called more than once**: a global stage calls it once over the reader’s complete output, but a per-class stage with batching enabled calls it once per batch (one class at `batchSize: 1`). Accumulate within an invocation, not across invocations – or keep the transform per-quad, where the number of calls makes no difference.
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
import { DataFactory } from 'n3';
|
|
186
|
-
import {
|
|
187
|
-
Stage,
|
|
188
|
-
SparqlConstructReader,
|
|
189
|
-
type QuadTransform,
|
|
190
|
-
type ReaderContext,
|
|
191
|
-
} from '@lde/pipeline';
|
|
192
|
-
|
|
193
|
-
const cleanDates: QuadTransform<ReaderContext> = async function* (quads) {
|
|
194
|
-
for await (const quad of quads) {
|
|
195
|
-
if (quad.object.termType === 'Literal' && isMessyDate(quad.object)) {
|
|
196
|
-
yield DataFactory.quad(
|
|
197
|
-
quad.subject,
|
|
198
|
-
quad.predicate,
|
|
199
|
-
DataFactory.literal(
|
|
200
|
-
parseDutchDate(quad.object.value),
|
|
201
|
-
DataFactory.namedNode('http://www.w3.org/2001/XMLSchema#date'),
|
|
202
|
-
),
|
|
203
|
-
);
|
|
204
|
-
} else {
|
|
205
|
-
yield quad;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
new Stage({
|
|
211
|
-
name: 'dates',
|
|
212
|
-
readers: {
|
|
213
|
-
reader: await SparqlConstructReader.fromFile('dates.rq'),
|
|
214
|
-
transform: cleanDates,
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
`transform` accepts a single transform or an array applied in order, so a stage can compose several. This keeps SPARQL doing the heavy lifting while TypeScript handles the edge cases. See [@lde/pipeline-void](../pipeline-void)'s `withVocabularies` for a real-world example of this pattern.
|
|
220
|
-
|
|
221
|
-
#### Adaptive timeouts
|
|
222
|
-
|
|
223
|
-
By default, every SPARQL request uses the same 5-minute budget. When a pipeline runs against many third-party endpoints, that fixed budget can cost ~80 minutes on a single dataset whose endpoint times out repeatedly on heavy queries – light stages on the same endpoint then sit behind the heavy ones that will never succeed.
|
|
224
|
-
|
|
225
|
-
A `TimeoutPolicy` decides the budget for each SPARQL request and observes the outcome. Two are built in:
|
|
226
|
-
|
|
227
|
-
- **`ConstantTimeoutPolicy(timeoutMs)`** – returns the same budget for every request. The implicit default when `PipelineOptions.timeout` is omitted (`constantTimeoutPolicy(300_000)`).
|
|
228
|
-
- **`AdaptiveTimeoutPolicy({ defaultMs, tightenedMs, tightenAfterTimeouts })`** – per-endpoint state machine. Each endpoint is either _healthy_ (use `defaultMs`) or _tightened_ (use `tightenedMs`). After `tightenAfterTimeouts` consecutive `timeout` outcomes the endpoint flips to _tightened_; a single `ok` flips it back to _healthy_.
|
|
229
|
-
|
|
230
|
-
`PipelineOptions.timeout` accepts a `() => TimeoutPolicy` factory. The pipeline invokes it once per dataset, so policy state resets between datasets and one bad dataset can’t poison the next:
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { adaptiveTimeoutPolicy } from '@lde/pipeline';
|
|
234
|
-
|
|
235
|
-
new Pipeline({
|
|
236
|
-
// …
|
|
237
|
-
timeout: adaptiveTimeoutPolicy({
|
|
238
|
-
defaultMs: 300_000, // 5 min while the endpoint is healthy
|
|
239
|
-
tightenedMs: 10_000, // 10 s once the endpoint is tightened
|
|
240
|
-
tightenAfterTimeouts: 2, // flip to tightened after 2 consecutive timeouts
|
|
241
|
-
}),
|
|
242
|
-
});
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Outcomes are classified as:
|
|
246
|
-
|
|
247
|
-
| outcome | source |
|
|
248
|
-
| --------- | ------------------------------------------------------------------------ |
|
|
249
|
-
| `ok` | the request resolved |
|
|
250
|
-
| `timeout` | client-side `AbortSignal.timeout()` fired, or upstream returned HTTP 504 |
|
|
251
|
-
| `error` | anything else (other HTTP errors, parse errors, …) – neutral |
|
|
252
|
-
|
|
253
|
-
Transitions are forwarded to the `ProgressReporter` via `timeoutTightened` / `timeoutRelaxed`; `ConsoleReporter` prints them as `↘ Tightened` / `↗ Relaxed` lines so operators can tell a fast-failed stage from an unexpected speedup.
|
|
254
|
-
|
|
255
|
-
Implement `TimeoutPolicy` directly for custom strategies (closing over shared state in the factory if you want it to span datasets).
|
|
256
|
-
|
|
257
|
-
Timeouts live at the pipeline level – neither `SparqlConstructReader` nor `SparqlItemSelector` accept their own `timeout` option. Per-endpoint state belongs in the adaptive policy, and per-stage budgets aren’t supported. Reusable stage facades (`@lde/pipeline-void`, `@lde/pipeline-shacl-sampler`) follow the same convention.
|
|
258
|
-
|
|
259
|
-
### Validation
|
|
260
|
-
|
|
261
|
-
Stages can optionally validate their output quads against a `Validator`. Validation operates on the **combined output of all readers per batch**, not on individual quads or per-reader output. A batch produces a complete result set – a self-contained cluster of linked resources – that can be meaningfully matched against SHACL shapes. Even with a single reader, each batch is a complete unit; with multiple readers, shapes that reference triples from different readers are validated correctly.
|
|
262
|
-
|
|
263
|
-
Validating individual quads would be meaningless, since a single quad carries no structural context for shape matching. Validating the full pipeline output would also be problematic: because the pipeline streams results in batches, it doesn’t know where resource cluster boundaries fall. Batching the output could split a valid cluster across two batches, causing partial resources to fail validation even though the complete cluster is valid.
|
|
264
|
-
|
|
265
|
-
Quads are buffered, validated, and then written or discarded based on the `onInvalid` policy. When no validator is configured, quads stream directly with zero overhead.
|
|
266
|
-
|
|
267
|
-
```typescript
|
|
268
|
-
import { ShaclValidator } from '@lde/pipeline-shacl-validator';
|
|
269
|
-
import { FileWriter } from '@lde/pipeline';
|
|
270
|
-
|
|
271
|
-
new Stage({
|
|
272
|
-
name: 'transform',
|
|
273
|
-
readers: await SparqlConstructReader.fromFile('transform.rq'),
|
|
274
|
-
validation: {
|
|
275
|
-
validator: new ShaclValidator({
|
|
276
|
-
shapesFile: './shapes.ttl',
|
|
277
|
-
reportWriters: [
|
|
278
|
-
new FileWriter({ outputDir: './validation', format: 'turtle' }),
|
|
279
|
-
],
|
|
280
|
-
}),
|
|
281
|
-
onInvalid: 'write', // 'write' (default) | 'skip' | 'halt'
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
| `onInvalid` | Behaviour |
|
|
287
|
-
| ----------- | -------------------------------------------------- |
|
|
288
|
-
| `'write'` | Write quads even if validation fails **(default)** |
|
|
289
|
-
| `'skip'` | Discard the batch silently |
|
|
290
|
-
| `'halt'` | Throw an error, stopping the pipeline |
|
|
291
|
-
|
|
292
|
-
`Validator` is an interface, so you can implement your own validation strategy. See [@lde/pipeline-shacl-validator](../pipeline-shacl-validator) for the SHACL implementation.
|
|
293
|
-
|
|
294
|
-
#### Per-dataset reporting
|
|
295
|
-
|
|
296
|
-
After all stages for a dataset have run, the pipeline calls `validator.report(dataset)` once for each distinct validator attached to any stage and emits a `datasetValidated(dataset, report)` event on the reporter. The call happens **regardless of whether any stage actually invoked `validate()`** – for SHACL that means a dataset whose stages produced no input typically reports `quadsValidated: 0` and `conforms: true` (the SHACL vacuous-truth default). Consumers that want to distinguish ‘not tested’ from ‘tested and passed’ can read `quadsValidated`.
|
|
297
|
-
|
|
298
|
-
### Writer
|
|
299
|
-
|
|
300
|
-
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'`.
|
|
301
|
-
|
|
302
|
-
- `SparqlUpdateWriter` – writes to a SPARQL endpoint via UPDATE queries; writes are visible as they land, so `commit`/`abort` are no-ops
|
|
303
|
-
- `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
|
|
304
|
-
|
|
305
|
-
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.
|
|
306
|
-
|
|
307
|
-
A destination without run-level state implements the same contract with no-op `commit`/`abort` – `SparqlUpdateWriter` shows the pattern:
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
async openRun(): Promise<RunWriter> {
|
|
311
|
-
return {
|
|
312
|
-
write: async (dataset, quads) => {
|
|
313
|
-
// send quads somewhere
|
|
314
|
-
},
|
|
315
|
-
commit: () => Promise.resolve(),
|
|
316
|
-
abort: () => Promise.resolve(),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Reporter
|
|
322
|
-
|
|
323
|
-
A `ProgressReporter` observes the run, receiving lifecycle events such as `pipelineStart`, `stageComplete`, `datasetValidated` and `pipelineComplete`. Every method is optional, so a reporter implements only the events it cares about.
|
|
324
|
-
|
|
325
|
-
Pass a single reporter, or an array to have several observe the same run – for example a console reporter alongside one that collects validation verdicts:
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
new Pipeline({
|
|
329
|
-
// …
|
|
330
|
-
reporter: [new ConsoleReporter(), verdictCollector],
|
|
331
|
-
});
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
Each reporter receives every event, in array order; a reporter that does not implement a given event is skipped for it.
|
|
335
|
-
|
|
336
|
-
### Provenance store
|
|
337
|
-
|
|
338
|
-
A `ProvenanceStore` gives the pipeline a small per-dataset memory, so a future run can skip datasets that are genuinely unchanged. It is purely a storage seam: the framework owns the skip decision (see [`sourceFingerprint`](#source-change-fingerprint) and `shouldReprocess`), the store owns only how each record is persisted.
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
interface ProvenanceStore {
|
|
342
|
-
get(datasetUri: URL): Promise<ProcessingRecord | null>;
|
|
343
|
-
set(datasetUri: URL, record: ProcessingRecord): Promise<void>;
|
|
344
|
-
}
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
A `ProcessingRecord` holds the two opaque change fields – `sourceFingerprint` (derived automatically from source metadata) and `pipelineVersion` (consumer-declared) – plus `generatedAt` and a `status` of `'success'` or `'failed'`. The two change fields are compared only for equality, never parsed or ordered.
|
|
348
|
-
|
|
349
|
-
#### `FileLoadedSparqlProvenanceStore`
|
|
350
|
-
|
|
351
|
-
The reference implementation targets a triplestore that is served read-only and rebuilt by bulk-loading files (e.g. [QLever](https://github.com/ad-freiburg/qlever)). It reads through SPARQL queries against the live endpoint, and writes records as files for the next bulk-load – because the endpoint accepts no SPARQL UPDATE.
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
import { FileLoadedSparqlProvenanceStore } from '@lde/pipeline';
|
|
355
|
-
|
|
356
|
-
const store = new FileLoadedSparqlProvenanceStore({
|
|
357
|
-
queryEndpoint: new URL('http://localhost:7001/sparql'),
|
|
358
|
-
pipelineIri: new URL('https://example.org/pipelines/dkg'),
|
|
359
|
-
outputDir: './provenance',
|
|
360
|
-
});
|
|
7
|
+
```sh
|
|
8
|
+
npm install @lde/pipeline
|
|
361
9
|
```
|
|
362
10
|
|
|
363
|
-
- `get` runs a named-graph-scoped SPARQL `SELECT` against `queryEndpoint`, reading the records a previous run loaded.
|
|
364
|
-
- `set` writes one flat [PROV-O](https://www.w3.org/TR/prov-o/) N-Quads file per dataset into `outputDir`, in the pipeline-scoped named graph, to be bulk-loaded after the run.
|
|
365
|
-
|
|
366
|
-
Each record is stored as flat PROV-O on the dataset entity – `prov:generatedAtTime` plus `sourceFingerprint`, `pipelineVersion` and `status` under the `https://w3id.org/lde/provenance#` namespace. Scoping every record by `pipelineIri` (used as the named graph) lets multiple pipelines share one triplestore without colliding.
|
|
367
|
-
|
|
368
|
-
#### `FileProvenanceStore`
|
|
369
|
-
|
|
370
|
-
For pipelines that run without a triplestore, `FileProvenanceStore` persists all records to a single JSON file, keyed by dataset URI:
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
import { FileProvenanceStore } from '@lde/pipeline';
|
|
374
|
-
|
|
375
|
-
const store = new FileProvenanceStore({ path: './state/provenance.json' });
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Writes are atomic (temp file + rename), so a run killed mid-write cannot corrupt the next run’s skip decisions. Safe for a single writer: concurrent pipeline processes writing the same file lose each other’s updates – use the SPARQL-backed store (or file locking) instead. The file must sit on a durable volume to survive across runs, the same requirement the SPARQL-backed store already has.
|
|
379
|
-
|
|
380
|
-
#### Enabling skipping
|
|
381
|
-
|
|
382
|
-
Skipping is opt-in. Pass a `provenanceStore` and a `pipelineVersion` to the `Pipeline`:
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
new Pipeline({
|
|
386
|
-
// …
|
|
387
|
-
provenanceStore: store,
|
|
388
|
-
pipelineVersion: 'v3', // rotate only on releases that change output
|
|
389
|
-
});
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
For each dataset the pipeline probes its distributions, derives the source-change fingerprint, reads the stored record, and **skips before importing** when both change fields match:
|
|
393
|
-
|
|
394
|
-
```
|
|
395
|
-
skip iff recorded.sourceFingerprint === current.sourceFingerprint
|
|
396
|
-
AND recorded.pipelineVersion === current.pipelineVersion
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
Otherwise it imports (if needed), runs the stages, and writes an updated record. `pipelineVersion` is consumer-owned and opaque: rotate it only on releases that change output, and every dataset reprocesses on the next run. It is **required** when a `provenanceStore` is configured (a skip-enabled pipeline with no version would silently freeze); when no store is configured, every dataset is reprocessed – today’s behaviour. A dataset that failed but whose source is unchanged is recorded as `'failed'` and skipped on later runs until its source changes or the version rotates, so a deterministically failing import is not retried every run.
|
|
400
|
-
|
|
401
|
-
### Source-change fingerprint
|
|
402
|
-
|
|
403
|
-
`sourceFingerprint(distribution, probeResult)` derives a cheap, opaque change signal for a distribution from metadata the probe already collected – no body download. For a data dump it combines the most recent of the register’s `dct:modified` and the artifact’s HTTP `Last-Modified` with the byte size (the probe’s `Content-Length`, falling back to the declared `dcat:byteSize`). It returns `null` for a live SPARQL endpoint, or when no date and no size can be established – a `null` fingerprint never compares equal, so such a distribution is always reprocessed.
|
|
404
|
-
|
|
405
|
-
### Plugins
|
|
406
|
-
|
|
407
|
-
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`:
|
|
408
|
-
|
|
409
|
-
- **`beforeStageWrite`** runs over each stage’s output as it is written – scoped to one stage’s quads for one dataset.
|
|
410
|
-
- **`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.
|
|
411
|
-
|
|
412
|
-
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.
|
|
413
|
-
|
|
414
|
-
#### `provenancePlugin()`
|
|
415
|
-
|
|
416
|
-
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).
|
|
417
|
-
|
|
418
|
-
#### `namespaceNormalizationPlugin(options)`
|
|
419
|
-
|
|
420
|
-
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.
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
import { namespaceNormalizationPlugin } from '@lde/pipeline';
|
|
424
|
-
|
|
425
|
-
new Pipeline({
|
|
426
|
-
// ...
|
|
427
|
-
plugins: [
|
|
428
|
-
namespaceNormalizationPlugin({
|
|
429
|
-
from: 'http://example.org/',
|
|
430
|
-
to: 'https://example.org/',
|
|
431
|
-
}),
|
|
432
|
-
],
|
|
433
|
-
});
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
#### `schemaOrgNormalizationPlugin(options?)`
|
|
437
|
-
|
|
438
|
-
Convenience wrapper around `namespaceNormalizationPlugin` for the common case: rewrites `http://schema.org/` to `https://schema.org/` (pass `{ reverse: true }` for the opposite direction).
|
|
439
|
-
|
|
440
|
-
```typescript
|
|
441
|
-
import { schemaOrgNormalizationPlugin, provenancePlugin } from '@lde/pipeline';
|
|
442
|
-
|
|
443
|
-
new Pipeline({
|
|
444
|
-
// ...
|
|
445
|
-
plugins: [schemaOrgNormalizationPlugin(), provenancePlugin()],
|
|
446
|
-
});
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
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.
|
|
450
|
-
|
|
451
11
|
## Usage
|
|
452
12
|
|
|
453
13
|
```typescript
|
|
@@ -481,3 +41,7 @@ const pipeline = new Pipeline({
|
|
|
481
41
|
|
|
482
42
|
await pipeline.run();
|
|
483
43
|
```
|
|
44
|
+
|
|
45
|
+
## Documentation
|
|
46
|
+
|
|
47
|
+
See the [full documentation](https://ldelements.org/reference/pipeline).
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
|
package/dist/pipeline.js
CHANGED
|
@@ -273,6 +273,11 @@ export class Pipeline {
|
|
|
273
273
|
async run() {
|
|
274
274
|
const start = Date.now();
|
|
275
275
|
this.reporter?.pipelineStart?.(this.name);
|
|
276
|
+
// Fail fast on a store that cannot persist records (e.g. a provenance
|
|
277
|
+
// file on a mount the runtime user cannot write to): every dataset would
|
|
278
|
+
// process fine, but the records would be lost and skip-unchanged would
|
|
279
|
+
// silently never engage on any later run.
|
|
280
|
+
await this.provenanceStore?.check?.();
|
|
276
281
|
const selectStart = Date.now();
|
|
277
282
|
const datasets = await this.datasetSelector.select();
|
|
278
283
|
this.reporter?.datasetsSelected?.(datasets.total, Date.now() - selectStart);
|
|
@@ -486,9 +491,12 @@ export class Pipeline {
|
|
|
486
491
|
status,
|
|
487
492
|
});
|
|
488
493
|
}
|
|
489
|
-
catch {
|
|
490
|
-
// A failed write must not abort the run
|
|
491
|
-
// next run, its record not yet updated
|
|
494
|
+
catch (error) {
|
|
495
|
+
// A failed write must not abort the run – the dataset simply
|
|
496
|
+
// reprocesses next run, its record not yet updated – but it must be
|
|
497
|
+
// surfaced: a store that fails every write silently disables
|
|
498
|
+
// skip-unchanged.
|
|
499
|
+
this.reporter?.stageFailed?.('provenance', error instanceof Error ? error : new Error(String(error)));
|
|
492
500
|
}
|
|
493
501
|
}
|
|
494
502
|
async reportValidators(dataset) {
|
|
@@ -26,6 +26,13 @@ export interface FileProvenanceStoreOptions {
|
|
|
26
26
|
export declare class FileProvenanceStore implements ProvenanceStore {
|
|
27
27
|
private readonly path;
|
|
28
28
|
constructor(options: FileProvenanceStoreOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Verify the file can be written by actually creating (and removing) a
|
|
31
|
+
* probe file next to it. Catches the mount that a non-root runtime user
|
|
32
|
+
* cannot write to (e.g. a root-owned Docker volume, see issue #661) at the
|
|
33
|
+
* start of the run, before writes would silently fail per dataset.
|
|
34
|
+
*/
|
|
35
|
+
check(): Promise<void>;
|
|
29
36
|
get(datasetUri: URL): Promise<ProcessingRecord | null>;
|
|
30
37
|
set(datasetUri: URL, record: ProcessingRecord): Promise<void>;
|
|
31
38
|
private readAll;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileProvenanceStore.d.ts","sourceRoot":"","sources":["../../src/provenance/fileProvenanceStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,OAAO,EAAE,0BAA0B;
|
|
1
|
+
{"version":3,"file":"fileProvenanceStore.d.ts","sourceRoot":"","sources":["../../src/provenance/fileProvenanceStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,OAAO,EAAE,0BAA0B;IAI/C;;;;;OAKG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IActB,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAKtD,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;YAMrD,OAAO;YAcP,QAAQ;CAQvB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
/**
|
|
4
4
|
* A {@link ProvenanceStore} that persists all records to a single JSON file,
|
|
@@ -20,6 +20,23 @@ export class FileProvenanceStore {
|
|
|
20
20
|
constructor(options) {
|
|
21
21
|
this.path = options.path;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Verify the file can be written by actually creating (and removing) a
|
|
25
|
+
* probe file next to it. Catches the mount that a non-root runtime user
|
|
26
|
+
* cannot write to (e.g. a root-owned Docker volume, see issue #661) at the
|
|
27
|
+
* start of the run, before writes would silently fail per dataset.
|
|
28
|
+
*/
|
|
29
|
+
async check() {
|
|
30
|
+
try {
|
|
31
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
32
|
+
const probePath = `${this.path}.${process.pid}.check`;
|
|
33
|
+
await writeFile(probePath, '');
|
|
34
|
+
await unlink(probePath);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(`Provenance file ${this.path} is not writable; make its directory writable by the user the pipeline runs as: ${String(error)}`, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
async get(datasetUri) {
|
|
24
41
|
const records = await this.readAll();
|
|
25
42
|
return records[datasetUri.toString()] ?? null;
|
|
@@ -8,6 +8,14 @@ import type { ProcessingRecord } from './record.js';
|
|
|
8
8
|
* triplestore, files, or anything else.
|
|
9
9
|
*/
|
|
10
10
|
export interface ProvenanceStore {
|
|
11
|
+
/**
|
|
12
|
+
* Verify the store can persist records, throwing when it cannot. The
|
|
13
|
+
* pipeline calls this once at the start of a run, so a store that would
|
|
14
|
+
* fail every write – e.g. a provenance file on a mount the runtime user
|
|
15
|
+
* cannot write to – fails the run immediately instead of silently
|
|
16
|
+
* disabling skip-unchanged.
|
|
17
|
+
*/
|
|
18
|
+
check?(): Promise<void>;
|
|
11
19
|
/**
|
|
12
20
|
* The record from the dataset’s last processing, or `null` if it has never
|
|
13
21
|
* been processed (or the store was wiped). A `null` result drives a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/provenance/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACvD,oEAAoE;IACpE,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D"}
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/provenance/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;OAMG;IACH,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB;;;;OAIG;IACH,GAAG,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACvD,oEAAoE;IACpE,GAAG,CAAC,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D"}
|
package/dist/sparql/graph.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { type QueryConstruct } from '@traqula/rules-sparql-1-1';
|
|
1
|
+
import { type QueryConstruct, type QuerySelect } from '@traqula/rules-sparql-1-1';
|
|
2
2
|
/**
|
|
3
|
-
* Set the default graph (FROM clause) on a parsed CONSTRUCT query.
|
|
3
|
+
* Set the default graph (FROM clause) on a parsed CONSTRUCT or SELECT query.
|
|
4
4
|
*
|
|
5
5
|
* Mutates the query in place, replacing any existing FROM clause.
|
|
6
|
+
*
|
|
7
|
+
* Both query forms scope the same way, and a stage reading a
|
|
8
|
+
* {@link Distribution} with a `namedGraph` must scope **both**: were only the
|
|
9
|
+
* reader to honour it, an item selector would pick its roots from the whole
|
|
10
|
+
* endpoint and the reader would then find nothing for most of them.
|
|
11
|
+
*
|
|
12
|
+
* The graph IRI is {@link assertSafeIri}-checked before it becomes an `<…>`
|
|
13
|
+
* reference: a `namedGraph` is a plain string, typically carried in from
|
|
14
|
+
* third-party registry data, and one containing an angle bracket or whitespace
|
|
15
|
+
* would break out of the IRI reference and rewrite the query around it.
|
|
6
16
|
*/
|
|
7
|
-
export declare function withDefaultGraph(query: QueryConstruct, graphIri: string): void;
|
|
17
|
+
export declare function withDefaultGraph(query: QueryConstruct | QuerySelect, graphIri: string): void;
|
|
8
18
|
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/sparql/graph.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/sparql/graph.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,WAAW,EACjB,MAAM,2BAA2B,CAAC;AAInC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,GAAG,WAAW,EACnC,QAAQ,EAAE,MAAM,GACf,IAAI,CAMN"}
|
package/dist/sparql/graph.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertSafeIri } from '@lde/dataset';
|
|
2
|
+
import { AstFactory, } from '@traqula/rules-sparql-1-1';
|
|
2
3
|
const F = new AstFactory();
|
|
3
4
|
/**
|
|
4
|
-
* Set the default graph (FROM clause) on a parsed CONSTRUCT query.
|
|
5
|
+
* Set the default graph (FROM clause) on a parsed CONSTRUCT or SELECT query.
|
|
5
6
|
*
|
|
6
7
|
* Mutates the query in place, replacing any existing FROM clause.
|
|
8
|
+
*
|
|
9
|
+
* Both query forms scope the same way, and a stage reading a
|
|
10
|
+
* {@link Distribution} with a `namedGraph` must scope **both**: were only the
|
|
11
|
+
* reader to honour it, an item selector would pick its roots from the whole
|
|
12
|
+
* endpoint and the reader would then find nothing for most of them.
|
|
13
|
+
*
|
|
14
|
+
* The graph IRI is {@link assertSafeIri}-checked before it becomes an `<…>`
|
|
15
|
+
* reference: a `namedGraph` is a plain string, typically carried in from
|
|
16
|
+
* third-party registry data, and one containing an angle bracket or whitespace
|
|
17
|
+
* would break out of the IRI reference and rewrite the query around it.
|
|
7
18
|
*/
|
|
8
19
|
export function withDefaultGraph(query, graphIri) {
|
|
20
|
+
assertSafeIri(graphIri);
|
|
9
21
|
query.datasets = F.datasetClauses([{ clauseType: 'default', value: F.termNamed(F.gen(), graphIri) }], F.gen());
|
|
10
22
|
}
|
|
@@ -7,7 +7,7 @@ export interface SparqlItemSelectorOptions {
|
|
|
7
7
|
* SELECT query projecting at least one named variable.
|
|
8
8
|
*
|
|
9
9
|
* A `LIMIT` clause in the query overrides the stage's `batchSize` as the
|
|
10
|
-
* page size
|
|
10
|
+
* page size – use this when the SPARQL endpoint enforces a result limit.
|
|
11
11
|
* It does **not** cap the total number of bindings the selector yields;
|
|
12
12
|
* pagination continues with `OFFSET` until the source is exhausted. Use
|
|
13
13
|
* {@link maxResults} to cap the total.
|
|
@@ -15,7 +15,7 @@ export interface SparqlItemSelectorOptions {
|
|
|
15
15
|
query: string;
|
|
16
16
|
/**
|
|
17
17
|
* Maximum number of bindings the selector yields across all pages.
|
|
18
|
-
* Use this for sampling
|
|
18
|
+
* Use this for sampling – “give me at most N items, don’t walk the full
|
|
19
19
|
* source”. Independent of {@link query}’s `LIMIT`, which controls page
|
|
20
20
|
* size. Pagination stops as soon as `maxResults` bindings have been
|
|
21
21
|
* yielded. Must not be negative.
|
|
@@ -36,8 +36,12 @@ export interface SparqlItemSelectorOptions {
|
|
|
36
36
|
* pagination early. Dropped rows are still fetched; to skip them at the
|
|
37
37
|
* endpoint, filter in the query itself (e.g. `FILTER(isIRI(?s))`).
|
|
38
38
|
*
|
|
39
|
-
* The endpoint URL comes from the {@link Distribution} passed to {@link select}
|
|
40
|
-
*
|
|
39
|
+
* The endpoint URL comes from the {@link Distribution} passed to {@link select},
|
|
40
|
+
* as does the graph to scope to: a distribution declaring a `namedGraph` selects
|
|
41
|
+
* within that graph (a `FROM` clause), the same scoping `SparqlConstructReader`
|
|
42
|
+
* applies to the extraction. The two must scope alike, or the roots come from a
|
|
43
|
+
* wider source than the quads read for them.
|
|
44
|
+
* Pagination is an internal detail – consumers iterate binding rows directly.
|
|
41
45
|
*
|
|
42
46
|
* The page size (results per SPARQL request) is determined by, in order:
|
|
43
47
|
* 1. A `LIMIT` clause in the selector query (for endpoints with hard result limits)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../../src/sparql/selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAQ9D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../../src/sparql/selector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAQ9D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAoBpD,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;OAQG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAoB;IAClD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAwB;gBAEzC,OAAO,EAAE,yBAAyB;IA0BvC,MAAM,CACX,YAAY,EAAE,YAAY,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,aAAa,GACtB,qBAAqB,CAAC,gBAAgB,CAAC;IA8E1C;;;;OAIG;YACW,uBAAuB;CA8BtC"}
|
package/dist/sparql/selector.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SparqlEndpointFetcher } from 'fetch-sparql-endpoint';
|
|
|
2
2
|
import { Parser } from '@traqula/parser-sparql-1-1';
|
|
3
3
|
import { Generator } from '@traqula/generator-sparql-1-1';
|
|
4
4
|
import { AstFactory, } from '@traqula/rules-sparql-1-1';
|
|
5
|
+
import { withDefaultGraph } from './graph.js';
|
|
5
6
|
import { ConstantTimeoutPolicy, } from './timeoutPolicy.js';
|
|
6
7
|
const transientStatusPattern = /HTTP status (\d+)/;
|
|
7
8
|
/**
|
|
@@ -25,8 +26,12 @@ const F = new AstFactory();
|
|
|
25
26
|
* pagination early. Dropped rows are still fetched; to skip them at the
|
|
26
27
|
* endpoint, filter in the query itself (e.g. `FILTER(isIRI(?s))`).
|
|
27
28
|
*
|
|
28
|
-
* The endpoint URL comes from the {@link Distribution} passed to {@link select}
|
|
29
|
-
*
|
|
29
|
+
* The endpoint URL comes from the {@link Distribution} passed to {@link select},
|
|
30
|
+
* as does the graph to scope to: a distribution declaring a `namedGraph` selects
|
|
31
|
+
* within that graph (a `FROM` clause), the same scoping `SparqlConstructReader`
|
|
32
|
+
* applies to the extraction. The two must scope alike, or the roots come from a
|
|
33
|
+
* wider source than the quads read for them.
|
|
34
|
+
* Pagination is an internal detail – consumers iterate binding rows directly.
|
|
30
35
|
*
|
|
31
36
|
* The page size (results per SPARQL request) is determined by, in order:
|
|
32
37
|
* 1. A `LIMIT` clause in the selector query (for endpoints with hard result limits)
|
|
@@ -76,6 +81,12 @@ export class SparqlItemSelector {
|
|
|
76
81
|
throw new Error(`Page size must be positive; got ${basePageSize} (from the query’s LIMIT or the stage’s batchSize)`);
|
|
77
82
|
}
|
|
78
83
|
const endpoint = distribution.accessUrl;
|
|
84
|
+
// Per call, so concurrent selections over different datasets cannot see
|
|
85
|
+
// each other's graph scope or page window.
|
|
86
|
+
const query = structuredClone(this.parsed);
|
|
87
|
+
if (distribution.namedGraph) {
|
|
88
|
+
withDefaultGraph(query, distribution.namedGraph);
|
|
89
|
+
}
|
|
79
90
|
const policy = options?.timeout ?? defaultTimeoutPolicy;
|
|
80
91
|
let offset = 0;
|
|
81
92
|
let totalFetched = 0;
|
|
@@ -92,8 +103,8 @@ export class SparqlItemSelector {
|
|
|
92
103
|
const effectivePageSize = offset === 0 || totalFetched > totalYielded
|
|
93
104
|
? basePageSize
|
|
94
105
|
: Math.min(basePageSize, remaining);
|
|
95
|
-
|
|
96
|
-
const paginatedQuery = generator.generate(
|
|
106
|
+
query.solutionModifiers.limitOffset = F.solutionModifierLimitOffset(effectivePageSize, offset, F.gen());
|
|
107
|
+
const paginatedQuery = generator.generate(query);
|
|
97
108
|
const stream = await this.fetchBindingsWithPolicy(endpoint, paginatedQuery, policy);
|
|
98
109
|
// Fetched rows drive pagination, yielded rows only `maxResults` – see
|
|
99
110
|
// the class JSDoc for why dropped rows must keep their page slot.
|
package/dist/stage.d.ts
CHANGED
|
@@ -102,6 +102,35 @@ export interface StageOptions<Out = Quad> {
|
|
|
102
102
|
* @default 10
|
|
103
103
|
*/
|
|
104
104
|
maxConcurrency?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Where this stage reads, when that is **not** the dataset’s own
|
|
107
|
+
* distribution: given the dataset and the distribution the pipeline resolved
|
|
108
|
+
* for it, return the {@link Distribution} this stage should use instead.
|
|
109
|
+
*
|
|
110
|
+
* One hook covers the whole stage – the item selector and every reader are
|
|
111
|
+
* handed the same substitute – because a stage that reads elsewhere must
|
|
112
|
+
* *select* there too, or its roots and its quads come from different sources.
|
|
113
|
+
*
|
|
114
|
+
* The dataset is passed so the substitute can be derived per dataset. The
|
|
115
|
+
* motivating case is a stage sourced from the dataset registry rather than
|
|
116
|
+
* from the dataset’s data: the registry endpoint is fixed, but the graph to
|
|
117
|
+
* scope to is the dataset in hand.
|
|
118
|
+
*
|
|
119
|
+
* ```ts
|
|
120
|
+
* sourceFor: (dataset) => {
|
|
121
|
+
* const source = new Distribution(registryEndpoint, SPARQL_MEDIA_TYPE);
|
|
122
|
+
* source.namedGraph = dataset.iri.toString();
|
|
123
|
+
* return source;
|
|
124
|
+
* }
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* Omit it – the default – to read the dataset’s own distribution.
|
|
128
|
+
*
|
|
129
|
+
* A **chained child** stage may not declare one: a child is handed its
|
|
130
|
+
* parent’s output as its distribution, so substituting a source would discard
|
|
131
|
+
* the chain it exists to continue. The parent’s constructor rejects that.
|
|
132
|
+
*/
|
|
133
|
+
sourceFor?: (dataset: Dataset, distribution: Distribution) => Distribution;
|
|
105
134
|
/** Child stages that chain off this stage's output. */
|
|
106
135
|
stages?: Stage[];
|
|
107
136
|
/**
|
|
@@ -148,6 +177,9 @@ export declare class Stage<Out = Quad> {
|
|
|
148
177
|
readonly stages: readonly Stage[];
|
|
149
178
|
/** Whether an empty result is treated as a hard failure. @see {@link StageOptions.expectsOutput} */
|
|
150
179
|
readonly expectsOutput: boolean;
|
|
180
|
+
/** Whether this stage reads a source of its own rather than the dataset’s
|
|
181
|
+
* resolved distribution. @see {@link StageOptions.sourceFor} */
|
|
182
|
+
readonly sourcesOwnData: boolean;
|
|
151
183
|
private readonly readers;
|
|
152
184
|
private readonly itemSelector?;
|
|
153
185
|
private readonly batchSize;
|
|
@@ -155,10 +187,11 @@ export declare class Stage<Out = Quad> {
|
|
|
155
187
|
private readonly validation?;
|
|
156
188
|
private readonly project?;
|
|
157
189
|
private readonly queueCapacity?;
|
|
190
|
+
private readonly sourceFor?;
|
|
158
191
|
constructor(options: StageOptions<Out>);
|
|
159
192
|
/** The validator for this stage, if configured. */
|
|
160
193
|
get validator(): Validator | undefined;
|
|
161
|
-
run(dataset: Dataset,
|
|
194
|
+
run(dataset: Dataset, resolved: Distribution, writer: DatasetWriter<Out>, options?: RunOptions): Promise<NotSupported | void>;
|
|
162
195
|
/**
|
|
163
196
|
* Throw when {@link StageOptions.expectsOutput} is set but the stage produced
|
|
164
197
|
* no quads – a supported-but-empty result that signals a truncated or aborted
|
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;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,CAAC,GAAG,IAAI,CAChC,KAAK,EAAE,SAAS,IAAI,EAAE,EACtB,OAAO,EAAE,YAAY,KAClB,QAAQ,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACvC;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,
|
|
1
|
+
{"version":3,"file":"stage.d.ts","sourceRoot":"","sources":["../src/stage.ts"],"names":[],"mappings":"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"}
|
package/dist/stage.js
CHANGED
|
@@ -6,6 +6,9 @@ export class Stage {
|
|
|
6
6
|
stages;
|
|
7
7
|
/** Whether an empty result is treated as a hard failure. @see {@link StageOptions.expectsOutput} */
|
|
8
8
|
expectsOutput;
|
|
9
|
+
/** Whether this stage reads a source of its own rather than the dataset’s
|
|
10
|
+
* resolved distribution. @see {@link StageOptions.sourceFor} */
|
|
11
|
+
sourcesOwnData;
|
|
9
12
|
readers;
|
|
10
13
|
itemSelector;
|
|
11
14
|
batchSize;
|
|
@@ -13,6 +16,7 @@ export class Stage {
|
|
|
13
16
|
validation;
|
|
14
17
|
project;
|
|
15
18
|
queueCapacity;
|
|
19
|
+
sourceFor;
|
|
16
20
|
constructor(options) {
|
|
17
21
|
if (options.project && !options.itemSelector) {
|
|
18
22
|
throw new Error(`Stage '${options.name}': 'project' requires an 'itemSelector' – without one there is no batch to project, only the readers' whole output.`);
|
|
@@ -20,6 +24,14 @@ export class Stage {
|
|
|
20
24
|
if (options.project && (options.stages?.length ?? 0) > 0) {
|
|
21
25
|
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
26
|
}
|
|
27
|
+
// A child is handed its parent's output as its distribution; a `sourceFor`
|
|
28
|
+
// would substitute that away, silently reading elsewhere instead of
|
|
29
|
+
// continuing the chain. Caught here rather than at run time, where it looks
|
|
30
|
+
// like a chain that mysteriously produced nothing.
|
|
31
|
+
const sourcedChild = options.stages?.find((child) => child.sourcesOwnData);
|
|
32
|
+
if (sourcedChild) {
|
|
33
|
+
throw new Error(`Stage '${options.name}': chained stage '${sourcedChild.name}' declares 'sourceFor' – a chained stage reads its parent's output, so it cannot source its own data.`);
|
|
34
|
+
}
|
|
23
35
|
this.name = options.name;
|
|
24
36
|
this.stages = options.stages ?? [];
|
|
25
37
|
this.readers = normalizeReaders(options.readers);
|
|
@@ -30,13 +42,20 @@ export class Stage {
|
|
|
30
42
|
this.expectsOutput = options.expectsOutput ?? false;
|
|
31
43
|
this.project = options.project;
|
|
32
44
|
this.queueCapacity = options.queueCapacity;
|
|
45
|
+
this.sourceFor = options.sourceFor;
|
|
46
|
+
this.sourcesOwnData = options.sourceFor !== undefined;
|
|
33
47
|
}
|
|
34
48
|
/** The validator for this stage, if configured. */
|
|
35
49
|
get validator() {
|
|
36
50
|
return this.validation?.validator;
|
|
37
51
|
}
|
|
38
|
-
async run(dataset,
|
|
52
|
+
async run(dataset, resolved, writer, options) {
|
|
39
53
|
const timeout = options?.timeout;
|
|
54
|
+
// Substituted once, before anything reads: the selector and the readers
|
|
55
|
+
// must agree on where this stage's data comes from.
|
|
56
|
+
const distribution = this.sourceFor
|
|
57
|
+
? this.sourceFor(dataset, resolved)
|
|
58
|
+
: resolved;
|
|
40
59
|
if (this.itemSelector) {
|
|
41
60
|
return this.runWithSelector(this.itemSelector.select(distribution, this.batchSize, {
|
|
42
61
|
timeout,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/pipeline",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.5",
|
|
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.2",
|
|
43
43
|
"p-retry": "^8.0.0",
|
|
44
44
|
"rdf-string": "^2.0.1",
|
|
45
45
|
"tslib": "^2.3.0"
|