@astrale-os/sdk 0.6.0-beta.41 → 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.
Files changed (177) hide show
  1. package/README.md +22 -2
  2. package/dist/application/action/context.d.ts +49 -18
  3. package/dist/application/action/index.d.ts +1 -1
  4. package/dist/application/error/error.d.ts +16 -1
  5. package/dist/application/error/error.js +39 -5
  6. package/dist/application/error/index.d.ts +1 -1
  7. package/dist/application/error/index.js +1 -1
  8. package/dist/application/mutation/authoring/builder.d.ts +81 -33
  9. package/dist/application/mutation/authoring/builder.js +100 -30
  10. package/dist/application/mutation/authoring/guard.d.ts +37 -0
  11. package/dist/application/mutation/authoring/guard.js +150 -0
  12. package/dist/application/mutation/authoring/index.d.ts +5 -2
  13. package/dist/application/mutation/authoring/index.js +2 -1
  14. package/dist/application/mutation/authoring/names.d.ts +19 -0
  15. package/dist/application/mutation/authoring/names.js +83 -0
  16. package/dist/application/mutation/authoring/transition.d.ts +3 -2
  17. package/dist/application/mutation/define.d.ts +24 -10
  18. package/dist/application/mutation/define.js +49 -53
  19. package/dist/application/mutation/execution/execute.d.ts +2 -2
  20. package/dist/application/mutation/execution/execute.js +78 -7
  21. package/dist/application/mutation/execution/executor.d.ts +4 -1
  22. package/dist/application/mutation/execution/executor.js +6 -1
  23. package/dist/application/mutation/failure.d.ts +11 -1
  24. package/dist/application/mutation/failure.js +18 -3
  25. package/dist/application/mutation/index.d.ts +8 -4
  26. package/dist/application/mutation/index.js +5 -2
  27. package/dist/application/mutation/legacy/v6/builder.d.ts +84 -0
  28. package/dist/application/mutation/legacy/v6/define.d.ts +18 -0
  29. package/dist/application/mutation/legacy/v6/define.js +56 -0
  30. package/dist/application/mutation/legacy/v6/mutation.d.ts +28 -0
  31. package/dist/application/mutation/legacy/v6/mutation.js +1 -0
  32. package/dist/application/mutation/{authoring → legacy/v6}/precondition.d.ts +4 -0
  33. package/dist/application/mutation/mutation.d.ts +30 -19
  34. package/dist/application/query/answer/answer.d.ts +117 -0
  35. package/dist/application/query/answer/answer.js +1 -0
  36. package/dist/application/query/answer/conformance.d.ts +4 -0
  37. package/dist/application/query/answer/conformance.js +27 -0
  38. package/dist/application/query/answer/facts.d.ts +17 -0
  39. package/dist/application/query/answer/facts.js +57 -0
  40. package/dist/application/query/composite/builder.d.ts +1 -1
  41. package/dist/application/query/composite/builder.js +1 -1
  42. package/dist/application/query/composite/plan.d.ts +1 -1
  43. package/dist/application/query/composite/plan.js +1 -1
  44. package/dist/application/query/composite/query.d.ts +1 -2
  45. package/dist/application/query/define.d.ts +2 -33
  46. package/dist/application/query/define.js +3 -130
  47. package/dist/application/query/definition.d.ts +12 -0
  48. package/dist/application/query/definition.js +47 -0
  49. package/dist/application/query/execution/bounds.d.ts +22 -0
  50. package/dist/application/query/execution/bounds.js +88 -0
  51. package/dist/application/query/execution/crossings.d.ts +25 -0
  52. package/dist/application/query/execution/crossings.js +192 -0
  53. package/dist/application/query/execution/error.d.ts +68 -0
  54. package/dist/application/query/execution/error.js +50 -0
  55. package/dist/application/query/execution/execute.d.ts +2 -7
  56. package/dist/application/query/execution/execute.js +3 -51
  57. package/dist/application/query/execution/recipe.d.ts +85 -0
  58. package/dist/application/query/execution/recipe.js +324 -0
  59. package/dist/application/query/execution/refusal.d.ts +37 -0
  60. package/dist/application/query/execution/refusal.js +80 -0
  61. package/dist/application/query/execution/rows.d.ts +42 -0
  62. package/dist/application/query/execution/rows.js +308 -0
  63. package/dist/application/query/execution/split.d.ts +24 -0
  64. package/dist/application/query/execution/split.js +190 -0
  65. package/dist/application/query/handles/build.d.ts +10 -0
  66. package/dist/application/query/handles/build.js +53 -0
  67. package/dist/application/query/handles/conditions.d.ts +94 -0
  68. package/dist/application/query/handles/conditions.js +1 -0
  69. package/dist/application/query/handles/domain.d.ts +157 -0
  70. package/dist/application/query/handles/domain.js +1 -0
  71. package/dist/application/query/handles/handles.d.ts +268 -0
  72. package/dist/application/query/handles/handles.js +1 -0
  73. package/dist/application/query/handles/model.d.ts +96 -0
  74. package/dist/application/query/handles/model.js +246 -0
  75. package/dist/application/query/handles/plan.d.ts +99 -0
  76. package/dist/application/query/handles/plan.js +37 -0
  77. package/dist/application/query/handles/reads.d.ts +106 -0
  78. package/dist/application/query/handles/reads.js +839 -0
  79. package/dist/application/query/handles/relations.d.ts +100 -0
  80. package/dist/application/query/handles/relations.js +1 -0
  81. package/dist/application/query/handles/rows.d.ts +94 -0
  82. package/dist/application/query/handles/rows.js +4 -0
  83. package/dist/application/query/handles/terms.d.ts +103 -0
  84. package/dist/application/query/handles/terms.js +282 -0
  85. package/dist/application/query/index.d.ts +10 -7
  86. package/dist/application/query/index.js +9 -4
  87. package/dist/application/query/language/index.d.ts +2 -0
  88. package/dist/application/query/language/index.js +2 -0
  89. package/dist/application/query/language/query.d.ts +323 -0
  90. package/dist/application/query/language/query.js +2 -0
  91. package/dist/application/query/{collection → legacy/v6/collection}/identity.d.ts +2 -2
  92. package/dist/application/query/{collection → legacy/v6/collection}/metadata.d.ts +1 -1
  93. package/dist/application/query/{collection → legacy/v6/collection}/union.d.ts +3 -3
  94. package/dist/application/query/{collection → legacy/v6/collection}/union.js +1 -1
  95. package/dist/application/query/{conformance.d.ts → legacy/v6/conformance.d.ts} +3 -3
  96. package/dist/application/query/{conformance.js → legacy/v6/conformance.js} +1 -1
  97. package/dist/application/query/legacy/v6/define.d.ts +37 -0
  98. package/dist/application/query/legacy/v6/define.js +90 -0
  99. package/dist/application/query/legacy/v6/execution/execute.d.ts +16 -0
  100. package/dist/application/query/legacy/v6/execution/execute.js +50 -0
  101. package/dist/application/query/legacy/v6/index.d.ts +6 -0
  102. package/dist/application/query/legacy/v6/index.js +3 -0
  103. package/dist/application/query/legacy/v6/kernel.d.ts +6 -0
  104. package/dist/application/query/legacy/v6/kernel.js +5 -0
  105. package/dist/application/query/{single → legacy/v6/single}/pagination.js +1 -1
  106. package/dist/application/query/{single → legacy/v6/single}/query.d.ts +2 -14
  107. package/dist/application/query/legacy/v6/single/query.js +1 -0
  108. package/dist/application/query/query.d.ts +16 -1
  109. package/dist/application/query/recipe/define.d.ts +44 -0
  110. package/dist/application/query/recipe/define.js +95 -0
  111. package/dist/application/query/recipe/recipe.d.ts +134 -0
  112. package/dist/application/query/recipe/recipe.js +1 -0
  113. package/dist/application/workflow/durable.d.ts +22 -8
  114. package/dist/deployment/build/build.d.ts +8 -1
  115. package/dist/deployment/build/material.js +3 -0
  116. package/dist/execution/invocation/capabilities/bind.d.ts +6 -4
  117. package/dist/execution/invocation/capabilities/bind.js +39 -3
  118. package/dist/execution/invocation/dispatch/action.js +4 -2
  119. package/dist/execution/invocation/dispatch/context.js +1 -1
  120. package/dist/execution/invocation/dispatch/workflow.js +4 -2
  121. package/dist/execution/invocation/result/index.d.ts +1 -0
  122. package/dist/execution/invocation/result/index.js +1 -0
  123. package/dist/execution/invocation/result/recipe.d.ts +6 -0
  124. package/dist/execution/invocation/result/recipe.js +17 -0
  125. package/dist/execution/service/create.js +1 -0
  126. package/dist/execution/workflows/failure.d.ts +23 -6
  127. package/dist/execution/workflows/failure.js +78 -18
  128. package/dist/execution/workflows/receiver.d.ts +3 -0
  129. package/dist/execution/workflows/receiver.js +1 -0
  130. package/dist/execution/workflows/run.js +13 -9
  131. package/dist/execution/workflows/steps.d.ts +3 -0
  132. package/dist/execution/workflows/steps.js +2 -2
  133. package/dist/platform/schema/kernel-compiled.d.ts +2 -0
  134. package/dist/platform/schema/kernel-compiled.js +2 -0
  135. package/dist/testing/index.d.ts +2 -0
  136. package/dist/testing/index.js +1 -0
  137. package/dist/testing/recipe.d.ts +17 -0
  138. package/dist/testing/recipe.js +34 -0
  139. package/dist/tooling/cli/orchestrate.js +6 -2
  140. package/dist/tooling/linter/adapters/typescript/index.d.ts +2 -1
  141. package/dist/tooling/linter/adapters/typescript/index.js +2 -1
  142. package/dist/tooling/linter/adapters/typescript/query-observation.js +37 -12
  143. package/dist/tooling/linter/adapters/typescript/recipe-definitions.d.ts +34 -0
  144. package/dist/tooling/linter/adapters/typescript/recipe-definitions.js +88 -0
  145. package/dist/tooling/linter/adapters/typescript/type-analysis.d.ts +2 -0
  146. package/dist/tooling/linter/adapters/typescript/type-analysis.js +10 -0
  147. package/dist/tooling/linter/implementations/source/global.js +15 -9
  148. package/dist/tooling/linter/implementations/source/mutations.js +128 -19
  149. package/dist/tooling/linter/implementations/source/policy-edges.d.ts +2 -0
  150. package/dist/tooling/linter/implementations/source/policy-edges.js +113 -0
  151. package/dist/tooling/linter/implementations/source/recipes.d.ts +2 -0
  152. package/dist/tooling/linter/implementations/source/recipes.js +425 -0
  153. package/dist/tooling/linter/implementations/source/shared.d.ts +7 -1
  154. package/dist/tooling/linter/implementations/source/shared.js +31 -10
  155. package/dist/tooling/linter/implementations/source/verifiers.js +4 -0
  156. package/dist/tooling/linter/policy/generated.js +2 -2
  157. package/dist/tooling/linter/requirements/registry.js +4 -0
  158. package/docs/durable-workflows.md +48 -20
  159. package/docs/handler-dependencies.md +76 -4
  160. package/docs/legacy/query-v6.md +62 -0
  161. package/docs/queries.md +442 -0
  162. package/package.json +11 -8
  163. /package/dist/application/{query/single/query.js → mutation/legacy/v6/builder.js} +0 -0
  164. /package/dist/application/mutation/{authoring → legacy/v6}/precondition.js +0 -0
  165. /package/dist/application/query/{collection → legacy/v6/collection}/identity.js +0 -0
  166. /package/dist/application/query/{collection → legacy/v6/collection}/limits.d.ts +0 -0
  167. /package/dist/application/query/{collection → legacy/v6/collection}/limits.js +0 -0
  168. /package/dist/application/query/{collection → legacy/v6/collection}/metadata.js +0 -0
  169. /package/dist/application/query/{execution → legacy/v6/execution}/pages.d.ts +0 -0
  170. /package/dist/application/query/{execution → legacy/v6/execution}/pages.js +0 -0
  171. /package/dist/application/query/{single → legacy/v6/single}/index.d.ts +0 -0
  172. /package/dist/application/query/{single → legacy/v6/single}/index.js +0 -0
  173. /package/dist/application/query/{single → legacy/v6/single}/node.d.ts +0 -0
  174. /package/dist/application/query/{single → legacy/v6/single}/node.js +0 -0
  175. /package/dist/application/query/{single → legacy/v6/single}/pagination.d.ts +0 -0
  176. /package/dist/application/query/{single → legacy/v6/single}/selection.d.ts +0 -0
  177. /package/dist/application/query/{single → legacy/v6/single}/selection.js +0 -0
