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
+68 -16
View File
@@ -25,6 +25,7 @@ import {
type DirectiveNode,
} from "graphql";
import { createHash } from "node:crypto";
import { canonicalJson } from "../capability-model/evolution.js";
import { readRepositorySource } from "../capability-language/source-loader.js";
import {
valueType,
@@ -192,18 +193,28 @@ export async function compileQuery(
if (member.kind !== "relationship" || (member.cardinality !== "many" && member.cardinality !== "many-unique"))
return;
const target = generated.target(member);
const bounds = (node.arguments ?? []).filter(
(argument) => argument.name.value === "first" || argument.name.value === "all",
);
if (bounds.length !== 1) fail("QUERY_ROW_LIMIT", "Specify exactly one of first or all", node);
if (bounds[0]!.name.value === "all" && node.arguments?.some((argument) => argument.name.value === "after"))
fail("QUERY_UNSUPPORTED_FEATURE", "Bounded-all does not accept a continuation", node);
for (const argument of node.arguments ?? []) {
if (argument.name.value === "where") inputEffects(target, argument.value, "predicate");
if (argument.name.value === "orderBy") inputEffects(target, argument.value, "order");
if (argument.name.value === "after") hasContinuation = true;
if (
argument.name.value === "first" &&
["first", "all"].includes(argument.name.value) &&
argument.value.kind !== "Variable" &&
(argument.value.kind !== "IntValue" ||
Number(argument.value.value) < 1 ||
Number(argument.value.value) > declaration.budgets.rows)
)
fail("QUERY_ROW_LIMIT", `first must be between 1 and ${declaration.budgets.rows}`, argument);
fail(
"QUERY_ROW_LIMIT",
`${argument.name.value} must be between 1 and ${declaration.budgets.rows}`,
argument,
);
}
},
FragmentSpread(node) {
@@ -228,26 +239,47 @@ export async function compileQuery(
.map((entry) => [entry.name.value, entry]),
);
let expandedFields = 0;
const output = (type: GraphQLType, selections?: SelectionSetNode, depth = 0): ValueType => {
if (depth > declaration.budgets.depth * 4 + 4) fail("QUERY_DEPTH_LIMIT", "Expanded query exceeds its depth budget");
if (isNonNullType(type)) return required(type.ofType, selections, depth);
return valueType.optional(required(type, selections, depth));
// GraphQL merges repeated response keys. In particular, fragments may each
// contribute different children of one relationship; last-write-wins would
// silently remove fields from both the generated type and the wire codec.
const mergeOutput = (left: ValueType, right: ValueType): ValueType => {
const a = left.kind === "optional" ? left.value : left;
const b = right.kind === "optional" ? right.value : right;
let result = a;
if (a.kind === "record" && b.kind === "record") {
const fields = { ...a.fields };
for (const [key, type] of Object.entries(b.fields))
fields[key] = fields[key] ? mergeOutput(fields[key], type) : type;
result = { kind: "record", fields };
} else if (a.kind === "list" && b.kind === "list") result = valueType.list(mergeOutput(a.value, b.value));
return left.kind === "optional" && right.kind === "optional" ? valueType.optional(result) : result;
};
const required = (type: GraphQLType, selections?: SelectionSetNode, depth = 0): ValueType => {
if (isListType(type)) return valueType.list(output(type.ofType, selections, depth));
const output = (type: GraphQLType, selections?: SelectionSetNode, depth = 0, conditional = false): ValueType => {
if (depth > declaration.budgets.depth * 4 + 4) fail("QUERY_DEPTH_LIMIT", "Expanded query exceeds its depth budget");
if (isNonNullType(type)) return required(type.ofType, selections, depth, conditional);
return valueType.optional(required(type, selections, depth, conditional));
};
const required = (type: GraphQLType, selections?: SelectionSetNode, depth = 0, conditional = false): ValueType => {
if (isListType(type)) return valueType.list(output(type.ofType, selections, depth, conditional));
if (isObjectType(type)) {
const fields: Record<string, ValueType> = {};
const add = (set: SelectionSetNode) => {
const add = (set: SelectionSetNode, conditional = false) => {
for (const selection of set.selections) {
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
if (selection.kind === "FragmentSpread") {
add(fragments.get(selection.name.value)!.selectionSet);
add(fragments.get(selection.name.value)!.selectionSet, conditional || !!selection.directives?.length);
continue;
}
if (selection.kind !== "Field") continue;
const key = selection.alias?.value ?? selection.name.value;
const field = type.getFields()[selection.name.value]!;
fields[key] = output(field.type, selection.selectionSet, depth + 1);
const previous = fields[key];
fields[key] = output(
field.type,
selection.selectionSet,
depth + 1,
conditional || !!selection.directives?.length,
);
if (selection.name.value === "_qx") {
const contract = generated.byName.get(type.name)!;
const metadataFields: Record<string, ValueType> = {};
@@ -258,11 +290,12 @@ export async function compileQuery(
}
fields[key] = { kind: "record", fields: metadataFields };
}
if (selection.directives?.length && fields[key]!.kind !== "optional")
if ((conditional || selection.directives?.length) && fields[key]!.kind !== "optional")
fields[key] = valueType.optional(fields[key]!);
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
}
};
if (selections) add(selections);
if (selections) add(selections, conditional);
return { kind: "record", fields };
}
return shapeScalar(getNamedType(type)!.name);
@@ -354,7 +387,16 @@ export async function compileQuery(
const normalized = print(document),
schema = printSchema(generated.schema);
const definitionDigest = createHash("sha256")
.update(JSON.stringify({ semantics: 1, declaration, normalized, interfaces }))
.update(
canonicalJson({
semantics: 1,
declaration,
normalized,
interfaces: [...generated.byName.values()].sort((a, b) =>
a.revisionId < b.revisionId ? -1 : a.revisionId > b.revisionId ? 1 : 0,
),
}),
)
.digest("hex");
return {
declaration,
@@ -374,8 +416,18 @@ export async function compileQuery(
};
}
export const compileRepositoryQuery = (
export const compileRepositoryQuery = async (
root: string,
declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[],
) => compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
) => {
const started = performance.now();
try {
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
} finally {
// Timing belongs in check logs, never in immutable artifact identities.
console.error(
`[query-check] ${JSON.stringify(declaration.displayName)}: ${(performance.now() - started).toFixed(1)}ms`,
);
}
};