@astrale-os/sdk 0.5.0-beta.64 → 0.5.0-beta.65
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/CHANGELOG.md +12 -0
- package/dist/application/query/collection/identity.d.ts +4 -0
- package/dist/application/query/collection/identity.js +15 -0
- package/dist/application/query/collection/limits.d.ts +5 -0
- package/dist/application/query/collection/limits.js +5 -0
- package/dist/application/query/collection/metadata.d.ts +11 -0
- package/dist/application/query/collection/metadata.js +62 -0
- package/dist/application/query/collection/union.d.ts +19 -0
- package/dist/application/query/collection/union.js +157 -0
- package/dist/application/query/composite/builder.d.ts +7 -1
- package/dist/application/query/composite/builder.js +7 -0
- package/dist/application/query/composite/plan.d.ts +16 -1
- package/dist/application/query/composite/plan.js +45 -16
- package/dist/application/query/composite/query.d.ts +10 -2
- package/dist/application/query/define.d.ts +16 -4
- package/dist/application/query/define.js +48 -5
- package/dist/application/query/execution/execute.d.ts +2 -1
- package/dist/application/query/execution/execute.js +22 -5
- package/dist/application/query/execution/executor.d.ts +2 -1
- package/dist/application/query/execution/executor.js +1 -1
- package/dist/application/query/index.d.ts +3 -2
- package/dist/application/query/index.js +1 -1
- package/dist/application/query/query.d.ts +9 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tooling/linter/adapters/typescript/index.d.ts +1 -0
- package/dist/tooling/linter/adapters/typescript/index.js +1 -0
- package/dist/tooling/linter/adapters/typescript/query-collection-type.d.ts +4 -0
- package/dist/tooling/linter/adapters/typescript/query-collection-type.js +94 -0
- package/dist/tooling/linter/adapters/typescript/query-observation.js +18 -4
- package/dist/tooling/linter/analysis/query/observation.d.ts +1 -1
- package/dist/tooling/linter/implementations/source/global.js +2 -0
- package/dist/tooling/linter/implementations/source/queries.js +178 -14
- package/dist/tooling/linter/implementations/source/shared.d.ts +1 -1
- package/dist/tooling/linter/implementations/source/shared.js +6 -1
- package/dist/tooling/linter/policy/generated.d.ts +1 -1
- package/dist/tooling/linter/policy/generated.js +3 -3
- package/dist/tooling/linter/requirements/registry.js +1 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0-beta.65](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.64...sdk-v0.5.0-beta.65) (2026-08-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **query:** add portable collection unions ([#291](https://github.com/astrale-os/sdk/issues/291)) ([1ea53bc](https://github.com/astrale-os/sdk/commit/1ea53bcf4767ae5adb9637a5942e674d839fb231))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* document safe remote route refresh ([#298](https://github.com/astrale-os/sdk/issues/298)) ([bd0d9b4](https://github.com/astrale-os/sdk/commit/bd0d9b479b61ba5007fed3abd30e7aeee1035df4))
|
|
14
|
+
|
|
3
15
|
## [0.5.0-beta.64](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.63...sdk-v0.5.0-beta.64) (2026-08-27)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Edge } from '../../../platform/graph/edge/index.js';
|
|
2
|
+
import type { Node } from '../../../platform/graph/node/index.js';
|
|
3
|
+
export declare function collectionIdentityOf(value: Node | Edge): string;
|
|
4
|
+
export declare function requireUniqueCollectionValues<Value extends Node | Edge>(values: readonly Value[], context: string): readonly Value[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { edgeKeyOf, encodeEdgeKey } from '@astrale-os/kernel-core/graph/edge';
|
|
2
|
+
export function collectionIdentityOf(value) {
|
|
3
|
+
return 'id' in value ? `node:${value.id}` : `edge:${encodeEdgeKey(edgeKeyOf(value))}`;
|
|
4
|
+
}
|
|
5
|
+
export function requireUniqueCollectionValues(values, context) {
|
|
6
|
+
const identities = new Set();
|
|
7
|
+
for (const value of values) {
|
|
8
|
+
const identity = collectionIdentityOf(value);
|
|
9
|
+
if (identities.has(identity)) {
|
|
10
|
+
throw new TypeError(`${context} returned duplicate identity ${identity}.`);
|
|
11
|
+
}
|
|
12
|
+
identities.add(identity);
|
|
13
|
+
}
|
|
14
|
+
return values;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AnyResolvedClass } from '@astrale-os/kernel-dsl/v1';
|
|
2
|
+
import type { Domain } from '../../../platform/schema/index.js';
|
|
3
|
+
export interface CollectionQuerySpec<Class extends AnyResolvedClass = AnyResolvedClass> {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly class: Class;
|
|
6
|
+
}
|
|
7
|
+
type CollectionProject = (domain: Domain) => CollectionQuerySpec;
|
|
8
|
+
export declare function registerCollectionQuery(definition: object, project: CollectionProject): void;
|
|
9
|
+
export declare function isCollectionQueryDefinition(definition: unknown): definition is object;
|
|
10
|
+
export declare function realizeCollectionQuery(definition: object, domain: Domain, memberPath?: string): CollectionQuerySpec;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const projects = new WeakMap();
|
|
2
|
+
const specifications = new WeakMap();
|
|
3
|
+
export function registerCollectionQuery(definition, project) {
|
|
4
|
+
if (projects.has(definition))
|
|
5
|
+
throw new TypeError('Collection Query is already registered.');
|
|
6
|
+
projects.set(definition, project);
|
|
7
|
+
}
|
|
8
|
+
export function isCollectionQueryDefinition(definition) {
|
|
9
|
+
return definition !== null && typeof definition === 'object' && projects.has(definition);
|
|
10
|
+
}
|
|
11
|
+
export function realizeCollectionQuery(definition, domain, memberPath = 'Collection Query') {
|
|
12
|
+
const cached = specifications.get(definition)?.get(domain);
|
|
13
|
+
if (cached !== undefined)
|
|
14
|
+
return cached;
|
|
15
|
+
const project = projects.get(definition);
|
|
16
|
+
if (project === undefined) {
|
|
17
|
+
invalid(`${memberPath} is not a registered collection Query definition.`);
|
|
18
|
+
}
|
|
19
|
+
const authored = project(domain);
|
|
20
|
+
if (isPromiseLike(authored))
|
|
21
|
+
invalid(`${memberPath} projection must be synchronous.`);
|
|
22
|
+
if (!isExactSpec(authored))
|
|
23
|
+
invalid(`${memberPath} specification is invalid.`);
|
|
24
|
+
if (domain.definition(authored.class.key) !== authored.class) {
|
|
25
|
+
invalid(`${memberPath} selected a Class outside its Domain closure.`);
|
|
26
|
+
}
|
|
27
|
+
const admitted = Object.freeze({ id: authored.id, class: authored.class });
|
|
28
|
+
let byDomain = specifications.get(definition);
|
|
29
|
+
if (byDomain === undefined) {
|
|
30
|
+
byDomain = new WeakMap();
|
|
31
|
+
specifications.set(definition, byDomain);
|
|
32
|
+
}
|
|
33
|
+
byDomain.set(domain, admitted);
|
|
34
|
+
return admitted;
|
|
35
|
+
}
|
|
36
|
+
function isExactSpec(input) {
|
|
37
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
38
|
+
return false;
|
|
39
|
+
const keys = Reflect.ownKeys(input);
|
|
40
|
+
if (keys.length !== 2 ||
|
|
41
|
+
!Object.hasOwn(input, 'id') ||
|
|
42
|
+
!Object.hasOwn(input, 'class') ||
|
|
43
|
+
typeof Reflect.get(input, 'id') !== 'string' ||
|
|
44
|
+
Reflect.get(input, 'id').length === 0 ||
|
|
45
|
+
Reflect.get(input, 'id').trim() !== Reflect.get(input, 'id')) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const selected = Reflect.get(input, 'class');
|
|
49
|
+
return (selected !== null &&
|
|
50
|
+
typeof selected === 'object' &&
|
|
51
|
+
Reflect.get(selected, 'ref') !== undefined &&
|
|
52
|
+
Reflect.get(Reflect.get(selected, 'ref'), 'kind') === 'class' &&
|
|
53
|
+
(Reflect.get(selected, 'kind') === 'node' || Reflect.get(selected, 'kind') === 'edge'));
|
|
54
|
+
}
|
|
55
|
+
function isPromiseLike(input) {
|
|
56
|
+
return (input !== null &&
|
|
57
|
+
(typeof input === 'object' || typeof input === 'function') &&
|
|
58
|
+
typeof input.then === 'function');
|
|
59
|
+
}
|
|
60
|
+
function invalid(message) {
|
|
61
|
+
throw new TypeError(message);
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { QueryAST } from '@astrale-os/kernel-core/graph/query';
|
|
2
|
+
import type { AnyResolvedClass } from '@astrale-os/kernel-dsl/v1';
|
|
3
|
+
import type { Edge } from '../../../platform/graph/edge/index.js';
|
|
4
|
+
import type { Node } from '../../../platform/graph/node/index.js';
|
|
5
|
+
import type { Domain } from '../../../platform/schema/index.js';
|
|
6
|
+
export type CollectionDefinitionRecord = Readonly<Record<string, object>>;
|
|
7
|
+
export interface CollectionUnionMember {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly class: AnyResolvedClass;
|
|
11
|
+
}
|
|
12
|
+
export interface CompiledCollectionUnion {
|
|
13
|
+
readonly ast: QueryAST;
|
|
14
|
+
readonly kind: 'node' | 'edge';
|
|
15
|
+
readonly members: readonly CollectionUnionMember[];
|
|
16
|
+
}
|
|
17
|
+
export declare function admitCollectionDefinitions(definitions: unknown): readonly (readonly [string, object])[];
|
|
18
|
+
export declare function compileCollectionUnion(domain: Domain, definitions: CollectionDefinitionRecord, outputPath: readonly string[]): CompiledCollectionUnion;
|
|
19
|
+
export declare function partitionCollectionUnion(domain: Domain, compiled: CompiledCollectionUnion, values: readonly (Node | Edge)[], maximumLogicalAssignments?: number): Readonly<Record<string, readonly (Node | Edge)[]>>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Query, QueryAST as QueryDocument } from '@astrale-os/kernel-core/graph/query';
|
|
2
|
+
import { ClassKey } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
3
|
+
import { QueryCompositionError } from '../composite/errors.js';
|
|
4
|
+
import { collectionIdentityOf } from './identity.js';
|
|
5
|
+
import { COLLECTION_QUERY_LIMITS } from './limits.js';
|
|
6
|
+
import { isCollectionQueryDefinition, realizeCollectionQuery, } from './metadata.js';
|
|
7
|
+
export function admitCollectionDefinitions(definitions) {
|
|
8
|
+
if (!isPlainRecord(definitions))
|
|
9
|
+
invalid('Collection union must be a plain record.');
|
|
10
|
+
const keys = Reflect.ownKeys(definitions);
|
|
11
|
+
if (keys.length < 2)
|
|
12
|
+
invalid('Collection union requires at least two members.');
|
|
13
|
+
if (keys.some((key) => typeof key !== 'string' ||
|
|
14
|
+
key.length === 0 ||
|
|
15
|
+
key.trim() !== key ||
|
|
16
|
+
Object.getOwnPropertyDescriptor(definitions, key)?.enumerable !== true)) {
|
|
17
|
+
invalid('Collection union members must be own enumerable non-empty string keys.');
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze(keys.map((name) => {
|
|
20
|
+
if (typeof name !== 'string')
|
|
21
|
+
invalid('Collection union member key is invalid.');
|
|
22
|
+
const definition = definitions[name];
|
|
23
|
+
if (!isCollectionQueryDefinition(definition)) {
|
|
24
|
+
invalid(`${name} is not a collection Query definition.`);
|
|
25
|
+
}
|
|
26
|
+
return Object.freeze([name, definition]);
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
export function compileCollectionUnion(domain, definitions, outputPath) {
|
|
30
|
+
const realized = admitCollectionDefinitions(definitions).map(([name, definition]) => ({
|
|
31
|
+
name,
|
|
32
|
+
specification: realizeMember(definition, domain, outputPath, name),
|
|
33
|
+
}));
|
|
34
|
+
if (realized.length > QueryDocument.limits.sourceTerms) {
|
|
35
|
+
limit(`${pathOf(outputPath)} has ${realized.length} collection members; maximum is ${QueryDocument.limits.sourceTerms}.`);
|
|
36
|
+
}
|
|
37
|
+
const ids = new Map();
|
|
38
|
+
const classes = new Map();
|
|
39
|
+
for (const { name, specification } of realized) {
|
|
40
|
+
const memberPath = pathOf(outputPath, name);
|
|
41
|
+
const priorId = ids.get(specification.id);
|
|
42
|
+
if (priorId !== undefined) {
|
|
43
|
+
invalid(`${memberPath} duplicates Query ID selected by ${priorId}.`);
|
|
44
|
+
}
|
|
45
|
+
ids.set(specification.id, memberPath);
|
|
46
|
+
const priorClass = classes.get(specification.class.key);
|
|
47
|
+
if (priorClass !== undefined) {
|
|
48
|
+
invalid(`${memberPath} duplicates Class selected by ${priorClass}.`);
|
|
49
|
+
}
|
|
50
|
+
classes.set(specification.class.key, memberPath);
|
|
51
|
+
}
|
|
52
|
+
const first = realized[0];
|
|
53
|
+
const kind = first.specification.class.kind;
|
|
54
|
+
const mismatched = realized.find(({ specification }) => specification.class.kind !== kind);
|
|
55
|
+
if (mismatched !== undefined) {
|
|
56
|
+
invalid(`${pathOf(outputPath, mismatched.name)} selected an ${mismatched.specification.class.kind} Class; expected ${kind} selected by ${pathOf(outputPath, first.name)}.`);
|
|
57
|
+
}
|
|
58
|
+
const selected = realized
|
|
59
|
+
.map(({ specification }) => specification.class)
|
|
60
|
+
.sort((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0));
|
|
61
|
+
const ast = kind === 'node'
|
|
62
|
+
? nodeUnion(selected)
|
|
63
|
+
: edgeUnion(selected);
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
ast,
|
|
66
|
+
kind,
|
|
67
|
+
members: Object.freeze(realized.map(({ name, specification }) => Object.freeze({ name, id: specification.id, class: specification.class }))),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function partitionCollectionUnion(domain, compiled, values, maximumLogicalAssignments = COLLECTION_QUERY_LIMITS.maximumLogicalAssignments) {
|
|
71
|
+
if (!Number.isSafeInteger(maximumLogicalAssignments) || maximumLogicalAssignments < 1) {
|
|
72
|
+
limit('Collection union logical-assignment bound is invalid.');
|
|
73
|
+
}
|
|
74
|
+
const accepted = Object.fromEntries(compiled.members.map(({ name }) => [name, []]));
|
|
75
|
+
const memberships = new Map();
|
|
76
|
+
const identities = new Set();
|
|
77
|
+
let assignments = 0;
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
const identity = collectionIdentityOf(value);
|
|
80
|
+
if (identities.has(identity))
|
|
81
|
+
invalid(`Collection union returned duplicate identity ${identity}.`);
|
|
82
|
+
identities.add(identity);
|
|
83
|
+
const names = memberships.get(value.class) ?? memberNames(domain, compiled, value);
|
|
84
|
+
memberships.set(value.class, names);
|
|
85
|
+
if (names.length === 0) {
|
|
86
|
+
invalid(`Collection union returned ${value.class} outside every member extent.`);
|
|
87
|
+
}
|
|
88
|
+
assignments += names.length;
|
|
89
|
+
if (assignments > maximumLogicalAssignments) {
|
|
90
|
+
limit(`Collection union exceeded ${maximumLogicalAssignments} logical assignments.`);
|
|
91
|
+
}
|
|
92
|
+
for (const name of names)
|
|
93
|
+
accepted[name].push(value);
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze(Object.fromEntries(Object.entries(accepted).map(([name, members]) => [name, Object.freeze(members)])));
|
|
96
|
+
}
|
|
97
|
+
function realizeMember(definition, domain, outputPath, name) {
|
|
98
|
+
try {
|
|
99
|
+
return realizeCollectionQuery(definition, domain, pathOf(outputPath, name));
|
|
100
|
+
}
|
|
101
|
+
catch (cause) {
|
|
102
|
+
if (cause instanceof QueryCompositionError)
|
|
103
|
+
throw cause;
|
|
104
|
+
throw new QueryCompositionError('QUERY_COMPOSITION_INVALID', cause instanceof Error ? cause.message : `${pathOf(outputPath, name)} realization failed.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function memberNames(domain, compiled, value) {
|
|
108
|
+
const exact = domain.definition(ClassKey.ref(ClassKey(value.class)));
|
|
109
|
+
const kind = valueKind(value);
|
|
110
|
+
if (exact?.ref.kind !== 'class' || exact.kind !== kind || exact.abstract) {
|
|
111
|
+
invalid(`Collection union returned inactive ${kind} Class ${value.class}.`);
|
|
112
|
+
}
|
|
113
|
+
const names = compiled.members.flatMap((member) => {
|
|
114
|
+
const satisfies = exact.kind === 'node'
|
|
115
|
+
? member.class.kind === 'node' && exact.satisfies(member.class)
|
|
116
|
+
: member.class.kind === 'edge' && exact.satisfies(member.class);
|
|
117
|
+
return satisfies ? [member.name] : [];
|
|
118
|
+
});
|
|
119
|
+
return Object.freeze(names);
|
|
120
|
+
}
|
|
121
|
+
function nodeUnion(selected) {
|
|
122
|
+
const [first, second, ...rest] = selected;
|
|
123
|
+
if (first === undefined || second === undefined)
|
|
124
|
+
invalid('Node collection union is undersized.');
|
|
125
|
+
return Query.from({ nodes: [first, second, ...rest] }).select({
|
|
126
|
+
kind: 'nodes',
|
|
127
|
+
projection: { kind: 'value' },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function edgeUnion(selected) {
|
|
131
|
+
const [first, second, ...rest] = selected;
|
|
132
|
+
if (first === undefined || second === undefined)
|
|
133
|
+
invalid('Edge collection union is undersized.');
|
|
134
|
+
return Query.from({ edges: [first, second, ...rest] }).select({
|
|
135
|
+
kind: 'edges',
|
|
136
|
+
projection: { edge: 'value', source: 'reference', target: 'reference' },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function valueKind(value) {
|
|
140
|
+
return 'id' in value ? 'node' : 'edge';
|
|
141
|
+
}
|
|
142
|
+
function pathOf(path, member) {
|
|
143
|
+
const parts = member === undefined ? path : [...path, member];
|
|
144
|
+
return parts.length === 0 ? 'collection union' : parts.join('.');
|
|
145
|
+
}
|
|
146
|
+
function isPlainRecord(input) {
|
|
147
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
148
|
+
return false;
|
|
149
|
+
const prototype = Object.getPrototypeOf(input);
|
|
150
|
+
return prototype === Object.prototype || prototype === null;
|
|
151
|
+
}
|
|
152
|
+
function invalid(message) {
|
|
153
|
+
throw new QueryCompositionError('QUERY_COMPOSITION_INVALID', message);
|
|
154
|
+
}
|
|
155
|
+
function limit(message) {
|
|
156
|
+
throw new QueryCompositionError('QUERY_COMPOSITION_LIMIT', message);
|
|
157
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Domain } from '../../../platform/schema/index.js';
|
|
2
|
+
import type { CollectionDefinitionRecord } from '../collection/union.js';
|
|
2
3
|
import type { SingleQueryDefinition } from '../query.js';
|
|
3
4
|
import type { QueryComposition, QueryPlan } from './query.js';
|
|
4
5
|
declare const PLAN: unique symbol;
|
|
@@ -19,7 +20,12 @@ export interface CombinedPlan {
|
|
|
19
20
|
readonly key: number;
|
|
20
21
|
readonly plans: Readonly<Record<string, InternalPlan>>;
|
|
21
22
|
}
|
|
22
|
-
export
|
|
23
|
+
export interface UnionPlan {
|
|
24
|
+
readonly kind: 'union';
|
|
25
|
+
readonly key: number;
|
|
26
|
+
readonly definitions: CollectionDefinitionRecord;
|
|
27
|
+
}
|
|
28
|
+
export type PlanNode = LeafPlan | CombinedPlan | UnionPlan;
|
|
23
29
|
export interface InternalPlan extends QueryPlan<unknown> {
|
|
24
30
|
readonly [PLAN]: Readonly<{
|
|
25
31
|
token: object;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { admitCollectionDefinitions } from '../collection/union.js';
|
|
1
2
|
import { QueryCompositionError } from './errors.js';
|
|
2
3
|
const PLAN = Symbol('astrale.sdk.query.plan');
|
|
3
4
|
const REFERENCE = Symbol('astrale.sdk.query.reference');
|
|
@@ -21,6 +22,12 @@ export function createQueryComposition() {
|
|
|
21
22
|
input,
|
|
22
23
|
}));
|
|
23
24
|
},
|
|
25
|
+
union(definitions) {
|
|
26
|
+
requireOpen(open);
|
|
27
|
+
const entries = admitCollectionDefinitions(definitions);
|
|
28
|
+
const accepted = Object.freeze(Object.fromEntries(entries));
|
|
29
|
+
return createPlan(token, Object.freeze({ kind: 'union', key: nextKey++, definitions: accepted }));
|
|
30
|
+
},
|
|
24
31
|
combine(plans) {
|
|
25
32
|
requireOpen(open);
|
|
26
33
|
if (!isPlainRecord(plans) || Object.keys(plans).length === 0) {
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { Domain } from '../../../platform/schema/index.js';
|
|
2
|
+
import type { CollectionDefinitionRecord } from '../collection/union.js';
|
|
2
3
|
import type { SingleQueryDefinition } from '../query.js';
|
|
3
4
|
import type { InternalPlan } from './builder.js';
|
|
5
|
+
export declare const QUERY_COMPOSITION_LIMITS: Readonly<{
|
|
6
|
+
maximumPhysicalQueries: 32;
|
|
7
|
+
maximumDependencyDepth: 8;
|
|
8
|
+
}>;
|
|
4
9
|
export interface QueryPlanAnalysis {
|
|
10
|
+
/** @deprecated Use physicalQueries. */
|
|
5
11
|
readonly queries: number;
|
|
12
|
+
/** @deprecated Use dependencyDepth. */
|
|
6
13
|
readonly depth: number;
|
|
14
|
+
readonly logicalQueries: number;
|
|
15
|
+
readonly physicalQueries: number;
|
|
16
|
+
readonly dependencyDepth: number;
|
|
17
|
+
readonly maximumLogicalAssignments: number;
|
|
18
|
+
readonly unionGroups: readonly {
|
|
19
|
+
readonly outputPath: readonly string[];
|
|
20
|
+
readonly members: number;
|
|
21
|
+
}[];
|
|
7
22
|
}
|
|
8
23
|
export declare function analyzeQueryPlan(plan: InternalPlan): QueryPlanAnalysis;
|
|
9
|
-
export declare function executeQueryPlan(plan: InternalPlan, run: (definition: SingleQueryDefinition<Domain, unknown, unknown>, input: unknown) => Promise<unknown>): Promise<unknown>;
|
|
24
|
+
export declare function executeQueryPlan(plan: InternalPlan, run: (definition: SingleQueryDefinition<Domain, unknown, unknown>, input: unknown) => Promise<unknown>, runUnion: (definitions: CollectionDefinitionRecord, outputPath: readonly string[]) => Promise<unknown>): Promise<unknown>;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { COLLECTION_QUERY_LIMITS } from '../collection/limits.js';
|
|
1
2
|
import { inspectPlan, referencesIn, resolveInput } from './builder.js';
|
|
2
3
|
import { QueryCompositionError } from './errors.js';
|
|
3
|
-
const
|
|
4
|
-
|
|
4
|
+
export const QUERY_COMPOSITION_LIMITS = Object.freeze({
|
|
5
|
+
maximumPhysicalQueries: 32,
|
|
6
|
+
maximumDependencyDepth: 8,
|
|
7
|
+
});
|
|
5
8
|
export function analyzeQueryPlan(plan) {
|
|
6
|
-
const
|
|
9
|
+
const physicalKeys = new Set();
|
|
10
|
+
const logicalByKey = new Map();
|
|
7
11
|
const visiting = new Set();
|
|
8
12
|
const depths = new Map();
|
|
9
|
-
const
|
|
13
|
+
const unionGroups = [];
|
|
14
|
+
const depth = (candidate, outputPath = []) => {
|
|
10
15
|
const { node } = inspectPlan(candidate);
|
|
11
16
|
const cached = depths.get(node.key);
|
|
12
17
|
if (cached !== undefined)
|
|
@@ -16,36 +21,60 @@ export function analyzeQueryPlan(plan) {
|
|
|
16
21
|
visiting.add(node.key);
|
|
17
22
|
let value;
|
|
18
23
|
if (node.kind === 'combine') {
|
|
19
|
-
value = Math.max(...Object.
|
|
24
|
+
value = Math.max(...Object.entries(node.plans).map(([name, child]) => depth(child, [...outputPath, name])));
|
|
25
|
+
}
|
|
26
|
+
else if (node.kind === 'union') {
|
|
27
|
+
physicalKeys.add(node.key);
|
|
28
|
+
const members = Object.keys(node.definitions).length;
|
|
29
|
+
logicalByKey.set(node.key, members);
|
|
30
|
+
unionGroups.push(Object.freeze({ outputPath: Object.freeze([...outputPath]), members }));
|
|
31
|
+
value = 1;
|
|
20
32
|
}
|
|
21
33
|
else {
|
|
22
|
-
|
|
34
|
+
physicalKeys.add(node.key);
|
|
35
|
+
logicalByKey.set(node.key, 1);
|
|
23
36
|
const dependencies = referencesIn(node.input).map((reference) => reference.plan);
|
|
24
|
-
value =
|
|
37
|
+
value =
|
|
38
|
+
1 +
|
|
39
|
+
(dependencies.length === 0
|
|
40
|
+
? 0
|
|
41
|
+
: Math.max(...dependencies.map((dependency) => depth(dependency))));
|
|
25
42
|
}
|
|
26
43
|
visiting.delete(node.key);
|
|
27
44
|
depths.set(node.key, value);
|
|
28
45
|
return value;
|
|
29
46
|
};
|
|
30
47
|
const maximumDepth = depth(plan);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
const logicalQueries = [...logicalByKey.values()].reduce((total, count) => total + count, 0);
|
|
49
|
+
if (logicalQueries < 2)
|
|
50
|
+
invalid('Composite Query must contain at least two logical Queries.');
|
|
51
|
+
if (physicalKeys.size > QUERY_COMPOSITION_LIMITS.maximumPhysicalQueries ||
|
|
52
|
+
maximumDepth > QUERY_COMPOSITION_LIMITS.maximumDependencyDepth) {
|
|
53
|
+
throw new QueryCompositionError('QUERY_COMPOSITION_LIMIT', `Composite Query exceeds ${QUERY_COMPOSITION_LIMITS.maximumPhysicalQueries} physical Queries or dependency depth ${QUERY_COMPOSITION_LIMITS.maximumDependencyDepth}.`);
|
|
35
54
|
}
|
|
36
|
-
return Object.freeze({
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
queries: physicalKeys.size,
|
|
57
|
+
depth: maximumDepth,
|
|
58
|
+
logicalQueries,
|
|
59
|
+
physicalQueries: physicalKeys.size,
|
|
60
|
+
dependencyDepth: maximumDepth,
|
|
61
|
+
maximumLogicalAssignments: COLLECTION_QUERY_LIMITS.maximumLogicalAssignments,
|
|
62
|
+
unionGroups: Object.freeze(unionGroups),
|
|
63
|
+
});
|
|
37
64
|
}
|
|
38
|
-
export async function executeQueryPlan(plan, run) {
|
|
65
|
+
export async function executeQueryPlan(plan, run, runUnion) {
|
|
39
66
|
analyzeQueryPlan(plan);
|
|
40
67
|
const results = new Map();
|
|
41
|
-
const execute = (candidate) => {
|
|
68
|
+
const execute = (candidate, outputPath = []) => {
|
|
42
69
|
const { node } = inspectPlan(candidate);
|
|
43
70
|
const existing = results.get(node.key);
|
|
44
71
|
if (existing !== undefined)
|
|
45
72
|
return existing;
|
|
46
73
|
const pending = node.kind === 'combine'
|
|
47
|
-
? Promise.all(Object.entries(node.plans).map(async ([name, child]) => [name, await execute(child)])).then((entries) => Object.freeze(Object.fromEntries(entries)))
|
|
48
|
-
:
|
|
74
|
+
? Promise.all(Object.entries(node.plans).map(async ([name, child]) => [name, await execute(child, [...outputPath, name])])).then((entries) => Object.freeze(Object.fromEntries(entries)))
|
|
75
|
+
: node.kind === 'union'
|
|
76
|
+
? runUnion(node.definitions, outputPath)
|
|
77
|
+
: resolveInput(node.input, (dependency) => execute(dependency)).then((input) => run(node.definition, input));
|
|
49
78
|
results.set(node.key, pending);
|
|
50
79
|
return pending;
|
|
51
80
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { ResolvedClass } from '@astrale-os/kernel-dsl/v1';
|
|
1
2
|
import type { Domain } from '../../../platform/schema/index.js';
|
|
2
|
-
import type { SingleQueryDefinition } from '../query.js';
|
|
3
|
+
import type { CollectionQueryDefinition, QueryOutputOf, SingleQueryDefinition } from '../query.js';
|
|
3
4
|
import type { QueryLifecycle } from '../single/index.js';
|
|
4
5
|
declare const QUERY_PLAN_OUTPUT: unique symbol;
|
|
5
6
|
declare const QUERY_PLAN_VALUE: unique symbol;
|
|
@@ -16,8 +17,15 @@ export interface QueryPlan<Output> {
|
|
|
16
17
|
readonly value: QueryPlanValue<Output>;
|
|
17
18
|
}
|
|
18
19
|
export type QueryPlanOutput<Plan extends QueryPlan<unknown>> = Plan[typeof QUERY_PLAN_OUTPUT];
|
|
20
|
+
type CollectionOutputs<Definitions extends Readonly<Record<string, unknown>>> = Readonly<{
|
|
21
|
+
[Name in Extract<keyof Definitions, string>]: QueryOutputOf<Definitions[Name]>;
|
|
22
|
+
}>;
|
|
23
|
+
type StringOwnKeys<Definitions> = Record<Exclude<keyof Definitions, string>, never>;
|
|
19
24
|
export interface QueryComposition<DomainValue extends Domain = Domain> {
|
|
20
|
-
query<
|
|
25
|
+
query<Output>(id: string, query: SingleQueryDefinition<DomainValue, void, Output>): QueryPlan<Output>;
|
|
26
|
+
query<Input, Output>(id: string, query: SingleQueryDefinition<DomainValue, Input, Output>, input: QueryPlanInput<Input>): QueryPlan<Output>;
|
|
27
|
+
union<const Definitions extends Readonly<Record<string, CollectionQueryDefinition<DomainValue, ResolvedClass<'node'>>>>>(definitions: Definitions & StringOwnKeys<Definitions>): QueryPlan<CollectionOutputs<Definitions>>;
|
|
28
|
+
union<const Definitions extends Readonly<Record<string, CollectionQueryDefinition<DomainValue, ResolvedClass<'edge'>>>>>(definitions: Definitions & StringOwnKeys<Definitions>): QueryPlan<CollectionOutputs<Definitions>>;
|
|
21
29
|
combine<const Plans extends Readonly<Record<string, QueryPlan<unknown>>>>(plans: Plans): QueryPlan<{
|
|
22
30
|
readonly [Name in keyof Plans]: QueryPlanOutput<Plans[Name]>;
|
|
23
31
|
}>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { QueryAST } from '@astrale-os/kernel-core/graph/query';
|
|
2
|
+
import type { AnyResolvedClass } from '@astrale-os/kernel-dsl/v1';
|
|
2
3
|
import type { Domain, schema } from '../../platform/schema/index.js';
|
|
3
|
-
import type { ProjectedCompositeQuery, RawCompositeQuery } from './composite/index.js';
|
|
4
|
-
import type { CompositeQueryDefinition, QueryDefinition, RealizedQuery, SingleQueryDefinition } from './query.js';
|
|
4
|
+
import type { ProjectedCompositeQuery, QueryComposition, QueryPlan, QueryPlanOutput, RawCompositeQuery } from './composite/index.js';
|
|
5
|
+
import type { CompositeQueryDefinition, CollectionQueryDefinition, QueryDefinition, RealizedQuery, SingleQueryDefinition } from './query.js';
|
|
5
6
|
import type { PaginatedQuery, ProjectedQuery, RawQuery, SingleQuery } from './single/index.js';
|
|
7
|
+
import { type CollectionQuerySpec } from './collection/metadata.js';
|
|
6
8
|
type AuthoredRaw<Input, Ast extends QueryAST> = Omit<RawQuery<Input, Ast>, 'kind' | 'page'> & {
|
|
7
9
|
readonly page?: {
|
|
8
10
|
readonly size: number;
|
|
@@ -24,13 +26,23 @@ interface SingleQueryProjector<Schema extends schema.DomainSchema> {
|
|
|
24
26
|
<Input, Output, Ast extends QueryAST>(project: (domain: ResolvedDomainOf<Schema>) => AuthoredProjected<Input, Output, Ast>): SingleQueryDefinition<ResolvedDomainOf<Schema>, Input, Output>;
|
|
25
27
|
<Input, Output, Ast extends QueryAST>(project: (domain: ResolvedDomainOf<Schema>) => AuthoredPaginated<Input, Output, Ast>): SingleQueryDefinition<ResolvedDomainOf<Schema>, Input, Output>;
|
|
26
28
|
}
|
|
29
|
+
interface CollectionQueryProjector<Schema extends schema.DomainSchema> {
|
|
30
|
+
<Class extends AnyResolvedClass>(project: (domain: ResolvedDomainOf<Schema>) => CollectionQuerySpec<Class>): CollectionQueryDefinition<ResolvedDomainOf<Schema>, Class>;
|
|
31
|
+
}
|
|
32
|
+
type AuthoredInferredComposite<Input, Composed, Plan extends QueryPlan<unknown>, Output, DomainValue extends Domain> = Omit<ProjectedCompositeQuery<Input, Composed, Output, DomainValue>, 'kind' | 'compose' | 'project'> & {
|
|
33
|
+
compose(builder: QueryComposition<DomainValue>, input: Input): Plan;
|
|
34
|
+
project(value: QueryPlanOutput<Plan>, input: Input): Output;
|
|
35
|
+
};
|
|
27
36
|
interface CompositeQueryProjector<Schema extends schema.DomainSchema> {
|
|
37
|
+
<Plan extends QueryPlan<unknown>, Output>(project: (domain: ResolvedDomainOf<Schema>) => AuthoredInferredComposite<void, QueryPlanOutput<Plan>, Plan, Output, ResolvedDomainOf<Schema>>): CompositeQueryDefinition<ResolvedDomainOf<Schema>, void, Output>;
|
|
28
38
|
<Input, Output>(project: (domain: ResolvedDomainOf<Schema>) => Omit<RawCompositeQuery<Input, Output, ResolvedDomainOf<Schema>>, 'kind'>): CompositeQueryDefinition<ResolvedDomainOf<Schema>, Input, Output>;
|
|
29
|
-
<Input, Composed, Output>(project: (domain: ResolvedDomainOf<Schema>) =>
|
|
39
|
+
<Input, Composed, Output, Plan extends QueryPlan<Composed> = QueryPlan<Composed>>(project: (domain: ResolvedDomainOf<Schema>) => AuthoredInferredComposite<Input, Composed, Plan, Output, ResolvedDomainOf<Schema>>): CompositeQueryDefinition<ResolvedDomainOf<Schema>, Input, Output>;
|
|
30
40
|
}
|
|
31
41
|
/** Define one inert single Query recipe projected from its exact loaded Domain. */
|
|
32
42
|
export declare function defineQuery<Schema extends schema.DomainSchema>(): SingleQueryProjector<Schema>;
|
|
43
|
+
/** Define one reusable complete polymorphic Class-extent Query. */
|
|
44
|
+
export declare function defineCollectionQuery<Schema extends schema.DomainSchema>(): CollectionQueryProjector<Schema>;
|
|
33
45
|
/** Define one inert composite Query recipe projected from its exact loaded Domain. */
|
|
34
46
|
export declare function defineCompositeQuery<Schema extends schema.DomainSchema>(): CompositeQueryProjector<Schema>;
|
|
35
|
-
export declare function realizeQuery<DomainValue extends Domain, Input, Output>(
|
|
47
|
+
export declare function realizeQuery<DomainValue extends Domain, Input, Output>(query: QueryDefinition<DomainValue, Input, Output>, domain: DomainValue): RealizedQuery<Input, Output>;
|
|
36
48
|
export {};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { Query } from '@astrale-os/kernel-core/graph/query';
|
|
1
2
|
import { isDomain } from '@astrale-os/kernel-dsl/v1/domain';
|
|
3
|
+
import { requireUniqueCollectionValues } from './collection/identity.js';
|
|
4
|
+
import { COLLECTION_QUERY_LIMITS } from './collection/limits.js';
|
|
5
|
+
import { realizeCollectionQuery, registerCollectionQuery, } from './collection/metadata.js';
|
|
6
|
+
import { queryResult } from './single/index.js';
|
|
2
7
|
const DEFAULT_PAGE_SIZE = 256;
|
|
3
8
|
const MAXIMUM_PAGES = 1_024;
|
|
4
9
|
const projectors = new WeakMap();
|
|
@@ -7,27 +12,65 @@ const realized = new WeakMap();
|
|
|
7
12
|
export function defineQuery() {
|
|
8
13
|
return ((project) => recipe('query', project));
|
|
9
14
|
}
|
|
15
|
+
/** Define one reusable complete polymorphic Class-extent Query. */
|
|
16
|
+
export function defineCollectionQuery() {
|
|
17
|
+
return ((project) => {
|
|
18
|
+
const definition = defineQuery()((domain) => {
|
|
19
|
+
const specification = realizeCollectionQuery(definition, domain);
|
|
20
|
+
const selected = specification.class;
|
|
21
|
+
if (selected.kind === 'node') {
|
|
22
|
+
return {
|
|
23
|
+
id: specification.id,
|
|
24
|
+
page: { size: COLLECTION_QUERY_LIMITS.pageSize },
|
|
25
|
+
pagination: { maximumPages: COLLECTION_QUERY_LIMITS.maximumPages },
|
|
26
|
+
build: () => Query.from({ nodes: [selected] }).select({
|
|
27
|
+
kind: 'nodes',
|
|
28
|
+
projection: { kind: 'value' },
|
|
29
|
+
}),
|
|
30
|
+
project: (pages) => requireUniqueCollectionValues(queryResult.completeNodeValues(pages, specification.id), `Collection Query ${specification.id}`),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id: specification.id,
|
|
35
|
+
page: { size: COLLECTION_QUERY_LIMITS.pageSize },
|
|
36
|
+
pagination: { maximumPages: COLLECTION_QUERY_LIMITS.maximumPages },
|
|
37
|
+
build: () => Query.from({ edges: [selected] }).select({
|
|
38
|
+
kind: 'edges',
|
|
39
|
+
projection: { edge: 'value', source: 'reference', target: 'reference' },
|
|
40
|
+
}),
|
|
41
|
+
project: (pages) => requireUniqueCollectionValues(Object.freeze(pages.flatMap((page) => queryResult.edgeItems(page, specification.id).map((item) => {
|
|
42
|
+
if (item.edge.kind !== 'value') {
|
|
43
|
+
throw new TypeError(`Collection Query ${specification.id} must project Edge values.`);
|
|
44
|
+
}
|
|
45
|
+
return item.edge.value;
|
|
46
|
+
}))), `Collection Query ${specification.id}`),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
registerCollectionQuery(definition, project);
|
|
50
|
+
return definition;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
10
53
|
/** Define one inert composite Query recipe projected from its exact loaded Domain. */
|
|
11
54
|
export function defineCompositeQuery() {
|
|
12
55
|
return ((project) => recipe('composite-query', project));
|
|
13
56
|
}
|
|
14
|
-
export function realizeQuery(
|
|
15
|
-
const project = projectors.get(
|
|
57
|
+
export function realizeQuery(query, domain) {
|
|
58
|
+
const project = projectors.get(query);
|
|
16
59
|
if (project === undefined)
|
|
17
60
|
throw new TypeError('Query must be produced by defineQuery.');
|
|
18
61
|
if (!isDomain(domain))
|
|
19
62
|
throw new TypeError('Query projection requires an admitted Domain.');
|
|
20
|
-
let byDomain = realized.get(
|
|
63
|
+
let byDomain = realized.get(query);
|
|
21
64
|
const cached = byDomain?.get(domain);
|
|
22
65
|
if (cached !== undefined)
|
|
23
66
|
return cached;
|
|
24
67
|
const authored = project(domain);
|
|
25
68
|
if (isPromiseLike(authored))
|
|
26
69
|
throw new TypeError('Query projection must be synchronous.');
|
|
27
|
-
const admitted =
|
|
70
|
+
const admitted = query.kind === 'query' ? admitSingle(authored) : admitComposite(authored);
|
|
28
71
|
if (byDomain === undefined) {
|
|
29
72
|
byDomain = new WeakMap();
|
|
30
|
-
realized.set(
|
|
73
|
+
realized.set(query, byDomain);
|
|
31
74
|
}
|
|
32
75
|
byDomain.set(domain, admitted);
|
|
33
76
|
return admitted;
|
|
@@ -9,5 +9,6 @@ export interface QueryClient {
|
|
|
9
9
|
readonly expected?: Domain['closure'];
|
|
10
10
|
}): Promise<QueryResponse<QueryResultFor<Ast>>>;
|
|
11
11
|
}
|
|
12
|
-
export declare function executeQuery<DomainValue extends Domain,
|
|
12
|
+
export declare function executeQuery<DomainValue extends Domain, Output>(client: QueryClient, domain: DomainValue, query: QueryDefinition<DomainValue, void, Output>): Promise<Output>;
|
|
13
|
+
export declare function executeQuery<DomainValue extends Domain, Input, Output>(client: QueryClient, domain: DomainValue, query: QueryDefinition<DomainValue, Input, Output>, input: Input): Promise<Output>;
|
|
13
14
|
export type ExecutableCompositeQuery = CompositeQuery<unknown, unknown, unknown>;
|