Add queryable capability contracts and checked GraphQL artifacts
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user