Implement query execution, scoped RPC enrichment, live collections, and scaffold integration

This commit is contained in:
Timothy J. Aveni
2026-09-17 16:49:44 -07:00
parent 61a410f98f
commit cd13120937
37 changed files with 4406 additions and 2647 deletions
+6
View File
@@ -2,6 +2,8 @@ import { readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { loadQxSources, resolveQxSources } from "./source-loader.js";
import { compileRepositoryQuery } from "../query/compile.js";
import { checkQueryTemplate } from "../query/templates.js";
import { readRepositorySource } from "./source-loader.js";
import { linkQueries } from "../query/link.js";
import {
capabilityId,
@@ -237,6 +239,10 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
...(environment.interfaceClosure ?? []),
...(compiled.resource.specializations ?? []),
];
for (const template of compiled.resource.revision.queryTemplates ?? [])
await checkQueryTemplate(template, queryInterfaces, (name) =>
readRepositorySource(snapshot.directory, name),
);
if (compiled.resource.revision.queries?.length)
compiled.resource.revision.checkedQueries = await Promise.all(
compiled.resource.revision.queries.map((query) =>
File diff suppressed because one or more lines are too long
@@ -1,251 +1,253 @@
WORKSPACE=1
QUERY=2
ROOT=3
DOCUMENT=4
FRAGMENTS=5
VIEW=6
MAX=7
ALLOW=8
POLL=9
QUERYABLE=10
RPC=11
QUERY_REASON=12
TYPE=13
OBJECT=14
STORABLE=15
IMPLEMENTS=16
REF=17
FRAGMENT=18
IMPORT=19
EXTERNAL=20
ATOM=21
INTERFACE=22
INTERFACES=23
PACKAGE=24
VALUE=25
RELATION=26
OPERATION=27
FUNCTION=28
CONSTRUCTOR=29
CONSTRUCTS=30
INPUT=31
CONFORM=32
AS=33
BIND=34
STATIC=35
TO=36
PRIVATE=37
SHARED=38
STATE=39
EDGE=40
PROJECTION=41
WITH=42
USING=43
VIA=44
MATERIALIZE=45
IF=46
ABSENT=47
ON=48
POLICY=49
DEFAULT=50
SOURCE=51
REPOSITORY=52
COMMIT=53
REVISION=54
SEMANTIC_MAJOR=55
ON_DELETE=56
RETAIN_OTHER=57
KEYED=58
PUBLIC_TRAVERSAL=59
ID=60
DOC=61
MODE=62
EMITS=63
RECEIVER=64
REQUIRES=65
ANY=66
GET=67
SET=68
WATCH=69
START=70
STOP=71
READ=72
WRITE=73
RESOLVE=74
CONNECT=75
DISCONNECT=76
CALL=77
WATCH_START=78
WATCH_STOP=79
SUBSCRIBE=80
UNSUBSCRIBE=81
OPTIMISTIC_REGISTER=82
CRDT=83
OPTIONAL_ONE=84
EXACTLY_ONE=85
MANY_UNIQUE=86
MANY=87
ORDERED=88
UNIT=89
WATCH_HANDLE=90
MESSAGE=91
ATOM_REF=92
INTERFACE_REF=93
OPTIONAL=94
LIST=95
RECORD=96
BOOL=97
BYTES=98
DOUBLE=99
INT32=100
INT64=101
STRING=102
UINT32=103
UINT64=104
TRUE=105
FALSE=106
NULL=107
ARROW=108
COLON=109
SEMI=110
COMMA=111
DOT=112
LBRACE=113
RBRACE=114
LBRACK=115
RBRACK=116
LPAREN=117
RPAREN=118
LT=119
GT=120
AMP=121
EQUAL=122
INTEGER=123
JSON_NUMBER=124
IDENTIFIER=125
STRING_LITERAL=126
LINE_COMMENT=127
BLOCK_COMMENT=128
WS=129
SPECIALIZE=3
ROOT=4
DOCUMENT=5
FRAGMENTS=6
VIEW=7
MAX=8
ALLOW=9
POLL=10
QUERYABLE=11
RPC=12
QUERY_REASON=13
TYPE=14
OBJECT=15
STORABLE=16
IMPLEMENTS=17
REF=18
FRAGMENT=19
IMPORT=20
EXTERNAL=21
ATOM=22
INTERFACE=23
INTERFACES=24
PACKAGE=25
VALUE=26
RELATION=27
OPERATION=28
FUNCTION=29
CONSTRUCTOR=30
CONSTRUCTS=31
INPUT=32
CONFORM=33
AS=34
BIND=35
STATIC=36
TO=37
PRIVATE=38
SHARED=39
STATE=40
EDGE=41
PROJECTION=42
WITH=43
USING=44
VIA=45
MATERIALIZE=46
IF=47
ABSENT=48
ON=49
POLICY=50
DEFAULT=51
SOURCE=52
REPOSITORY=53
COMMIT=54
REVISION=55
SEMANTIC_MAJOR=56
ON_DELETE=57
RETAIN_OTHER=58
KEYED=59
PUBLIC_TRAVERSAL=60
ID=61
DOC=62
MODE=63
EMITS=64
RECEIVER=65
REQUIRES=66
ANY=67
GET=68
SET=69
WATCH=70
START=71
STOP=72
READ=73
WRITE=74
RESOLVE=75
CONNECT=76
DISCONNECT=77
CALL=78
WATCH_START=79
WATCH_STOP=80
SUBSCRIBE=81
UNSUBSCRIBE=82
OPTIMISTIC_REGISTER=83
CRDT=84
OPTIONAL_ONE=85
EXACTLY_ONE=86
MANY_UNIQUE=87
MANY=88
ORDERED=89
UNIT=90
WATCH_HANDLE=91
MESSAGE=92
ATOM_REF=93
INTERFACE_REF=94
OPTIONAL=95
LIST=96
RECORD=97
BOOL=98
BYTES=99
DOUBLE=100
INT32=101
INT64=102
STRING=103
UINT32=104
UINT64=105
TRUE=106
FALSE=107
NULL=108
ARROW=109
COLON=110
SEMI=111
COMMA=112
DOT=113
LBRACE=114
RBRACE=115
LBRACK=116
RBRACK=117
LPAREN=118
RPAREN=119
LT=120
GT=121
AMP=122
EQUAL=123
INTEGER=124
JSON_NUMBER=125
IDENTIFIER=126
STRING_LITERAL=127
LINE_COMMENT=128
BLOCK_COMMENT=129
WS=130
'workspace'=1
'query'=2
'root'=3
'document'=4
'fragments'=5
'view'=6
'max'=7
'allow'=8
'poll'=9
'queryable'=10
'rpc'=11
'query-reason'=12
'type'=13
'object'=14
'storable'=15
'implements'=16
'ref'=17
'fragment'=18
'import'=19
'external'=20
'atom'=21
'interface'=22
'interfaces'=23
'package'=24
'value'=25
'relation'=26
'operation'=27
'function'=28
'constructor'=29
'constructs'=30
'input'=31
'conform'=32
'as'=33
'bind'=34
'static'=35
'to'=36
'private'=37
'shared'=38
'state'=39
'edge'=40
'projection'=41
'with'=42
'using'=43
'via'=44
'materialize'=45
'if'=46
'absent'=47
'on'=48
'policy'=49
'default'=50
'source'=51
'repository'=52
'commit'=53
'revision'=54
'semantic-major'=55
'on-delete'=56
'retain-other'=57
'keyed'=58
'public-traversal'=59
'id'=60
'doc'=61
'mode'=62
'emits'=63
'receiver'=64
'requires'=65
'any'=66
'get'=67
'set'=68
'watch'=69
'start'=70
'stop'=71
'read'=72
'write'=73
'resolve'=74
'connect'=75
'disconnect'=76
'call'=77
'watch-start'=78
'watch-stop'=79
'subscribe'=80
'unsubscribe'=81
'optimistic-register'=82
'crdt'=83
'optional-one'=84
'exactly-one'=85
'many-unique'=86
'many'=87
'ordered'=88
'unit'=89
'watch-handle'=90
'message'=91
'atom-ref'=92
'interface-ref'=93
'optional'=94
'list'=95
'record'=96
'bool'=97
'bytes'=98
'double'=99
'int32'=100
'int64'=101
'string'=102
'uint32'=103
'uint64'=104
'true'=105
'false'=106
'null'=107
'->'=108
':'=109
';'=110
','=111
'.'=112
'{'=113
'}'=114
'['=115
']'=116
'('=117
')'=118
'<'=119
'>'=120
'&'=121
'='=122
'specialize'=3
'root'=4
'document'=5
'fragments'=6
'view'=7
'max'=8
'allow'=9
'poll'=10
'queryable'=11
'rpc'=12
'query-reason'=13
'type'=14
'object'=15
'storable'=16
'implements'=17
'ref'=18
'fragment'=19
'import'=20
'external'=21
'atom'=22
'interface'=23
'interfaces'=24
'package'=25
'value'=26
'relation'=27
'operation'=28
'function'=29
'constructor'=30
'constructs'=31
'input'=32
'conform'=33
'as'=34
'bind'=35
'static'=36
'to'=37
'private'=38
'shared'=39
'state'=40
'edge'=41
'projection'=42
'with'=43
'using'=44
'via'=45
'materialize'=46
'if'=47
'absent'=48
'on'=49
'policy'=50
'default'=51
'source'=52
'repository'=53
'commit'=54
'revision'=55
'semantic-major'=56
'on-delete'=57
'retain-other'=58
'keyed'=59
'public-traversal'=60
'id'=61
'doc'=62
'mode'=63
'emits'=64
'receiver'=65
'requires'=66
'any'=67
'get'=68
'set'=69
'watch'=70
'start'=71
'stop'=72
'read'=73
'write'=74
'resolve'=75
'connect'=76
'disconnect'=77
'call'=78
'watch-start'=79
'watch-stop'=80
'subscribe'=81
'unsubscribe'=82
'optimistic-register'=83
'crdt'=84
'optional-one'=85
'exactly-one'=86
'many-unique'=87
'many'=88
'ordered'=89
'unit'=90
'watch-handle'=91
'message'=92
'atom-ref'=93
'interface-ref'=94
'optional'=95
'list'=96
'record'=97
'bool'=98
'bytes'=99
'double'=100
'int32'=101
'int64'=102
'string'=103
'uint32'=104
'uint64'=105
'true'=106
'false'=107
'null'=108
'->'=109
':'=110
';'=111
','=112
'.'=113
'{'=114
'}'=115
'['=116
']'=117
'('=118
')'=119
'<'=120
'>'=121
'&'=122
'='=123
File diff suppressed because one or more lines are too long
@@ -1,251 +1,253 @@
WORKSPACE=1
QUERY=2
ROOT=3
DOCUMENT=4
FRAGMENTS=5
VIEW=6
MAX=7
ALLOW=8
POLL=9
QUERYABLE=10
RPC=11
QUERY_REASON=12
TYPE=13
OBJECT=14
STORABLE=15
IMPLEMENTS=16
REF=17
FRAGMENT=18
IMPORT=19
EXTERNAL=20
ATOM=21
INTERFACE=22
INTERFACES=23
PACKAGE=24
VALUE=25
RELATION=26
OPERATION=27
FUNCTION=28
CONSTRUCTOR=29
CONSTRUCTS=30
INPUT=31
CONFORM=32
AS=33
BIND=34
STATIC=35
TO=36
PRIVATE=37
SHARED=38
STATE=39
EDGE=40
PROJECTION=41
WITH=42
USING=43
VIA=44
MATERIALIZE=45
IF=46
ABSENT=47
ON=48
POLICY=49
DEFAULT=50
SOURCE=51
REPOSITORY=52
COMMIT=53
REVISION=54
SEMANTIC_MAJOR=55
ON_DELETE=56
RETAIN_OTHER=57
KEYED=58
PUBLIC_TRAVERSAL=59
ID=60
DOC=61
MODE=62
EMITS=63
RECEIVER=64
REQUIRES=65
ANY=66
GET=67
SET=68
WATCH=69
START=70
STOP=71
READ=72
WRITE=73
RESOLVE=74
CONNECT=75
DISCONNECT=76
CALL=77
WATCH_START=78
WATCH_STOP=79
SUBSCRIBE=80
UNSUBSCRIBE=81
OPTIMISTIC_REGISTER=82
CRDT=83
OPTIONAL_ONE=84
EXACTLY_ONE=85
MANY_UNIQUE=86
MANY=87
ORDERED=88
UNIT=89
WATCH_HANDLE=90
MESSAGE=91
ATOM_REF=92
INTERFACE_REF=93
OPTIONAL=94
LIST=95
RECORD=96
BOOL=97
BYTES=98
DOUBLE=99
INT32=100
INT64=101
STRING=102
UINT32=103
UINT64=104
TRUE=105
FALSE=106
NULL=107
ARROW=108
COLON=109
SEMI=110
COMMA=111
DOT=112
LBRACE=113
RBRACE=114
LBRACK=115
RBRACK=116
LPAREN=117
RPAREN=118
LT=119
GT=120
AMP=121
EQUAL=122
INTEGER=123
JSON_NUMBER=124
IDENTIFIER=125
STRING_LITERAL=126
LINE_COMMENT=127
BLOCK_COMMENT=128
WS=129
SPECIALIZE=3
ROOT=4
DOCUMENT=5
FRAGMENTS=6
VIEW=7
MAX=8
ALLOW=9
POLL=10
QUERYABLE=11
RPC=12
QUERY_REASON=13
TYPE=14
OBJECT=15
STORABLE=16
IMPLEMENTS=17
REF=18
FRAGMENT=19
IMPORT=20
EXTERNAL=21
ATOM=22
INTERFACE=23
INTERFACES=24
PACKAGE=25
VALUE=26
RELATION=27
OPERATION=28
FUNCTION=29
CONSTRUCTOR=30
CONSTRUCTS=31
INPUT=32
CONFORM=33
AS=34
BIND=35
STATIC=36
TO=37
PRIVATE=38
SHARED=39
STATE=40
EDGE=41
PROJECTION=42
WITH=43
USING=44
VIA=45
MATERIALIZE=46
IF=47
ABSENT=48
ON=49
POLICY=50
DEFAULT=51
SOURCE=52
REPOSITORY=53
COMMIT=54
REVISION=55
SEMANTIC_MAJOR=56
ON_DELETE=57
RETAIN_OTHER=58
KEYED=59
PUBLIC_TRAVERSAL=60
ID=61
DOC=62
MODE=63
EMITS=64
RECEIVER=65
REQUIRES=66
ANY=67
GET=68
SET=69
WATCH=70
START=71
STOP=72
READ=73
WRITE=74
RESOLVE=75
CONNECT=76
DISCONNECT=77
CALL=78
WATCH_START=79
WATCH_STOP=80
SUBSCRIBE=81
UNSUBSCRIBE=82
OPTIMISTIC_REGISTER=83
CRDT=84
OPTIONAL_ONE=85
EXACTLY_ONE=86
MANY_UNIQUE=87
MANY=88
ORDERED=89
UNIT=90
WATCH_HANDLE=91
MESSAGE=92
ATOM_REF=93
INTERFACE_REF=94
OPTIONAL=95
LIST=96
RECORD=97
BOOL=98
BYTES=99
DOUBLE=100
INT32=101
INT64=102
STRING=103
UINT32=104
UINT64=105
TRUE=106
FALSE=107
NULL=108
ARROW=109
COLON=110
SEMI=111
COMMA=112
DOT=113
LBRACE=114
RBRACE=115
LBRACK=116
RBRACK=117
LPAREN=118
RPAREN=119
LT=120
GT=121
AMP=122
EQUAL=123
INTEGER=124
JSON_NUMBER=125
IDENTIFIER=126
STRING_LITERAL=127
LINE_COMMENT=128
BLOCK_COMMENT=129
WS=130
'workspace'=1
'query'=2
'root'=3
'document'=4
'fragments'=5
'view'=6
'max'=7
'allow'=8
'poll'=9
'queryable'=10
'rpc'=11
'query-reason'=12
'type'=13
'object'=14
'storable'=15
'implements'=16
'ref'=17
'fragment'=18
'import'=19
'external'=20
'atom'=21
'interface'=22
'interfaces'=23
'package'=24
'value'=25
'relation'=26
'operation'=27
'function'=28
'constructor'=29
'constructs'=30
'input'=31
'conform'=32
'as'=33
'bind'=34
'static'=35
'to'=36
'private'=37
'shared'=38
'state'=39
'edge'=40
'projection'=41
'with'=42
'using'=43
'via'=44
'materialize'=45
'if'=46
'absent'=47
'on'=48
'policy'=49
'default'=50
'source'=51
'repository'=52
'commit'=53
'revision'=54
'semantic-major'=55
'on-delete'=56
'retain-other'=57
'keyed'=58
'public-traversal'=59
'id'=60
'doc'=61
'mode'=62
'emits'=63
'receiver'=64
'requires'=65
'any'=66
'get'=67
'set'=68
'watch'=69
'start'=70
'stop'=71
'read'=72
'write'=73
'resolve'=74
'connect'=75
'disconnect'=76
'call'=77
'watch-start'=78
'watch-stop'=79
'subscribe'=80
'unsubscribe'=81
'optimistic-register'=82
'crdt'=83
'optional-one'=84
'exactly-one'=85
'many-unique'=86
'many'=87
'ordered'=88
'unit'=89
'watch-handle'=90
'message'=91
'atom-ref'=92
'interface-ref'=93
'optional'=94
'list'=95
'record'=96
'bool'=97
'bytes'=98
'double'=99
'int32'=100
'int64'=101
'string'=102
'uint32'=103
'uint64'=104
'true'=105
'false'=106
'null'=107
'->'=108
':'=109
';'=110
','=111
'.'=112
'{'=113
'}'=114
'['=115
']'=116
'('=117
')'=118
'<'=119
'>'=120
'&'=121
'='=122
'specialize'=3
'root'=4
'document'=5
'fragments'=6
'view'=7
'max'=8
'allow'=9
'poll'=10
'queryable'=11
'rpc'=12
'query-reason'=13
'type'=14
'object'=15
'storable'=16
'implements'=17
'ref'=18
'fragment'=19
'import'=20
'external'=21
'atom'=22
'interface'=23
'interfaces'=24
'package'=25
'value'=26
'relation'=27
'operation'=28
'function'=29
'constructor'=30
'constructs'=31
'input'=32
'conform'=33
'as'=34
'bind'=35
'static'=36
'to'=37
'private'=38
'shared'=39
'state'=40
'edge'=41
'projection'=42
'with'=43
'using'=44
'via'=45
'materialize'=46
'if'=47
'absent'=48
'on'=49
'policy'=50
'default'=51
'source'=52
'repository'=53
'commit'=54
'revision'=55
'semantic-major'=56
'on-delete'=57
'retain-other'=58
'keyed'=59
'public-traversal'=60
'id'=61
'doc'=62
'mode'=63
'emits'=64
'receiver'=65
'requires'=66
'any'=67
'get'=68
'set'=69
'watch'=70
'start'=71
'stop'=72
'read'=73
'write'=74
'resolve'=75
'connect'=76
'disconnect'=77
'call'=78
'watch-start'=79
'watch-stop'=80
'subscribe'=81
'unsubscribe'=82
'optimistic-register'=83
'crdt'=84
'optional-one'=85
'exactly-one'=86
'many-unique'=87
'many'=88
'ordered'=89
'unit'=90
'watch-handle'=91
'message'=92
'atom-ref'=93
'interface-ref'=94
'optional'=95
'list'=96
'record'=97
'bool'=98
'bytes'=99
'double'=100
'int32'=101
'int64'=102
'string'=103
'uint32'=104
'uint64'=105
'true'=106
'false'=107
'null'=108
'->'=109
':'=110
';'=111
','=112
'.'=113
'{'=114
'}'=115
'['=116
']'=117
'('=118
')'=119
'<'=120
'>'=121
'&'=122
'='=123
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -30,6 +30,7 @@ import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
import { PackageExportContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
@@ -249,6 +250,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitPackageQuery?: (ctx: PackageQueryContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuerySpecialization`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPackageQuerySpecialization?: (ctx: PackageQuerySpecializationContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.queryClause`.
* @param ctx the parse tree
+252 -79
View File
@@ -46,7 +46,14 @@ import {
specializePackageExport,
} from "../capability-model/index.js";
import { GenericSourceTypes } from "./generic-types.js";
import { defaultQueryBudgets, type QueryDeclaration, type QueryBudgets, type QueryUse } from "../query/types.js";
import {
defaultQueryBudgets,
type QueryDeclaration,
type QueryBudgets,
type QueryUse,
type QueryTemplate,
} from "../query/types.js";
import { specializeQueryTemplate } from "../query/templates.js";
import { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
import {
QuixosCapabilityParser,
@@ -177,6 +184,7 @@ interface PackageExportSymbol {
}
interface PackageSymbol {
queries?: Map<string, string>;
definition?: PackageRevision;
revisionId: PackageRevision["revisionId"];
exports: Map<string, PackageExportSymbol>;
@@ -535,6 +543,7 @@ const lowerRelationshipMember = (
}
return {
kind: "relationship",
...(context.KEYED() ? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
...(context.queryReadContract()
? {
queryRead: {
@@ -542,7 +551,7 @@ const lowerRelationshipMember = (
},
}
: {}),
id: capabilityId.member(stringValue(context.stringLiteral())),
id: capabilityId.member(stringValue(context.stringLiteral(0))),
displayName: identifier(context.identifier()),
target,
cardinality,
@@ -747,6 +756,9 @@ const lowerInterfaceTemplate = (
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
return {
kind: "relationship",
...(relationship.KEYED()
? { keyType: stringValue(relationship.stringLiteral(1)) as "string" | "boolean" | "int64" }
: {}),
...(relationship.queryReadContract()
? {
queryRead: {
@@ -754,7 +766,7 @@ const lowerInterfaceTemplate = (
},
}
: {}),
id: capabilityId.member(stringValue(relationship.stringLiteral())),
id: capabilityId.member(stringValue(relationship.stringLiteral(0))),
displayName: identifier(relationship.identifier()),
target,
cardinality,
@@ -818,9 +830,34 @@ const lowerInterfaceTemplate = (
};
};
const lowerQueryReference = (
state: LoweringState,
packageName: string,
queryName: string,
context: ParserRuleContext,
) => {
const pkg = requireSymbol(state, state.packages, packageName, context, "package");
const queryId =
pkg?.queries?.get(queryName) ?? pkg?.definition?.queries?.find((query) => query.displayName === queryName)?.id;
if (!pkg || !queryId) {
loweringIssue(state, context, "unknown-query", `Unknown query export ${packageName}.${queryName}`);
return undefined;
}
return { packageRevisionId: pkg.revisionId, queryId };
};
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
const name = identifier(context.identifier(0));
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
if (context.QUERY()) {
const query = lowerQueryReference(
state,
identifier(context.identifier(1)),
identifier(context.identifier(2)),
context,
);
return query ? { id, displayName: name, requirement: { kind: "query", ...query } } : undefined;
}
if (context.STATE()) {
const primitives = context
.primitiveList()!
@@ -1060,6 +1097,16 @@ const lowerGenericPackageExport = (
.primitiveList()
?.primitive()
.map((item) => text(item)) ?? [];
if (port.QUERY()) {
const query = lowerQueryReference(state, identifier(port.identifier(1)), identifier(port.identifier(2)), port);
if (!query)
throw new GenericTypeError(
"unknown-query",
base.displayName,
"Query port requires an existing closed query export",
);
return { ...base, requirement: { kind: "query", ...query } };
}
if (port.STATE())
return {
...base,
@@ -1158,91 +1205,178 @@ const lowerPackage = (
source: SourceRevision,
): PackageRevision => {
const alias = identifier(context.identifier());
state.packages.get(alias)!.queries = new Map(
context.packageExport().flatMap((entry) => {
const query = entry.packageQuery();
const specialization = entry.packageQuerySpecialization();
return query && !query.typeParameters()
? [[identifier(query.identifier()), stringValue(query.stringLiteral(0))] as const]
: specialization
? [[identifier(specialization.identifier(0)), stringValue(specialization.stringLiteral())] as const]
: [];
}),
);
const genericExports: GenericPackageExport[] = [];
const queries: QueryDeclaration[] = [];
const queryTemplates: QueryTemplate[] = [];
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
if (exportContext.packageQuerySpecialization()) return [];
const query = exportContext.packageQuery();
if (query) {
const close = (ctx: import("./generated/QuixosCapabilityParser.js").InterfaceTypeContext) =>
new TypeSubstitution(state.types.environment()).application(state.types.interface(ctx));
const declaration: QueryDeclaration = {
id: stringValue(query.stringLiteral(0)),
displayName: identifier(query.identifier()),
root: close(query.interfaceType()),
document: stringValue(query.stringLiteral(1)),
operation: stringValue(query.stringLiteral(2)),
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets },
watch: false,
};
const budgetNames: Record<string, keyof QueryBudgets> = {
rows: "rows",
depth: "depth",
"result-bytes": "resultBytes",
candidates: "candidates",
"rpc-calls": "rpcCalls",
concurrency: "concurrency",
"deadline-ms": "deadlineMs",
};
const assigned = new Set<string>();
for (const clause of query.queryClause()) {
if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
else if (clause.VIEW()) {
const name = identifier(clause.identifier(0));
const atomId = requireSymbol(state, state.atoms, name, clause, "atom");
if (atomId) declaration.views.push({ atomId, interfaceRevisionId: close(clause.interfaceType()!) });
} else if (clause.MAX()) {
const name = identifier(clause.identifier(0));
const key = budgetNames[name],
n = Number(clause.INTEGER()!.getText());
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key])
loweringIssue(
state,
clause,
"invalid-query-budget",
`Invalid, duplicate, or excessive query budget ${name}`,
const previousParameters = state.types.parameters;
state.types.parameters = new Map();
try {
const parameters = state.types.declareParameters(
query.typeParameters(),
`query:${stringValue(query.stringLiteral(0))}`,
);
const expressions = new Map<string, ReturnType<GenericSourceTypes["interface"]>>();
const templateViews: QueryTemplate["views"] = [];
const templateAllowances: QueryTemplate["allowances"] = [];
const close = (ctx: import("./generated/QuixosCapabilityParser.js").InterfaceTypeContext) =>
parameters.length
? (() => {
const id = capabilityId.interfaceRevision(`query-expression:${expressions.size}`);
expressions.set(id, state.types.interface(ctx));
return id;
})()
: new TypeSubstitution(state.types.environment()).application(state.types.interface(ctx));
const declaration: QueryDeclaration = {
id: stringValue(query.stringLiteral(0)),
displayName: identifier(query.identifier()),
root: close(query.interfaceType()),
document: stringValue(query.stringLiteral(1)),
operation: stringValue(query.stringLiteral(2)),
fragments: [],
views: [],
allowances: [],
budgets: { ...defaultQueryBudgets },
watch: false,
};
const budgetNames: Record<string, keyof QueryBudgets> = {
rows: "rows",
depth: "depth",
"result-bytes": "resultBytes",
candidates: "candidates",
"rpc-calls": "rpcCalls",
concurrency: "concurrency",
"deadline-ms": "deadlineMs",
};
const assigned = new Set<string>();
for (const clause of query.queryClause()) {
if (clause.FRAGMENTS()) declaration.fragments.push(stringValue(clause.stringLiteral()!));
else if (clause.VIEW()) {
const name = identifier(clause.identifier(0));
if (parameters.length) {
const parameter = state.types.parameters.get(name);
const atomId = state.atoms.get(name);
if (clause.OBJECT() && parameter?.kind === "object")
templateViews.push({
target: { kind: "parameter", parameterId: parameter.id },
interface: state.types.interface(clause.interfaceType()!),
});
else if (!clause.OBJECT() && atomId)
templateViews.push({
target: { kind: "atom", atomId },
interface: state.types.interface(clause.interfaceType()!),
});
else
loweringIssue(state, clause, "invalid-query-view", "Expected declared object parameter or atom view");
continue;
}
if (clause.OBJECT())
loweringIssue(state, clause, "invalid-query-view", "Object parameter views require a query template");
const atomId = requireSymbol(state, state.atoms, name, clause, "atom");
if (atomId) declaration.views.push({ atomId, interfaceRevisionId: close(clause.interfaceType()!) });
} else if (clause.MAX()) {
const name = identifier(clause.identifier(0));
const key = budgetNames[name],
n = Number(clause.INTEGER()!.getText());
if (!key || assigned.has(name) || !Number.isSafeInteger(n) || n < 1 || n > defaultQueryBudgets[key])
loweringIssue(
state,
clause,
"invalid-query-budget",
`Invalid, duplicate, or excessive query budget ${name}`,
);
else {
declaration.budgets[key] = n;
assigned.add(name);
}
} else if (clause.ALLOW()) {
if (parameters.length) {
const use = identifier(clause.identifier(1));
const reason = stringValue(clause.stringLiteral()!);
if (!["select", "predicate", "order"].includes(use) || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-allowance",
"Expected select/predicate/order and a nonempty reason",
);
templateAllowances.push({
interface: state.types.interface(clause.interfaceType()!),
memberName: identifier(clause.identifier(0)),
uses: [use as QueryUse],
reason,
});
continue;
}
const interfaceRevisionId = close(clause.interfaceType()!);
const contract = [...state.types.interfaces.values(), ...state.types.applications.values()].find(
(entry) => entry.revisionId === interfaceRevisionId,
);
else {
declaration.budgets[key] = n;
assigned.add(name);
const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0)));
const use = identifier(clause.identifier(1));
const reason = stringValue(clause.stringLiteral()!);
if (!member || !["select", "predicate", "order"].includes(use) || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-allowance",
"Query allowances name an exact member, use (select/predicate/order), and nonempty reason",
);
else
declaration.allowances.push({
interfaceRevisionId,
memberId: member.id,
uses: [use as QueryUse],
reason,
});
} else if (clause.WATCH()) declaration.watch = true;
else if (clause.POLL()) {
const intervalMs = Number(clause.INTEGER()!.getText()),
reason = stringValue(clause.stringLiteral()!);
if (declaration.polling || !Number.isSafeInteger(intervalMs) || intervalMs < 1000 || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-polling",
"Polling needs one interval of at least 1000ms and a reason",
);
else declaration.polling = { intervalMs, reason };
}
} else if (clause.ALLOW()) {
const interfaceRevisionId = close(clause.interfaceType()!);
const contract = [...state.types.interfaces.values(), ...state.types.applications.values()].find(
(entry) => entry.revisionId === interfaceRevisionId,
);
const member = contract?.members.find((entry) => entry.displayName === identifier(clause.identifier(0)));
const use = identifier(clause.identifier(1));
const reason = stringValue(clause.stringLiteral()!);
if (!member || !["select", "predicate", "order"].includes(use) || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-allowance",
"Query allowances name an exact member, use (select/predicate/order), and nonempty reason",
);
else
declaration.allowances.push({ interfaceRevisionId, memberId: member.id, uses: [use as QueryUse], reason });
} else if (clause.WATCH()) declaration.watch = true;
else if (clause.POLL()) {
const intervalMs = Number(clause.INTEGER()!.getText()),
reason = stringValue(clause.stringLiteral()!);
if (declaration.polling || !Number.isSafeInteger(intervalMs) || intervalMs < 1000 || !reason.trim())
loweringIssue(
state,
clause,
"invalid-query-polling",
"Polling needs one interval of at least 1000ms and a reason",
);
else declaration.polling = { intervalMs, reason };
}
if (
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
)
)
loweringIssue(state, query, "duplicate-query", "Duplicate query name or ID");
if (parameters.length) {
const { root, views: _views, allowances: _allowances, ...common } = declaration;
queryTemplates.push({
declaration: common,
parameters,
root: expressions.get(root)!,
views: templateViews,
allowances: templateAllowances,
});
} else queries.push(declaration);
return [];
} finally {
state.types.parameters = previousParameters;
}
if (queries.some((entry) => entry.id === declaration.id || entry.displayName === declaration.displayName))
loweringIssue(state, query, "duplicate-query", "Duplicate query name or ID");
queries.push(declaration);
return [];
}
const operation = exportContext.packageOperationExport();
const generic = operation ?? exportContext.packageFunctionExport();
@@ -1268,6 +1402,34 @@ const lowerPackage = (
state.types.self = previousSelf;
}
});
for (const entry of context.packageExport()) {
const specialized = entry.packageQuerySpecialization();
if (!specialized) continue;
const templateName = identifier(specialized.identifier(1));
const template = queryTemplates.find((entry) => entry.declaration.displayName === templateName);
if (!template) {
loweringIssue(state, specialized, "unknown-query-template", `Unknown local query template ${templateName}`);
continue;
}
const substitution = new TypeSubstitution(state.types.environment());
const arguments_ = state.types
.arguments(specialized.typeArguments())
.map((argument) => substitution.argument(argument));
const declaration = specializeQueryTemplate(
template,
arguments_,
state.types.environment(),
() => [...state.types.definitions.values()],
{ id: stringValue(specialized.stringLiteral()), displayName: identifier(specialized.identifier(0)) },
);
if (
[...queries, ...queryTemplates.map((entry) => entry.declaration)].some(
(entry) => entry.id === declaration.id || entry.displayName === declaration.displayName,
)
)
loweringIssue(state, specialized, "duplicate-query", "Duplicate query name or ID");
queries.push(declaration);
}
return {
packageId: capabilityId.package(stringValue(context.stringLiteral(0))),
revisionId: state.packages.get(alias)!.revisionId,
@@ -1276,6 +1438,10 @@ const lowerPackage = (
source,
exports,
...(queries.length ? { queries } : {}),
...(queryTemplates.length ? { queryTemplates } : {}),
...(queries.some((query) => query.argumentRequirements?.length)
? { argumentRequirements: queries.flatMap((query) => query.argumentRequirements ?? []) }
: {}),
...(genericExports.length ? { genericExports } : {}),
};
};
@@ -1405,6 +1571,13 @@ const lowerBoundDependencies = (
);
return projectionId ? { edgeTypeId: attachment.attachment.id, projectionId } : undefined;
};
if (entry.QUERY()) {
const query = lowerQueryReference(state, identifier(entry.identifier(1)), identifier(entry.identifier(2)), entry);
const via = entry.VIA() ? traversal(identifier(entry.identifier(3)), identifier(entry.identifier(4))) : undefined;
return query && (!entry.VIA() || via)
? [{ portId, binding: { kind: "query", ...query, ...(via ? { via } : {}) } }]
: [];
}
if (entry.STATE()) {
const attachmentName = identifier(entry.identifier(1));
const attachment = requireSymbol(state, state.attachments, attachmentName, entry, "attachment");
+1 -1
View File
@@ -28,7 +28,7 @@ type Change = { file: string; before: string | null; after: string; mode: number
type Journal = { schemaVersion: 1; id: string; root: string; phase: "prepared" | "complete"; changes: Change[] };
const safeFile = (file: string) => {
if (
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*(?:\.gitignore|\.yarnrc.yml|[A-Za-z0-9_-][A-Za-z0-9_.-]*\.(?:qx|graphql|lock|ts|tsx|css|mjs|json|nix|txtpb|md))$/.test(
file,
) ||
file.split("/").some((part) => [".git", ".jj", ".quixos", "node_modules"].includes(part))