@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.
Files changed (71) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +106 -0
  3. package/dist/_/_decode.d.ts +20 -0
  4. package/dist/_/_decode.d.ts.map +1 -0
  5. package/dist/_/_decode.js +55 -0
  6. package/dist/_/_decode.js.map +1 -0
  7. package/dist/_/_encode.d.ts +112 -0
  8. package/dist/_/_encode.d.ts.map +1 -0
  9. package/dist/_/_encode.js +262 -0
  10. package/dist/_/_encode.js.map +1 -0
  11. package/dist/_/_flake.d.ts +16 -0
  12. package/dist/_/_flake.d.ts.map +1 -0
  13. package/dist/_/_flake.js +65 -0
  14. package/dist/_/_flake.js.map +1 -0
  15. package/dist/_/_union.d.ts +28 -0
  16. package/dist/_/_union.d.ts.map +1 -0
  17. package/dist/_/_union.js +51 -0
  18. package/dist/_/_union.js.map +1 -0
  19. package/dist/detect/decode.d.ts +25 -0
  20. package/dist/detect/decode.d.ts.map +1 -0
  21. package/dist/detect/decode.js +34 -0
  22. package/dist/detect/decode.js.map +1 -0
  23. package/dist/detect/encode.d.ts +25 -0
  24. package/dist/detect/encode.d.ts.map +1 -0
  25. package/dist/detect/encode.js +30 -0
  26. package/dist/detect/encode.js.map +1 -0
  27. package/dist/detect/index.d.ts +17 -0
  28. package/dist/detect/index.d.ts.map +1 -0
  29. package/dist/detect/index.js +55 -0
  30. package/dist/detect/index.js.map +1 -0
  31. package/dist/index.d.ts +95 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +70 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lookup/decode.d.ts +33 -0
  36. package/dist/lookup/decode.d.ts.map +1 -0
  37. package/dist/lookup/decode.js +193 -0
  38. package/dist/lookup/decode.js.map +1 -0
  39. package/dist/lookup/encode.d.ts +46 -0
  40. package/dist/lookup/encode.d.ts.map +1 -0
  41. package/dist/lookup/encode.js +111 -0
  42. package/dist/lookup/encode.js.map +1 -0
  43. package/dist/lookup/index.d.ts +21 -0
  44. package/dist/lookup/index.d.ts.map +1 -0
  45. package/dist/lookup/index.js +72 -0
  46. package/dist/lookup/index.js.map +1 -0
  47. package/dist/modify/decode.d.ts +22 -0
  48. package/dist/modify/decode.d.ts.map +1 -0
  49. package/dist/modify/decode.js +26 -0
  50. package/dist/modify/decode.js.map +1 -0
  51. package/dist/modify/encode.d.ts +21 -0
  52. package/dist/modify/encode.d.ts.map +1 -0
  53. package/dist/modify/encode.js +184 -0
  54. package/dist/modify/encode.js.map +1 -0
  55. package/dist/modify/index.d.ts +22 -0
  56. package/dist/modify/index.d.ts.map +1 -0
  57. package/dist/modify/index.js +44 -0
  58. package/dist/modify/index.js.map +1 -0
  59. package/dist/select/decode.d.ts +16 -0
  60. package/dist/select/decode.d.ts.map +1 -0
  61. package/dist/select/decode.js +229 -0
  62. package/dist/select/decode.js.map +1 -0
  63. package/dist/select/encode.d.ts +16 -0
  64. package/dist/select/encode.d.ts.map +1 -0
  65. package/dist/select/encode.js +434 -0
  66. package/dist/select/encode.js.map +1 -0
  67. package/dist/select/index.d.ts +16 -0
  68. package/dist/select/index.d.ts.map +1 -0
  69. package/dist/select/index.js +38 -0
  70. package/dist/select/index.js.map +1 -0
  71. 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"}
@@ -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