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
+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;
}