@astrale-os/sdk 0.5.0-beta.63 → 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 +20 -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/dist/tooling/packaging/compile.js +11 -7
- package/package.json +6 -5
|
@@ -1,24 +1,27 @@
|
|
|
1
|
+
import { COLLECTION_QUERY_LIMITS } from '../collection/limits.js';
|
|
2
|
+
import { compileCollectionUnion, partitionCollectionUnion } from '../collection/union.js';
|
|
1
3
|
import { createQueryComposition, executeQueryPlan } from '../composite/index.js';
|
|
2
4
|
import { realizeQuery } from '../define.js';
|
|
3
5
|
import { queryFailure } from '../failure.js';
|
|
4
|
-
import { completeSelection } from '../single/index.js';
|
|
6
|
+
import { completeSelection, queryResult } from '../single/index.js';
|
|
5
7
|
import { executePages } from './pages.js';
|
|
6
|
-
export async function executeQuery(client, domain,
|
|
8
|
+
export async function executeQuery(client, domain, query, input) {
|
|
7
9
|
if (client === null || typeof client !== 'object' || typeof client.query !== 'function') {
|
|
8
10
|
throw new TypeError('Query execution requires a Client query capability.');
|
|
9
11
|
}
|
|
10
12
|
const execute = (ast, options) => client.query(ast, { ...options, expected: domain.closure });
|
|
11
|
-
const operation = realizeQuery(
|
|
13
|
+
const operation = realizeQuery(query, domain);
|
|
12
14
|
try {
|
|
13
|
-
if (operation.kind === 'single')
|
|
15
|
+
if (operation.kind === 'single') {
|
|
14
16
|
return (await runSingle(execute, operation, input));
|
|
17
|
+
}
|
|
15
18
|
const prepared = operation.prepare?.(input) ?? input;
|
|
16
19
|
const session = createQueryComposition();
|
|
17
20
|
const composed = operation.compose(session.builder, prepared);
|
|
18
21
|
if (isPromiseLike(composed))
|
|
19
22
|
throw new TypeError('Composite Query composition must be synchronous.');
|
|
20
23
|
const plan = session.close(composed);
|
|
21
|
-
const result = await executeQueryPlan(plan, (child, childInput) => runSingle(execute, realizeSingle(child, domain), childInput));
|
|
24
|
+
const result = await executeQueryPlan(plan, (child, childInput) => runSingle(execute, realizeSingle(child, domain), childInput), (definitions, outputPath) => runCollectionUnion(execute, domain, definitions, outputPath));
|
|
22
25
|
return (operation.project === undefined ? result : operation.project(result, prepared));
|
|
23
26
|
}
|
|
24
27
|
catch (cause) {
|
|
@@ -29,6 +32,20 @@ export async function executeQuery(client, domain, definition, input) {
|
|
|
29
32
|
throw cause;
|
|
30
33
|
}
|
|
31
34
|
}
|
|
35
|
+
async function runCollectionUnion(execute, domain, definitions, outputPath) {
|
|
36
|
+
const compiled = compileCollectionUnion(domain, definitions, outputPath);
|
|
37
|
+
const pages = await executePages(execute, compiled.ast, { size: COLLECTION_QUERY_LIMITS.pageSize }, { maximumPages: COLLECTION_QUERY_LIMITS.maximumPages });
|
|
38
|
+
const context = outputPath.length === 0 ? 'collection union' : outputPath.join('.');
|
|
39
|
+
const values = compiled.kind === 'node'
|
|
40
|
+
? queryResult.completeNodeValues(pages, context)
|
|
41
|
+
: Object.freeze(pages.flatMap((page) => queryResult.edgeItems(page, context).map((item) => {
|
|
42
|
+
if (item.edge.kind !== 'value') {
|
|
43
|
+
throw new TypeError(`${context} must project Edge values.`);
|
|
44
|
+
}
|
|
45
|
+
return item.edge.value;
|
|
46
|
+
})));
|
|
47
|
+
return partitionCollectionUnion(domain, compiled, values);
|
|
48
|
+
}
|
|
32
49
|
function realizeSingle(definition, domain) {
|
|
33
50
|
const operation = realizeQuery(definition, domain);
|
|
34
51
|
if (operation.kind !== 'single')
|
|
@@ -3,7 +3,8 @@ import type { QueryDefinition } from '../query.js';
|
|
|
3
3
|
import type { QueryClient } from './execute.js';
|
|
4
4
|
/** Invocation-bound executor for authored Query definitions. */
|
|
5
5
|
export interface QueryExecutor<DomainValue extends Domain = Domain> {
|
|
6
|
-
<
|
|
6
|
+
<Output>(query: QueryDefinition<DomainValue, void, Output>): Promise<Output>;
|
|
7
|
+
<Input, Output>(query: QueryDefinition<DomainValue, Input, Output>, input: Input): Promise<Output>;
|
|
7
8
|
}
|
|
8
9
|
type ExecutableDomain<DomainValue extends Domain> = DomainValue extends {
|
|
9
10
|
readonly source: infer Source extends schema.DomainSchema;
|
|
@@ -2,6 +2,6 @@ import { executeQuery } from './execute.js';
|
|
|
2
2
|
/** Private Runtime binding to one Client and the exact loaded root Domain. */
|
|
3
3
|
export function bindQuery(client, domain) {
|
|
4
4
|
const executable = domain;
|
|
5
|
-
const execute = (
|
|
5
|
+
const execute = (query, input) => executeQuery(client, executable, query, input);
|
|
6
6
|
return execute;
|
|
7
7
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { defineCompositeQuery, defineQuery, realizeQuery } from './define.js';
|
|
1
|
+
export { defineCollectionQuery, defineCompositeQuery, defineQuery, realizeQuery } from './define.js';
|
|
2
|
+
export type { CollectionQuerySpec } from './collection/metadata.js';
|
|
2
3
|
export { executeQuery, type QueryClient, type QueryExecutor } from './execution/index.js';
|
|
3
4
|
export { QueryPaginationError, type QueryFailure, type QueryPaginationErrorCode, } from './failure.js';
|
|
4
|
-
export type { CompositeQueryDefinition, QueryAstOf, QueryDefinition, QueryInputOf, QueryOutputOf, SingleQueryDefinition, } from './query.js';
|
|
5
|
+
export type { CollectionQueryDefinition, CollectionValue, CompositeQueryDefinition, QueryAstOf, QueryDefinition, QueryInputOf, QueryOutputOf, SingleQueryDefinition, } from './query.js';
|
|
5
6
|
export { queryResult } from './single/index.js';
|
|
6
7
|
export type { CompleteQueryProjectionContext, PaginatedQuery, ProjectedQuery, QueryPages, QueryPagination, QueryProjectionContext, RawQuery, SingleQuery, } from './single/index.js';
|
|
7
8
|
export type { CompositeQuery, ProjectedCompositeQuery, QueryComposition, QueryPlan, QueryPlanInput, QueryPlanOutput, QueryPlanValue, RawCompositeQuery, } from './composite/index.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defineCompositeQuery, defineQuery, realizeQuery } from './define.js';
|
|
1
|
+
export { defineCollectionQuery, defineCompositeQuery, defineQuery, realizeQuery } from './define.js';
|
|
2
2
|
export { executeQuery } from './execution/index.js';
|
|
3
3
|
export { QueryPaginationError, } from './failure.js';
|
|
4
4
|
export { queryResult } from './single/index.js';
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { QueryAST, QueryResult } from '@astrale-os/kernel-core/graph/query';
|
|
2
|
+
import type { AnyResolvedClass, ResolvedClass } 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';
|
|
2
5
|
import type { Domain } from '../../platform/schema/index.js';
|
|
3
6
|
import type { CompositeQuery } from './composite/index.js';
|
|
4
7
|
import type { QueryLifecycle, SingleQuery } from './single/index.js';
|
|
5
8
|
declare const QUERY_DOMAIN: unique symbol;
|
|
6
9
|
declare const QUERY_INPUT: unique symbol;
|
|
7
10
|
declare const QUERY_OUTPUT: unique symbol;
|
|
11
|
+
declare const COLLECTION_QUERY_CLASS: unique symbol;
|
|
8
12
|
/** Inert Domain-bound Query recipe. Its projector is retained privately by the SDK. */
|
|
9
13
|
export interface QueryDefinition<DomainValue extends Domain = Domain, Input = unknown, Output = QueryResult> {
|
|
10
14
|
readonly kind: 'query' | 'composite-query';
|
|
@@ -15,6 +19,11 @@ export interface QueryDefinition<DomainValue extends Domain = Domain, Input = un
|
|
|
15
19
|
export interface SingleQueryDefinition<DomainValue extends Domain = Domain, Input = unknown, Output = QueryResult> extends QueryDefinition<DomainValue, Input, Output> {
|
|
16
20
|
readonly kind: 'query';
|
|
17
21
|
}
|
|
22
|
+
export type CollectionValue<Class extends AnyResolvedClass> = Class extends ResolvedClass<'node'> ? Node : Class extends ResolvedClass<'edge'> ? Edge : never;
|
|
23
|
+
/** Closed complete-Class Query recipe admitted for explicit physical union planning. */
|
|
24
|
+
export interface CollectionQueryDefinition<DomainValue extends Domain = Domain, Class extends AnyResolvedClass = AnyResolvedClass> extends SingleQueryDefinition<DomainValue, void, readonly CollectionValue<Class>[]> {
|
|
25
|
+
readonly [COLLECTION_QUERY_CLASS]: Class;
|
|
26
|
+
}
|
|
18
27
|
export interface CompositeQueryDefinition<DomainValue extends Domain = Domain, Input = unknown, Output = unknown> extends QueryDefinition<DomainValue, Input, Output> {
|
|
19
28
|
readonly kind: 'composite-query';
|
|
20
29
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export { defineIntegration, defineProvider } from './application/integration/ind
|
|
|
7
7
|
export type { IntegrationClient, IntegrationDefinition, Provider, } from './application/integration/index.js';
|
|
8
8
|
export { defineMutation, executeMutation } from './application/mutation/index.js';
|
|
9
9
|
export type { Mutation, MutationFailure } from './application/mutation/index.js';
|
|
10
|
-
export { defineCompositeQuery, defineQuery, executeQuery } from './application/query/index.js';
|
|
11
|
-
export type { QueryDefinition, QueryFailure } from './application/query/index.js';
|
|
10
|
+
export { defineCollectionQuery, defineCompositeQuery, defineQuery, executeQuery, } from './application/query/index.js';
|
|
11
|
+
export type { CollectionQueryDefinition, QueryDefinition, QueryFailure, } from './application/query/index.js';
|
|
12
12
|
export { defineRuntime } from './application/runtime/index.js';
|
|
13
13
|
export type { Runtime } from './application/runtime/index.js';
|
|
14
14
|
export { stateMachine } from './application/state/index.js';
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { defineAction } from './application/action/index.js';
|
|
|
3
3
|
export { defineApplication, defineFrontend, external, requirements, route, routes, vite, } from './application/index.js';
|
|
4
4
|
export { defineIntegration, defineProvider } from './application/integration/index.js';
|
|
5
5
|
export { defineMutation, executeMutation } from './application/mutation/index.js';
|
|
6
|
-
export { defineCompositeQuery, defineQuery, executeQuery } from './application/query/index.js';
|
|
6
|
+
export { defineCollectionQuery, defineCompositeQuery, defineQuery, executeQuery, } from './application/query/index.js';
|
|
7
7
|
export { defineRuntime } from './application/runtime/index.js';
|
|
8
8
|
export { stateMachine } from './application/state/index.js';
|
|
9
9
|
export { defineWorkflow } from './application/workflow/index.js';
|
|
@@ -4,6 +4,7 @@ export { collectDependencyIssues, resolveProjectImport } from './imports.js';
|
|
|
4
4
|
export { importedSymbol, isLocallyDeclared, resolveImportedSymbol, location, objectProperty, propertyChain, staticText, unwrap, visit, } from './syntax.js';
|
|
5
5
|
export { assertSingleTypeUniverse } from './type-universe.js';
|
|
6
6
|
export { observeQueryDefinitions } from './query-observation.js';
|
|
7
|
+
export { queryCollectionKind } from './query-collection-type.js';
|
|
7
8
|
export { collectSuppressionDirectives } from './suppression-directives.js';
|
|
8
9
|
import type ts from 'typescript';
|
|
9
10
|
import type { DomainPolicy } from '../../policy/index.js';
|
|
@@ -4,4 +4,5 @@ export { collectDependencyIssues, resolveProjectImport } from './imports.js';
|
|
|
4
4
|
export { importedSymbol, isLocallyDeclared, resolveImportedSymbol, location, objectProperty, propertyChain, staticText, unwrap, visit, } from './syntax.js';
|
|
5
5
|
export { assertSingleTypeUniverse } from './type-universe.js';
|
|
6
6
|
export { observeQueryDefinitions } from './query-observation.js';
|
|
7
|
+
export { queryCollectionKind } from './query-collection-type.js';
|
|
7
8
|
export { collectSuppressionDirectives } from './suppression-directives.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { SourceFile, SourceProject } from './index.js';
|
|
3
|
+
/** Infer the closed collection graph kind only when the Domain's real TypeScript program proves it. */
|
|
4
|
+
export declare function queryCollectionKind(project: SourceProject, file: SourceFile, expression: ts.Expression): 'node' | 'edge' | undefined;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
const contexts = new WeakMap();
|
|
4
|
+
/** Infer the closed collection graph kind only when the Domain's real TypeScript program proves it. */
|
|
5
|
+
export function queryCollectionKind(project, file, expression) {
|
|
6
|
+
const context = typeContext(project);
|
|
7
|
+
if (context === null)
|
|
8
|
+
return undefined;
|
|
9
|
+
const source = context.program.getSourceFile(file.absolutePath);
|
|
10
|
+
if (source === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
const node = matchingNode(source, expression.getStart(file.source), expression.end);
|
|
13
|
+
if (node === undefined || !ts.isExpression(node))
|
|
14
|
+
return undefined;
|
|
15
|
+
const kinds = graphKinds(context.checker, context.checker.getTypeAtLocation(node), node);
|
|
16
|
+
return kinds.size === 1 ? [...kinds][0] : undefined;
|
|
17
|
+
}
|
|
18
|
+
function typeContext(project) {
|
|
19
|
+
const existing = contexts.get(project);
|
|
20
|
+
if (existing !== undefined)
|
|
21
|
+
return existing;
|
|
22
|
+
try {
|
|
23
|
+
const configPath = ts.findConfigFile(project.root, ts.sys.fileExists);
|
|
24
|
+
if (configPath === undefined)
|
|
25
|
+
return remember(project, null);
|
|
26
|
+
const loaded = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
27
|
+
if (loaded.error !== undefined)
|
|
28
|
+
return remember(project, null);
|
|
29
|
+
const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, dirname(configPath), { noEmit: true }, configPath);
|
|
30
|
+
const context = {
|
|
31
|
+
program: ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options }),
|
|
32
|
+
};
|
|
33
|
+
return remember(project, {
|
|
34
|
+
program: context.program,
|
|
35
|
+
checker: context.program.getTypeChecker(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return remember(project, null);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function remember(project, context) {
|
|
43
|
+
contexts.set(project, context);
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
function matchingNode(source, start, end) {
|
|
47
|
+
let match;
|
|
48
|
+
const visit = (node) => {
|
|
49
|
+
if (node.getStart(source) > start || node.end < end)
|
|
50
|
+
return;
|
|
51
|
+
if (node.getStart(source) === start && node.end === end)
|
|
52
|
+
match = node;
|
|
53
|
+
node.forEachChild(visit);
|
|
54
|
+
};
|
|
55
|
+
visit(source);
|
|
56
|
+
return match;
|
|
57
|
+
}
|
|
58
|
+
function graphKinds(checker, type, location) {
|
|
59
|
+
const kinds = new Set();
|
|
60
|
+
const inspect = (candidate) => {
|
|
61
|
+
if (candidate.isUnionOrIntersection()) {
|
|
62
|
+
for (const member of candidate.types)
|
|
63
|
+
inspect(member);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const property of candidate.getProperties()) {
|
|
67
|
+
if (!isCollectionClassBrand(property))
|
|
68
|
+
continue;
|
|
69
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(property, location);
|
|
70
|
+
const kind = propertyType.getProperty('kind');
|
|
71
|
+
if (kind === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
const kindType = checker.getTypeOfSymbolAtLocation(kind, location);
|
|
74
|
+
if (!(kindType.flags & ts.TypeFlags.StringLiteral))
|
|
75
|
+
continue;
|
|
76
|
+
const value = kindType.value;
|
|
77
|
+
if (value === 'node' || value === 'edge')
|
|
78
|
+
kinds.add(value);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
inspect(type);
|
|
82
|
+
return kinds;
|
|
83
|
+
}
|
|
84
|
+
function isCollectionClassBrand(property) {
|
|
85
|
+
return Boolean(property.declarations?.some((declaration) => {
|
|
86
|
+
if (!ts.isPropertySignature(declaration) ||
|
|
87
|
+
!ts.isInterfaceDeclaration(declaration.parent) ||
|
|
88
|
+
declaration.parent.name.text !== 'CollectionQueryDefinition') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const path = declaration.getSourceFile().fileName.replaceAll('\\', '/');
|
|
92
|
+
return path.includes('/node_modules/@astrale-os/sdk/');
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
@@ -22,7 +22,7 @@ function observeFile(file) {
|
|
|
22
22
|
return;
|
|
23
23
|
const origin = resolveImportedSymbol(file, factory.expression);
|
|
24
24
|
if ((origin.kind !== 'resolved' && origin.kind !== 'ambiguous') ||
|
|
25
|
-
origin.name !== 'defineQuery' ||
|
|
25
|
+
(origin.name !== 'defineQuery' && origin.name !== 'defineCollectionQuery') ||
|
|
26
26
|
(origin.kind === 'resolved' && !SDK_MODULES.has(origin.module))) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
@@ -41,6 +41,18 @@ function observeFile(file) {
|
|
|
41
41
|
}));
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
+
if (origin.name === 'defineCollectionQuery') {
|
|
45
|
+
observations.push(Object.freeze({
|
|
46
|
+
subjectId,
|
|
47
|
+
location: sourceLocation,
|
|
48
|
+
constructorIdentity: known(sdkConstructorIdentity(file, factory.expression, origin.name)),
|
|
49
|
+
definitionKind: known('single'),
|
|
50
|
+
canonicalRequestCount: known(1),
|
|
51
|
+
buildCallbackCount: known(1),
|
|
52
|
+
projectCallbackCount: known(0),
|
|
53
|
+
}));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
44
56
|
const object = projectedObject(node);
|
|
45
57
|
if (!object) {
|
|
46
58
|
const reason = 'definition input is not a static object literal';
|
|
@@ -129,12 +141,14 @@ function localConstInitializers(build) {
|
|
|
129
141
|
}
|
|
130
142
|
return declarations;
|
|
131
143
|
}
|
|
132
|
-
function sdkConstructorIdentity(file, expression) {
|
|
144
|
+
function sdkConstructorIdentity(file, expression, name = 'defineQuery') {
|
|
133
145
|
const spelling = expression.getText(file.source);
|
|
134
146
|
return Object.freeze({
|
|
135
147
|
kind: 'sdk-query',
|
|
136
|
-
canonicalId: '
|
|
137
|
-
|
|
148
|
+
canonicalId: name === 'defineCollectionQuery'
|
|
149
|
+
? 'astrale.sdk.defineCollectionQuery'
|
|
150
|
+
: 'astrale.sdk.defineQuery',
|
|
151
|
+
aliases: Object.freeze(spelling === name ? [] : [spelling]),
|
|
138
152
|
});
|
|
139
153
|
}
|
|
140
154
|
function canonicalRoots(file, node) {
|
|
@@ -14,7 +14,7 @@ export type Epistemic<T> = Readonly<{
|
|
|
14
14
|
}>;
|
|
15
15
|
export type QueryConstructorIdentity = Readonly<{
|
|
16
16
|
kind: 'sdk-query';
|
|
17
|
-
canonicalId: 'astrale.sdk.defineQuery';
|
|
17
|
+
canonicalId: 'astrale.sdk.defineQuery' | 'astrale.sdk.defineCollectionQuery';
|
|
18
18
|
aliases: readonly string[];
|
|
19
19
|
}> | Readonly<{
|
|
20
20
|
kind: 'other';
|
|
@@ -494,6 +494,7 @@ export const globalRules = [
|
|
|
494
494
|
const evidence = [];
|
|
495
495
|
const owners = new Map();
|
|
496
496
|
const definitions = [
|
|
497
|
+
{ layer: 'queries', name: 'defineCollectionQuery', members: [undefined] },
|
|
497
498
|
{ layer: 'queries', name: 'defineQuery', members: [undefined] },
|
|
498
499
|
{ layer: 'queries', name: 'defineCompositeQuery', members: [undefined] },
|
|
499
500
|
{ layer: 'mutations', name: 'defineMutation', members: [undefined] },
|
|
@@ -637,6 +638,7 @@ export const globalRules = [
|
|
|
637
638
|
if (file.layer === 'queries' || file.layer === 'mutations') {
|
|
638
639
|
const definitions = file.layer === 'queries'
|
|
639
640
|
? [
|
|
641
|
+
...definitionObjects(file, 'defineCollectionQuery'),
|
|
640
642
|
...definitionObjects(file, 'defineQuery'),
|
|
641
643
|
...definitionObjects(file, 'defineCompositeQuery'),
|
|
642
644
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
|
-
import { importedSymbol, observeQueryDefinitions, propertyChain, unwrap, visit, } from '../../adapters/typescript/index.js';
|
|
2
|
+
import { importedSymbol, observeQueryDefinitions, propertyChain, queryCollectionKind, unwrap, visit, } from '../../adapters/typescript/index.js';
|
|
3
3
|
import { decideCanonicalQuery, decideSingleQuery } from '../../analysis/query/index.js';
|
|
4
|
-
import { ambiguity, callback, definitionOriginAmbiguity, definitionObjects, domainProjectorEvidence, production, staticId, violation, } from './shared.js';
|
|
4
|
+
import { ambiguity, callback, definitionOriginAmbiguity, definitionObjects, domainProjectorEvidence, propertyExpression, production, returnedExpressions, staticId, violation, } from './shared.js';
|
|
5
5
|
import { sourceEvidence } from './verifier.js';
|
|
6
6
|
const FORBIDDEN_COMPOSE_NAMES = new Set(['fetch', 'invoke', 'mutate', 'run', 'step', 'then']);
|
|
7
7
|
export const queryRules = [
|
|
@@ -12,6 +12,7 @@ export const queryRules = [
|
|
|
12
12
|
return [
|
|
13
13
|
...sourceEvidence(decideCanonicalQuery(observeQueryDefinitions(project))),
|
|
14
14
|
...production(project, 'queries').flatMap((file) => [
|
|
15
|
+
...domainProjectorEvidence(file, 'defineCollectionQuery', 'Collection Query'),
|
|
15
16
|
...domainProjectorEvidence(file, 'defineQuery', 'Query'),
|
|
16
17
|
...domainProjectorEvidence(file, 'defineCompositeQuery', 'Composite Query'),
|
|
17
18
|
]),
|
|
@@ -170,7 +171,161 @@ export const queryRules = [
|
|
|
170
171
|
return evidence;
|
|
171
172
|
},
|
|
172
173
|
},
|
|
174
|
+
{
|
|
175
|
+
id: 'QRY-COLL-FANOUT',
|
|
176
|
+
ruleRevision: '50279cef3618d44cc1c3212a0d72213eca26a78c540f7a2998df244c38fc54ca',
|
|
177
|
+
evaluate(project) {
|
|
178
|
+
const evidence = [];
|
|
179
|
+
for (const file of production(project, 'queries')) {
|
|
180
|
+
for (const definition of definitionObjects(file, 'defineCompositeQuery')) {
|
|
181
|
+
if (definition.origin !== 'resolved' || !definition.object)
|
|
182
|
+
continue;
|
|
183
|
+
const compose = callback(definition.object, 'compose');
|
|
184
|
+
if (!compose)
|
|
185
|
+
continue;
|
|
186
|
+
const builder = composeBuilder(compose);
|
|
187
|
+
if (!builder)
|
|
188
|
+
continue;
|
|
189
|
+
const groups = {
|
|
190
|
+
node: [],
|
|
191
|
+
edge: [],
|
|
192
|
+
};
|
|
193
|
+
let logicalQueries = 0;
|
|
194
|
+
let physicalQueries = 0;
|
|
195
|
+
const returns = returnedExpressions(compose);
|
|
196
|
+
if (returns.length !== 1)
|
|
197
|
+
continue;
|
|
198
|
+
visit(returns[0], (node) => {
|
|
199
|
+
if (!ts.isCallExpression(node))
|
|
200
|
+
return;
|
|
201
|
+
const method = composeBuilderMethod(node.expression, builder);
|
|
202
|
+
if (method === 'query') {
|
|
203
|
+
logicalQueries += 1;
|
|
204
|
+
physicalQueries += 1;
|
|
205
|
+
if (!isVoidCollectionLeaf(node))
|
|
206
|
+
return;
|
|
207
|
+
const selected = node.arguments[1];
|
|
208
|
+
if (selected === undefined)
|
|
209
|
+
return;
|
|
210
|
+
const kind = queryCollectionKind(project, file, selected);
|
|
211
|
+
const member = kind && collectionMember(file, selected, node);
|
|
212
|
+
if (kind && member)
|
|
213
|
+
groups[kind].push(member);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (method !== 'union')
|
|
217
|
+
return;
|
|
218
|
+
const members = staticRecordMembers(node.arguments[0]);
|
|
219
|
+
if (members === undefined)
|
|
220
|
+
return;
|
|
221
|
+
logicalQueries += members;
|
|
222
|
+
physicalQueries += 1;
|
|
223
|
+
});
|
|
224
|
+
const candidates = ['node', 'edge'].filter((kind) => {
|
|
225
|
+
const members = groups[kind];
|
|
226
|
+
return (members.length >= 3 &&
|
|
227
|
+
new Set(members.map(({ id }) => id)).size === members.length &&
|
|
228
|
+
new Set(members.map(({ classKey }) => classKey)).size === members.length);
|
|
229
|
+
});
|
|
230
|
+
if (candidates.length === 0)
|
|
231
|
+
continue;
|
|
232
|
+
const optimizedPhysical = physicalQueries -
|
|
233
|
+
candidates.reduce((total, kind) => total + groups[kind].length, 0) +
|
|
234
|
+
candidates.length;
|
|
235
|
+
const scaffold = candidates
|
|
236
|
+
.map((kind) => {
|
|
237
|
+
const outer = kind === 'node' ? 'nodes' : 'relations';
|
|
238
|
+
const members = groups[kind]
|
|
239
|
+
.map(({ name, value }) => (name === value ? name : `${name}: ${value}`))
|
|
240
|
+
.join(', ');
|
|
241
|
+
return `${outer}: query.union({ ${members} })`;
|
|
242
|
+
})
|
|
243
|
+
.join('\n');
|
|
244
|
+
evidence.push(violation(file, compose, `Compatible collection fan-out has a direct replacement:\n${scaffold}\nPlan estimate: ${logicalQueries} logical → ${optimizedPhysical} physical cursor chains (currently ${physicalQueries} physical).`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return evidence;
|
|
248
|
+
},
|
|
249
|
+
},
|
|
173
250
|
];
|
|
251
|
+
function isVoidCollectionLeaf(call) {
|
|
252
|
+
const input = call.arguments[2];
|
|
253
|
+
if (input === undefined)
|
|
254
|
+
return true;
|
|
255
|
+
const value = unwrap(input);
|
|
256
|
+
return ((ts.isIdentifier(value) && value.text === 'undefined') ||
|
|
257
|
+
(ts.isVoidExpression(value) && ts.isNumericLiteral(unwrap(value.expression))));
|
|
258
|
+
}
|
|
259
|
+
function collectionMember(file, expression, call) {
|
|
260
|
+
const value = unwrap(expression);
|
|
261
|
+
if (!ts.isIdentifier(value))
|
|
262
|
+
return undefined;
|
|
263
|
+
const outputName = directOutputName(call);
|
|
264
|
+
const semantics = collectionDefinitionSemantics(file, value.text);
|
|
265
|
+
if (outputName === undefined || semantics === undefined)
|
|
266
|
+
return undefined;
|
|
267
|
+
return { name: outputName, value: value.text, ...semantics };
|
|
268
|
+
}
|
|
269
|
+
function directOutputName(call) {
|
|
270
|
+
const property = call.parent;
|
|
271
|
+
if (!ts.isPropertyAssignment(property) || unwrap(property.initializer) !== call)
|
|
272
|
+
return undefined;
|
|
273
|
+
const name = property.name;
|
|
274
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
275
|
+
}
|
|
276
|
+
function collectionDefinitionSemantics(file, name) {
|
|
277
|
+
const declarations = [];
|
|
278
|
+
visit(file.source, (node) => {
|
|
279
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
|
|
280
|
+
declarations.push(node);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
if (declarations.length !== 1)
|
|
284
|
+
return undefined;
|
|
285
|
+
const application = declarations[0].initializer && unwrap(declarations[0].initializer);
|
|
286
|
+
if (!application || !ts.isCallExpression(application))
|
|
287
|
+
return undefined;
|
|
288
|
+
const factory = unwrap(application.expression);
|
|
289
|
+
if (!ts.isCallExpression(factory))
|
|
290
|
+
return undefined;
|
|
291
|
+
const origin = importedSymbol(file, factory.expression);
|
|
292
|
+
if (origin?.name !== 'defineCollectionQuery' ||
|
|
293
|
+
(origin.module !== '@astrale-os/sdk' && origin.module !== '@astrale-os/sdk/query')) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const projected = application.arguments[0] && unwrap(application.arguments[0]);
|
|
297
|
+
if (!projected || (!ts.isArrowFunction(projected) && !ts.isFunctionExpression(projected))) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
const domain = projected.parameters[0]?.name;
|
|
301
|
+
if (!domain || !ts.isIdentifier(domain))
|
|
302
|
+
return undefined;
|
|
303
|
+
const returns = returnedExpressions(projected);
|
|
304
|
+
if (returns.length !== 1)
|
|
305
|
+
return undefined;
|
|
306
|
+
const object = unwrap(returns[0]);
|
|
307
|
+
if (!ts.isObjectLiteralExpression(object))
|
|
308
|
+
return undefined;
|
|
309
|
+
const id = staticId(propertyExpression(object, 'id'));
|
|
310
|
+
const selected = propertyExpression(object, 'class');
|
|
311
|
+
const chain = selected && propertyChain(selected);
|
|
312
|
+
if (!id || chain?.length !== 3 || chain[0] !== domain.text || chain[1] !== 'classes') {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
return { id, classKey: chain.slice(1).join('.') };
|
|
316
|
+
}
|
|
317
|
+
function staticRecordMembers(expression) {
|
|
318
|
+
if (!expression)
|
|
319
|
+
return undefined;
|
|
320
|
+
const value = unwrap(expression);
|
|
321
|
+
if (!ts.isObjectLiteralExpression(value))
|
|
322
|
+
return undefined;
|
|
323
|
+
if (value.properties.some((property) => ts.isSpreadAssignment(property) ||
|
|
324
|
+
('name' in property && property.name && ts.isComputedPropertyName(property.name)))) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
return value.properties.length;
|
|
328
|
+
}
|
|
174
329
|
function composeBuilder(compose) {
|
|
175
330
|
const name = compose.parameters[0]?.name;
|
|
176
331
|
if (!name)
|
|
@@ -188,20 +343,24 @@ function composeBuilder(compose) {
|
|
|
188
343
|
? element.propertyName.text
|
|
189
344
|
: undefined
|
|
190
345
|
: element.name.text;
|
|
191
|
-
if (source !== 'query' && source !== 'combine')
|
|
346
|
+
if (source !== 'query' && source !== 'union' && source !== 'combine')
|
|
192
347
|
continue;
|
|
193
348
|
if (bindings[source])
|
|
194
349
|
return undefined;
|
|
195
350
|
bindings[source] = element.name;
|
|
196
351
|
}
|
|
197
|
-
return bindings.query || bindings.
|
|
352
|
+
return bindings.query || bindings.union || bindings.combine
|
|
353
|
+
? { owner: compose, ...bindings }
|
|
354
|
+
: undefined;
|
|
198
355
|
}
|
|
199
356
|
function composeBuilderMethod(expression, builder) {
|
|
200
357
|
const target = unwrap(expression);
|
|
201
358
|
if (builder.root && ts.isPropertyAccessExpression(target)) {
|
|
202
359
|
const root = unwrap(target.expression);
|
|
203
360
|
if (ts.isIdentifier(root) &&
|
|
204
|
-
(target.name.text === 'query' ||
|
|
361
|
+
(target.name.text === 'query' ||
|
|
362
|
+
target.name.text === 'union' ||
|
|
363
|
+
target.name.text === 'combine') &&
|
|
205
364
|
referencesComposeBinding(root, builder.root, builder.owner)) {
|
|
206
365
|
return target.name.text;
|
|
207
366
|
}
|
|
@@ -210,6 +369,8 @@ function composeBuilderMethod(expression, builder) {
|
|
|
210
369
|
return undefined;
|
|
211
370
|
if (builder.query && referencesComposeBinding(target, builder.query, builder.owner))
|
|
212
371
|
return 'query';
|
|
372
|
+
if (builder.union && referencesComposeBinding(target, builder.union, builder.owner))
|
|
373
|
+
return 'union';
|
|
213
374
|
if (builder.combine && referencesComposeBinding(target, builder.combine, builder.owner)) {
|
|
214
375
|
return 'combine';
|
|
215
376
|
}
|
|
@@ -320,10 +481,10 @@ function composeReturnEvidence(file, compose, builder) {
|
|
|
320
481
|
if (!compose.body)
|
|
321
482
|
return [];
|
|
322
483
|
if (!ts.isBlock(compose.body)) {
|
|
323
|
-
return
|
|
484
|
+
return isBuilderClosedPlanCall(compose.body, builder)
|
|
324
485
|
? []
|
|
325
486
|
: [
|
|
326
|
-
violation(file, compose.body, 'Composite Query compose does not return the closed plan from combine.'),
|
|
487
|
+
violation(file, compose.body, 'Composite Query compose does not return the closed plan from combine or union.'),
|
|
327
488
|
];
|
|
328
489
|
}
|
|
329
490
|
const evidence = [];
|
|
@@ -331,8 +492,8 @@ function composeReturnEvidence(file, compose, builder) {
|
|
|
331
492
|
visitOwnBody(compose, (node) => {
|
|
332
493
|
if (ts.isReturnStatement(node)) {
|
|
333
494
|
returns += 1;
|
|
334
|
-
if (!node.expression || !
|
|
335
|
-
evidence.push(violation(file, node, 'Composite Query compose return does not produce the closed plan from combine.'));
|
|
495
|
+
if (!node.expression || !isBuilderClosedPlanCall(node.expression, builder)) {
|
|
496
|
+
evidence.push(violation(file, node, 'Composite Query compose return does not produce the closed plan from combine or union.'));
|
|
336
497
|
}
|
|
337
498
|
}
|
|
338
499
|
else if (ts.isThrowStatement(node)) {
|
|
@@ -344,9 +505,12 @@ function composeReturnEvidence(file, compose, builder) {
|
|
|
344
505
|
}
|
|
345
506
|
return evidence;
|
|
346
507
|
}
|
|
347
|
-
function
|
|
508
|
+
function isBuilderClosedPlanCall(expression, builder) {
|
|
348
509
|
const value = unwrap(expression);
|
|
349
|
-
|
|
510
|
+
if (!ts.isCallExpression(value))
|
|
511
|
+
return false;
|
|
512
|
+
const method = composeBuilderMethod(value.expression, builder);
|
|
513
|
+
return method === 'combine' || method === 'union';
|
|
350
514
|
}
|
|
351
515
|
function composePlanReachabilityEvidence(file, compose, builder) {
|
|
352
516
|
const authored = new Set();
|
|
@@ -360,7 +524,7 @@ function composePlanReachabilityEvidence(file, compose, builder) {
|
|
|
360
524
|
const roots = [];
|
|
361
525
|
if (!ts.isBlock(compose.body)) {
|
|
362
526
|
const root = unwrap(compose.body);
|
|
363
|
-
if (ts.isCallExpression(root) &&
|
|
527
|
+
if (ts.isCallExpression(root) && isBuilderClosedPlanCall(root, builder)) {
|
|
364
528
|
roots.push(root);
|
|
365
529
|
}
|
|
366
530
|
}
|
|
@@ -369,8 +533,7 @@ function composePlanReachabilityEvidence(file, compose, builder) {
|
|
|
369
533
|
if (!ts.isReturnStatement(node) || !node.expression)
|
|
370
534
|
return;
|
|
371
535
|
const root = unwrap(node.expression);
|
|
372
|
-
if (ts.isCallExpression(root) &&
|
|
373
|
-
composeBuilderMethod(root.expression, builder) === 'combine') {
|
|
536
|
+
if (ts.isCallExpression(root) && isBuilderClosedPlanCall(root, builder)) {
|
|
374
537
|
roots.push(root);
|
|
375
538
|
}
|
|
376
539
|
});
|
|
@@ -937,6 +1100,7 @@ function writeTargetReferencesBuilder(node, builder) {
|
|
|
937
1100
|
function isComposeBuilderReference(reference, builder) {
|
|
938
1101
|
return Boolean((builder.root && referencesComposeBinding(reference, builder.root, builder.owner)) ||
|
|
939
1102
|
(builder.query && referencesComposeBinding(reference, builder.query, builder.owner)) ||
|
|
1103
|
+
(builder.union && referencesComposeBinding(reference, builder.union, builder.owner)) ||
|
|
940
1104
|
(builder.combine && referencesComposeBinding(reference, builder.combine, builder.owner)));
|
|
941
1105
|
}
|
|
942
1106
|
function statementShadowsComposeBinding(statement, name, scope, owner) {
|
|
@@ -21,7 +21,7 @@ export declare function memberCalls(file: SourceFile, importedName: string, memb
|
|
|
21
21
|
export declare function definitionObjects(file: SourceFile, importedName: string, member?: string, modules?: Set<string>, argumentIndex?: number): DefinitionObject[];
|
|
22
22
|
export declare function definitionOriginAmbiguity(file: SourceFile, definition: DefinitionObject, name: string): SourceRuleEvidence | undefined;
|
|
23
23
|
/** Prove the clean curried Domain projector shape for Query and Mutation recipes. */
|
|
24
|
-
export declare function domainProjectorEvidence(file: SourceFile, importedName: 'defineCompositeQuery' | 'defineMutation' | 'defineQuery', label: string): SourceRuleEvidence[];
|
|
24
|
+
export declare function domainProjectorEvidence(file: SourceFile, importedName: 'defineCollectionQuery' | 'defineCompositeQuery' | 'defineMutation' | 'defineQuery', label: string): SourceRuleEvidence[];
|
|
25
25
|
export declare function violation(file: SourceFile, node: ts.Node, evidence: string): SourceRuleEvidence;
|
|
26
26
|
export declare function ambiguity(file: SourceFile, node: ts.Node, evidence: string): SourceRuleEvidence;
|
|
27
27
|
export declare function localTarget(project: SourceProject, file: SourceFile, sourceImport: SourceImport): SourceFile | undefined;
|
|
@@ -15,7 +15,12 @@ export const DSL_MODULES = new Set([
|
|
|
15
15
|
'@astrale-os/kernel-dsl/v1',
|
|
16
16
|
'@astrale-os/kernel-core',
|
|
17
17
|
]);
|
|
18
|
-
const DOMAIN_BOUND_DEFINITIONS = new Set([
|
|
18
|
+
const DOMAIN_BOUND_DEFINITIONS = new Set([
|
|
19
|
+
'defineCollectionQuery',
|
|
20
|
+
'defineCompositeQuery',
|
|
21
|
+
'defineMutation',
|
|
22
|
+
'defineQuery',
|
|
23
|
+
]);
|
|
19
24
|
export function production(project, layer) {
|
|
20
25
|
return project.files.filter((file) => file.role === 'production' && (layer === undefined || file.layer === layer));
|
|
21
26
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { DomainPolicySource } from './index.js';
|
|
2
|
-
/** Generated from the validated Domain knowledge catalog.
|
|
2
|
+
/** Generated from the validated Domain knowledge catalog. Regenerate with pnpm sync:linter-policy. */
|
|
3
3
|
export declare const domainPolicyDigest: string;
|
|
4
4
|
export declare const domainPolicySource: DomainPolicySource;
|