Add exported query fragments and scoped coverage observations

This commit is contained in:
Timothy J. Aveni
2026-09-22 16:18:57 -07:00
parent eba85ba158
commit a8869e4545
13 changed files with 2503 additions and 1715 deletions
+30 -7
View File
@@ -37,6 +37,7 @@ import { querySchema } from "./schema.js";
import { objectRow } from "./relational-schema.js";
import { workspaceQueryRootId } from "./root.js";
import { relationalCompiler } from "./relational-compile.js";
import { fragmentSources, type FragmentResolver } from "./fragments.js";
import {
QueryCompileError,
type QueryDeclaration,
@@ -67,6 +68,7 @@ export async function compileQuery(
declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[],
read: (name: string) => Promise<string>,
resolveFragment?: FragmentResolver,
): Promise<CheckedQuery> {
const paths = [declaration.document, ...declaration.fragments];
if (paths.length > 128 || new Set(paths).size !== paths.length)
@@ -86,7 +88,10 @@ export async function compileQuery(
const document = parse(new Source(text, path), { maxTokens: 10000 });
definitions.push(...document.definitions);
}
const fragmentContracts = await fragmentSources(declaration, definitions, resolveFragment);
const 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, {
Directive(node) {
if (node.name.value === "live")
@@ -112,7 +117,7 @@ export async function compileQuery(
if (node.name.value.startsWith("__")) fail("QUERY_UNSUPPORTED_FEATURE", "Introspection is not supported", node);
},
Directive(node) {
if (!["include", "skip", "live"].includes(node.name.value))
if (!["include", "skip"].includes(node.name.value))
fail("QUERY_UNSUPPORTED_FEATURE", `Unsupported directive ${node.name.value}`, node);
},
InlineFragment(node) {
@@ -400,11 +405,27 @@ export async function compileQuery(
inherited: QuerySelection["conditions"] = [],
): QuerySelection[] =>
set.selections.flatMap((node): QuerySelection[] => {
if (node.kind === "FragmentSpread")
return selection(fragments.get(node.name.value)!.selectionSet, parent, [
...inherited,
...conditions(node.directives),
]);
if (node.kind === "FragmentSpread") {
const inheritedConditions = [...inherited, ...conditions(node.directives)];
const contract = fragmentContracts.get(node.name.value);
if (contract && generated.byName.get(parent.name)?.revisionId !== contract.root)
fail("FRAGMENT_ROOT_MISMATCH", "Imported fragment requires its exact nominal root interface", node);
return [
...(contract
? [
{
name: "$fragment",
key: "",
selection: [],
arguments: {},
conditions: inheritedConditions,
fragmentSpread: contract,
},
]
: []),
...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions),
];
}
if (node.kind !== "Field") return [];
const contract = generated.byName.get(parent.name);
const member = contract?.members.find((entry) => entry.displayName === node.name.value);
@@ -471,6 +492,7 @@ export async function compileQuery(
.map((entry) => [entry.variable.name.value, argument(entry.defaultValue!)]),
),
sourceFiles: paths,
...(fragmentContracts.size ? { fragmentContracts: Object.fromEntries(fragmentContracts) } : {}),
};
}
@@ -478,10 +500,11 @@ export const compileRepositoryQuery = async (
root: string,
declaration: QueryDeclaration,
interfaces: readonly InterfaceRevision[],
resolveFragment?: FragmentResolver,
) => {
const started = performance.now();
try {
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name));
return await compileQuery(declaration, interfaces, (name) => readRepositorySource(root, name), resolveFragment);
} finally {
// Timing belongs in check logs, never in immutable artifact identities.
console.error(
+120
View File
@@ -0,0 +1,120 @@
import { parse, print, visit, type DocumentNode, type FragmentDefinitionNode } from "graphql";
import { QueryCompileError, type CheckedQuery, type QueryDeclaration, type QuerySelection } from "./types.js";
export type FragmentResolver = (packageRevisionId: string, fragmentId: string) => Promise<CheckedQuery>;
/** Expand only explicitly imported exports. Each variable mapping is checked by
* GraphQL against the consuming operation, rather than silently inheriting names. */
export async function fragmentSources(
declaration: QueryDeclaration,
definitions: DocumentNode["definitions"][number][],
resolve?: FragmentResolver,
) {
const markers = new Map<string, NonNullable<QuerySelection["fragmentSpread"]>>();
for (const imported of declaration.importedFragments ?? []) {
if (!resolve)
throw new QueryCompileError(
"FRAGMENT_RESOLVER_REQUIRED",
"Resolve imported fragments through package dependencies",
);
const checked = await resolve(imported.packageRevisionId, imported.fragmentId);
const exported = checked.declaration.fragment;
if (!exported || exported.admission) throw new QueryCompileError("NOT_EXPORTED_FRAGMENT", imported.fragmentId);
const variables = checked.variables.kind === "record" ? Object.keys(checked.variables.fields) : [];
if (JSON.stringify(Object.keys(imported.variables).sort()) !== JSON.stringify(variables.sort()))
throw new QueryCompileError(
"FRAGMENT_VARIABLE_MAPPING",
`Map every variable of ${checked.declaration.displayName} explicitly`,
);
const source = parse(checked.document);
const sourceOperation = source.definitions.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === checked.declaration.operation,
);
const consumingOperation = definitions.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === declaration.operation,
);
for (const variable of sourceOperation?.kind === "OperationDefinition"
? (sourceOperation.variableDefinitions ?? [])
: []) {
const name = imported.variables[variable.variable.name.value];
const target =
consumingOperation?.kind === "OperationDefinition"
? consumingOperation.variableDefinitions?.find((v) => v.variable.name.value === name)
: undefined;
if (!target || print(target.type) !== print(variable.type))
throw new QueryCompileError(
"FRAGMENT_VARIABLE_TYPE",
`Mapped variable ${name} must have type ${print(variable.type)}`,
);
}
const fragments = source.definitions.filter(
(node): node is FragmentDefinitionNode => node.kind === "FragmentDefinition",
);
if (definitions.some((node) => "name" in node && node.name?.value === exported.name))
throw new QueryCompileError("FRAGMENT_NAME_COLLISION", exported.name);
const privateNames = new Map(
fragments.map((node) => [
node.name.value,
node.name.value === exported.name ? exported.name : `${exported.name}__${node.name.value}`,
]),
);
for (const fragment of fragments) {
definitions.push(
visit(fragment, {
FragmentDefinition(node) {
return { ...node, name: { ...node.name, value: privateNames.get(node.name.value)! } };
},
FragmentSpread(node) {
return { ...node, name: { ...node.name, value: privateNames.get(node.name.value) ?? node.name.value } };
},
Variable(node) {
return { ...node, name: { ...node.name, value: imported.variables[node.name.value] ?? node.name.value } };
},
}),
);
}
markers.set(exported.name, {
package: imported.packageRevisionId,
fragment: imported.fragmentId,
digest: checked.definitionDigest,
root: checked.declaration.root.interface,
variables: imported.variables,
});
for (const [name, contract] of Object.entries(checked.fragmentContracts ?? {})) {
markers.set(privateNames.get(name) ?? name, {
...contract,
variables: Object.fromEntries(
Object.entries(contract.variables).map(([inner, outer]) => [inner, imported.variables[outer] ?? outer]),
),
});
}
}
if (declaration.fragment) {
const exported = declaration.fragment;
if (!definitions.some((node) => node.kind === "FragmentDefinition" && node.name.value === exported.name))
throw new QueryCompileError("FRAGMENT_MISSING", exported.name);
const operations = definitions.filter((node) => node.kind === "OperationDefinition");
if (!exported.operation) {
if (operations.length)
throw new QueryCompileError(
"FRAGMENT_OPERATION_REQUIRED",
"Name the standalone operation explicitly when a fragment document includes variables or an operation",
);
definitions.push(...parse(`query ${declaration.operation} { root { ...${exported.name} } }`).definitions);
} else {
const operation = operations.find(
(node) => node.kind === "OperationDefinition" && node.name?.value === exported.operation,
);
if (!operation || operation.kind !== "OperationDefinition")
throw new QueryCompileError("FRAGMENT_OPERATION_MISSING", exported.operation);
// The standalone operation specifies variables only. Its result must be the
// exact exported fragment, not additional fields or a different window.
const expected = parse(`query ${declaration.operation} { root { ...${exported.name} } }`).definitions[0]!;
if (expected.kind !== "OperationDefinition" || print(operation.selectionSet) !== print(expected.selectionSet))
throw new QueryCompileError(
"FRAGMENT_STANDALONE_SHAPE",
"Standalone operation must select root { ...ExportedFragment }",
);
}
}
return markers;
}
+17
View File
@@ -35,6 +35,14 @@ export type QueryRoot<Interface = InterfaceRevisionId> =
| { kind: "object"; interface: Interface }
| { kind: "workspace-interface"; interface: Interface };
export interface QueryDeclaration {
/** Exported local projection; compilation also creates its direct admission. */
fragment?: { name: string; operation?: string; admission?: boolean };
importedFragments?: {
packageRevisionId: string;
packageAlias?: string;
fragmentId: string;
variables: Record<string, string>;
}[];
placement?: { kind: "replica" } | { kind: "local"; sourceQueryId: string };
id: string;
displayName: string;
@@ -83,6 +91,7 @@ export class QueryCompileError extends Error {
}
}
export interface CheckedQuery {
fragmentContracts?: Record<string, NonNullable<QuerySelection["fragmentSpread"]>>;
declaration: QueryDeclaration;
definitionDigest: string;
/** Fixed, validated document: never supplied by runtime callers. */
@@ -102,6 +111,14 @@ export type QueryArgument =
| { kind: "list"; values: QueryArgument[] }
| { kind: "object"; fields: Record<string, QueryArgument> };
export interface QuerySelection {
/** A checked occurrence marker, not an output field or storage capability. */
fragmentSpread?: {
package: string;
fragment: string;
digest: string;
root: InterfaceRevisionId;
variables: Record<string, string>;
};
name: string;
key: string;
interfaceRevisionId?: InterfaceRevisionId;