UNPKG

@theguild/federation-composition

Version:
501 lines (500 loc) • 21.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FromContextDirectiveRules = FromContextDirectiveRules; const graphql_1 = require("graphql"); const context_js_1 = require("../../../context.js"); const helpers_js_1 = require("../../../helpers.js"); function FromContextDirectiveRules(context) { const contextOwnersByName = indexContextOwners(context); return { DirectiveDefinition(node) { (0, helpers_js_1.validateDirectiveAgainstOriginal)(node, "fromContext", context); }, Directive(node) { if (!context.isAvailableFederationDirective("fromContext", node)) { return; } context.stateBuilder.markSpecAsUsed("context"); const typeDef = context.typeNodeInfo.getTypeDef(); const fieldDef = context.typeNodeInfo.getFieldDef(); const argDef = context.typeNodeInfo.getArgumentDef(); const argumentNode = typeDef?.kind === graphql_1.Kind.DIRECTIVE_DEFINITION ? fieldDef?.kind === graphql_1.Kind.INPUT_VALUE_DEFINITION ? fieldDef : undefined : (argDef ?? undefined); if (!typeDef || !fieldDef || !argumentNode) { return; } if (typeDef.kind === graphql_1.Kind.DIRECTIVE_DEFINITION) { context.reportError(new graphql_1.GraphQLError(`@fromContext argument cannot be used on a directive definition "@${typeDef.name.value}(${argumentNode.name.value}:)".`, { extensions: { code: "CONTEXT_NOT_SET" }, })); return; } if (typeDef.kind !== graphql_1.Kind.OBJECT_TYPE_DEFINITION && typeDef.kind !== graphql_1.Kind.OBJECT_TYPE_EXTENSION) { context.reportError(new graphql_1.GraphQLError(`@fromContext argument cannot be used on a field that exists on an abstract type "${argCoordinate(context)}".`, { extensions: { code: "CONTEXT_NOT_SET" }, })); return; } const objectType = context .getSubgraphObjectOrInterfaceTypes() .get(typeDef.name.value); if (objectType?.interfaces?.length) { for (const implementedInterface of objectType.interfaces) { const interfaceName = implementedInterface.name.value; const interfaceType = context .getSubgraphObjectOrInterfaceTypes() .get(interfaceName); if (interfaceType?.kind === graphql_1.Kind.INTERFACE_TYPE_DEFINITION || interfaceType?.kind === graphql_1.Kind.INTERFACE_TYPE_EXTENSION) { const implementedField = interfaceType.fields?.find((field) => field.name.value === fieldDef.name.value); if (implementedField) { context.reportError(new graphql_1.GraphQLError(`@fromContext argument cannot be used on a field implementing an interface field "${interfaceName}.${fieldDef.name.value}".`, { extensions: { code: "CONTEXT_NOT_SET" }, })); } } } } if (typeof argumentNode.defaultValue !== "undefined") { context.reportError(new graphql_1.GraphQLError(`@fromContext arguments may not have a default value: "${argCoordinate(context)}".`, { extensions: { code: "CONTEXT_NOT_SET" }, })); } const fieldValue = node.arguments?.find((arg) => arg.name.value === "field")?.value; if (!fieldValue || fieldValue.kind !== graphql_1.Kind.STRING) { return; } const { context: contextName, selection } = (0, context_js_1.parseContextReference)(fieldValue.value); if (!contextName || !selection) { context.reportError(new graphql_1.GraphQLError(`@fromContext argument does not reference a context "${fieldValue.value}".`, { extensions: { code: "NO_CONTEXT_IN_SELECTION" }, })); return; } const contextOwnerNames = contextOwnersByName.get(contextName); if (!contextOwnerNames?.length) { context.reportError(new graphql_1.GraphQLError(`Context "${contextName}" is used at location "${argCoordinate(context)}" but is never set.`, { extensions: { code: "CONTEXT_NOT_SET" }, })); return; } validateSelection({ context, contextOwnerNames, contextName, selection, argDef: argumentNode, }); const hasResolvableKey = (objectType?.directives ?? []).some((directive) => { if (!context.isAvailableFederationDirective("key", directive)) { return false; } const resolvable = directive.arguments?.find((arg) => arg.name.value === "resolvable")?.value; return resolvable?.kind === graphql_1.Kind.BOOLEAN ? resolvable.value : true; }); if (!hasResolvableKey) { context.reportError(new graphql_1.GraphQLError(`Object "${typeDef.name.value}" has no resolvable key but has a field with a contextual argument.`, { extensions: { code: "CONTEXT_NO_RESOLVABLE_KEY" }, })); } context.stateBuilder.objectType.field.arg.setFromContext(typeDef.name.value, fieldDef.name.value, argumentNode.name.value, { context: contextName, selection, }); }, }; } function indexContextOwners(context) { const ownersByContext = new Map(); for (const [typeName, typeDef,] of context.getSubgraphObjectOrInterfaceTypes()) { for (const directive of typeDef.directives ?? []) { rememberContextOwner(ownersByContext, readContextDirectiveName(context, directive), typeName); } } for (const definition of context.getDocument().definitions) { if (definition.kind !== graphql_1.Kind.UNION_TYPE_DEFINITION && definition.kind !== graphql_1.Kind.UNION_TYPE_EXTENSION) { continue; } for (const directive of definition.directives ?? []) { rememberContextOwner(ownersByContext, readContextDirectiveName(context, directive), definition.name.value); } } return ownersByContext; } function readContextDirectiveName(context, directive) { if (!context.isAvailableFederationDirective("context", directive)) { return undefined; } const name = directive.arguments?.find((arg) => arg.name.value === "name")?.value; return name?.kind === graphql_1.Kind.STRING ? name.value : undefined; } function rememberContextOwner(ownersByContext, contextName, ownerName) { if (!contextName) { return; } const owners = ownersByContext.get(contextName); if (owners) { owners.push(ownerName); } else { ownersByContext.set(contextName, [ownerName]); } } function argCoordinate(context) { const typeDef = context.typeNodeInfo.getTypeDef(); const fieldDef = context.typeNodeInfo.getFieldDef(); const argDef = context.typeNodeInfo.getArgumentDef(); return `${typeDef?.name.value}.${fieldDef?.name.value}(${argDef?.name.value}:)`; } function validateSelection(input) { const { context, contextName, argDef } = input; const typeViews = new Map(); const runtimeTypeSets = new Map(); function invalid(reason) { reportInvalidSelection(context, contextName, reason); } function getSelectableType(typeName) { if (!typeViews.has(typeName)) { typeViews.set(typeName, buildSelectableType(context, typeName)); } return typeViews.get(typeName) ?? null; } function runtimeSetFor(type) { const cached = runtimeTypeSets.get(type.name); if (cached) { return cached; } let runtimeTypes; if (type.kind === "OBJECT") { runtimeTypes = new Set([type.name]); } else if (type.kind === "UNION") { runtimeTypes = new Set(type.members); } else { runtimeTypes = new Set(Array.from(context.getSubgraphObjectOrInterfaceTypes()) .filter(([_, candidate]) => (candidate.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || candidate.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION) && (candidate.interfaces ?? []).some((item) => item.name.value === type.name)) .map(([candidateName]) => candidateName)); } runtimeTypeSets.set(type.name, runtimeTypes); return runtimeTypes; } function getFieldDefinition(type, fieldName) { const field = type.fields.get(fieldName); if (field) { return field; } if (type.kind !== "OBJECT") { return null; } for (const interfaceName of type.interfaces) { const interfaceType = getSelectableType(interfaceName); if (interfaceType?.kind !== "INTERFACE") { continue; } const interfaceField = interfaceType.fields.get(fieldName); if (interfaceField) { return interfaceField; } } return null; } function resolveSelectedType(currentType, selectionSet) { if (currentType.kind === "OBJECT" && hasInterfaceObjectDirective(context, currentType)) { return { kind: "interfaceObject", typeName: currentType.name }; } let resolvedType; for (const selection of selectionSet.selections) { const next = resolveSelectionType(currentType, selection); if (next.kind !== "resolved") { return next; } if (!resolvedType) { resolvedType = next.type; continue; } if (resolvedType !== next.type) { return { kind: "unresolved" }; } } return resolvedType ? { kind: "resolved", type: resolvedType } : { kind: "unresolved" }; } function resolveSelectionType(currentType, selection) { if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) { return { kind: "unresolved" }; } if (selection.kind === graphql_1.Kind.INLINE_FRAGMENT) { const fragmentType = selection.typeCondition ? getSelectableType(selection.typeCondition.name.value) : null; return fragmentType ? resolveSelectedType(fragmentType, selection.selectionSet) : { kind: "unresolved" }; } if (selection.name.value === "__typename") { return { kind: "resolved", type: "String!" }; } if (currentType.kind === "UNION") { return { kind: "unresolved" }; } const field = getFieldDefinition(currentType, selection.name.value); if (!field) { return { kind: "unknownField", owner: currentType.name, field: selection.name.value, }; } markFieldAsUsed(context, currentType, selection.name.value); if (!selection.selectionSet) { return { kind: "resolved", type: removeNonNullWrappers(field.type) }; } const childType = getSelectableType((0, helpers_js_1.namedTypeFromTypeNode)(field.type).name.value); if (!childType) { return { kind: "unresolved" }; } const nested = resolveSelectedType(childType, selection.selectionSet); if (nested.kind !== "resolved") { return nested; } return { kind: "resolved", type: wrapListModifiers(field.type, nested.type), }; } function validateSelectedType(result) { if (result.kind === "unknownField") { reportUnknownField(context, contextName, result.owner, result.field); return false; } if (result.kind === "interfaceObject") { reportInterfaceObject(context, contextName, result.typeName); return false; } if (result.kind === "unresolved") { invalid(`the type of the selection does not match the expected type "${(0, helpers_js_1.printOutputType)(argDef.type)}"`); return false; } if (!matchesArgType((0, graphql_1.parseType)(result.type), argDef.type)) { invalid(`the type of the selection "${result.type}" does not match the expected type "${(0, helpers_js_1.printOutputType)(argDef.type)}"`); return false; } return true; } function validateConditionalSelections(owner, selectionSet, touchedTypeConditions) { let sawApplicableBranch = false; const runtimeTypes = runtimeSetFor(owner); for (const selection of selectionSet.selections) { if (selection.kind !== graphql_1.Kind.INLINE_FRAGMENT || !selection.typeCondition) { throw new Error("Expected conditional @fromContext selections to contain only inline fragments with type conditions"); } const branchName = selection.typeCondition.name.value; if (!runtimeTypes.has(branchName)) { continue; } sawApplicableBranch = true; touchedTypeConditions.add(branchName); const branchType = getSelectableType(branchName); if (!branchType || branchType.kind !== "OBJECT") { invalid("type conditions must be an object type"); return false; } if (!validateSelectedType(resolveSelectedType(branchType, selection.selectionSet))) { return false; } } if (!sawApplicableBranch) { invalid(`no type condition matches the location "${owner.name}"`); return false; } return true; } const selectionSet = (0, helpers_js_1.parseFields)(input.selection); if (!selectionSet) { invalid("no selection is made"); return; } const unsupportedSyntax = findUnsupportedSelectionSyntax(selectionSet); if (unsupportedSyntax) { invalid(unsupportedSyntax); return; } const layout = classifySelectionLayout(selectionSet); if (layout.kind === "invalid") { invalid(layout.reason); return; } const touchedTypeConditions = new Set(); for (const ownerName of input.contextOwnerNames) { const owner = getSelectableType(ownerName); if (!owner) { continue; } const isValid = layout.kind === "direct" ? validateSelectedType(resolveSelectedType(owner, selectionSet)) : validateConditionalSelections(owner, selectionSet, touchedTypeConditions); if (!isValid) { return; } } if (layout.kind === "conditional") { for (const typeCondition of layout.typeConditions) { if (!touchedTypeConditions.has(typeCondition)) { invalid(`type condition "${typeCondition}" is never used.`); return; } } } } function markFieldAsUsed(context, currentType, fieldName) { context.markAsUsed("fields", currentType.kind === "OBJECT" ? graphql_1.Kind.OBJECT_TYPE_DEFINITION : graphql_1.Kind.INTERFACE_TYPE_DEFINITION, currentType.name, fieldName); } function reportInvalidSelection(context, contextName, reason) { context.reportError(new graphql_1.GraphQLError(`Context "${contextName}" is used in "${argCoordinate(context)}" but the selection is invalid: ${reason}`, { extensions: { code: "CONTEXT_INVALID_SELECTION" }, })); } function reportUnknownField(context, contextName, owner, field) { context.reportError(new graphql_1.GraphQLError(`Context "${contextName}" is used in "${argCoordinate(context)}" but the selection is invalid for type ${owner}. Error: Cannot query field "${field}" on type "${owner}".`, { extensions: { code: "CONTEXT_INVALID_SELECTION" }, })); } function reportInterfaceObject(context, contextName, typeName) { context.reportError(new graphql_1.GraphQLError(`Context is used in "${argCoordinate(context)}" but the selection is invalid: One of the types in the selection is an interfaceObject: "${typeName}"`, { extensions: { code: "CONTEXT_INVALID_SELECTION" }, })); } function classifySelectionLayout(selectionSet) { const [firstSelection, ...remainingSelections] = selectionSet.selections; if (!firstSelection) { return { kind: "invalid", reason: "no selection is made" }; } if (firstSelection.kind === graphql_1.Kind.FIELD) { return remainingSelections.length === 0 ? { kind: "direct" } : { kind: "invalid", reason: "multiple selections are made" }; } if (firstSelection.kind !== graphql_1.Kind.INLINE_FRAGMENT) { return { kind: "invalid", reason: "fragment spread is not allowed" }; } const typeConditions = new Set(); for (const selection of selectionSet.selections) { if (selection.kind !== graphql_1.Kind.INLINE_FRAGMENT) { return { kind: "invalid", reason: "multiple fields could be selected" }; } if (!selection.typeCondition) { return { kind: "invalid", reason: "inline fragments must have type conditions", }; } typeConditions.add(selection.typeCondition.name.value); } return typeConditions.size === selectionSet.selections.length ? { kind: "conditional", typeConditions } : { kind: "invalid", reason: "type conditions have same name" }; } function buildSelectableType(context, typeName) { const objectOrInterface = context .getSubgraphObjectOrInterfaceTypes() .get(typeName); if (objectOrInterface) { return { kind: objectOrInterface.kind === graphql_1.Kind.INTERFACE_TYPE_DEFINITION || objectOrInterface.kind === graphql_1.Kind.INTERFACE_TYPE_EXTENSION ? "INTERFACE" : "OBJECT", name: typeName, fields: new Map((objectOrInterface.fields ?? []).map((field) => [ field.name.value, field, ])), interfaces: (objectOrInterface.interfaces ?? []).map((item) => item.name.value), directives: objectOrInterface.directives ?? [], }; } const unionMembers = context.getSubgraphUnionTypes().get(typeName); if (unionMembers) { const unionState = context.stateBuilder.state.types.get(typeName); return { kind: "UNION", name: typeName, members: Array.from(unionMembers), directives: unionState?.kind === "UNION" ? unionState.ast.directives : [], }; } return null; } function hasInterfaceObjectDirective(context, type) { return type.directives.some((directive) => context.isAvailableFederationDirective("interfaceObject", directive)); } function removeNonNullWrappers(type) { if (type.kind === graphql_1.Kind.NON_NULL_TYPE) { return removeNonNullWrappers(type.type); } return (0, helpers_js_1.printOutputType)(type); } function wrapListModifiers(type, resolvedType) { if (type.kind === graphql_1.Kind.NON_NULL_TYPE) { return wrapListModifiers(type.type, resolvedType); } if (type.kind === graphql_1.Kind.LIST_TYPE) { return `[${wrapListModifiers(type.type, resolvedType)}]`; } return resolvedType; } function matchesArgType(selectType, argType) { if (argType.kind === graphql_1.Kind.NON_NULL_TYPE) { if (selectType.kind !== graphql_1.Kind.NON_NULL_TYPE) { return false; } return matchesArgType(selectType.type, argType.type); } if (selectType.kind === graphql_1.Kind.NON_NULL_TYPE) { return matchesArgType(selectType.type, argType); } if (argType.kind === graphql_1.Kind.LIST_TYPE) { if (selectType.kind !== graphql_1.Kind.LIST_TYPE) { return false; } return matchesArgType(selectType.type, argType.type); } if (selectType.kind === graphql_1.Kind.LIST_TYPE) { return false; } return selectType.name.value === argType.name.value; } function findUnsupportedSelectionSyntax(selectionSet) { for (const selection of selectionSet.selections) { if (selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) { return "fragment spread is not allowed"; } if (selection.kind === graphql_1.Kind.FIELD && Boolean(selection.alias)) { return "aliases are not allowed in the selection"; } if (selection.directives && selection.directives.length) { return "directives are not allowed in the selection"; } if (selection.selectionSet) { let reason = findUnsupportedSelectionSyntax(selection.selectionSet); if (reason) { return reason; } } } return null; }