@metreeca/keep-sparql 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +106 -0
- package/dist/_/_decode.d.ts +20 -0
- package/dist/_/_decode.d.ts.map +1 -0
- package/dist/_/_decode.js +55 -0
- package/dist/_/_decode.js.map +1 -0
- package/dist/_/_encode.d.ts +112 -0
- package/dist/_/_encode.d.ts.map +1 -0
- package/dist/_/_encode.js +262 -0
- package/dist/_/_encode.js.map +1 -0
- package/dist/_/_flake.d.ts +16 -0
- package/dist/_/_flake.d.ts.map +1 -0
- package/dist/_/_flake.js +65 -0
- package/dist/_/_flake.js.map +1 -0
- package/dist/_/_union.d.ts +28 -0
- package/dist/_/_union.d.ts.map +1 -0
- package/dist/_/_union.js +51 -0
- package/dist/_/_union.js.map +1 -0
- package/dist/detect/decode.d.ts +25 -0
- package/dist/detect/decode.d.ts.map +1 -0
- package/dist/detect/decode.js +34 -0
- package/dist/detect/decode.js.map +1 -0
- package/dist/detect/encode.d.ts +25 -0
- package/dist/detect/encode.d.ts.map +1 -0
- package/dist/detect/encode.js +30 -0
- package/dist/detect/encode.js.map +1 -0
- package/dist/detect/index.d.ts +17 -0
- package/dist/detect/index.d.ts.map +1 -0
- package/dist/detect/index.js +55 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/lookup/decode.d.ts +33 -0
- package/dist/lookup/decode.d.ts.map +1 -0
- package/dist/lookup/decode.js +193 -0
- package/dist/lookup/decode.js.map +1 -0
- package/dist/lookup/encode.d.ts +46 -0
- package/dist/lookup/encode.d.ts.map +1 -0
- package/dist/lookup/encode.js +111 -0
- package/dist/lookup/encode.js.map +1 -0
- package/dist/lookup/index.d.ts +21 -0
- package/dist/lookup/index.d.ts.map +1 -0
- package/dist/lookup/index.js +72 -0
- package/dist/lookup/index.js.map +1 -0
- package/dist/modify/decode.d.ts +22 -0
- package/dist/modify/decode.d.ts.map +1 -0
- package/dist/modify/decode.js +26 -0
- package/dist/modify/decode.js.map +1 -0
- package/dist/modify/encode.d.ts +21 -0
- package/dist/modify/encode.d.ts.map +1 -0
- package/dist/modify/encode.js +184 -0
- package/dist/modify/encode.js.map +1 -0
- package/dist/modify/index.d.ts +22 -0
- package/dist/modify/index.d.ts.map +1 -0
- package/dist/modify/index.js +44 -0
- package/dist/modify/index.js.map +1 -0
- package/dist/select/decode.d.ts +16 -0
- package/dist/select/decode.d.ts.map +1 -0
- package/dist/select/decode.js +229 -0
- package/dist/select/decode.js.map +1 -0
- package/dist/select/encode.d.ts +16 -0
- package/dist/select/encode.d.ts.map +1 -0
- package/dist/select/encode.js +434 -0
- package/dist/select/encode.js.map +1 -0
- package/dist/select/index.d.ts +16 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/index.js +38 -0
- package/dist/select/index.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025-2026 Metreeca srl
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Detect-pass driver.
|
|
18
|
+
*
|
|
19
|
+
* Owns one `select` round per drain iteration covering every queued {@link Detect}. The cycle is:
|
|
20
|
+
*
|
|
21
|
+
* **plan** → **encode** → `client.select` → **decode**
|
|
22
|
+
*
|
|
23
|
+
* - the batch's candidate entries are collected;
|
|
24
|
+
* - {@link encode} folds them into one batched existence `select`;
|
|
25
|
+
* - `client.select` runs the unified query;
|
|
26
|
+
* - {@link decode} folds the entries that came back into the present-entry set and resolves each
|
|
27
|
+
* request to whether its entry is present.
|
|
28
|
+
*
|
|
29
|
+
* Variable allocation across the query is shared between {@link encode} and {@link decode} through
|
|
30
|
+
* a single {@link createScope | Scope}.
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
import { createScope } from "@metreeca/core/scope";
|
|
35
|
+
import { variable } from "@metreeca/wire-sparql";
|
|
36
|
+
import { decode } from "./decode.js";
|
|
37
|
+
import { encode } from "./encode.js";
|
|
38
|
+
/**
|
|
39
|
+
* Per-batch body for the detect handler.
|
|
40
|
+
*
|
|
41
|
+
* Two phases run in sequence: **fetch** by folding the batch's candidate entries into one batched
|
|
42
|
+
* existence `select` through {@link encode} and running it; **deliver** by {@link decode | decoding}
|
|
43
|
+
* the solution to the present entries, folding them into a set, and resolving each request to whether
|
|
44
|
+
* its entry is present.
|
|
45
|
+
*
|
|
46
|
+
* @param batch The queued detect requests to resolve
|
|
47
|
+
* @param client The repository the existence query runs against
|
|
48
|
+
*
|
|
49
|
+
* @returns A promise settling once every request in the batch has been resolved
|
|
50
|
+
*/
|
|
51
|
+
export async function detect(batch, client) {
|
|
52
|
+
const scope = createScope(variable);
|
|
53
|
+
decode(scope, batch, await client.select(encode(scope, batch)));
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/detect/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAyB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAC3B,KAAkC,EAClC,MAAwB;IAGxB,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEpC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC,MAAM,CACvC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CACpB,CAAC,CAAC;AAEJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPARQL 1.1 repository connector.
|
|
3
|
+
*
|
|
4
|
+
* Composes a pluggable {@link Repository} into a fully-featured {@link Store} via
|
|
5
|
+
* {@link createSPARQLStore}, translating Store-level read and write operations into SPARQL queries and updates.
|
|
6
|
+
* Application developers select a ready-made connector (Oxigraph, HTTP, …) or implement {@link Repository}
|
|
7
|
+
* directly to support a new SPARQL backend.
|
|
8
|
+
*
|
|
9
|
+
* The SPARQL layer shared by every connector is defined in `@metreeca/wire-sparql`:
|
|
10
|
+
* {@link @metreeca/wire-sparql!SPARQL | SPARQL} text and {@link @metreeca/wire-sparql!Tuple | Tuple} solution rows.
|
|
11
|
+
* The RDF data model those rows carry is defined in `@metreeca/trio`: {@link @metreeca/trio!Triple | Triple}
|
|
12
|
+
* statements and {@link @metreeca/trio!Term | Term} values ({@link @metreeca/trio!Named | Named},
|
|
13
|
+
* {@link @metreeca/trio!Blank | Blank}, {@link @metreeca/trio!Tagged | Tagged}, {@link @metreeca/trio!Typed | Typed}),
|
|
14
|
+
* together with the {@link @metreeca/trio!named | named}/{@link @metreeca/trio!tagged |
|
|
15
|
+
* tagged}/{@link @metreeca/trio!typed | typed} constructors used to mint
|
|
16
|
+
* {@link @metreeca/trio!Term | Term} values from backend results.
|
|
17
|
+
*
|
|
18
|
+
* **Wiring a Connector**
|
|
19
|
+
*
|
|
20
|
+
* Pick a ready-made connector and feed it to {@link createSPARQLStore} to obtain a store:
|
|
21
|
+
*
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { createSPARQLStore } from "@metreeca/keep-sparql";
|
|
24
|
+
* import { createOxiRepository } from "@metreeca/wire-sparql-oxigraph";
|
|
25
|
+
*
|
|
26
|
+
* const store = createSPARQLStore(createOxiRepository());
|
|
27
|
+
*
|
|
28
|
+
* await store.create({
|
|
29
|
+
* entry: "http://example.com/products/1",
|
|
30
|
+
* shape: ProductShape,
|
|
31
|
+
* state: { name: "Widget", price: 9.99 }
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* **Implementing a Connector**
|
|
36
|
+
*
|
|
37
|
+
* To support a new SPARQL backend, implement a {@link @metreeca/wire-sparql!Repository | Repository} and use
|
|
38
|
+
* {@link @metreeca/trio!named | named}, {@link @metreeca/trio!tagged | tagged}, and
|
|
39
|
+
* {@link @metreeca/trio!typed | typed} to lift backend node values into the shared
|
|
40
|
+
* {@link @metreeca/trio!Term | Term} representation. The reference implementation in
|
|
41
|
+
* `@metreeca/wire-sparql-oxigraph` exemplifies the pattern:
|
|
42
|
+
*
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import { named, tagged, type Term, typed } from "@metreeca/trio";
|
|
45
|
+
* import { type Repository } from "@metreeca/wire-sparql";
|
|
46
|
+
*
|
|
47
|
+
* function decode(node: BackendNode): Term {
|
|
48
|
+
* return node.kind === "iri" ? named(node.iri)
|
|
49
|
+
* : node.kind === "language" ? tagged(node.text, node.language)
|
|
50
|
+
* : typed(node.text, node.datatype);
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* export function createMyRepository(): Repository {
|
|
54
|
+
* // ... satisfy ask/select/construct/update/execute/close against the backend
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @see {@link https://www.w3.org/TR/sparql11-query/ SPARQL 1.1 Query Language}
|
|
59
|
+
* @see {@link https://www.w3.org/TR/sparql11-update/ SPARQL 1.1 Update}
|
|
60
|
+
* @see {@link https://www.w3.org/TR/rdf11-concepts/ RDF 1.1 Concepts and Abstract Syntax}
|
|
61
|
+
*
|
|
62
|
+
* @group Connectors
|
|
63
|
+
*
|
|
64
|
+
* @module index
|
|
65
|
+
*/
|
|
66
|
+
import type { Store } from "@metreeca/keep";
|
|
67
|
+
import { type Repository } from "@metreeca/wire-sparql";
|
|
68
|
+
/**
|
|
69
|
+
* Creates a SPARQL store backed by a {@link Repository}.
|
|
70
|
+
*
|
|
71
|
+
* Each StoreClient call (standalone or inside {@link Store.execute execute}) is dispatched through a fresh
|
|
72
|
+
* {@link Repository} scope obtained from {@link Repository.execute repository.execute}, so concurrent calls share no
|
|
73
|
+
* transaction. Inside {@link Store.execute execute}, the user task receives a StoreClient wired to the scoped
|
|
74
|
+
* Repository; mutations are batched and flushed to the repository as a single update when the task completes, then
|
|
75
|
+
* committed by the repository transaction. Queries within the task do not observe the task's own pending mutations
|
|
76
|
+
* (no read-your-own-writes); split dependent reads across separate `execute` calls.
|
|
77
|
+
*
|
|
78
|
+
* > [!IMPORTANT]
|
|
79
|
+
* > **Transaction Isolation** — Determined by the supplied {@link Repository}.
|
|
80
|
+
*
|
|
81
|
+
* > [!IMPORTANT]
|
|
82
|
+
* > **Mutation Events** — Limited to mutations issued through this store; mutations from other clients on the
|
|
83
|
+
* > underlying repository are not observed.
|
|
84
|
+
*
|
|
85
|
+
* > [!IMPORTANT]
|
|
86
|
+
* > Updates issued within `execute` are always buffered and flushed as a single update on commit, regardless of
|
|
87
|
+
* > native transaction support. Native transactions remain relevant nonetheless, isolating the task's reads
|
|
88
|
+
* > for consistency.
|
|
89
|
+
*
|
|
90
|
+
* @param repository - The repository for SPARQL query, update, and transaction execution
|
|
91
|
+
*
|
|
92
|
+
* @returns An immutable {@link Store}
|
|
93
|
+
*/
|
|
94
|
+
export declare function createSPARQLStore(repository: Repository): Store;
|
|
95
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,gBAAgB,CAAC;AAKzD,OAAO,EAGN,KAAK,UAAU,EAEf,MAAM,uBAAuB,CAAC;AAY/B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,UAAU,GAAG,KAAK,CAgC/D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025-2026 Metreeca srl
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { createBatchingStore } from "@metreeca/keep/batching";
|
|
17
|
+
import { createManagingStore } from "@metreeca/keep/managing";
|
|
18
|
+
import { createValidatingStore } from "@metreeca/keep/validating";
|
|
19
|
+
import { log } from "@metreeca/tape";
|
|
20
|
+
import { createBufferingRepository, createLoggingRepository } from "@metreeca/wire-sparql";
|
|
21
|
+
import { detect } from "./detect/index.js";
|
|
22
|
+
import { lookup } from "./lookup/index.js";
|
|
23
|
+
import { modify } from "./modify/index.js";
|
|
24
|
+
import { select } from "./select/index.js";
|
|
25
|
+
const logger = log(import.meta.url);
|
|
26
|
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
27
|
+
/**
|
|
28
|
+
* Creates a SPARQL store backed by a {@link Repository}.
|
|
29
|
+
*
|
|
30
|
+
* Each StoreClient call (standalone or inside {@link Store.execute execute}) is dispatched through a fresh
|
|
31
|
+
* {@link Repository} scope obtained from {@link Repository.execute repository.execute}, so concurrent calls share no
|
|
32
|
+
* transaction. Inside {@link Store.execute execute}, the user task receives a StoreClient wired to the scoped
|
|
33
|
+
* Repository; mutations are batched and flushed to the repository as a single update when the task completes, then
|
|
34
|
+
* committed by the repository transaction. Queries within the task do not observe the task's own pending mutations
|
|
35
|
+
* (no read-your-own-writes); split dependent reads across separate `execute` calls.
|
|
36
|
+
*
|
|
37
|
+
* > [!IMPORTANT]
|
|
38
|
+
* > **Transaction Isolation** — Determined by the supplied {@link Repository}.
|
|
39
|
+
*
|
|
40
|
+
* > [!IMPORTANT]
|
|
41
|
+
* > **Mutation Events** — Limited to mutations issued through this store; mutations from other clients on the
|
|
42
|
+
* > underlying repository are not observed.
|
|
43
|
+
*
|
|
44
|
+
* > [!IMPORTANT]
|
|
45
|
+
* > Updates issued within `execute` are always buffered and flushed as a single update on commit, regardless of
|
|
46
|
+
* > native transaction support. Native transactions remain relevant nonetheless, isolating the task's reads
|
|
47
|
+
* > for consistency.
|
|
48
|
+
*
|
|
49
|
+
* @param repository - The repository for SPARQL query, update, and transaction execution
|
|
50
|
+
*
|
|
51
|
+
* @returns An immutable {@link Store}
|
|
52
|
+
*/
|
|
53
|
+
export function createSPARQLStore(repository) {
|
|
54
|
+
const buffering = createBufferingRepository(createLoggingRepository(repository, message => logger.debug `${message}`));
|
|
55
|
+
return createManagingStore(store(buffering), {
|
|
56
|
+
execute: task => buffering.execute(repository => task(store(repository))),
|
|
57
|
+
close: () => buffering.close()
|
|
58
|
+
});
|
|
59
|
+
function store(client) {
|
|
60
|
+
return createValidatingStore(createBatchingStore({
|
|
61
|
+
detect: (batch) => detect(batch, client),
|
|
62
|
+
lookup: (batch, broker) => lookup(batch, client, broker),
|
|
63
|
+
select: (batch, broker) => select(batch, client, broker),
|
|
64
|
+
modify: (batch) => modify(batch, client)
|
|
65
|
+
}), {
|
|
66
|
+
trusted: true
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAqEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AACrC,OAAO,EACN,yBAAyB,EACzB,uBAAuB,EAGvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAGpC,wHAAwH;AAExH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAsB;IAEvD,MAAM,SAAS,GAAG,yBAAyB,CAC1C,uBAAuB,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAA,GAAG,OAAO,EAAE,CAAC,CACxE,CAAC;IAGF,OAAO,mBAAmB,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE;QAE5C,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QACzE,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE;KAE9B,CAAC,CAAC;IAGH,SAAS,KAAK,CAAC,MAAwB;QAEtC,OAAO,qBAAqB,CAAC,mBAAmB,CAAC;YAEhD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC;YACxC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;YACxD,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC;YACxD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC;SAExC,CAAC,EAAE;YAEH,OAAO,EAAE,IAAI;SAEb,CAAC,CAAC;IAEJ,CAAC;AAEF,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Scope } from "@metreeca/core/scope";
|
|
2
|
+
import { type Flake } from "@metreeca/keep-flake";
|
|
3
|
+
import type { Broker, Deferred, Lookup } from "@metreeca/keep/batching";
|
|
4
|
+
import type { Tuple, Variable } from "@metreeca/wire-sparql";
|
|
5
|
+
/**
|
|
6
|
+
* Decodes a SELECT result tuple set into a resource state.
|
|
7
|
+
*
|
|
8
|
+
* Reads back the arms the emitter produced, walking the same {@link Flake | lookup plan} and resolving
|
|
9
|
+
* each property from its column in the returned `tuples` through the shared {@link Scope}. Each property
|
|
10
|
+
* is read by the counterpart of the arm that emitted it:
|
|
11
|
+
*
|
|
12
|
+
* - a **scalar** property reads its column and recurses into the nested subject as an embedded or
|
|
13
|
+
* expanded resource;
|
|
14
|
+
* - a **localised** property collects every tagged term across its arm's rows into a tag-keyed
|
|
15
|
+
* dictionary or the shorthand the model requests;
|
|
16
|
+
* - a **variant** property reads whichever requested variant's column bound ({@link getUnionPlaceholders},
|
|
17
|
+
* union.md §Model), fixing the variant by the bound column with no term classification: the read-side
|
|
18
|
+
* dual of the emitter's membership gate.
|
|
19
|
+
*
|
|
20
|
+
* Properties with no emitted column resolve inline: set-valued properties forward to the collections
|
|
21
|
+
* pass through {@link select}, and `id` / `type` markers read from the focus and shape. Each item's
|
|
22
|
+
* deferred settles with its decoded resource, or rejects with a `TypeError` on a malformed tuple set
|
|
23
|
+
* that breaches the shape contract write-time validation upholds.
|
|
24
|
+
*
|
|
25
|
+
* @param scope The variable scope shared with the encoder
|
|
26
|
+
* @param items The batched requests, each paired with its {@link Flake | lookup plan} and deferred
|
|
27
|
+
* @param broker The cross-pass channel forwarding set-valued slots to the collections pass
|
|
28
|
+
* @param tuples The solution rows returned by the batched SELECT query
|
|
29
|
+
*/
|
|
30
|
+
export declare function decode(scope: Scope<Variable>, items: readonly (Deferred<Lookup> & {
|
|
31
|
+
readonly flake: Flake;
|
|
32
|
+
})[], broker: Broker, tuples: readonly Tuple[]): void;
|
|
33
|
+
//# sourceMappingURL=decode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/lookup/decode.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAElD,OAAO,EAEN,KAAK,KAAK,EAKV,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAIxE,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAK7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,MAAM,CACrB,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,EACtB,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC,EAAE,EAChE,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,KAAK,EAAE,GACtB,IAAI,CA2PN"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025-2026 Metreeca srl
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { getShapeVariants } from "@metreeca/blue/union";
|
|
17
|
+
import { eager } from "@metreeca/blue/value";
|
|
18
|
+
import { isAny, isArray, isObject } from "@metreeca/core";
|
|
19
|
+
import { unique } from "@metreeca/core/arrays";
|
|
20
|
+
import { matchTag } from "@metreeca/core/language";
|
|
21
|
+
import { equals } from "@metreeca/core/structures";
|
|
22
|
+
import { getFlakeEntries, getFlakeVariant, isModelBranch, isQueryBranch } from "@metreeca/keep-flake";
|
|
23
|
+
import { isTemplate } from "@metreeca/qest/template";
|
|
24
|
+
import { column } from "../_/_decode.js";
|
|
25
|
+
import { getUnionPlaceholders } from "../_/_union.js";
|
|
26
|
+
/**
|
|
27
|
+
* Decodes a SELECT result tuple set into a resource state.
|
|
28
|
+
*
|
|
29
|
+
* Reads back the arms the emitter produced, walking the same {@link Flake | lookup plan} and resolving
|
|
30
|
+
* each property from its column in the returned `tuples` through the shared {@link Scope}. Each property
|
|
31
|
+
* is read by the counterpart of the arm that emitted it:
|
|
32
|
+
*
|
|
33
|
+
* - a **scalar** property reads its column and recurses into the nested subject as an embedded or
|
|
34
|
+
* expanded resource;
|
|
35
|
+
* - a **localised** property collects every tagged term across its arm's rows into a tag-keyed
|
|
36
|
+
* dictionary or the shorthand the model requests;
|
|
37
|
+
* - a **variant** property reads whichever requested variant's column bound ({@link getUnionPlaceholders},
|
|
38
|
+
* union.md §Model), fixing the variant by the bound column with no term classification: the read-side
|
|
39
|
+
* dual of the emitter's membership gate.
|
|
40
|
+
*
|
|
41
|
+
* Properties with no emitted column resolve inline: set-valued properties forward to the collections
|
|
42
|
+
* pass through {@link select}, and `id` / `type` markers read from the focus and shape. Each item's
|
|
43
|
+
* deferred settles with its decoded resource, or rejects with a `TypeError` on a malformed tuple set
|
|
44
|
+
* that breaches the shape contract write-time validation upholds.
|
|
45
|
+
*
|
|
46
|
+
* @param scope The variable scope shared with the encoder
|
|
47
|
+
* @param items The batched requests, each paired with its {@link Flake | lookup plan} and deferred
|
|
48
|
+
* @param broker The cross-pass channel forwarding set-valued slots to the collections pass
|
|
49
|
+
* @param tuples The solution rows returned by the batched SELECT query
|
|
50
|
+
*/
|
|
51
|
+
export function decode(scope, items, broker, tuples) {
|
|
52
|
+
items.forEach(({ request, flake, resolve, reject }) => decodeResource(request.entry, flake, request.locale)
|
|
53
|
+
.then(resolve)
|
|
54
|
+
.catch(reject));
|
|
55
|
+
function decodeResource(entry, flake, locale) {
|
|
56
|
+
return Promise.all(flake.range.variants.flatMap(variant => variant.kind !== "resource" ? [] : [decodeVariant(entry, variant, locale, getFlakeVariant(flake, variant))])).then(resources => resources.reduce((merged, resource) => ({ ...merged, ...resource }), {}));
|
|
57
|
+
}
|
|
58
|
+
function decodeVariant(entry, shape, locale, branches) {
|
|
59
|
+
return Promise.all(branches.map((branch) => {
|
|
60
|
+
// a value set resolving to no content is never surfaced as an empty array or map (§4):
|
|
61
|
+
// the owning property is omitted from the decoded resource instead, except on a
|
|
62
|
+
// constrained collection (a query tuple carrying a selection), whose value is the
|
|
63
|
+
// filtered result set and legitimately empty
|
|
64
|
+
return isQueryBranch(branch) ? broker
|
|
65
|
+
.select({ entry, shape, field: branch.entry, query: branch.drain.mould, locale })
|
|
66
|
+
.then(values => [branch.path[branch.path.length - 1], isSelected(branch.drain.mould) || !isEmpty(values) ? values : undefined])
|
|
67
|
+
: isModelBranch(branch) ? Promise
|
|
68
|
+
.resolve(decodeProperty(locale, branch))
|
|
69
|
+
.then(value => [branch.path[branch.path.length - 1], value])
|
|
70
|
+
: branch.entry.kind === "id" ? [branch.path[branch.path.length - 1], entry]
|
|
71
|
+
: [branch.path[branch.path.length - 1], shape.class];
|
|
72
|
+
})).then(slots => slots.reduce((resource, [key, value]) => value === undefined ? resource : { ...resource, [key]: value }, {}));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Reads a single-valued model property, dispatching by range kind in the emitter's order: a **union** to
|
|
76
|
+
* {@link decodeUnion}, a **localised** `dictionary` leaf to {@link decodeDictionary}, any other **scalar**
|
|
77
|
+
* shape to {@link decodeValue}. The property is single-valued, so the first decoded value stands; an empty
|
|
78
|
+
* result omits the owning property (§4).
|
|
79
|
+
*/
|
|
80
|
+
function decodeProperty(locale, branch) {
|
|
81
|
+
const rangeShape = branch.entry.range.shape;
|
|
82
|
+
const model = branch.drain?.mould;
|
|
83
|
+
if (rangeShape.kind === "union") {
|
|
84
|
+
return decodeUnion(rangeShape, locale, branch, model)[0];
|
|
85
|
+
}
|
|
86
|
+
else if (rangeShape.kind === "dictionary") {
|
|
87
|
+
// localised slots resolve as a single structured `Localised` value, not a collection:
|
|
88
|
+
// the `Locales` placeholder's tag ranges select the languages and fix the per-tag cardinality
|
|
89
|
+
return decodeDictionary(locale, model, unique(column(scope.resolve(branch), tuples), equals));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
return decodeValue(rangeShape, locale, getFlakeEntries(branch), model, unique(column(scope.resolve(branch), tuples), equals))[0];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Decodes the localised arm: the language-tagged terms matched against a localised placeholder.
|
|
97
|
+
*
|
|
98
|
+
* Structural access (a {@link @metreeca/qest/template!Locales | Locales} placeholder) yields the
|
|
99
|
+
* {@link Dictionary} map of the tags matching the requested ranges by RFC 4647 basic filtering (the wildcard `*`
|
|
100
|
+
* or an empty map admits every tag), with the per-tag cardinality fixed by the placeholder's value shape. Coalesced
|
|
101
|
+
* access (a plain string or singleton-array placeholder) reduces the map to the first locale-priority
|
|
102
|
+
* tag present (§6.2), at the matching per-tag cardinality. A typed literal carrying no language tag
|
|
103
|
+
* lands under the `und` tag. A result carrying no content resolves to `undefined`, so the owning
|
|
104
|
+
* property is omitted (§4).
|
|
105
|
+
*/
|
|
106
|
+
function decodeDictionary(locale, placeholder, terms) {
|
|
107
|
+
const pairs = terms.flatMap((t) => {
|
|
108
|
+
return t.kind === "blank" || t.kind === "named" ? []
|
|
109
|
+
: t.kind === "tagged" ? [{ tag: t.language === "" ? "und" : t.language, text: t.text }]
|
|
110
|
+
: [{ tag: "und", text: t.text }];
|
|
111
|
+
});
|
|
112
|
+
if (isObject(placeholder)) {
|
|
113
|
+
const ranges = Object.keys(placeholder);
|
|
114
|
+
const matching = ranges.length === 0 ? pairs : pairs.filter(({ tag }) => ranges.some(range => matchTag(tag, range)));
|
|
115
|
+
if (matching.length === 0) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
else if (Object.values(placeholder).some(v => isArray(v))) {
|
|
119
|
+
return matching.reduce((values, { tag, text }) => ({ ...values, [tag]: [...(values[tag] ?? []), text] }), {});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
return matching.reduce((values, { tag, text }) => ({ ...values, [tag]: text }), {});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const winning = locale.find(tag => pairs.some(pair => pair.tag === tag));
|
|
127
|
+
const texts = pairs.filter(pair => pair.tag === winning).map(pair => pair.text);
|
|
128
|
+
return winning === undefined ? undefined
|
|
129
|
+
: isArray(placeholder) ? texts
|
|
130
|
+
: texts[0];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Decodes the variant arms, the read-side dual of the emitter's membership gate: each requested variant
|
|
135
|
+
* owns its own arm and object column, so the value's variant is fixed by which column bound, with no
|
|
136
|
+
* term classification (union.md §Model). The property is single-valued, so the first requested variant
|
|
137
|
+
* whose column is bound stands; same-kind variants decode an identical bare payload. Resolves to no
|
|
138
|
+
* value when no variant column bound, omitting the owning property (§4).
|
|
139
|
+
*/
|
|
140
|
+
function decodeUnion(shape, locale, branch, placeholder) {
|
|
141
|
+
const variants = getShapeVariants(shape);
|
|
142
|
+
const requested = getUnionPlaceholders(variants, placeholder);
|
|
143
|
+
const present = variants.find((variant) => variant.kind !== "dictionary" && requested.has(variant) && unique(column(scope.resolve(variant), tuples), equals).length > 0);
|
|
144
|
+
return present === undefined ? []
|
|
145
|
+
: decodeValue(present, locale, getFlakeVariant(branch, present), requested.get(present), unique(column(scope.resolve(present), tuples), equals));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Coerces a bound column into values of `shape`, shared by the scalar and variant readers: literal
|
|
149
|
+
* shapes map each typed term to its JavaScript value; a `reference` yields bare {@link Reference}s, or
|
|
150
|
+
* expands them as nested resources when the placeholder is a template; a `resource` always expands.
|
|
151
|
+
*/
|
|
152
|
+
function decodeValue(shape, locale, branches, placeholder, terms) {
|
|
153
|
+
switch (shape.kind) {
|
|
154
|
+
case "boolean":
|
|
155
|
+
return terms.filter(t => t.kind === "typed").map(t => t.text === "true");
|
|
156
|
+
case "number":
|
|
157
|
+
return terms.filter(t => t.kind === "typed").map(t => Number(t.text));
|
|
158
|
+
case "string":
|
|
159
|
+
return terms.filter(t => t.kind === "typed").map(t => t.text);
|
|
160
|
+
case "reference":
|
|
161
|
+
return isTemplate(placeholder)
|
|
162
|
+
? entries().map(entry => decodeVariant(entry, eager(shape.shape), locale, branches))
|
|
163
|
+
: entries();
|
|
164
|
+
case "resource":
|
|
165
|
+
return entries().map(entry => decodeVariant(entry, shape, locale, branches));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The IRIs bound by the column, the named terms of a reference- or resource-ranged property.
|
|
169
|
+
*/
|
|
170
|
+
function entries() {
|
|
171
|
+
return terms.filter(t => t.kind === "named").map(t => t.iri);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Tests whether a resolved slot value carries no content (`undefined`, an empty array, or an empty
|
|
176
|
+
* map) and must therefore be omitted from the decoded resource (§4).
|
|
177
|
+
*/
|
|
178
|
+
function isEmpty(value) {
|
|
179
|
+
return value === undefined
|
|
180
|
+
|| isArray(value, [])
|
|
181
|
+
|| isObject(value, {});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Tests whether a collection query carries a non-vacuous selection: such a slot is a filtered query
|
|
185
|
+
* whose result set is returned even when empty, rather than an unconstrained value set subject to
|
|
186
|
+
* empty-value omission (§4).
|
|
187
|
+
*/
|
|
188
|
+
function isSelected(query) {
|
|
189
|
+
return isArray(query, [isAny, selection => isObject(selection) && Object.keys(selection).length > 0
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=decode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.js","sourceRoot":"","sources":["../../src/lookup/decode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resources-pass SPARQL emitter.
|
|
3
|
+
*
|
|
4
|
+
* Folds every batched request's {@link Flake | lookup plan} into one `SELECT` whose `WHERE` is a
|
|
5
|
+
* `UNION` of arms. `UNION` sums solutions rather than joining them, so giving each fan-out source its
|
|
6
|
+
* own arm keeps the row count a sum of per-slot cardinalities instead of a cross-product. Set-valued
|
|
7
|
+
* properties go to the collections pass, so this pass sees only single-valued slots and the only
|
|
8
|
+
* fan-out sources are localised (`dictionary`) leaves and union variants.
|
|
9
|
+
*
|
|
10
|
+
* A resource contributes two descents, {@link scalars} and {@link composites}, together emitting three
|
|
11
|
+
* arm kinds:
|
|
12
|
+
*
|
|
13
|
+
* - a **scalar** arm (from {@link scalars}): the whole plain single-valued subtree (scalars, references,
|
|
14
|
+
* embedded resources), every edge `OPTIONAL` so one row carries all bound slots;
|
|
15
|
+
* - a **localised** arm per `dictionary` leaf (from {@link composites}): the required path to the leaf's
|
|
16
|
+
* parent, then the localised edge binding one tagged-term column;
|
|
17
|
+
* - a **variant** arm per requested union variant (from {@link composites}; {@link getUnionPlaceholders},
|
|
18
|
+
* union.md §Model): the variant's object column gated by its {@link membership} constraint, then the
|
|
19
|
+
* variant's subtree.
|
|
20
|
+
*
|
|
21
|
+
* Variables come from the shared {@link Scope}, keyed on {@link Branch} identity (or the variant shape
|
|
22
|
+
* for a variant column), so the decoder recovers each column by resolving the same node. Requests whose
|
|
23
|
+
* flake has no descent branch contribute no arm and are pre-filtered by the caller.
|
|
24
|
+
*
|
|
25
|
+
* @module
|
|
26
|
+
*/
|
|
27
|
+
import type { Scope } from "@metreeca/core/scope";
|
|
28
|
+
import { type Flake } from "@metreeca/keep-flake";
|
|
29
|
+
import type { Lookup } from "@metreeca/keep/batching";
|
|
30
|
+
import { type SPARQL, type Variable } from "@metreeca/wire-sparql";
|
|
31
|
+
/**
|
|
32
|
+
* Emits one batched SELECT covering every request.
|
|
33
|
+
*
|
|
34
|
+
* Builds the `WHERE` from the `UNION` of every request's arms, all sharing `scope` so the decoder can
|
|
35
|
+
* recover each request's columns.
|
|
36
|
+
*
|
|
37
|
+
* @param scope The variable allocator shared with the decoder, keyed on {@link Branch} identity
|
|
38
|
+
* @param batch The root entries paired with their {@link Flake | lookup plans}
|
|
39
|
+
*
|
|
40
|
+
* @returns The unified SELECT query
|
|
41
|
+
*/
|
|
42
|
+
export declare function encode(scope: Scope<Variable>, batch: readonly {
|
|
43
|
+
readonly request: Lookup;
|
|
44
|
+
readonly flake: Flake;
|
|
45
|
+
}[]): SPARQL;
|
|
46
|
+
//# sourceMappingURL=encode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/lookup/encode.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAe,KAAK,KAAK,EAAmD,MAAM,sBAAsB,CAAC;AAChH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAMnE;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CACrB,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,EACtB,KAAK,EAAE,SAAS;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EAAE,GACnE,MAAM,CA4HR"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025-2026 Metreeca srl
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { getFlakeEntries, getFlakeVariant, isModelBranch } from "@metreeca/keep-flake";
|
|
17
|
+
import { named } from "@metreeca/trio";
|
|
18
|
+
import { all, fragment, optional, select, union, where } from "@metreeca/wire-sparql/builder";
|
|
19
|
+
import { link, membership } from "../_/_encode.js";
|
|
20
|
+
import { getUnionPlaceholders } from "../_/_union.js";
|
|
21
|
+
/**
|
|
22
|
+
* Emits one batched SELECT covering every request.
|
|
23
|
+
*
|
|
24
|
+
* Builds the `WHERE` from the `UNION` of every request's arms, all sharing `scope` so the decoder can
|
|
25
|
+
* recover each request's columns.
|
|
26
|
+
*
|
|
27
|
+
* @param scope The variable allocator shared with the decoder, keyed on {@link Branch} identity
|
|
28
|
+
* @param batch The root entries paired with their {@link Flake | lookup plans}
|
|
29
|
+
*
|
|
30
|
+
* @returns The unified SELECT query
|
|
31
|
+
*/
|
|
32
|
+
export function encode(scope, batch) {
|
|
33
|
+
return select(all(), where(union(...batch.flatMap(({ request, flake }) => resource([], named(request.entry), getFlakeEntries(flake))))));
|
|
34
|
+
/**
|
|
35
|
+
* The arms reading the subtree at `anchor`. A resource has two descents, its {@link scalars} and its
|
|
36
|
+
* {@link composites}, each an independent arm of the enclosing `UNION` and each prefixed by `path`:
|
|
37
|
+
* the required edges reaching `anchor`, empty at the root, a variant's membership-gated reach in
|
|
38
|
+
* recursion.
|
|
39
|
+
*/
|
|
40
|
+
function resource(path, anchor, entries) {
|
|
41
|
+
return [
|
|
42
|
+
...scalars(path, anchor, entries),
|
|
43
|
+
...composites(path, anchor, entries)
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The single arm reading `anchor`'s scalar properties: `path` followed by the `OPTIONAL` block of the
|
|
48
|
+
* plain single-valued subtree. Emitted only when non-empty, so a bare `path` still binds the edges
|
|
49
|
+
* reaching `anchor` even when it has no scalar leaf.
|
|
50
|
+
*/
|
|
51
|
+
function scalars(path, anchor, entries) {
|
|
52
|
+
return arm(...path, ...optionals(anchor, entries));
|
|
53
|
+
/**
|
|
54
|
+
* The `OPTIONAL` clauses of the plain single-valued subtree at `anchor`, one per branch wrapping its
|
|
55
|
+
* edge and nested descent. Composite branches (`dictionary` leaves, unions) are skipped:
|
|
56
|
+
* {@link composites} emits them as standalone arms.
|
|
57
|
+
*/
|
|
58
|
+
function optionals(anchor, entries) {
|
|
59
|
+
return entries.filter(isModelBranch).flatMap(branch => {
|
|
60
|
+
const shape = branch.entry.range.shape;
|
|
61
|
+
if (shape.kind === "dictionary" || shape.kind === "union") {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const target = scope.resolve(branch);
|
|
66
|
+
return [optional(link([anchor, branch.entry, target]), ...optionals(target, getFlakeEntries(branch)))];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The standalone composite arms under `anchor`, the complement of {@link scalars}: one arm per
|
|
73
|
+
* `dictionary` leaf, one per requested union variant (each with its own subtree and nested composites), recursing
|
|
74
|
+
* through plain nested resources. Each arm carries `path` (the required edges reaching here) so it
|
|
75
|
+
* yields rows only when that path exists.
|
|
76
|
+
*/
|
|
77
|
+
function composites(path, anchor, entries) {
|
|
78
|
+
return entries.filter(isModelBranch).flatMap(branch => {
|
|
79
|
+
const shape = branch.entry.range.shape;
|
|
80
|
+
if (shape.kind === "union") {
|
|
81
|
+
return [...getUnionPlaceholders(shape.variants, branch.drain?.mould).keys()].flatMap(variant => {
|
|
82
|
+
const target = scope.resolve(variant);
|
|
83
|
+
return resource([
|
|
84
|
+
...path,
|
|
85
|
+
link([anchor, branch.entry, target]),
|
|
86
|
+
membership(target, variant)
|
|
87
|
+
], target, getFlakeVariant(branch, variant));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else if (shape.kind === "dictionary") {
|
|
91
|
+
const target = scope.resolve(branch);
|
|
92
|
+
return arm(...path, link([anchor, branch.entry, target]));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const target = scope.resolve(branch);
|
|
96
|
+
return composites([
|
|
97
|
+
...path,
|
|
98
|
+
link([anchor, branch.entry, target])
|
|
99
|
+
], target, getFlakeEntries(branch));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* An arm wrapping `clauses`, or nothing when there are none: an empty {@link fragment} would surface
|
|
105
|
+
* as a stray `{ }` branch matching everything once the enclosing {@link union} groups the arms.
|
|
106
|
+
*/
|
|
107
|
+
function arm(...clauses) {
|
|
108
|
+
return clauses.length > 0 ? [fragment(...clauses)] : [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=encode.js.map
|