Implement independent presentation readiness boundaries

This commit is contained in:
Timothy J. Aveni
2026-09-27 20:30:07 -07:00
parent b499298299
commit ad72185a6c
8 changed files with 140 additions and 31 deletions
+28 -3
View File
@@ -4,6 +4,8 @@ import {
validate,
specifiedRules,
PossibleFragmentSpreadsRule,
NoUnusedVariablesRule,
GraphQLError,
print,
printSchema,
visit,
@@ -201,10 +203,29 @@ export async function compileQuery(
},
}),
);
const bindingVariables = new Set(
[...fragmentContracts.values()]
.filter((entry) => entry.mode === "reference")
.flatMap((entry) => Object.values(entry.variables)),
);
const errors = validate(
generated.schema,
document,
specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule),
[
...specifiedRules.filter((rule) => rule !== PossibleFragmentSpreadsRule && rule !== NoUnusedVariablesRule),
(context) => ({
OperationDefinition: {
leave(operation) {
const used = new Set(context.getRecursiveVariableUsages(operation).map(({ node }) => node.name.value));
for (const variable of operation.variableDefinitions ?? []) {
const name = variable.variable.name.value;
if (!used.has(name) && !bindingVariables.has(name))
context.reportError(new GraphQLError(`Variable "$${name}" is never used.`, { nodes: variable }));
}
},
},
}),
],
{ maxErrors: 20 },
);
if (errors.length) {
@@ -399,6 +420,7 @@ export async function compileQuery(
for (const selection of set.selections) {
if (++expandedFields > 10000) fail("QUERY_WORK_LIMIT", "Expanded query exceeds 10000 fields", selection);
if (selection.kind === "FragmentSpread") {
if (fragmentContracts.get(selection.name.value)?.mode === "reference") continue;
add(
fragments.get(selection.name.value)!.selectionSet,
conditional || !!selection.directives?.length,
@@ -530,7 +552,9 @@ export async function compileQuery(
},
]
: []),
...selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions),
...(contract?.mode === "reference"
? []
: selection(fragments.get(node.name.value)!.selectionSet, parent, inheritedConditions)),
];
}
if (node.kind !== "Field") return [];
@@ -610,9 +634,10 @@ export async function compileQuery(
const definitionDigest = createHash("sha256")
.update(
canonicalJson({
semantics: 4,
semantics: 5,
declaration,
normalized,
fragmentContracts: Object.fromEntries(fragmentContracts),
// Source provenance belongs to the checked installation. Identical public
// contracts keep their query identity when retained in another repository.
// Closed generic identity remains explicit in revisionId/application.