Implement query execution, scoped RPC enrichment, live collections, and scaffold integration
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user