Implement implicit object references and capability query foundations

This commit is contained in:
Timothy J. Aveni
2026-09-26 11:44:32 -07:00
parent 58a75da83f
commit 3728612e8c
13 changed files with 401 additions and 35 deletions
+147 -14
View File
@@ -3,6 +3,7 @@ import {
Source,
validate,
specifiedRules,
PossibleFragmentSpreadsRule,
print,
printSchema,
visit,
@@ -89,7 +90,7 @@ export async function compileQuery(
definitions.push(...document.definitions);
}
const fragmentContracts = await fragmentSources(declaration, definitions, resolveFragment);
const document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
let document: DocumentNode = { kind: "Document" as DocumentNode["kind"], definitions };
if (definitions.length > 256 || Buffer.byteLength(print(document)) > 262144)
fail("QUERY_SOURCE_LIMIT", "Expanded fragment sources exceed the query source budget");
visit(document, {
@@ -103,7 +104,44 @@ export async function compileQuery(
},
});
const generated = querySchema(declaration, interfaces, document);
const operations = definitions.filter((entry) => entry.kind === "OperationDefinition");
// Identity belongs to the checked object result, including fragment results.
// Schema membership deliberately excludes structured values and query wrappers.
const identity = parse("query Identity { _camino { ref } }", { noLocation: true }).definitions[0]!;
if (identity.kind !== "OperationDefinition") throw new Error("Invalid identity selection");
const identitySelection = identity.selectionSet.selections[0]!;
const identityTypes = new TypeInfo(generated.schema);
document = visit(
document,
visitWithTypeInfo(identityTypes, {
SelectionSet: {
leave(node, _key, parentNode) {
// An inline projection is part of its containing object. Its identity
// remains that object's base interface, not the projected interface.
if (parentNode && !Array.isArray(parentNode) && "kind" in parentNode && parentNode.kind === "InlineFragment")
return;
const parent = identityTypes.getParentType();
const contract = parent && generated.byName.get(parent.name);
if (!contract || contract.revisionId === workspaceQueryRootId) return;
const hasIdentity = node.selections.some(
(field) =>
field.kind === "Field" &&
field.name.value === "_camino" &&
!field.alias &&
!field.directives?.length &&
field.selectionSet?.selections.some(
(child) =>
child.kind === "Field" && child.name.value === "ref" && !child.alias && !child.directives?.length,
),
);
// GraphQL validation caches comparisons by AST identity. Each nominal
// object must get its own metadata subtree, even though the spelling is
// identical (its reference scalar can be different).
if (!hasIdentity) return { ...node, selections: [...node.selections, structuredClone(identitySelection)] };
},
},
}),
);
const operations = document.definitions.filter((entry) => entry.kind === "OperationDefinition");
if (
operations.length !== 1 ||
operations[0]!.operation !== "query" ||
@@ -120,18 +158,56 @@ export async function compileQuery(
if (!["include", "skip"].includes(node.name.value))
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
},
InlineFragment(node) {
fail("QUERY_UNSUPPORTED_FEATURE", "Use named fragments on the exact selected type", node);
},
});
const errors = validate(generated.schema, document, specifiedRules, { maxErrors: 20 });
// Capability interfaces are nominal views of one graph object, not mutually
// exclusive GraphQL object variants. Check overlap using declared requirements.
const projectionTypes = new TypeInfo(generated.schema);
const requires = (from: InterfaceRevisionId, to: InterfaceRevisionId, seen = new Set<string>()): boolean => {
if (from === to) return true;
if (seen.has(from)) return false;
seen.add(from);
return generated.contracts.get(from)?.requiredInterfaces?.some((id) => requires(id, to, seen)) ?? false;
};
visit(
document,
visitWithTypeInfo(projectionTypes, {
InlineFragment(node) {
if (!node.typeCondition) return;
const from = projectionTypes.getParentType();
const a = from && generated.byName.get(from.name);
const b = generated.byName.get(node.typeCondition.name.value);
if (!a || !b || !requires(a.revisionId, b.revisionId))
fail(
"QUERY_UNSUPPORTED_FEATURE",
"Inline interface projections currently require a declared interface requirement",
node,
);
},
FragmentSpread(node) {
const fragment = document.definitions.find(
(entry) => entry.kind === "FragmentDefinition" && entry.name.value === node.name.value,
);
if (
fragment?.kind === "FragmentDefinition" &&
fragment.typeCondition.name.value !== projectionTypes.getParentType()?.name
)
fail("QUERY_UNSUPPORTED_FEATURE", "Named fragments currently require the exact nominal root interface", node);
},
}),
);
const errors = validate(
generated.schema,
document,
specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule),
{ maxErrors: 20 },
);
if (errors.length) {
const error = errors[0]!;
fail("QUERY_VALIDATION", error.message, error.nodes?.[0]);
}
const effects = new Map<string, QueryFieldEffect>();
const fragments = new Map(
definitions
document.definitions
.filter((entry): entry is FragmentDefinitionNode => entry.kind === "FragmentDefinition")
.map((entry) => [entry.name.value, entry]),
);
@@ -313,16 +389,28 @@ export async function compileQuery(
if (isListType(type)) return valueType.list(output(type.ofType, selections, depth, conditional));
if (isObjectType(type)) {
const fields: Record<string, ValueType> = {};
const add = (set: SelectionSetNode, conditional = false) => {
const add = (set: SelectionSetNode, conditional = false, selectedType = type) => {
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, conditional || !!selection.directives?.length);
add(
fragments.get(selection.name.value)!.selectionSet,
conditional || !!selection.directives?.length,
selectedType,
);
continue;
}
if (selection.kind === "InlineFragment") {
const projected = selection.typeCondition
? generated.schema.getType(selection.typeCondition.name.value)
: selectedType;
if (!isObjectType(projected)) return fail("QUERY_TYPE", "Expected an object projection", selection);
add(selection.selectionSet, conditional || !!selection.directives?.length, projected);
continue;
}
if (selection.kind !== "Field") continue;
const key = selection.alias?.value ?? selection.name.value;
const field = type.getFields()[selection.name.value]!;
const field = selectedType.getFields()[selection.name.value]!;
const previous = fields[key];
fields[key] = output(
field.type,
@@ -338,7 +426,9 @@ export async function compileQuery(
if (previous) fields[key] = mergeOutput(previous, fields[key]!);
}
};
if (selections) add(selections, conditional);
// A condition on this object makes the object optional, not every child
// of the object when it is present. Fragment conditions are handled by add.
if (selections) add(selections);
return { kind: "record", fields };
}
return (
@@ -409,8 +499,13 @@ export async function compileQuery(
set: SelectionSetNode,
parent: import("graphql").GraphQLObjectType,
inherited: QuerySelection["conditions"] = [],
): QuerySelection[] =>
set.selections.flatMap((node): QuerySelection[] => {
): QuerySelection[] => {
const selections = set.selections.flatMap((node): QuerySelection[] => {
if (node.kind === "InlineFragment") {
const projected = node.typeCondition ? generated.schema.getType(node.typeCondition.name.value) : parent;
if (!isObjectType(projected)) return fail("QUERY_TYPE", "Expected an object projection", node);
return selection(node.selectionSet, projected, [...inherited, ...conditions(node.directives)]);
}
if (node.kind === "FragmentSpread") {
const inheritedConditions = [...inherited, ...conditions(node.directives)];
const contract = fragmentContracts.get(node.name.value);
@@ -436,6 +531,7 @@ export async function compileQuery(
const contract = generated.byName.get(parent.name);
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
const type = getNamedType(parent.getFields()[node.name.value]!.type);
const referenceType = type && generated.scalarTypes.get(type.name);
const role = generated.relational.roles.get(parent.name);
const relationship =
role?.kind === "relations"
@@ -455,6 +551,11 @@ export async function compileQuery(
key: node.alias?.value ?? node.name.value,
...(member && contract ? { interfaceRevisionId: contract.revisionId, memberId: member.id } : {}),
...(member?.kind === "relationship" ? { targetInterfaceRevisionId: generated.target(member) } : {}),
...(node.name.value === "ref" &&
referenceType?.kind === "object-ref" &&
referenceType.expectation.kind === "interface"
? { targetInterfaceRevisionId: referenceType.expectation.interfaceRevisionId }
: {}),
conditions: [...inherited, ...conditions(node.directives)],
arguments: Object.fromEntries(
(node.arguments ?? []).map((entry) => [entry.name.value, argument(entry.value)]),
@@ -465,13 +566,45 @@ export async function compileQuery(
},
];
});
const contract = generated.byName.get(parent.name);
if (!contract || contract.revisionId === workspaceQueryRootId) return selections;
// Fragment expansion may repeat metadata. Combine its children so a later
// implicit reference cannot overwrite an explicitly selected aggregate.
const metadata = selections.filter((field) => field.key === "_camino" && field.name === "_camino");
const responseFields = new Map<string, QuerySelection>();
for (const field of selections.filter((field) => field.memberId)) {
const previous = responseFields.get(field.key);
if (
previous &&
(previous.interfaceRevisionId !== field.interfaceRevisionId || previous.memberId !== field.memberId)
)
fail("QUERY_PROJECTION_CONFLICT", `Alias distinct interface fields selected as ${field.key}`);
responseFields.set(field.key, field);
}
if (!metadata.length) return selections;
const children = metadata.flatMap((field) =>
field.selection.map((child) => ({
...child,
conditions: [...field.conditions, ...child.conditions],
})),
);
const ref = children.find((child) => child.key === "ref" && child.name === "ref")!;
return [
...selections.filter((field) => !metadata.includes(field)),
{
...metadata[0]!,
conditions: [],
selection: [...children.filter((child) => child.key !== "ref"), { ...ref, conditions: [] }],
},
];
};
const normalized = print(document),
schema = printSchema(generated.schema);
const selections = selection(operations[0]!.selectionSet, generated.schema.getQueryType()!);
const definitionDigest = createHash("sha256")
.update(
canonicalJson({
semantics: 3,
semantics: 4,
declaration,
normalized,
// Source provenance belongs to the checked installation. Identical public