Add queryable capability contracts and checked GraphQL artifacts

This commit is contained in:
Timothy J. Aveni
2026-09-17 14:55:10 -07:00
parent e3501772c5
commit 61a410f98f
27 changed files with 4798 additions and 2333 deletions
+17
View File
@@ -1,6 +1,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 { linkQueries } from "../query/link.js";
import {
capabilityId,
compileWorkspaceRevision,
@@ -231,6 +233,16 @@ const createResourceGraphResolver = (quixosCommit: string, resolveResource: Capa
}
assertImportsMatchLock(manifestPath, compiled.resource.imports, lockResult.lock.resources);
if (compiled.resource.kind === "package") {
const queryInterfaces = [
...(environment.interfaceClosure ?? []),
...(compiled.resource.specializations ?? []),
];
if (compiled.resource.revision.queries?.length)
compiled.resource.revision.checkedQueries = await Promise.all(
compiled.resource.revision.queries.map((query) =>
compileRepositoryQuery(snapshot.directory, query, queryInterfaces),
),
);
let catalogText: string | undefined;
try {
catalogText = await readFile(path.join(snapshot.directory, "quixos.migrations.json"), "utf8");
@@ -389,6 +401,11 @@ export const compileWorkspaceRepository = async (options: {
.join("\n")}`,
);
}
const linkedQueries = linkQueries(workspace);
if (linkedQueries.length) {
workspace.linkedQueries = linkedQueries;
checked.plan.source.linkedQueries = structuredClone(linkedQueries);
}
return {
workspace,
plan: checked.plan,
File diff suppressed because one or more lines are too long
@@ -1,229 +1,251 @@
WORKSPACE=1
TYPE=2
OBJECT=3
STORABLE=4
IMPLEMENTS=5
REF=6
FRAGMENT=7
IMPORT=8
EXTERNAL=9
ATOM=10
INTERFACE=11
INTERFACES=12
PACKAGE=13
VALUE=14
RELATION=15
OPERATION=16
FUNCTION=17
CONSTRUCTOR=18
CONSTRUCTS=19
INPUT=20
CONFORM=21
AS=22
BIND=23
STATIC=24
TO=25
PRIVATE=26
SHARED=27
STATE=28
EDGE=29
PROJECTION=30
WITH=31
USING=32
VIA=33
MATERIALIZE=34
IF=35
ABSENT=36
ON=37
POLICY=38
DEFAULT=39
SOURCE=40
REPOSITORY=41
COMMIT=42
REVISION=43
SEMANTIC_MAJOR=44
ON_DELETE=45
RETAIN_OTHER=46
KEYED=47
PUBLIC_TRAVERSAL=48
ID=49
DOC=50
MODE=51
EMITS=52
RECEIVER=53
REQUIRES=54
ANY=55
GET=56
SET=57
WATCH=58
START=59
STOP=60
READ=61
WRITE=62
RESOLVE=63
CONNECT=64
DISCONNECT=65
CALL=66
WATCH_START=67
WATCH_STOP=68
SUBSCRIBE=69
UNSUBSCRIBE=70
OPTIMISTIC_REGISTER=71
CRDT=72
OPTIONAL_ONE=73
EXACTLY_ONE=74
MANY_UNIQUE=75
MANY=76
ORDERED=77
UNIT=78
WATCH_HANDLE=79
MESSAGE=80
ATOM_REF=81
INTERFACE_REF=82
OPTIONAL=83
LIST=84
RECORD=85
BOOL=86
BYTES=87
DOUBLE=88
INT32=89
INT64=90
STRING=91
UINT32=92
UINT64=93
TRUE=94
FALSE=95
NULL=96
ARROW=97
COLON=98
SEMI=99
COMMA=100
DOT=101
LBRACE=102
RBRACE=103
LBRACK=104
RBRACK=105
LPAREN=106
RPAREN=107
LT=108
GT=109
AMP=110
EQUAL=111
INTEGER=112
JSON_NUMBER=113
IDENTIFIER=114
STRING_LITERAL=115
LINE_COMMENT=116
BLOCK_COMMENT=117
WS=118
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
'workspace'=1
'type'=2
'object'=3
'storable'=4
'implements'=5
'ref'=6
'fragment'=7
'import'=8
'external'=9
'atom'=10
'interface'=11
'interfaces'=12
'package'=13
'value'=14
'relation'=15
'operation'=16
'function'=17
'constructor'=18
'constructs'=19
'input'=20
'conform'=21
'as'=22
'bind'=23
'static'=24
'to'=25
'private'=26
'shared'=27
'state'=28
'edge'=29
'projection'=30
'with'=31
'using'=32
'via'=33
'materialize'=34
'if'=35
'absent'=36
'on'=37
'policy'=38
'default'=39
'source'=40
'repository'=41
'commit'=42
'revision'=43
'semantic-major'=44
'on-delete'=45
'retain-other'=46
'keyed'=47
'public-traversal'=48
'id'=49
'doc'=50
'mode'=51
'emits'=52
'receiver'=53
'requires'=54
'any'=55
'get'=56
'set'=57
'watch'=58
'start'=59
'stop'=60
'read'=61
'write'=62
'resolve'=63
'connect'=64
'disconnect'=65
'call'=66
'watch-start'=67
'watch-stop'=68
'subscribe'=69
'unsubscribe'=70
'optimistic-register'=71
'crdt'=72
'optional-one'=73
'exactly-one'=74
'many-unique'=75
'many'=76
'ordered'=77
'unit'=78
'watch-handle'=79
'message'=80
'atom-ref'=81
'interface-ref'=82
'optional'=83
'list'=84
'record'=85
'bool'=86
'bytes'=87
'double'=88
'int32'=89
'int64'=90
'string'=91
'uint32'=92
'uint64'=93
'true'=94
'false'=95
'null'=96
'->'=97
':'=98
';'=99
','=100
'.'=101
'{'=102
'}'=103
'['=104
']'=105
'('=106
')'=107
'<'=108
'>'=109
'&'=110
'='=111
'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
File diff suppressed because one or more lines are too long
@@ -1,229 +1,251 @@
WORKSPACE=1
TYPE=2
OBJECT=3
STORABLE=4
IMPLEMENTS=5
REF=6
FRAGMENT=7
IMPORT=8
EXTERNAL=9
ATOM=10
INTERFACE=11
INTERFACES=12
PACKAGE=13
VALUE=14
RELATION=15
OPERATION=16
FUNCTION=17
CONSTRUCTOR=18
CONSTRUCTS=19
INPUT=20
CONFORM=21
AS=22
BIND=23
STATIC=24
TO=25
PRIVATE=26
SHARED=27
STATE=28
EDGE=29
PROJECTION=30
WITH=31
USING=32
VIA=33
MATERIALIZE=34
IF=35
ABSENT=36
ON=37
POLICY=38
DEFAULT=39
SOURCE=40
REPOSITORY=41
COMMIT=42
REVISION=43
SEMANTIC_MAJOR=44
ON_DELETE=45
RETAIN_OTHER=46
KEYED=47
PUBLIC_TRAVERSAL=48
ID=49
DOC=50
MODE=51
EMITS=52
RECEIVER=53
REQUIRES=54
ANY=55
GET=56
SET=57
WATCH=58
START=59
STOP=60
READ=61
WRITE=62
RESOLVE=63
CONNECT=64
DISCONNECT=65
CALL=66
WATCH_START=67
WATCH_STOP=68
SUBSCRIBE=69
UNSUBSCRIBE=70
OPTIMISTIC_REGISTER=71
CRDT=72
OPTIONAL_ONE=73
EXACTLY_ONE=74
MANY_UNIQUE=75
MANY=76
ORDERED=77
UNIT=78
WATCH_HANDLE=79
MESSAGE=80
ATOM_REF=81
INTERFACE_REF=82
OPTIONAL=83
LIST=84
RECORD=85
BOOL=86
BYTES=87
DOUBLE=88
INT32=89
INT64=90
STRING=91
UINT32=92
UINT64=93
TRUE=94
FALSE=95
NULL=96
ARROW=97
COLON=98
SEMI=99
COMMA=100
DOT=101
LBRACE=102
RBRACE=103
LBRACK=104
RBRACK=105
LPAREN=106
RPAREN=107
LT=108
GT=109
AMP=110
EQUAL=111
INTEGER=112
JSON_NUMBER=113
IDENTIFIER=114
STRING_LITERAL=115
LINE_COMMENT=116
BLOCK_COMMENT=117
WS=118
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
'workspace'=1
'type'=2
'object'=3
'storable'=4
'implements'=5
'ref'=6
'fragment'=7
'import'=8
'external'=9
'atom'=10
'interface'=11
'interfaces'=12
'package'=13
'value'=14
'relation'=15
'operation'=16
'function'=17
'constructor'=18
'constructs'=19
'input'=20
'conform'=21
'as'=22
'bind'=23
'static'=24
'to'=25
'private'=26
'shared'=27
'state'=28
'edge'=29
'projection'=30
'with'=31
'using'=32
'via'=33
'materialize'=34
'if'=35
'absent'=36
'on'=37
'policy'=38
'default'=39
'source'=40
'repository'=41
'commit'=42
'revision'=43
'semantic-major'=44
'on-delete'=45
'retain-other'=46
'keyed'=47
'public-traversal'=48
'id'=49
'doc'=50
'mode'=51
'emits'=52
'receiver'=53
'requires'=54
'any'=55
'get'=56
'set'=57
'watch'=58
'start'=59
'stop'=60
'read'=61
'write'=62
'resolve'=63
'connect'=64
'disconnect'=65
'call'=66
'watch-start'=67
'watch-stop'=68
'subscribe'=69
'unsubscribe'=70
'optimistic-register'=71
'crdt'=72
'optional-one'=73
'exactly-one'=74
'many-unique'=75
'many'=76
'ordered'=77
'unit'=78
'watch-handle'=79
'message'=80
'atom-ref'=81
'interface-ref'=82
'optional'=83
'list'=84
'record'=85
'bool'=86
'bytes'=87
'double'=88
'int32'=89
'int64'=90
'string'=91
'uint32'=92
'uint64'=93
'true'=94
'false'=95
'null'=96
'->'=97
':'=98
';'=99
','=100
'.'=101
'{'=102
'}'=103
'['=104
']'=105
'('=106
')'=107
'<'=108
'>'=109
'&'=110
'='=111
'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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,10 +24,13 @@ import { OperationMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberOperationContext } from "./QuixosCapabilityParser.js";
import { RelationshipMemberContext } from "./QuixosCapabilityParser.js";
import { QueryReadContractContext } from "./QuixosCapabilityParser.js";
import { RelationshipOperationContext } from "./QuixosCapabilityParser.js";
import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
import { PackageExportContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { QueryClauseContext } from "./QuixosCapabilityParser.js";
import { PackageOperationExportContext } from "./QuixosCapabilityParser.js";
import { PackageFunctionExportContext } from "./QuixosCapabilityParser.js";
import { PackageConstructorExportContext } from "./QuixosCapabilityParser.js";
@@ -210,6 +213,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitRelationshipMember?: (ctx: RelationshipMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.queryReadContract`.
* @param ctx the parse tree
* @return the visitor result
*/
visitQueryReadContract?: (ctx: QueryReadContractContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.relationshipOperation`.
* @param ctx the parse tree
@@ -234,6 +243,18 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitPackageExport?: (ctx: PackageExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageQuery`.
* @param ctx the parse tree
* @return the visitor result
*/
visitPackageQuery?: (ctx: PackageQueryContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.queryClause`.
* @param ctx the parse tree
* @return the visitor result
*/
visitQueryClause?: (ctx: QueryClauseContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.packageOperationExport`.
* @param ctx the parse tree
+115
View File
@@ -46,6 +46,7 @@ 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 { QuixosCapabilityLexer } from "./generated/QuixosCapabilityLexer.js";
import {
QuixosCapabilityParser,
@@ -463,6 +464,13 @@ const lowerValueMember = (state: LoweringState, context: ValueMemberContext): In
}
return {
kind: "value",
...(context.queryReadContract()
? {
queryRead: {
execution: context.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(context.stringLiteral())),
displayName: memberName,
valueType: memberValueType,
@@ -527,6 +535,13 @@ const lowerRelationshipMember = (
}
return {
kind: "relationship",
...(context.queryReadContract()
? {
queryRead: {
execution: context.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(context.stringLiteral())),
displayName: identifier(context.identifier()),
target,
@@ -672,6 +687,13 @@ const lowerInterfaceTemplate = (
const type = types.expression(value.valueType());
return {
kind: "value",
...(value.queryReadContract()
? {
queryRead: {
execution: value.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(value.stringLiteral())),
displayName: identifier(value.identifier()),
valueType: type,
@@ -725,6 +747,13 @@ const lowerInterfaceTemplate = (
: { kind: cardinality === "optional-one" ? "optional" : "list", value: targetType };
return {
kind: "relationship",
...(relationship.queryReadContract()
? {
queryRead: {
execution: relationship.queryReadContract()!.RPC() ? ("rpc-permitted" as const) : ("native" as const),
},
}
: {}),
id: capabilityId.member(stringValue(relationship.stringLiteral())),
displayName: identifier(relationship.identifier()),
target,
@@ -1130,7 +1159,91 @@ const lowerPackage = (
): PackageRevision => {
const alias = identifier(context.identifier());
const genericExports: GenericPackageExport[] = [];
const queries: QueryDeclaration[] = [];
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
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}`,
);
else {
declaration.budgets[key] = n;
assigned.add(name);
}
} 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.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();
if (generic?.typeParameters()) {
@@ -1162,6 +1275,7 @@ const lowerPackage = (
displayName: alias,
source,
exports,
...(queries.length ? { queries } : {}),
...(genericExports.length ? { genericExports } : {}),
};
};
@@ -1568,6 +1682,7 @@ const lowerConformance = (
? [
{
operationId,
...(bindingContext.stringLiteral() ? { queryReason: stringValue(bindingContext.stringLiteral()!) } : {}),
binding: {
kind: "package" as const,
packageRevisionId: packageSymbol.revisionId,
+8 -1
View File
@@ -4,13 +4,20 @@ import { parseQx, validateQxImportPath, walkSyntax } from "./source.js";
export const readQxSource = async (root: string, name: string) => {
validateQxImportPath(name);
return readRepositorySource(root, name);
};
/** Refuse symlinks at every component, including the final source file. */
export const readRepositorySource = async (root: string, name: string) => {
if (!/^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$/.test(name))
throw new Error(`Invalid repository-relative source path: ${name}`);
let current = path.resolve(root);
const segments = name.split("/");
for (const [index, segment] of segments.entries()) {
current = path.join(current, segment);
const stat = await lstat(current);
if (stat.isSymbolicLink() || (index === segments.length - 1 ? !stat.isFile() : !stat.isDirectory()))
throw new Error(`QX imports must be ordinary files beneath ordinary directories: ${name}`);
throw new Error(`Sources must be ordinary files beneath ordinary directories: ${name}`);
}
return readFile(current, "utf8");
};