@astrale-os/sdk 0.6.0-beta.42 → 0.6.0-beta.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/application/query/execution/bounds.d.ts +3 -1
- package/dist/application/query/execution/bounds.js +3 -1
- package/dist/application/query/execution/crossings.js +33 -10
- package/dist/application/query/execution/recipe.d.ts +25 -10
- package/dist/application/query/execution/recipe.js +80 -32
- package/dist/application/query/handles/reads.js +3 -3
- package/dist/application/query/handles/terms.d.ts +9 -1
- package/dist/application/query/handles/terms.js +20 -3
- package/dist/application/query/recipe/define.d.ts +5 -6
- package/dist/application/query/recipe/define.js +8 -15
- package/dist/application/query/recipe/recipe.d.ts +36 -36
- package/dist/deployment/build/build.d.ts +8 -1
- package/dist/deployment/build/material.js +3 -0
- package/dist/platform/schema/kernel-compiled.d.ts +1 -1
- package/dist/platform/schema/kernel-compiled.js +2 -2
- package/dist/testing/recipe.d.ts +2 -2
- package/dist/testing/recipe.js +10 -5
- package/dist/tooling/linter/implementations/source/recipes.js +39 -23
- package/dist/tooling/linter/policy/generated.js +2 -2
- package/docs/legacy/query-v6.md +1 -1
- package/docs/queries.md +26 -22
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ their verbs, a pure `output` and declared errors. An Action calls
|
|
|
75
75
|
`query(recipe, input, { as: 'executor' | 'caller' })`; a durable Workflow reads in
|
|
76
76
|
`step.query(step, recipe, input, { as: 'executor' })`. Before anything is sent, the SDK checks the
|
|
77
77
|
recipe's bounds and the requirements its relations need; `checkRecipe` from
|
|
78
|
-
`@astrale-os/sdk/testing` runs both checks in a test, on the first
|
|
78
|
+
`@astrale-os/sdk/testing` runs both checks in a test, on the first step it builds for a sample
|
|
79
79
|
input. A recipe reads on a Host whose Kernel serves Query v7 and the Class of each row. Query V6
|
|
80
80
|
definitions keep their meaning and are deprecated. See [the Query guide](docs/queries.md) and
|
|
81
81
|
[Query V6 and the move to v7](docs/legacy/query-v6.md).
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ReadPlan } from '../handles/plan.js';
|
|
6
6
|
import type { AnyQuery } from '../language/query.js';
|
|
7
|
-
/** Each read of each
|
|
7
|
+
/** Each read of each step, and each element of an `items.map`. */
|
|
8
8
|
export declare const MAXIMUM_READS = 32;
|
|
9
|
+
/** Steps of one recipe: `steps(first, (earlier) => next, …)` takes two to this many. */
|
|
10
|
+
export declare const MAXIMUM_STEPS = 8;
|
|
9
11
|
/** Across a recipe's nested lists. */
|
|
10
12
|
export declare const MAXIMUM_ROWS = 100000;
|
|
11
13
|
/** A verb's bound: its list, plus one for `all` and `first(n, { more })`, fits a Kernel page. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/** Each read of each
|
|
1
|
+
/** Each read of each step, and each element of an `items.map`. */
|
|
2
2
|
export const MAXIMUM_READS = 32;
|
|
3
|
+
/** Steps of one recipe: `steps(first, (earlier) => next, …)` takes two to this many. */
|
|
4
|
+
export const MAXIMUM_STEPS = 8;
|
|
3
5
|
/** Across a recipe's nested lists. */
|
|
4
6
|
export const MAXIMUM_ROWS = 100_000;
|
|
5
7
|
/** A verb's bound: its list, plus one for `all` and `first(n, { more })`, fits a Kernel page. */
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Classes a recipe's relations cross, checked against what its Domain declares (S12, spec §
|
|
3
|
-
* Details settled: "When S12 runs"). A relation that crosses a Class of a dependency
|
|
4
|
-
* and `traverse` on it in the Domain's requirements, or the read is a
|
|
5
|
-
* lowering before anything is sent, never answered as empty. It reads
|
|
6
|
-
* so an empty list the input gives refuses what any other list does. A
|
|
7
|
-
* covers its children, and an abstract Class with concrete children, each
|
|
8
|
-
* the check refuses only what no declaration serves.
|
|
3
|
+
* Details settled: "When S12 runs"). A relation that crosses a Class of a dependency other than the
|
|
4
|
+
* Kernel needs `read` and `traverse` on it in the Domain's requirements, or the read is a
|
|
5
|
+
* programming error, raised at lowering before anything is sent, never answered as empty. It reads
|
|
6
|
+
* the document as written, so an empty list the input gives refuses what any other list does. A
|
|
7
|
+
* requirement on a parent covers its children, and an abstract Class with concrete children, each
|
|
8
|
+
* covered, is covered: the check refuses only what no declaration serves.
|
|
9
9
|
*/
|
|
10
10
|
import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
11
11
|
/** What a relation needs on each foreign Class it crosses. */
|
|
12
12
|
const NEEDED = ['read', 'traverse'];
|
|
13
|
+
/**
|
|
14
|
+
* The Kernel's origin. Its Classes need nothing, even when the Domain declares it a dependency, but
|
|
15
|
+
* the dependency Classes under an abstract one (`Identity`, `Node`) each do.
|
|
16
|
+
*/
|
|
17
|
+
const KERNEL = 'kernel.astrale.ai';
|
|
18
|
+
/** The Kernel's abstract Edge Class. Every Edge Class is under it, though none names it a parent. */
|
|
19
|
+
const EDGE = `${KERNEL}:class.Edge`;
|
|
13
20
|
/**
|
|
14
21
|
* The S12 refusal: a `TypeError`, the same for every input and every attempt, so a durable Step
|
|
15
22
|
* fails at once on it.
|
|
@@ -27,7 +34,7 @@ export class UndeclaredCrossing extends TypeError {
|
|
|
27
34
|
/** The S12 check of one Domain: its dependencies, and the operations it declares on their Classes. */
|
|
28
35
|
export class Crossings {
|
|
29
36
|
model;
|
|
30
|
-
/** Each direct dependency's origin, by the alias the Domain gives it. */
|
|
37
|
+
/** Each direct dependency's origin but the Kernel's, by the alias the Domain gives it. */
|
|
31
38
|
#dependencies = new Map();
|
|
32
39
|
/** The operations declared on each Class, by key. */
|
|
33
40
|
#declared = new Map();
|
|
@@ -36,7 +43,8 @@ export class Crossings {
|
|
|
36
43
|
constructor(model, domain, declared) {
|
|
37
44
|
this.model = model;
|
|
38
45
|
for (const [alias, dependency] of Object.entries(domain.dependencies))
|
|
39
|
-
|
|
46
|
+
if (dependency.origin !== KERNEL)
|
|
47
|
+
this.#dependencies.set(dependency.origin, alias);
|
|
40
48
|
for (const requirement of declared) {
|
|
41
49
|
const key = Key.of(requirement.class);
|
|
42
50
|
const operations = this.#declared.get(key) ?? new Set();
|
|
@@ -115,6 +123,12 @@ export class Crossings {
|
|
|
115
123
|
return [...new Set(classes)];
|
|
116
124
|
}
|
|
117
125
|
#require(recipe, relation, c) {
|
|
126
|
+
if (c.ref.origin === KERNEL) {
|
|
127
|
+
if (c.abstract)
|
|
128
|
+
for (const child of this.#under(c))
|
|
129
|
+
this.#require(recipe, relation, child);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
118
132
|
const alias = this.#dependencies.get(c.ref.origin);
|
|
119
133
|
if (alias === undefined)
|
|
120
134
|
return;
|
|
@@ -127,14 +141,23 @@ export class Crossings {
|
|
|
127
141
|
}
|
|
128
142
|
/** What a Class lacks of `read` and `traverse`, declared on it or on a parent. */
|
|
129
143
|
#missing(c) {
|
|
130
|
-
const declared = new Set([...this
|
|
144
|
+
const declared = new Set([...this.#satisfies(c)].flatMap((key) => [...(this.#declared.get(key) ?? [])]));
|
|
131
145
|
return NEEDED.filter((operation) => !declared.has(operation));
|
|
132
146
|
}
|
|
133
147
|
/** An abstract Class has concrete Classes under it, and the Domain may cross each. */
|
|
134
148
|
#covered(c) {
|
|
135
|
-
const children = this.#
|
|
149
|
+
const children = this.#under(c);
|
|
136
150
|
return children.length > 0 && children.every((k) => this.#served(k));
|
|
137
151
|
}
|
|
152
|
+
/** The concrete Classes under a Class, of the Domain and its dependencies. */
|
|
153
|
+
#under(c) {
|
|
154
|
+
return this.#concrete.filter((k) => this.#satisfies(k).has(c.key));
|
|
155
|
+
}
|
|
156
|
+
/** The keys a Class satisfies, the Kernel's `Edge` included for an Edge Class. */
|
|
157
|
+
#satisfies(c) {
|
|
158
|
+
const keys = this.model.satisfies(c);
|
|
159
|
+
return c.kind === 'edge' ? new Set([...keys, EDGE]) : keys;
|
|
160
|
+
}
|
|
138
161
|
/** Whether a relation may cross a Class: the Domain's own, or a dependency's it declares. */
|
|
139
162
|
#served(c) {
|
|
140
163
|
return !this.#dependencies.has(c.ref.origin) || this.#missing(c).length === 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Run a v7 recipe (WP-S3): its
|
|
3
|
-
* each read at its own snapshot (the stand-in); then `output`. A
|
|
2
|
+
* Run a v7 recipe (WP-S3): its steps in order, each one's reads together, at most 8 in flight,
|
|
3
|
+
* each read at its own snapshot (the stand-in); then `output`. A step's bounds (S11) are checked
|
|
4
4
|
* before it sends anything, and every Kernel refusal of a read is a `QueryError`.
|
|
5
5
|
*/
|
|
6
6
|
import type { Domain } from '../../../platform/schema/index.js';
|
|
@@ -28,6 +28,7 @@ export interface RecipeCallOptions {
|
|
|
28
28
|
export type RecipeOptions<R, As extends string> = {
|
|
29
29
|
readonly as: As;
|
|
30
30
|
} & ('~page' extends keyof R ? RecipeCallOptions : {});
|
|
31
|
+
type Step = (...parameters: readonly unknown[]) => unknown;
|
|
31
32
|
/** One recipe call: its reads so far, against S11, and the Classes they cross, against S12. */
|
|
32
33
|
export declare class Run {
|
|
33
34
|
#private;
|
|
@@ -37,29 +38,43 @@ export declare class Run {
|
|
|
37
38
|
readonly model: Model;
|
|
38
39
|
constructor(client: RecipeClient, domain: Domain, recipe: string, declared?: readonly DeclaredClass[]);
|
|
39
40
|
/**
|
|
40
|
-
* S11, before a
|
|
41
|
+
* S11, before a step sends: its reads, its verbs' bounds, its rows and each property `in` its
|
|
41
42
|
* documents write. Then S12: each foreign Class its relations cross.
|
|
42
43
|
*/
|
|
43
44
|
admit(plans: readonly ReadPlan[]): void;
|
|
44
45
|
/** One read's result: its verb's empty answer, or its documents' answers merged and rendered. */
|
|
45
46
|
read(plan: ReadPlan, paging?: RecipeCallOptions): Promise<unknown>;
|
|
46
47
|
}
|
|
48
|
+
/** What `steps(…)` returned: its first record of facets, and the functions of the next ones. */
|
|
49
|
+
interface StepsOf {
|
|
50
|
+
readonly first: unknown;
|
|
51
|
+
readonly next: readonly Step[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The `steps` a recipe's `build` receives: two to eight steps, the first a record of facets and
|
|
55
|
+
* each next one a function of the merged results before it. It reads nothing: what it returns is
|
|
56
|
+
* read when `build` returns it.
|
|
57
|
+
*/
|
|
58
|
+
export declare function stepsFor(recipe: string): (...steps: readonly unknown[]) => object;
|
|
59
|
+
/** The steps `build` returned, or `undefined` for one read or one record of facets. */
|
|
60
|
+
export declare function stepsOf(built: unknown): StepsOf | undefined;
|
|
47
61
|
/** Where each facet's results go: one read's index, a list of them, or nothing. */
|
|
48
62
|
type Shape = readonly (readonly [string, number | readonly number[] | undefined])[];
|
|
49
63
|
/**
|
|
50
|
-
* The reads of one
|
|
51
|
-
* shape that puts their results back. A facet takes a name no `earlier`
|
|
64
|
+
* The reads of one step, admitted (S11, S12): a single read, or each facet's in order and the
|
|
65
|
+
* shape that puts their results back. A facet takes a name no `earlier` step read: the merged
|
|
52
66
|
* results keep one value per name.
|
|
53
67
|
*/
|
|
54
|
-
export declare function
|
|
68
|
+
export declare function planStep(run: Run, built: unknown, single: boolean, earlier?: Readonly<Record<string, unknown>>): {
|
|
55
69
|
readonly plans: readonly ReadPlan[];
|
|
56
70
|
readonly shape?: Shape;
|
|
57
71
|
};
|
|
58
72
|
/**
|
|
59
|
-
* Run a recipe for `input` against `domain`, the resolved Domain it was written for:
|
|
60
|
-
* then its `output`. A page recipe takes its call's `size`
|
|
61
|
-
* raises a `QueryError`; `output` raises the recipe's
|
|
62
|
-
*
|
|
73
|
+
* Run a recipe for `input` against `domain`, the resolved Domain it was written for: what `build`
|
|
74
|
+
* returns, each of its steps in order, then its `output`. A page recipe takes its call's `size`
|
|
75
|
+
* and `after`. A read the Kernel refuses raises a `QueryError`; `output` raises the recipe's
|
|
76
|
+
* declared errors. With the Class requirements
|
|
77
|
+
* the Domain's build `declared`, each step's relations are checked against them first (S12).
|
|
63
78
|
*/
|
|
64
79
|
export declare function runQueryRecipe<R extends Recipe>(client: RecipeClient, domain: Domain, recipe: R, input: InputOf<R>, call?: RecipeCallOptions, declared?: readonly DeclaredClass[]): Promise<OutputOf<R>>;
|
|
65
80
|
/**
|
|
@@ -7,13 +7,13 @@ import { modelOf } from '../handles/model.js';
|
|
|
7
7
|
import { isRead, planOf } from '../handles/plan.js';
|
|
8
8
|
import { describe } from '../handles/terms.js';
|
|
9
9
|
import { recipeSpecOf } from '../recipe/define.js';
|
|
10
|
-
import { MAXIMUM_BOUND, MAXIMUM_READS, MAXIMUM_ROWS, pastPage, rowsOf, wideIn } from './bounds.js';
|
|
10
|
+
import { MAXIMUM_BOUND, MAXIMUM_READS, MAXIMUM_ROWS, MAXIMUM_STEPS, pastPage, rowsOf, wideIn, } from './bounds.js';
|
|
11
11
|
import { Crossings } from './crossings.js';
|
|
12
12
|
import { QueryError } from './error.js';
|
|
13
13
|
import { describeRefusals, isUnserved, refusalsOf } from './refusal.js';
|
|
14
14
|
import { emptyResult, rootResult } from './rows.js';
|
|
15
15
|
import { merge, split } from './split.js';
|
|
16
|
-
/** Reads of one
|
|
16
|
+
/** Reads of one step in flight at once. */
|
|
17
17
|
const IN_FLIGHT = 8;
|
|
18
18
|
/** `QUERY_SNAPSHOT_STALE`: nothing was read, so the read is sent again at once, this many times. */
|
|
19
19
|
const STALE_RESENDS = 3;
|
|
@@ -37,7 +37,7 @@ export class Run {
|
|
|
37
37
|
declared === undefined ? undefined : new Crossings(this.model, domain, declared);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* S11, before a
|
|
40
|
+
* S11, before a step sends: its reads, its verbs' bounds, its rows and each property `in` its
|
|
41
41
|
* documents write. Then S12: each foreign Class its relations cross.
|
|
42
42
|
*/
|
|
43
43
|
admit(plans) {
|
|
@@ -151,28 +151,72 @@ async function pooled(tasks, limit) {
|
|
|
151
151
|
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
|
|
152
152
|
return results;
|
|
153
153
|
}
|
|
154
|
+
const stepped = new WeakMap();
|
|
154
155
|
/**
|
|
155
|
-
* The
|
|
156
|
-
*
|
|
156
|
+
* The `steps` a recipe's `build` receives: two to eight steps, the first a record of facets and
|
|
157
|
+
* each next one a function of the merged results before it. It reads nothing: what it returns is
|
|
158
|
+
* read when `build` returns it.
|
|
159
|
+
*/
|
|
160
|
+
export function stepsFor(recipe) {
|
|
161
|
+
return (...steps) => {
|
|
162
|
+
if (steps.length < 2 || steps.length > MAXIMUM_STEPS)
|
|
163
|
+
throw new TypeError(`Recipe ${recipe}: steps takes 2 to ${MAXIMUM_STEPS} steps; got ${steps.length}. ${steps.length < 2
|
|
164
|
+
? 'Reads that wait for none are what build returns: one read, or a record of facets.'
|
|
165
|
+
: 'Read the facets that wait for the same results in one step.'}`);
|
|
166
|
+
const [first, ...next] = steps;
|
|
167
|
+
if (!isFacets(first))
|
|
168
|
+
throw new TypeError(`Recipe ${recipe}: the first step is a record of facets; got ${described(first)}.`);
|
|
169
|
+
const position = next.findIndex((step) => typeof step !== 'function');
|
|
170
|
+
if (position !== -1)
|
|
171
|
+
throw new TypeError(`Recipe ${recipe}: each step after the first is a function of the results before it; step ${position + 2} is ${describe(next[position])}.`);
|
|
172
|
+
const value = Object.freeze({});
|
|
173
|
+
stepped.set(value, Object.freeze({ first, next: Object.freeze(next) }));
|
|
174
|
+
return value;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** A record of facets: a plain object, neither a read nor what `steps` returned. */
|
|
178
|
+
function isFacets(value) {
|
|
179
|
+
if (typeof value !== 'object' || value === null || isRead(value) || stepped.has(value))
|
|
180
|
+
return false;
|
|
181
|
+
const prototype = Object.getPrototypeOf(value);
|
|
182
|
+
return prototype === Object.prototype || prototype === null;
|
|
183
|
+
}
|
|
184
|
+
/** What a step was, for its refusal: `steps(…)` and a Promise are named with their fix. */
|
|
185
|
+
function described(value) {
|
|
186
|
+
if (typeof value === 'object' && value !== null && stepped.has(value))
|
|
187
|
+
return 'steps(…), which only build returns';
|
|
188
|
+
if (value instanceof Promise)
|
|
189
|
+
return 'a Promise: build and its steps are not async';
|
|
190
|
+
return describe(value);
|
|
191
|
+
}
|
|
192
|
+
/** The steps `build` returned, or `undefined` for one read or one record of facets. */
|
|
193
|
+
export function stepsOf(built) {
|
|
194
|
+
return typeof built === 'object' && built !== null ? stepped.get(built) : undefined;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The facets a step returned, each a read, a list of reads (`items.map`) or nothing. One read is
|
|
198
|
+
* `build`'s alone, without steps: each step returns a record of facets.
|
|
157
199
|
*/
|
|
158
200
|
function facetsOf(recipe, built, single) {
|
|
159
|
-
if (
|
|
160
|
-
const expected = single
|
|
161
|
-
|
|
201
|
+
if (!isFacets(built)) {
|
|
202
|
+
const expected = single
|
|
203
|
+
? 'build returns one read, a record of facets, or steps(…)'
|
|
204
|
+
: 'each step returns a record of facets';
|
|
205
|
+
throw new TypeError(`Recipe ${recipe}: ${expected}; got ${described(built)}.`);
|
|
162
206
|
}
|
|
163
207
|
return Object.entries(built);
|
|
164
208
|
}
|
|
165
|
-
/** A page is a recipe's single read: one
|
|
209
|
+
/** A page is a recipe's single read: one step that returns it. */
|
|
166
210
|
function refusePage(recipe, plan) {
|
|
167
211
|
if (plan.verb === 'page')
|
|
168
212
|
throw new TypeError(`Recipe ${recipe}: a page is a recipe's single read (D22); read the others with first or all.`);
|
|
169
213
|
}
|
|
170
214
|
/**
|
|
171
|
-
* The reads of one
|
|
172
|
-
* shape that puts their results back. A facet takes a name no `earlier`
|
|
215
|
+
* The reads of one step, admitted (S11, S12): a single read, or each facet's in order and the
|
|
216
|
+
* shape that puts their results back. A facet takes a name no `earlier` step read: the merged
|
|
173
217
|
* results keep one value per name.
|
|
174
218
|
*/
|
|
175
|
-
export function
|
|
219
|
+
export function planStep(run, built, single, earlier = {}) {
|
|
176
220
|
if (single && isRead(built)) {
|
|
177
221
|
const plans = [planOf(built)];
|
|
178
222
|
run.admit(plans);
|
|
@@ -181,7 +225,7 @@ export function planStage(run, built, single, earlier = {}) {
|
|
|
181
225
|
const facets = facetsOf(run.recipe, built, single);
|
|
182
226
|
for (const [name] of facets)
|
|
183
227
|
if (Object.hasOwn(earlier, name))
|
|
184
|
-
throw new TypeError(`Recipe ${run.recipe}: an earlier
|
|
228
|
+
throw new TypeError(`Recipe ${run.recipe}: an earlier step read the facet ${name}; name this step's facet otherwise.`);
|
|
185
229
|
const plans = [];
|
|
186
230
|
const shape = [];
|
|
187
231
|
for (const [name, facet] of facets) {
|
|
@@ -196,9 +240,9 @@ export function planStage(run, built, single, earlier = {}) {
|
|
|
196
240
|
run.admit(plans);
|
|
197
241
|
return { plans, shape };
|
|
198
242
|
}
|
|
199
|
-
/** The results of one
|
|
200
|
-
async function
|
|
201
|
-
const { plans, shape } =
|
|
243
|
+
/** The results of one step: a read's result, or a record of each facet's. */
|
|
244
|
+
async function runStep(run, built, single, call, earlier) {
|
|
245
|
+
const { plans, shape } = planStep(run, built, single, earlier);
|
|
202
246
|
if (shape === undefined) {
|
|
203
247
|
const plan = plans[0];
|
|
204
248
|
return run.read(plan, plan.verb === 'page' ? call : undefined);
|
|
@@ -229,30 +273,34 @@ function pageCall(recipe, call, max) {
|
|
|
229
273
|
return { size: size ?? max, ...(after === undefined ? {} : { after }) };
|
|
230
274
|
}
|
|
231
275
|
/**
|
|
232
|
-
* Run a recipe for `input` against `domain`, the resolved Domain it was written for:
|
|
233
|
-
* then its `output`. A page recipe takes its call's `size`
|
|
234
|
-
* raises a `QueryError`; `output` raises the recipe's
|
|
235
|
-
*
|
|
276
|
+
* Run a recipe for `input` against `domain`, the resolved Domain it was written for: what `build`
|
|
277
|
+
* returns, each of its steps in order, then its `output`. A page recipe takes its call's `size`
|
|
278
|
+
* and `after`. A read the Kernel refuses raises a `QueryError`; `output` raises the recipe's
|
|
279
|
+
* declared errors. With the Class requirements
|
|
280
|
+
* the Domain's build `declared`, each step's relations are checked against them first (S12).
|
|
236
281
|
*/
|
|
237
282
|
export async function runQueryRecipe(client, domain, recipe, input, call = {}, declared) {
|
|
238
283
|
const spec = recipeSpecOf(recipe);
|
|
239
284
|
if (spec === undefined)
|
|
240
285
|
throw new TypeError(`Not a v7 recipe: define one with defineQuery<Schema>()(id, { build }); got ${describe(recipe)}.`);
|
|
241
286
|
const run = new Run(client, domain, recipe.id, declared);
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
let
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const built = index === 0
|
|
248
|
-
? stage(input, d)
|
|
249
|
-
: stage(Object.freeze({ ...merged }), input, d);
|
|
250
|
-
const page = single && isRead(built) && planOf(built).verb === 'page';
|
|
287
|
+
const built = spec.build(input, handlesOf(domain), stepsFor(recipe.id));
|
|
288
|
+
const steps = stepsOf(built);
|
|
289
|
+
let answered;
|
|
290
|
+
if (steps === undefined) {
|
|
291
|
+
const page = isRead(built) && planOf(built).verb === 'page';
|
|
251
292
|
const options = pageCall(recipe.id, call, page ? planOf(built).bound : undefined);
|
|
252
|
-
|
|
253
|
-
|
|
293
|
+
answered = await runStep(run, built, true, options, {});
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
pageCall(recipe.id, call, undefined);
|
|
297
|
+
let merged = {};
|
|
298
|
+
for (const step of [() => steps.first, ...steps.next]) {
|
|
299
|
+
const results = await runStep(run, step(Object.freeze({ ...merged })), false, {}, merged);
|
|
300
|
+
merged = { ...merged, ...results };
|
|
301
|
+
}
|
|
302
|
+
answered = Object.freeze(merged);
|
|
254
303
|
}
|
|
255
|
-
const answered = single ? results : Object.freeze(merged);
|
|
256
304
|
if (spec.output === undefined)
|
|
257
305
|
return answered;
|
|
258
306
|
return spec.output(answered, input, outputContext(recipe, run.model));
|
|
@@ -2,7 +2,7 @@ var _a, _b, _c;
|
|
|
2
2
|
import { QUERY } from '../language/query.js';
|
|
3
3
|
import { HANDLE_MEMBERS, ROW_KEYS } from './model.js';
|
|
4
4
|
import { RelationRead, RootRead } from './plan.js';
|
|
5
|
-
import { and, atom, Condition, describe, foldedTo, folding, kept, orderKeys, propertyHandle, scope, targetCondition, targetLabel, whereOf, within, } from './terms.js';
|
|
5
|
+
import { and, atom, Condition, describe, foldedTo, folding, isPathText, kept, orderKeys, propertyHandle, scope, targetCondition, targetLabel, whereOf, within, } from './terms.js';
|
|
6
6
|
const defined = (fields) => Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
|
|
7
7
|
const nonEmpty = (items) => items.length > 0 ? items : undefined;
|
|
8
8
|
const keysOf = (classes) => classes.map((c) => c.key);
|
|
@@ -221,8 +221,8 @@ function pathCondition(context, paths) {
|
|
|
221
221
|
if (paths.length === 0)
|
|
222
222
|
return foldedTo(Condition.NEVER, atom(context, { op: 'path', paths: [] }));
|
|
223
223
|
for (const path of paths)
|
|
224
|
-
if (
|
|
225
|
-
throw new TypeError(`A path starts with "/"; got ${describe(path)}.`);
|
|
224
|
+
if (!isPathText(path))
|
|
225
|
+
throw new TypeError(`A path starts with "/" or "@"; got ${describe(path)}.`);
|
|
226
226
|
return atom(context, { op: 'path', paths });
|
|
227
227
|
}
|
|
228
228
|
/** The named to-one relation of a handle's Classes: on a union, one every Class names alike. */
|
|
@@ -89,7 +89,15 @@ export declare class CoreNode {
|
|
|
89
89
|
readonly '~core': string;
|
|
90
90
|
constructor(key: string, path: string);
|
|
91
91
|
}
|
|
92
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Whether text is written as a NodePath: it starts with `/` (a Domain anchor) or `@` (a Node's id
|
|
94
|
+
* as its anchor). A NodeId holds neither, so the two never meet.
|
|
95
|
+
*/
|
|
96
|
+
export declare function isPathText(value: unknown): value is string;
|
|
97
|
+
/**
|
|
98
|
+
* The condition a list of targets gives: ids by `id`, an `@<id>` path by its id, so a long list
|
|
99
|
+
* splits as ids do; any other path, and a Core node, by `path`.
|
|
100
|
+
*/
|
|
93
101
|
export declare function targetCondition(context: Context, targets: readonly unknown[]): Condition;
|
|
94
102
|
/** A target as an error names it: its id or its path. */
|
|
95
103
|
export declare function targetLabel(target: unknown): string;
|
|
@@ -239,7 +239,19 @@ export class CoreNode {
|
|
|
239
239
|
Object.freeze(this);
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* Whether text is written as a NodePath: it starts with `/` (a Domain anchor) or `@` (a Node's id
|
|
244
|
+
* as its anchor). A NodeId holds neither, so the two never meet.
|
|
245
|
+
*/
|
|
246
|
+
export function isPathText(value) {
|
|
247
|
+
return typeof value === 'string' && (value.startsWith('/') || value.startsWith('@'));
|
|
248
|
+
}
|
|
249
|
+
/** A NodePath that is a Node's id and no step: `@<id>`, a NodeId being 1 to 255 of `A-Za-z0-9._-`. */
|
|
250
|
+
const ID_PATH = /^@([A-Za-z0-9._-]{1,255})$/;
|
|
251
|
+
/**
|
|
252
|
+
* The condition a list of targets gives: ids by `id`, an `@<id>` path by its id, so a long list
|
|
253
|
+
* splits as ids do; any other path, and a Core node, by `path`.
|
|
254
|
+
*/
|
|
243
255
|
export function targetCondition(context, targets) {
|
|
244
256
|
if (targets.length === 0)
|
|
245
257
|
return foldedTo(Condition.NEVER, atom(context, { op: 'id', ids: [] }));
|
|
@@ -248,8 +260,13 @@ export function targetCondition(context, targets) {
|
|
|
248
260
|
for (const target of targets) {
|
|
249
261
|
if (target instanceof CoreNode)
|
|
250
262
|
paths.push(target.path);
|
|
251
|
-
else if (
|
|
252
|
-
|
|
263
|
+
else if (isPathText(target)) {
|
|
264
|
+
const id = ID_PATH.exec(target)?.[1];
|
|
265
|
+
if (id === undefined)
|
|
266
|
+
paths.push(target);
|
|
267
|
+
else
|
|
268
|
+
ids.push(id);
|
|
269
|
+
}
|
|
253
270
|
else if (typeof target === 'string' && target.length > 0)
|
|
254
271
|
ids.push(target);
|
|
255
272
|
else
|
|
@@ -9,14 +9,13 @@ import type { Recipe, RecipeDefinitions } from './recipe.js';
|
|
|
9
9
|
/** A recipe id: lowercase segments of letters, digits and hyphens, joined by dots. */
|
|
10
10
|
export declare const RECIPE_ID: RegExp;
|
|
11
11
|
export declare const MAXIMUM_RECIPE_ID = 128;
|
|
12
|
-
type
|
|
13
|
-
/** What a recipe reads and answers: its
|
|
12
|
+
type Callback = (...parameters: never[]) => unknown;
|
|
13
|
+
/** What a recipe reads and answers: its `build`, of `(input, d, steps)`, and its `output`. */
|
|
14
14
|
export interface RecipeSpec {
|
|
15
|
-
|
|
16
|
-
readonly
|
|
17
|
-
readonly output?: Stage;
|
|
15
|
+
readonly build: Callback;
|
|
16
|
+
readonly output?: Callback;
|
|
18
17
|
}
|
|
19
|
-
/** The
|
|
18
|
+
/** The build and the output of a v7 recipe; `undefined` for anything else. */
|
|
20
19
|
export declare function recipeSpecOf(recipe: unknown): RecipeSpec | undefined;
|
|
21
20
|
/** A recipe of the Domain's namespace, a Query's or a Mutation's: its id, and its errors. */
|
|
22
21
|
export type NamedRecipe = Pick<Recipe, 'id' | 'errors'>;
|
|
@@ -4,12 +4,11 @@ import { defineQuery as defineV6Query } from '../legacy/v6/define.js';
|
|
|
4
4
|
/** A recipe id: lowercase segments of letters, digits and hyphens, joined by dots. */
|
|
5
5
|
export const RECIPE_ID = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/u;
|
|
6
6
|
export const MAXIMUM_RECIPE_ID = 128;
|
|
7
|
-
const MAXIMUM_STAGES = 8;
|
|
8
7
|
const FIELDS = new Set(['build', 'output', 'errors']);
|
|
9
8
|
const specs = new WeakMap();
|
|
10
9
|
/** The collections open while a build loads a Domain; no recipe is kept outside one. */
|
|
11
10
|
const collections = new Set();
|
|
12
|
-
/** The
|
|
11
|
+
/** The build and the output of a v7 recipe; `undefined` for anything else. */
|
|
13
12
|
export function recipeSpecOf(recipe) {
|
|
14
13
|
return typeof recipe === 'object' && recipe !== null ? specs.get(recipe) : undefined;
|
|
15
14
|
}
|
|
@@ -22,8 +21,9 @@ function defineRecipe(id, spec) {
|
|
|
22
21
|
const unknown = Reflect.ownKeys(fields).filter((key) => typeof key !== 'string' || !FIELDS.has(key));
|
|
23
22
|
if (unknown.length > 0)
|
|
24
23
|
throw new TypeError(`Recipe ${id} takes build, output and errors only; got ${unknown.map(String).join(', ')}: do that work in build or output.`);
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const { build, output } = fields;
|
|
25
|
+
if (typeof build !== 'function')
|
|
26
|
+
throw new TypeError(`Recipe ${id}'s build is a function of (input, d, steps) that returns a read, a record of facets, or steps(first, (earlier) => next, …); got ${describe(build)}.`);
|
|
27
27
|
if (output !== undefined && typeof output !== 'function')
|
|
28
28
|
throw new TypeError(`Recipe ${id}'s output is a function of (results, input, context); got ${describe(output)}.`);
|
|
29
29
|
const errors = fields.errors ?? {};
|
|
@@ -31,19 +31,12 @@ function defineRecipe(id, spec) {
|
|
|
31
31
|
throw new TypeError(`Recipe ${id}'s errors is a record of declared errors by UPPER_SNAKE code; got ${describe(errors)}.`);
|
|
32
32
|
const recipe = Object.freeze({ id, errors: Object.freeze({ ...errors }) });
|
|
33
33
|
collectRecipe(recipe);
|
|
34
|
-
specs.set(recipe, Object.freeze({
|
|
34
|
+
specs.set(recipe, Object.freeze({
|
|
35
|
+
build: build,
|
|
36
|
+
...(output !== undefined ? { output: output } : {}),
|
|
37
|
+
}));
|
|
35
38
|
return recipe;
|
|
36
39
|
}
|
|
37
|
-
function stagesOf(id, build) {
|
|
38
|
-
if (typeof build === 'function')
|
|
39
|
-
return Object.freeze([build]);
|
|
40
|
-
if (!Array.isArray(build) ||
|
|
41
|
-
build.length < 2 ||
|
|
42
|
-
build.length > MAXIMUM_STAGES ||
|
|
43
|
-
build.some((stage) => typeof stage !== 'function'))
|
|
44
|
-
throw new TypeError(`Recipe ${id}'s build is a function, or a list of 2 to ${MAXIMUM_STAGES} stage functions; got ${describe(build)}.`);
|
|
45
|
-
return Object.freeze([...build]);
|
|
46
|
-
}
|
|
47
40
|
/**
|
|
48
41
|
* Keep the errors a recipe declares, a Query's or a Mutation's, and collect it into the
|
|
49
42
|
* collections open: a Mutation recipe shares its Domain's recipe ids and error definitions.
|