@@ -0,0 +1,50 @@
1
+ function sentence(code, details) {
2
+ switch (code) {
3
+ case 'NOT_FOUND':
4
+ return details.target === undefined
5
+ ? `no ${String(details.class)} was found`
6
+ : `no ${String(details.class)} ${String(details.target)} was found`;
7
+ case 'NOT_READABLE':
8
+ return `${String(details.class)} ${typeof details.id === 'string' ? details.id : JSON.stringify(details.id)} may not be read`;
9
+ case 'INTEGRITY': {
10
+ const of = details.parent === undefined ? '' : ` of ${String(details.parent)}`;
11
+ return details.found === 0
12
+ ? `${String(details.relation)}${of} found none where one is required`
13
+ : `${String(details.relation)}${of} found a second row where one is allowed`;
14
+ }
15
+ case 'TOO_MANY':
16
+ return `past ${String(details.bound)} ${details.limit === 'max' ? 'rows (max)' : String(details.limit)}`;
17
+ case 'REFUSED':
18
+ return `the Kernel refused the read (${String(details.kernelCode)})`;
19
+ case 'INPUT_INVALID':
20
+ return details.limit === 'in'
21
+ ? `an in takes at most 16 values (at ${String(details.path)})`
22
+ : `the Kernel refused the query (${String(details.kernelCode)}${details.path === undefined ? '' : ` at ${String(details.path)}`})`;
23
+ case 'OUTDATED':
24
+ return "the Host's Registry no longer matches this Domain's build";
25
+ case 'UNSUPPORTED':
26
+ return details.feature === 'v7'
27
+ ? 'the Host does not serve Query v7'
28
+ : 'the Host does not give the Class of each row';
29
+ }
30
+ }
31
+ export class QueryError extends Error {
32
+ code;
33
+ recipe;
34
+ details;
35
+ name = 'QueryError';
36
+ constructor(code,
37
+ /** The recipe's id. */
38
+ recipe, details, options) {
39
+ const reason = options?.reason === undefined ? '' : `: ${options.reason}`;
40
+ super(`${recipe}: ${sentence(code, details)}${reason}`, options);
41
+ this.code = code;
42
+ this.recipe = recipe;
43
+ this.details = details;
44
+ Object.freeze(this.details);
45
+ }
46
+ }
47
+ /** Whether `error` is a `QueryError`, of `code` if given. */
48
+ export function isQueryError(error, code) {
49
+ return error instanceof QueryError && (code === undefined || error.code === code);
50
+ }
@@ -1,13 +1,8 @@
1
- import type { QueryPageRequest, QueryResponse } from '@astrale-os/kernel-client/graph';
2
- import type { QueryAST, QueryResultFor } from '@astrale-os/kernel-core/graph/query';
3
1
  import type { Domain } from '../../../platform/schema/index.js';
