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; /** 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>(); 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; }