4
2
  import type { CompositeQuery } from '../composite/index.js';
3
+ import type { QueryClient } from '../legacy/v6/execution/execute.js';
5
4
  import type { QueryDefinition } from '../query.js';
6
- export interface QueryClient {
7
- query<Ast extends QueryAST>(ast: Ast, options: {
8
- readonly page: QueryPageRequest;
9
- }): Promise<QueryResponse<QueryResultFor<Ast>>>;
10
- }
5
+ export type { QueryClient } from '../legacy/v6/execution/execute.js';
11
6
  export declare function executeQuery<DomainValue extends Domain, Output>(client: QueryClient, domain: DomainValue, query: QueryDefinition<DomainValue, void, Output>): Promise<Output>;
12
7
  export declare function executeQuery<DomainValue extends Domain, Input, Output>(client: QueryClient, domain: DomainValue, query: QueryDefinition<DomainValue, Input, Output>, input: Input): Promise<Output>;
13
8
  export type ExecutableCompositeQuery = CompositeQuery<unknown, unknown, unknown>;
@@ -1,21 +1,13 @@
1
- import { COLLECTION_QUERY_LIMITS } from '../collection/limits.js';
2
- import { compileCollectionUnion, partitionCollectionUnion } from '../collection/union.js';
3
1
  import { createQueryComposition, executeQueryPlan } from '../composite/index.js';
4
- import { requireConformance } from '../conformance.js';
5
2
  import { realizeQuery } from '../define.js';
3
+ import { isPromiseLike } from '../definition.js';
6
4
  import { queryFailure } from '../failure.js';
7
- import { completeSelection, queryResult } from '../single/index.js';
8
- import { executePages } from './pages.js';
5
+ import { conformingClient, realizeSingle, runCollectionUnion, runSingle, } from '../legacy/v6/execution/execute.js';
9
6
  export async function executeQuery(client, domain, query, input) {
10
7
  if (client === null || typeof client !== 'object' || typeof client.query !== 'function') {
11
8
  throw new TypeError('Query execution requires a Client query capability.');
12
9
  }
13
- // The Kernel answers in its active Schema; each value is checked against this client's Domain.
14
- const execute = async (ast, options) => {
15
- const response = await client.query(ast, options);
16
- requireConformance(domain, response.result);
17
- return response;
18
- };
10
+ const execute = conformingClient(client, domain);
19
11
  const operation = realizeQuery(query, domain);
20
12
  try {
21
13
  if (operation.kind === 'single') {
@@ -38,43 +30,3 @@ export async function executeQuery(client, domain, query, input) {
38
30
  throw cause;
39
31
  }
40
32
  }
41
- async function runCollectionUnion(execute, domain, definitions, outputPath) {
42
- const compiled = compileCollectionUnion(domain, definitions, outputPath);
43
- const pages = await executePages(execute, compiled.ast, { size: COLLECTION_QUERY_LIMITS.pageSize }, { maximumPages: COLLECTION_QUERY_LIMITS.maximumPages });
44
- const context = outputPath.length === 0 ? 'collection union' : outputPath.join('.');
45
- const values = compiled.kind === 'node'
46
- ? queryResult.completeNodeValues(pages, context)
47
- : Object.freeze(pages.flatMap((page) => queryResult.edgeItems(page, context).map((item) => {
48
- if (item.edge.kind !== 'value') {
49
- throw new TypeError(`${context} must project Edge values.`);
50
- }
51
- return item.edge.value;
52
- })));
53
- return partitionCollectionUnion(domain, compiled, values);
54
- }
55
- function realizeSingle(definition, domain) {
56
- const operation = realizeQuery(definition, domain);
57
- if (operation.kind !== 'single')
58
- throw new TypeError('Composite Query leaves must be single.');
59
- return operation;
60
- }
61
- async function runSingle(execute, definition, input) {
62
- const prepared = definition.prepare?.(input) ?? input;
63
- const ast = definition.build(prepared);
64
- if (definition.pagination !== undefined) {
65
- const collected = await executePages(execute, ast, definition.page, definition.pagination);
66
- return definition.project(collected, prepared, {
67
- ast,
68
- complete: completeSelection(collected),
69
- });
70
- }
71
- const response = await execute(ast, { page: definition.page });
72
- return definition.project === undefined
73
- ? response.result
74
- : definition.project(response.result, prepared, { ast });
75
- }
76
- function isPromiseLike(input) {
77
- return (input !== null &&
78
- (typeof input === 'object' || typeof input === 'function') &&
79
- typeof input.then === 'function');
80
- }
@@ -0,0 +1,85 @@
1
+ /**
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
+ * before it sends anything, and every Kernel refusal of a read is a `QueryError`.
5
+ */
6
+ import type { Domain } from '../../../platform/schema/index.js';
7
+ import type { ReadPlan } from '../handles/plan.js';
8
+ import type { AnyQuery } from '../language/query.js';
9
+ import type { Errors, InputOf, OutputContext, OutputOf, Recipe } from '../recipe/recipe.js';
10
+ import { type Model } from '../handles/model.js';
11
+ import { type DeclaredClass } from './crossings.js';
12
+ /** What a recipe sends a read with: the cursor a page continues. */
13
+ export interface RecipeQueryOptions {
14
+ readonly paging?: {
15
+ readonly after: string;
16
+ };
17
+ }
18
+ /** The client a recipe reads through: one v7 document per request. */
19
+ export interface RecipeClient {
20
+ query(document: AnyQuery, options: RecipeQueryOptions): Promise<unknown>;
21
+ }
22
+ /** A page recipe's call: its size, from 1 to its `max` (by default), and the cursor it continues. */
23
+ export interface RecipeCallOptions {
24
+ readonly size?: number;
25
+ readonly after?: string;
26
+ }
27
+ /** A recipe call's options: the authority it reads under, and a page recipe's size and cursor. */
28
+ export type RecipeOptions<R, As extends string> = {
29
+ readonly as: As;
30
+ } & ('~page' extends keyof R ? RecipeCallOptions : {});
31
+ type Step = (...parameters: readonly unknown[]) => unknown;
32
+ /** One recipe call: its reads so far, against S11, and the Classes they cross, against S12. */
33
+ export declare class Run {
34
+ #private;
35
+ readonly client: RecipeClient;
36
+ readonly domain: Domain;
37
+ readonly recipe: string;
38
+ readonly model: Model;
39
+ constructor(client: RecipeClient, domain: Domain, recipe: string, declared?: readonly DeclaredClass[]);
40
+ /**
41
+ * S11, before a step sends: its reads, its verbs' bounds, its rows and each property `in` its
42
+ * documents write. Then S12: each foreign Class its relations cross.
43
+ */
44
+ admit(plans: readonly ReadPlan[]): void;
45
+ /** One read's result: its verb's empty answer, or its documents' answers merged and rendered. */
46
+ read(plan: ReadPlan, paging?: RecipeCallOptions): Promise<unknown>;
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;
61
+ /** Where each facet's results go: one read's index, a list of them, or nothing. */
62
+ type Shape = readonly (readonly [string, number | readonly number[] | undefined])[];
63
+ /**
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
66
+ * results keep one value per name.
67
+ */
68
+ export declare function planStep(run: Run, built: unknown, single: boolean, earlier?: Readonly<Record<string, unknown>>): {
69
+ readonly plans: readonly ReadPlan[];
70
+ readonly shape?: Shape;
71
+ };
72
+ /**
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).
78
+ */
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>>;
80
+ /**
81
+ * The context a recipe's `output`, or a Mutation recipe's `reject`, receives: its declared errors,
82
+ * and `is`, which narrows a row by Class.
83
+ */
84
+ export declare function outputContext(recipe: object, model: Model): OutputContext<unknown, Errors>;
85
+ export {};
@@ -0,0 +1,324 @@
1
+ import * as errors from '@astrale-os/kernel-protocol/errors';
2
+ import { isSnapshotStale, ResponseError, retryAfterOf } from '../../../platform/client/index.js';
3
+ import { declaredErrorFactory } from '../../error/error.js';
4
+ import { requireAnswerConformance } from '../answer/conformance.js';
5
+ import { handlesOf } from '../handles/build.js';
6
+ import { modelOf } from '../handles/model.js';
7
+ import { isRead, planOf } from '../handles/plan.js';
8
+ import { describe } from '../handles/terms.js';
9
+ import { recipeSpecOf } from '../recipe/define.js';
10
+ import { MAXIMUM_BOUND, MAXIMUM_READS, MAXIMUM_ROWS, MAXIMUM_STEPS, pastPage, rowsOf, wideIn, } from './bounds.js';
11
+ import { Crossings } from './crossings.js';
12
+ import { QueryError } from './error.js';
13
+ import { describeRefusals, isUnserved, refusalsOf } from './refusal.js';
14
+ import { emptyResult, rootResult } from './rows.js';
15
+ import { merge, split } from './split.js';
16
+ /** Reads of one step in flight at once. */
17
+ const IN_FLIGHT = 8;
18
+ /** `QUERY_SNAPSHOT_STALE`: nothing was read, so the read is sent again at once, this many times. */
19
+ const STALE_RESENDS = 3;
20
+ /** A refusal's protocol code by name, for one that gives no reason. */
21
+ const CODE_NAMES = new Map(Object.entries(errors.CODES).map(([name, code]) => [code, name]));
22
+ /** One recipe call: its reads so far, against S11, and the Classes they cross, against S12. */
23
+ export class Run {
24
+ client;
25
+ domain;
26
+ recipe;
27
+ #reads = 0;
28
+ #rows = 0;
29
+ model;
30
+ #crossings;
31
+ constructor(client, domain, recipe, declared) {
32
+ this.client = client;
33
+ this.domain = domain;
34
+ this.recipe = recipe;
35
+ this.model = modelOf(domain);
36
+ this.#crossings =
37
+ declared === undefined ? undefined : new Crossings(this.model, domain, declared);
38
+ }
39
+ /**
40
+ * S11, before a step sends: its reads, its verbs' bounds, its rows and each property `in` its
41
+ * documents write. Then S12: each foreign Class its relations cross.
42
+ */
43
+ admit(plans) {
44
+ this.#reads += plans.length;
45
+ if (this.#reads > MAXIMUM_READS)
46
+ throw new QueryError('TOO_MANY', this.recipe, { limit: 'reads', bound: MAXIMUM_READS });
47
+ if (plans.some(pastPage))
48
+ throw new QueryError('TOO_MANY', this.recipe, { limit: 'rows', bound: MAXIMUM_BOUND });
49
+ this.#rows += plans.reduce((total, plan) => total + rowsOf(plan), 0);
50
+ if (this.#rows > MAXIMUM_ROWS)
51
+ throw new QueryError('TOO_MANY', this.recipe, { limit: 'rows', bound: MAXIMUM_ROWS });
52
+ for (const plan of plans) {
53
+ const path = plan.document === undefined ? undefined : wideIn(plan.document);
54
+ // Into the request `{ document, paging }`, as the Kernel's refusals point.
55
+ if (path !== undefined)
56
+ throw new QueryError('INPUT_INVALID', this.recipe, {
57
+ limit: 'in',
58
+ path: `/document${path}`,
59
+ });
60
+ }
61
+ for (const plan of plans)
62
+ this.#crossings?.check(this.recipe, plan);
63
+ }
64
+ /** One read's result: its verb's empty answer, or its documents' answers merged and rendered. */
65
+ async read(plan, paging) {
66
+ if (plan.empty)
67
+ return emptyResult(this.recipe, plan);
68
+ let document = plan.document;
69
+ if (plan.verb === 'page' && paging?.size !== undefined)
70
+ document = withLimit(document, paging.size);
71
+ const documents = plan.verb === 'page' || plan.verb === 'document'
72
+ ? [document]
73
+ : split(document, document.list?.orderBy !== undefined);
74
+ const answers = [];
75
+ for (const sent of documents) {
76
+ const answer = await this.#send(sent, paging?.after);
77
+ if ('list' in answer)
78
+ requireAnswerConformance(this.domain, answer);
79
+ answers.push(answer);
80
+ }
81
+ return rootResult(this.recipe, this.model, plan, merge(document, answers));
82
+ }
83
+ async #send(document, after) {
84
+ const paging = after === undefined ? {} : { paging: { after } };
85
+ for (let attempt = 0;; attempt += 1) {
86
+ try {
87
+ return await this.client.query(document, paging);
88
+ }
89
+ catch (error) {
90
+ if (isSnapshotStale(error) && attempt < STALE_RESENDS)
91
+ continue;
92
+ throw this.#refusal(error);
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * A Kernel refusal as a `QueryError`. A network failure, or a refusal that prescribes when to
98
+ * send again (capacity, draining), is not the read's: it passes as it came, for the caller to
99
+ * retry.
100
+ */
101
+ #refusal(error) {
102
+ if (!(error instanceof ResponseError) || retryAfterOf(error) !== undefined)
103
+ return error;
104
+ const kernelCode = error.reason?.code ?? CODE_NAMES.get(error.code) ?? String(error.code);
105
+ const details = (error.reason?.details ?? {});
106
+ const cause = { cause: error };
107
+ if (isSnapshotStale(error))
108
+ return new QueryError('REFUSED', this.recipe, { kernelCode, retryable: true }, cause);
109
+ const refusals = refusalsOf(error);
110
+ if (refusals !== undefined) {
111
+ const path = refusals[0]?.path ?? (typeof details.path === 'string' ? details.path : undefined);
112
+ return new QueryError('INPUT_INVALID', this.recipe, {
113
+ kernelCode,
114
+ ...(typeof details.phase === 'string' ? { phase: details.phase } : {}),
115
+ ...(path === undefined ? {} : { path }),
116
+ }, { ...cause, ...(refusals.length === 0 ? {} : { reason: describeRefusals(refusals) }) });
117
+ }
118
+ if (kernelCode === 'QUERY_EXPECTATION_MISMATCH')
119
+ return new QueryError('OUTDATED', this.recipe, { kernelCode: 'QUERY_EXPECTATION_MISMATCH' }, cause);
120
+ // A Kernel that predates v7 refuses its request at the Function's input.
121
+ if (isUnserved(error))
122
+ return new QueryError('UNSUPPORTED', this.recipe, { feature: 'v7' }, cause);
123
+ // Only a trusted backend answers v7 (a maintenance session's refusal says when to retry).
124
+ if (kernelCode === 'QUERY_BACKEND_CONTRACT')
125
+ return new QueryError('UNSUPPORTED', this.recipe, { feature: 'v7' }, cause);
126
+ return new QueryError('REFUSED', this.recipe, { kernelCode, retryable: false }, cause);
127
+ }
128
+ }
129
+ /** A page document of `size` rows. */
130
+ function withLimit(document, size) {
131
+ const list = document.list;
132
+ return { ...document, list: { ...list, limit: size } };
133
+ }
134
+ /** Each task, at most `limit` at once, the results in order; after a failure, no task starts. */
135
+ async function pooled(tasks, limit) {
136
+ const results = [];
137
+ let next = 0;
138
+ let failed = false;
139
+ const worker = async () => {
140
+ while (!failed && next < tasks.length) {
141
+ const index = next++;
142
+ try {
143
+ results[index] = await tasks[index]();
144
+ }
145
+ catch (error) {
146
+ failed = true;
147
+ throw error;
148
+ }
149
+ }
150
+ };
151
+ await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
152
+ return results;
153
+ }
154
+ const stepped = new WeakMap();
155
+ /**
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.
199
+ */
200
+ function facetsOf(recipe, built, single) {
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)}.`);
206
+ }
207
+ return Object.entries(built);
208
+ }
209
+ /** A page is a recipe's single read: one step that returns it. */
210
+ function refusePage(recipe, plan) {
211
+ if (plan.verb === 'page')
212
+ throw new TypeError(`Recipe ${recipe}: a page is a recipe's single read (D22); read the others with first or all.`);
213
+ }
214
+ /**
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
217
+ * results keep one value per name.
218
+ */
219
+ export function planStep(run, built, single, earlier = {}) {
220
+ if (single && isRead(built)) {
221
+ const plans = [planOf(built)];
222
+ run.admit(plans);
223
+ return { plans };
224
+ }
225
+ const facets = facetsOf(run.recipe, built, single);
226
+ for (const [name] of facets)
227
+ if (Object.hasOwn(earlier, name))
228
+ throw new TypeError(`Recipe ${run.recipe}: an earlier step read the facet ${name}; name this step's facet otherwise.`);
229
+ const plans = [];
230
+ const shape = [];
231
+ for (const [name, facet] of facets) {
232
+ if (facet === undefined)
233
+ shape.push([name, undefined]);
234
+ else if (Array.isArray(facet))
235
+ shape.push([name, facet.map((read) => plans.push(planOf(read)) - 1)]);
236
+ else
237
+ shape.push([name, plans.push(planOf(facet)) - 1]);
238
+ }
239
+ plans.forEach((plan) => refusePage(run.recipe, plan));
240
+ run.admit(plans);
241
+ return { plans, shape };
242
+ }
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);
246
+ if (shape === undefined) {
247
+ const plan = plans[0];
248
+ return run.read(plan, plan.verb === 'page' ? call : undefined);
249
+ }
250
+ const results = await pooled(plans.map((plan) => () => run.read(plan)), IN_FLIGHT);
251
+ const record = {};
252
+ for (const [name, at] of shape)
253
+ record[name] =
254
+ at === undefined
255
+ ? undefined
256
+ : typeof at === 'number'
257
+ ? results[at]
258
+ : Object.freeze(at.map((index) => results[index]));
259
+ return Object.freeze(record);
260
+ }
261
+ /** The size and cursor of a page call, checked against the page's `max`. */
262
+ function pageCall(recipe, call, max) {
263
+ const { size, after } = call;
264
+ if (max === undefined) {
265
+ if (size !== undefined || after !== undefined)
266
+ throw new TypeError(`Recipe ${recipe} is not a page: size and after continue a page recipe.`);
267
+ return {};
268
+ }
269
+ if (size !== undefined && (!Number.isInteger(size) || size < 1 || size > max))
270
+ throw new TypeError(`Recipe ${recipe}: size is a whole number from 1 to the page's max, ${max}; got ${describe(size)}.`);
271
+ if (after !== undefined && (typeof after !== 'string' || after.length === 0))
272
+ throw new TypeError(`Recipe ${recipe}: after is the cursor a page returned as next.`);
273
+ return { size: size ?? max, ...(after === undefined ? {} : { after }) };
274
+ }
275
+ /**
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).
281
+ */
282
+ export async function runQueryRecipe(client, domain, recipe, input, call = {}, declared) {
283
+ const spec = recipeSpecOf(recipe);
284
+ if (spec === undefined)
285
+ throw new TypeError(`Not a v7 recipe: define one with defineQuery<Schema>()(id, { build }); got ${describe(recipe)}.`);
286
+ const run = new Run(client, domain, recipe.id, declared);
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';
292
+ const options = pageCall(recipe.id, call, page ? planOf(built).bound : undefined);
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);
303
+ }
304
+ if (spec.output === undefined)
305
+ return answered;
306
+ return spec.output(answered, input, outputContext(recipe, run.model));
307
+ }
308
+ /**
309
+ * The context a recipe's `output`, or a Mutation recipe's `reject`, receives: its declared errors,
310
+ * and `is`, which narrows a row by Class.
311
+ */
312
+ export function outputContext(recipe, model) {
313
+ return {
314
+ error: declaredErrorFactory(recipe),
315
+ is: (row, cls) => {
316
+ try {
317
+ return model.satisfies(model.classOf(row.$class)).has(cls);
318
+ }
319
+ catch {
320
+ return false;
321
+ }
322
+ },
323
+ };
324
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The Kernel's rejections of a v7 Query, made readable: its refusals (`QUERY_INPUT_INVALID`), and
3
+ * a Host that does not serve v7 at all (`FUNCTION_INPUT_INVALID`).
4
+ */
5
+ import { ResponseError } from '../../../platform/client/index.js';
6
+ /** A decode refusal, as the wire writes it: the fault's JSON pointer, what it takes, a fix. */
7
+ export interface QueryDecodeRefusal {
8
+ readonly phase: 'decode';
9
+ readonly code: string;
10
+ /** RFC 6901, into the request `{document, paging}` the SDK sent. */
11
+ readonly path: string;
12
+ readonly expected: string;
13
+ readonly fix: string;
14
+ readonly tier?: string;
15
+ }
16
+ /** A planning refusal: the issue and its pointer into the request. */
17
+ export interface QueryPlanRefusal {
18
+ readonly phase: 'plan';
19
+ readonly issue: string;
20
+ readonly path?: string;
21
+ }
22
+ /** A caller-visible plan bound the Query exceeds. */
23
+ export interface QueryLimitRefusal {
24
+ readonly phase: 'plan';
25
+ readonly issue: 'QUERY_PLAN_LIMIT_EXCEEDED';
26
+ readonly limit: string;
27
+ readonly maximum: number;
28
+ readonly actual: number;
29
+ readonly path?: string;
30
+ }
31
+ export type QueryRefusal = QueryDecodeRefusal | QueryPlanRefusal | QueryLimitRefusal;
32
+ /** The refusals a `QUERY_INPUT_INVALID` rejection carries, or `undefined` for any other error. */
33
+ export declare function refusalsOf(error: unknown): readonly QueryRefusal[] | undefined;
34
+ /** The refusals in one line: the first three, then how many more. */
35
+ export declare function describeRefusals(refusals: readonly QueryRefusal[]): string;
36
+ /** Whether a rejection of a v7 request says the Host does not serve it. */
37
+ export declare function isUnserved(error: unknown): error is ResponseError;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The Kernel's rejections of a v7 Query, made readable: its refusals (`QUERY_INPUT_INVALID`), and
3
+ * a Host that does not serve v7 at all (`FUNCTION_INPUT_INVALID`).
4
+ */
5
+ import { ResponseError } from '../../../platform/client/index.js';
6
+ const SHOWN = 3;
7
+ const text = (value) => (typeof value === 'string' ? value : undefined);
8
+ const count = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
9
+ /** The refusals a `QUERY_INPUT_INVALID` rejection carries, or `undefined` for any other error. */
10
+ export function refusalsOf(error) {
11
+ if (!(error instanceof ResponseError) || error.reason?.code !== 'QUERY_INPUT_INVALID')
12
+ return undefined;
13
+ const details = error.reason.details;
14
+ if (details.phase === 'decode' && Array.isArray(details.refusals)) {
15
+ return Object.freeze(details.refusals.flatMap((entry) => {
16
+ const refusal = (entry ?? {});
17
+ const code = text(refusal.code);
18
+ const path = text(refusal.path);
19
+ if (code === undefined || path === undefined)
20
+ return [];
21
+ const tier = text(refusal.tier);
22
+ return [
23
+ Object.freeze({
24
+ phase: 'decode',
25
+ code,
26
+ path,
27
+ expected: text(refusal.expected) ?? '',
28
+ fix: text(refusal.fix) ?? '',
29
+ ...(tier === undefined ? {} : { tier }),
30
+ }),
31
+ ];
32
+ }));
33
+ }
34
+ const issue = text(details.issue);
35
+ if (details.phase !== 'plan' || issue === undefined)
36
+ return Object.freeze([]);
37
+ const path = text(details.path);
38
+ const limit = text(details.limit);
39
+ const maximum = count(details.maximum);
40
+ const actual = count(details.actual);
41
+ if (issue === 'QUERY_PLAN_LIMIT_EXCEEDED' &&
42
+ limit !== undefined &&
43
+ maximum !== undefined &&
44
+ actual !== undefined) {
45
+ return Object.freeze([
46
+ Object.freeze({
47
+ phase: 'plan',
48
+ issue,
49
+ limit,
50
+ maximum,
51
+ actual,
52
+ ...(path === undefined ? {} : { path }),
53
+ }),
54
+ ]);
55
+ }
56
+ return Object.freeze([
57
+ Object.freeze({ phase: 'plan', issue, ...(path === undefined ? {} : { path }) }),
58
+ ]);
59
+ }
60
+ function describe(refusal) {
61
+ if (refusal.phase === 'decode') {
62
+ const fix = refusal.fix.length === 0 ? '' : ` (${refusal.fix})`;
63
+ return `${refusal.code} at ${refusal.path}: expected ${refusal.expected}${fix}`;
64
+ }
65
+ const at = refusal.path === undefined ? '' : ` at ${refusal.path}`;
66
+ if ('limit' in refusal) {
67
+ return `${refusal.issue}${at}: its ${refusal.limit} limit is ${String(refusal.maximum)}, it needs ${String(refusal.actual)}`;
68
+ }
69
+ return `${refusal.issue}${at}`;
70
+ }
71
+ /** The refusals in one line: the first three, then how many more. */
72
+ export function describeRefusals(refusals) {
73
+ const listed = refusals.slice(0, SHOWN).map(describe);
74
+ const rest = refusals.length > SHOWN ? `; and ${String(refusals.length - SHOWN)} more` : '';
75
+ return `${listed.join('; ')}${rest}`;
76
+ }
77
+ /** Whether a rejection of a v7 request says the Host does not serve it. */
78
+ export function isUnserved(error) {
79
+ return error instanceof ResponseError && error.reason?.code === 'FUNCTION_INPUT_INVALID';
80
+ }