import { allSchemaRootKinds, baseType, CompositeType, CoreFeature, defaultRootName, Directive, DirectiveDefinition, ErrGraphQLValidationFailed, Extension, FieldDefinition, InputFieldDefinition, InterfaceType, isCompositeType, isInterfaceType, isObjectType, isUnionType, ListType, NamedType, NonNullType, ObjectType, ScalarType, Schema, SchemaBlueprint, SchemaConfig, SchemaDefinition, SchemaElement, sourceASTs, UnionType, ArgumentDefinition, InputType, OutputType, WrapperType, isNonNullType, isLeafType, isListType, isWrapperType, possibleRuntimeTypes, isIntType, Type, isFieldDefinition, isElementNamedType, } from "./definitions"; import { assert, MultiMap, printHumanReadableList, OrderedMap, mapValues, assertUnreachable } from "./utils"; import { SDLValidationRule } from "graphql/validation/ValidationContext"; import { specifiedSDLRules } from "graphql/validation/specifiedRules"; import { ASTNode, DocumentNode, GraphQLError, Kind, KnownTypeNamesRule, PossibleTypeExtensionsRule, print as printAST, Source, GraphQLErrorOptions, SchemaDefinitionNode, OperationTypeNode, OperationTypeDefinitionNode, ConstDirectiveNode, } from "graphql"; import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule"; import { buildSchema, buildSchemaFromAST } from "./buildSchema"; import { FragmentSelection, hasSelectionWithPredicate, parseOperationAST, parseSelectionSet, Selection, SelectionSet } from './operations'; import { TAG_VERSIONS } from "./specs/tagSpec"; import { errorCodeDef, ErrorCodeDefinition, ERROR_CATEGORIES, ERRORS, withModifiedErrorMessage, extractGraphQLErrorOptions, errorCauses, } from "./error"; import { computeShareables } from "./precompute"; import { CoreSpecDefinition, FeatureVersion, LINK_VERSIONS, LinkDirectiveArgs, linkDirectiveDefaultName, linkIdentity, FeatureUrl, CoreImport, extractCoreFeatureImports, CoreOrLinkDirectiveArgs, } from "./specs/coreSpec"; import { FEDERATION_VERSIONS, federationIdentity, FederationDirectiveName, FederationTypeName, FEDERATION1_TYPES, FEDERATION1_DIRECTIVES, FederationSpecDefinition, } from "./specs/federationSpec"; import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print"; import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification"; import { didYouMean, suggestionList } from "./suggestions"; import { coreFeatureDefinitionIfKnown } from "./knownCoreFeatures"; import { joinIdentity } from "./specs/joinSpec"; import { CostDirectiveArguments, ListSizeDirectiveArguments } from "./specs/costSpec"; const linkSpec = LINK_VERSIONS.latest(); const tagSpec = TAG_VERSIONS.latest(); const federationSpec = (version?: FeatureVersion): FederationSpecDefinition => { if (!version) return FEDERATION_VERSIONS.latest(); const spec = FEDERATION_VERSIONS.find(version); assert(spec, `Federation spec version ${version} is not known`); return spec; }; // Some users rely on auto-expanding fed v1 graphs with fed v2 directives. While technically we should only expand @tag // directive from v2 definitions, we will continue expanding other directives (up to v2.4) to ensure backwards compatibility. const autoExpandedFederationSpec = federationSpec(new FeatureVersion(2, 4)); const latestFederationSpec = federationSpec(); // We don't let user use this as a subgraph name. That allows us to use it in `query graphs` to name the source of roots // in the "federated query graph" without worrying about conflict (see `FEDERATED_GRAPH_ROOT_SOURCE` in `querygraph.ts`). // (note that we could deal with this in other ways, but having a graph named '_' feels like a terrible idea anyway, so // disallowing it feels like more a good thing than a real restriction). export const FEDERATION_RESERVED_SUBGRAPH_NAME = '_'; export const FEDERATION_UNNAMED_SUBGRAPH_NAME = ''; const FEDERATION_OMITTED_VALIDATION_RULES = [ // We allow subgraphs to declare an extension even if the subgraph itself doesn't have a corresponding definition. // The implication being that the definition is in another subgraph. PossibleTypeExtensionsRule, // The `KnownTypeNamesRule` of graphQL-js only looks at type definitions, so this goes against our previous // desire to let a subgraph only have an extension for a type. Below, we add a replacement rules that looks // at both type definitions _and_ extensions. KnownTypeNamesRule ]; const FEDERATION_SPECIFIC_VALIDATION_RULES = [ KnownTypeNamesInFederationRule ]; const FEDERATION_VALIDATION_RULES = specifiedSDLRules.filter(rule => !FEDERATION_OMITTED_VALIDATION_RULES.includes(rule)).concat(FEDERATION_SPECIFIC_VALIDATION_RULES); const ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES: string[] = Object.values(FederationDirectiveName); /** * Federation 1 has that specificity that it wasn't using @link to name-space federation elements, * and so to "distinguish" the few federation type names, it prefixed those with a `_`. That is, * the `FieldSet` type was named `_FieldSet` in federation1. To handle this without too much effort, * we use a fake `CoreFeature` with imports for all the fed1 types to use those specific "aliases" * and we pass it when adding those types. This allows to reuse the same `TypeSpecification` objects * for both fed1 and fed2. Note that in the object below, all that is used is the imports, the rest * is just filling the blanks. */ const FAKE_FED1_CORE_FEATURE_TO_RENAME_TYPES: CoreFeature = new CoreFeature( new FeatureUrl('', 'fed1', new FeatureVersion(0, 1)), 'fed1', new Directive('fed1'), FEDERATION1_TYPES.map((spec) => ({ name: spec.name, as: '_' + spec.name})), ); function validateFieldSetSelections({ directiveName, selectionSet, hasExternalInParents, metadata, onError, allowOnNonExternalLeafFields, allowFieldsWithArguments, }: { directiveName: string, selectionSet: SelectionSet, hasExternalInParents: boolean, metadata: FederationMetadata, onError: (error: GraphQLError) => void, allowOnNonExternalLeafFields: boolean, allowFieldsWithArguments: boolean, }): void { for (const selection of selectionSet.selections()) { const appliedDirectives = selection.element.appliedDirectives; if (appliedDirectives.length > 0) { onError(ERROR_CATEGORIES.DIRECTIVE_IN_FIELDS_ARG.get(directiveName).err( `cannot have directive applications in the @${directiveName}(fields:) argument but found ${appliedDirectives.join(', ')}.`, )); } if (selection.kind === 'FieldSelection') { const field = selection.element.definition; const isExternal = metadata.isFieldExternal(field); if (!allowFieldsWithArguments && field.hasArguments()) { onError(ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err( `field ${field.coordinate} cannot be included because it has arguments (fields with argument are not allowed in @${directiveName})`, { nodes: field.sourceAST }, )); } // The field must be external if we don't allow non-external leaf fields, it's a leaf, and we haven't traversed an external field in parent chain leading here. const mustBeExternal = !selection.selectionSet && !allowOnNonExternalLeafFields && !hasExternalInParents; if (!isExternal && mustBeExternal) { const errorCode = ERROR_CATEGORIES.DIRECTIVE_FIELDS_MISSING_EXTERNAL.get(directiveName); if (metadata.isFieldFakeExternal(field)) { onError(errorCode.err( `field "${field.coordinate}" should not be part of a @${directiveName} since it is already "effectively" provided by this subgraph ` + `(while it is marked @${FederationDirectiveName.EXTERNAL}, it is a @${FederationDirectiveName.KEY} field of an extension type, which are not internally considered external for historical/backward compatibility reasons)`, { nodes: field.sourceAST } )); } else { onError(errorCode.err( `field "${field.coordinate}" should not be part of a @${directiveName} since it is already provided by this subgraph (it is not marked @${FederationDirectiveName.EXTERNAL})`, { nodes: field.sourceAST } )); } } if (selection.selectionSet) { // When passing the 'hasExternalInParents', the field might be external himself, but we may also have // the case where the field parent is an interface and some implementation of the field are external, in // which case we should say we have an external on the path, because we may have one. let newHasExternalInParents = hasExternalInParents || isExternal; const parentType = field.parent; if (!newHasExternalInParents && isInterfaceType(parentType)) { for (const implem of parentType.possibleRuntimeTypes()) { const fieldInImplem = implem.field(field.name); if (fieldInImplem && metadata.isFieldExternal(fieldInImplem)) { newHasExternalInParents = true; break; } } } validateFieldSetSelections({ directiveName, selectionSet: selection.selectionSet, hasExternalInParents: newHasExternalInParents, metadata, onError, allowOnNonExternalLeafFields, allowFieldsWithArguments, }); } } else { validateFieldSetSelections({ directiveName, selectionSet: selection.selectionSet, hasExternalInParents, metadata, onError, allowOnNonExternalLeafFields, allowFieldsWithArguments, }); } } } function validateFieldSet({ type, directive, metadata, errorCollector, allowOnNonExternalLeafFields, allowFieldsWithArguments, onFields, }: { type: CompositeType, directive: Directive, metadata: FederationMetadata, errorCollector: GraphQLError[], allowOnNonExternalLeafFields: boolean, allowFieldsWithArguments: boolean, onFields?: (field: FieldDefinition) => void, }): void { try { // Note that `parseFieldSetArgument` already properly format the error, hence the separate try-catch. // TODO: `parseFieldSetArgument` throws on the first issue found and never accumulate multiple // errors. We could fix this, but this require changes that reaches beyond this single file, so // we leave this for "later" (the `fields` value are rarely very big, so the benefit of accumulating // multiple errors within one such value is not tremendous, so that this doesn't feel like a pressing // issue). const fieldAccessor = onFields ? (type: CompositeType, fieldName: string) => { const field = type.field(fieldName); if (field) { onFields(field); } return field; } : undefined; const selectionSet = parseFieldSetArgument({parentType: type, directive, fieldAccessor}); validateFieldSetSelections({ directiveName: directive.name, selectionSet, hasExternalInParents: false, metadata, onError: (error) => errorCollector.push(handleFieldSetValidationError(directive, error)), allowOnNonExternalLeafFields, allowFieldsWithArguments, }); } catch (e) { if (e instanceof GraphQLError) { errorCollector.push(e); } else { throw e; } } } function handleFieldSetValidationError( directive: Directive, originalError: GraphQLError, messageUpdater?: (msg: string) => string, ): GraphQLError { const nodes = sourceASTs(directive); if (originalError.nodes) { nodes.push(...originalError.nodes); } let codeDef = errorCodeDef(originalError); // "INVALID_GRAPHQL" errors happening during validation means that the selection set is invalid, and // that's where we want to use a more precise code. if (!codeDef || codeDef === ERRORS.INVALID_GRAPHQL) { codeDef = ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS.get(directive.name); } let msg = originalError.message.trim(); if (messageUpdater) { msg = messageUpdater(msg); } return codeDef.err( `${fieldSetErrorDescriptor(directive)}: ${msg}`, { nodes, originalError, } ); } function fieldSetErrorDescriptor(directive: Directive): string { return `On ${fieldSetTargetDescription(directive)}, for ${directiveStrUsingASTIfPossible(directive)}`; } // This method is called to display @key, @provides or @requires directives in error message in place where the directive `fields` // argument might be invalid because it was not a string in the underlying AST. If that's the case, we want to use the AST to // print the directive or the message might be a bit confusing for the user. function directiveStrUsingASTIfPossible(directive: Directive): string { return directive.sourceAST ? printAST(directive.sourceAST) : directive.toString(); } function fieldSetTargetDescription(directive: Directive): string { const targetKind = directive.parent instanceof FieldDefinition ? "field" : "type"; return `${targetKind} "${directive.parent?.coordinate}"`; } export function parseContext(input: string) { const regex = /^(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*\$(?:[\n\r\t ,]|#[^\n\r]*(?![^\n\r]))*([A-Za-z_]\w*(?!\w))([\s\S]*)$/; const match = input.match(regex); if (!match) { return { context: undefined, selection: undefined }; } const [, context, selection] = match; return { context, selection, }; } const wrapResolvedType = ({ originalType, resolvedType, }: { originalType: OutputType, resolvedType: InputType, }): InputType | undefined => { const stack = []; let unwrappedType: NamedType | WrapperType = originalType; while(unwrappedType.kind === 'NonNullType' || unwrappedType.kind === 'ListType') { stack.push(unwrappedType.kind); unwrappedType = unwrappedType.ofType; } let type: NamedType | WrapperType = resolvedType; while(stack.length > 0) { const kind = stack.pop(); if (kind === 'ListType') { type = new ListType(type); } } return type; }; const validateFieldValueType = ({ currentType, selectionSet, errorCollector, metadata, fromContextParent, }: { currentType: CompositeType, selectionSet: SelectionSet, errorCollector: GraphQLError[], metadata: FederationMetadata, fromContextParent: ArgumentDefinition>, }): { resolvedType: InputType | undefined } => { const selections = selectionSet.selections(); // ensure that type is not an interfaceObject const interfaceObjectDirective = metadata.interfaceObjectDirective(); if (currentType.kind === 'ObjectType' && isFederationDirectiveDefinedInSchema(interfaceObjectDirective) && (currentType.appliedDirectivesOf(interfaceObjectDirective).length > 0)) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "is used in "${fromContextParent.coordinate}" but the selection is invalid: One of the types in the selection is an interfaceObject: "${currentType.name}"`, { nodes: sourceASTs(fromContextParent) } )); } const typesArray = selections.map((selection): { resolvedType: InputType | undefined } => { if (selection.kind !== 'FieldSelection') { return { resolvedType: undefined }; } const { element, selectionSet: childSelectionSet } = selection; assert(element.definition.type, 'Element type definition should exist'); let type = element.definition.type; if (childSelectionSet) { assert(isCompositeType(baseType(type)), 'Child selection sets should only exist on composite types'); const { resolvedType } = validateFieldValueType({ currentType: baseType(type) as CompositeType, selectionSet: childSelectionSet, errorCollector, metadata, fromContextParent, }); if (!resolvedType) { return { resolvedType: undefined }; } return { resolvedType: wrapResolvedType({ originalType: type, resolvedType}) }; } assert(isLeafType(baseType(type)), 'Expected a leaf type'); return { resolvedType: wrapResolvedType({ originalType: type, resolvedType: baseType(type) as InputType }) }; }); return typesArray.reduce((acc, { resolvedType }) => { if (acc.resolvedType?.toString() === resolvedType?.toString()) { return { resolvedType }; } return { resolvedType: undefined }; }); }; const validateSelectionFormat = ({ context, selection, fromContextParent, errorCollector, } : { context: string, selection: string, fromContextParent: ArgumentDefinition>, errorCollector: GraphQLError[], }): { selectionType: 'error' | 'field', } | { selectionType: 'inlineFragment', typeConditions: Set, } => { // we only need to parse the selection once, not do it for each location try { const node = parseOperationAST(selection.trim().startsWith('{') ? selection : `{${selection}}`); const selections = node.selectionSet.selections; if (selections.length === 0) { // a selection must be made. errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: no selection is made`, { nodes: sourceASTs(fromContextParent) } )); return { selectionType: 'error' }; } const firstSelectionKind = selections[0].kind; if (firstSelectionKind === 'Field') { // if the first selection is a field, there should be only one if (selections.length !== 1) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: multiple selections are made`, { nodes: sourceASTs(fromContextParent) } )); return { selectionType: 'error' }; } return { selectionType: 'field' }; } else if (firstSelectionKind === 'InlineFragment') { const inlineFragmentTypeConditions: Set = new Set(); if (!selections.every((s) => s.kind === 'InlineFragment')) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: multiple fields could be selected`, { nodes: sourceASTs(fromContextParent) } )); return { selectionType: 'error' }; } selections.forEach((s) => { assert(s.kind === 'InlineFragment', 'Expected an inline fragment'); const { typeCondition }= s; if (typeCondition) { inlineFragmentTypeConditions.add(typeCondition.name.value); } }); if (inlineFragmentTypeConditions.size !== selections.length) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type conditions have same name`, { nodes: sourceASTs(fromContextParent) } )); return { selectionType: 'error' }; } return { selectionType: 'inlineFragment', typeConditions: inlineFragmentTypeConditions, }; } else if (firstSelectionKind === 'FragmentSpread') { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: fragment spread is not allowed`, { nodes: sourceASTs(fromContextParent) } )); return { selectionType: 'error' }; } else { assertUnreachable(firstSelectionKind); } } catch (err) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: ${err.message}`, { nodes: sourceASTs(fromContextParent) } )); return { selectionType: 'error' }; } } // implementation of spec https://spec.graphql.org/draft/#IsValidImplementationFieldType() function isValidImplementationFieldType(fieldType: InputType, implementedFieldType: InputType): boolean { if (isNonNullType(fieldType)) { if (isNonNullType(implementedFieldType)) { return isValidImplementationFieldType(fieldType.ofType, implementedFieldType.ofType); } else { return isValidImplementationFieldType(fieldType.ofType, implementedFieldType); } } if (isListType(fieldType) && isListType(implementedFieldType)) { return isValidImplementationFieldType(fieldType.ofType, implementedFieldType.ofType); } return !isWrapperType(fieldType) && !isWrapperType(implementedFieldType) && fieldType.name === implementedFieldType.name; } function selectionSetHasDirectives(selectionSet: SelectionSet): boolean { return hasSelectionWithPredicate(selectionSet, (s: Selection) => { if (s.kind === 'FieldSelection') { return s.element.appliedDirectives.length > 0; } else if (s.kind === 'FragmentSelection') { return s.element.appliedDirectives.length > 0; } else { assertUnreachable(s); } }); } function selectionSetHasAlias(selectionSet: SelectionSet): boolean { return hasSelectionWithPredicate(selectionSet, (s: Selection) => { if (s.kind === 'FieldSelection') { return s.element.alias !== undefined; } return false; }); } function validateFieldValue({ context, selection, fromContextParent, setContextLocations, errorCollector, metadata, } : { context: string, selection: string, fromContextParent: ArgumentDefinition>, setContextLocations: (ObjectType | InterfaceType | UnionType)[], errorCollector: GraphQLError[], metadata: FederationMetadata, }): void { const expectedType = fromContextParent.type; assert(expectedType, 'Expected a type'); const validateSelectionFormatResults = validateSelectionFormat({ context, selection, fromContextParent, errorCollector }); const selectionType = validateSelectionFormatResults.selectionType; // if there was an error, just return, we've already added it to the errorCollector if (selectionType === 'error') { return; } const usedTypeConditions = new Set; for (const location of setContextLocations) { // for each location, we need to validate that the selection will result in exactly one field being selected // the number of selection sets created will be the same let selectionSet: SelectionSet; try { selectionSet = parseSelectionSet({ parentType: location, source: selection}); } catch (e) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid for type ${location.name}. Error: ${e.message}`, { nodes: sourceASTs(fromContextParent) } )); return; } if (selectionSetHasDirectives(selectionSet)) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: directives are not allowed in the selection`, { nodes: sourceASTs(fromContextParent) } )); } if (selectionSetHasAlias(selectionSet)) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: aliases are not allowed in the selection`, { nodes: sourceASTs(fromContextParent) } )); } if (selectionType === 'field') { const { resolvedType } = validateFieldValueType({ currentType: location, selectionSet, errorCollector, metadata, fromContextParent, }); if (resolvedType === undefined || !isValidImplementationFieldType(resolvedType, expectedType!)) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection "${resolvedType}" does not match the expected type "${expectedType?.toString()}"`, { nodes: sourceASTs(fromContextParent) } )); return; } } else if (selectionType === 'inlineFragment') { // ensure that each location maps to exactly one fragment const selections: FragmentSelection[] = []; for (const selection of selectionSet.selections()) { if (selection.kind !== 'FragmentSelection') { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: selection should only contain a single field or at least one inline fragment}"`, { nodes: sourceASTs(fromContextParent) } )); continue; } const { typeCondition } = selection.element; if (!typeCondition) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: inline fragments must have type conditions"`, { nodes: sourceASTs(fromContextParent) } )); continue; } if (typeCondition.kind === 'ObjectType') { if (possibleRuntimeTypes(location).includes(typeCondition)) { selections.push(selection); usedTypeConditions.add(typeCondition.name); } } else { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type conditions must be an object type"`, { nodes: sourceASTs(fromContextParent) } )); } } if (selections.length === 0) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: no type condition matches the location "${location.coordinate}"`, { nodes: sourceASTs(fromContextParent) } )); return; } else { for (const selection of selections) { let { resolvedType } = validateFieldValueType({ currentType: selection.element.typeCondition!, selectionSet: selection.selectionSet, errorCollector, metadata, fromContextParent, }); if (resolvedType === undefined) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection does not match the expected type "${expectedType?.toString()}"`, { nodes: sourceASTs(fromContextParent) } )); return; } // Because other subgraphs may define members of the location type, // it's always possible that none of the type conditions map, so we // must remove any surrounding non-null wrapper if present. if (isNonNullType(resolvedType)) { resolvedType = resolvedType.ofType; } if (!isValidImplementationFieldType(resolvedType!, expectedType!)) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: the type of the selection "${resolvedType?.toString()}" does not match the expected type "${expectedType?.toString()}"`, { nodes: sourceASTs(fromContextParent) } )); return; } } } } } if (validateSelectionFormatResults.selectionType === 'inlineFragment') { for (const typeCondition of validateSelectionFormatResults.typeConditions) { if (!usedTypeConditions.has(typeCondition)) { errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err( `Context "${context}" is used in "${fromContextParent.coordinate}" but the selection is invalid: type condition "${typeCondition}" is never used.`, { nodes: sourceASTs(fromContextParent) } )); } } } } function validateAllFieldSet>({ definition, targetTypeExtractor, errorCollector, metadata, isOnParentType = false, allowOnNonExternalLeafFields = false, allowFieldsWithArguments = false, allowOnInterface = false, onFields, }: { definition: DirectiveDefinition<{fields: any}>, targetTypeExtractor: (element: TParent) => CompositeType, errorCollector: GraphQLError[], metadata: FederationMetadata, isOnParentType?: boolean, allowOnNonExternalLeafFields?: boolean, allowFieldsWithArguments?: boolean, allowOnInterface?: boolean, onFields?: (field: FieldDefinition) => void, }): void { for (const application of definition.applications()) { const elt = application.parent as TParent; const type = targetTypeExtractor(elt); const parentType = isOnParentType ? type : (elt.parent as NamedType); if (isInterfaceType(parentType) && !allowOnInterface) { const code = ERROR_CATEGORIES.DIRECTIVE_UNSUPPORTED_ON_INTERFACE.get(definition.name); errorCollector.push(code.err( isOnParentType ? `Cannot use ${definition.coordinate} on interface "${parentType.coordinate}": ${definition.coordinate} is not yet supported on interfaces` : `Cannot use ${definition.coordinate} on ${fieldSetTargetDescription(application)} of parent type "${parentType}": ${definition.coordinate} is not yet supported within interfaces`, { nodes: sourceASTs(application).concat(isOnParentType ? [] : sourceASTs(type)) }, )); } validateFieldSet({ type, directive: application, metadata, errorCollector, allowOnNonExternalLeafFields, allowFieldsWithArguments, onFields, }); } } export function collectUsedFields(metadata: FederationMetadata): Set> { const usedFields = new Set>(); // Collects all external fields used by a key, requires or provides collectUsedFieldsForDirective( metadata.keyDirective(), type => type, usedFields, ); collectUsedFieldsForDirective>( metadata.requiresDirective(), field => field.parent!, usedFields, ); collectUsedFieldsForDirective>( metadata.providesDirective(), field => { const type = baseType(field.type!); return isCompositeType(type) ? type : undefined; }, usedFields, ); // also for @fromContext collectUsedFieldsForFromContext( metadata, usedFields, ); // Collects all fields used to satisfy an interface constraint for (const itfType of metadata.schema.interfaceTypes()) { const runtimeTypes = itfType.possibleRuntimeTypes(); for (const field of itfType.fields()) { for (const runtimeType of runtimeTypes) { const implemField = runtimeType.field(field.name); if (implemField) { usedFields.add(implemField); } } } } return usedFields; } function collectUsedFieldsForFromContext>( metadata: FederationMetadata, usedFieldDefs: Set> ) { const fromContextDirective = metadata.fromContextDirective(); const contextDirective = metadata.contextDirective(); // if one of the directives is not defined, there's nothing to validate if (!isFederationDirectiveDefinedInSchema(fromContextDirective) || !isFederationDirectiveDefinedInSchema(contextDirective)) { return; } // build the list of context entry points const entryPoints = new Map>(); for (const application of contextDirective.applications()) { const type = application.parent; if (!type) { // Means the application is wrong: we ignore it here as later validation will detect it continue; } const context = application.arguments().name; if (!entryPoints.has(context)) { entryPoints.set(context, new Set()); } entryPoints.get(context)!.add(type as CompositeType); } for (const application of fromContextDirective.applications()) { const type = application.parent as TParent; if (!type) { // Means the application is wrong: we ignore it here as later validation will detect it continue; } const fieldValue = application.arguments().field; const { context, selection } = parseContext(fieldValue); if (!context) { continue; } // now we need to collect all the fields used for every type that they could be used for const contextTypes = entryPoints.get(context); if (!contextTypes) { continue; } for (const contextType of contextTypes) { try { // helper function const fieldAccessor = (t: CompositeType, f: string) => { const field = t.field(f); if (field) { usedFieldDefs.add(field); if (isInterfaceType(t)) { for (const implType of t.possibleRuntimeTypes()) { const implField = implType.field(f); if (implField) { usedFieldDefs.add(implField); } } } } return field; }; parseSelectionSet({ parentType: contextType, source: selection, fieldAccessor }); } catch (e) { // ignore the error, it will be caught later } } } } function collectUsedFieldsForDirective>( definition: DirectiveDefinition<{fields: any}>, targetTypeExtractor: (element: TParent) => CompositeType | undefined, usedFieldDefs: Set> ) { for (const application of definition.applications()) { const type = targetTypeExtractor(application.parent! as TParent); if (!type) { // Means the application is wrong: we ignore it here as later validation will detect it continue; } // Note that we don't want to 'validate', because even if a field set is invalid for some reason, we still want to consider // its field as "used". This avoid, when a `fields` argument is invalid, to get one error for the `fields` itself, but also // a bunch of other errors that says some external fields are unused that are just a consequence of not considering that // particular `fields` argument. In other words, this avoid cascading errors that would be confusing to the user without // being of any concrete use. collectTargetFields({ parentType: type, directive: application as Directive, includeInterfaceFieldsImplementations: true, validate: false, }).forEach((field) => usedFieldDefs.add(field)); } } /** * Checks that all fields marked @external is used in a federation directive (@key, @provides or @requires) _or_ to satisfy an * interface implementation. Otherwise, the field declaration is somewhat useless. */ function validateAllExternalFieldsUsed(metadata: FederationMetadata, errorCollector: GraphQLError[]): void { for (const type of metadata.schema.types()) { if (!isObjectType(type) && !isInterfaceType(type)) { continue; } for (const field of type.fields()) { if (!metadata.isFieldExternal(field) || metadata.isFieldUsed(field)) { continue; } errorCollector.push(ERRORS.EXTERNAL_UNUSED.err( `Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;` + ' the field declaration has no use and should be removed (or the field should not be @external).', { nodes: field.sourceAST }, )); } } } function validateNoExternalOnInterfaceFields(metadata: FederationMetadata, errorCollector: GraphQLError[]) { for (const itf of metadata.schema.interfaceTypes()) { for (const field of itf.fields()) { if (metadata.isFieldExternal(field)) { errorCollector.push(ERRORS.EXTERNAL_ON_INTERFACE.err( `Interface type field "${field.coordinate}" is marked @external but @external is not allowed on interface fields (it is nonsensical).`, { nodes: field.sourceAST }, )); } } } } function validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata: FederationMetadata, errorCollector: GraphQLError[]): void { for (const itfType of metadata.schema.interfaceTypes()) { const implementations = itfType.possibleRuntimeTypes(); for (const keyApplication of itfType.appliedDirectivesOf(metadata.keyDirective())) { // Note that we will always have validated all @key fields at this point, so not bothering with extra validation const fields = parseFieldSetArgument({parentType: itfType, directive: keyApplication, validate: false}); const isResolvable = !(keyApplication.arguments().resolvable === false); const implementationsWithKeyButNotResolvable = new Array(); const implementationsMissingKey = new Array(); for (const type of implementations) { const matchingApp = type.appliedDirectivesOf(metadata.keyDirective()).find((app) => { const appFields = parseFieldSetArgument({parentType: type, directive: app, validate: false}); return fields.equals(appFields); }); if (matchingApp) { if (isResolvable && matchingApp.arguments().resolvable === false) { implementationsWithKeyButNotResolvable.push(type); } } else { implementationsMissingKey.push(type); } } if (implementationsMissingKey.length > 0) { const typesString = printHumanReadableList( implementationsMissingKey.map((i) => `"${i.coordinate}"`), { prefix: 'type', prefixPlural: 'types', } ); errorCollector.push(ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err( `Key ${keyApplication} on interface type "${itfType.coordinate}" is missing on implementation ${typesString}.`, { nodes: sourceASTs(...implementationsMissingKey) }, )); } else if (implementationsWithKeyButNotResolvable.length > 0) { const typesString = printHumanReadableList( implementationsWithKeyButNotResolvable.map((i) => `"${i.coordinate}"`), { prefix: 'type', prefixPlural: 'types', } ); errorCollector.push(ERRORS.INTERFACE_KEY_NOT_ON_IMPLEMENTATION.err( `Key ${keyApplication} on interface type "${itfType.coordinate}" should be resolvable on all implementation types, but is declared with argument "@key(resolvable:)" set to false in ${typesString}.`, { nodes: sourceASTs(...implementationsWithKeyButNotResolvable) }, )); } } } } function validateInterfaceObjectsAreOnEntities(metadata: FederationMetadata, errorCollector: GraphQLError[]): void { for (const application of metadata.interfaceObjectDirective().applications()) { if (!isEntityType(application.parent)) { errorCollector.push(ERRORS.INTERFACE_OBJECT_USAGE_ERROR.err( `The @interfaceObject directive can only be applied to entity types but type "${application.parent.coordinate}" has no @key in this subgraph.`, { nodes: application.parent.sourceAST } )); } } } function validateShareableNotRepeatedOnSameDeclaration( element: ObjectType | FieldDefinition, metadata: FederationMetadata, errorCollector: GraphQLError[], ) { const shareableApplications: Directive[] = element.appliedDirectivesOf(metadata.shareableDirective()); if (shareableApplications.length <= 1) { return; } type ByExtensions = { without: Directive[], with: MultiMap, Directive>, }; const byExtensions = shareableApplications.reduce( (acc, v) => { const ext = v.ofExtension(); if (ext) { acc.with.add(ext, v); } else { acc.without.push(v); } return acc; }, { without: [], with: new MultiMap() } ); const groups = [ byExtensions.without ].concat(mapValues(byExtensions.with)); for (const group of groups) { if (group.length > 1) { const eltStr = element.kind === 'ObjectType' ? `the same type declaration of "${element.coordinate}"` : `field "${element.coordinate}"`; errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err( `Invalid duplicate application of @shareable on ${eltStr}: ` + '@shareable is only repeatable on types so it can be used simultaneously on a type definition and its extensions, but it should not be duplicated on the same definition/extension declaration', { nodes: sourceASTs(...group) }, )); } } } function validateCostNotAppliedToInterface(application: Directive, CostDirectiveArguments>, errorCollector: GraphQLError[]) { const parent = application.parent; // @cost cannot be used on interfaces https://ibm.github.io/graphql-specs/cost-spec.html#sec-No-Cost-on-Interface-Fields if (parent instanceof FieldDefinition && parent.parent instanceof InterfaceType) { errorCollector.push(ERRORS.COST_APPLIED_TO_INTERFACE_FIELD.err( `@cost cannot be applied to interface "${parent.coordinate}"`, { nodes: sourceASTs(application, parent) } )); } } function validateListSizeAppliedToList( application: Directive, ListSizeDirectiveArguments>, parent: FieldDefinition, errorCollector: GraphQLError[], ) { const { sizedFields = [] } = application.arguments(); // @listSize must be applied to a list https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-List-Size-Target if (!sizedFields.length && parent.type && !isListType(parent.type) && !isNonNullListType(parent.type)) { errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err( `"${parent.coordinate}" is not a list`, { nodes: sourceASTs(application, parent) }, )); } } function validateAssumedSizeNotNegative( application: Directive, ListSizeDirectiveArguments>, parent: FieldDefinition, errorCollector: GraphQLError[] ) { const { assumedSize } = application.arguments(); // Validate assumed size, but we differ from https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Assumed-Size. // Assumed size is used as a backup for slicing arguments in the event they are both specified. // The spec aims to rule out cases when the assumed size will never be used because there is always // a slicing argument. Two applications which are compliant with that validation rule can be merged // into an application which is not compliant, thus we need to handle this case gracefully at runtime regardless. // We omit this check to keep the validations to those that will otherwise cause runtime failures. // // With all that said, assumed size should not be negative. if (assumedSize !== undefined && assumedSize !== null && assumedSize < 0) { errorCollector.push(ERRORS.LIST_SIZE_INVALID_ASSUMED_SIZE.err( `Assumed size of "${parent.coordinate}" cannot be negative`, { nodes: sourceASTs(application, parent) }, )); } } function isNonNullIntType(ty: Type): boolean { return isNonNullType(ty) && isIntType(ty.ofType) } function validateSlicingArgumentsAreValidIntegers( application: Directive, ListSizeDirectiveArguments>, parent: FieldDefinition, errorCollector: GraphQLError[] ) { const { slicingArguments = [] } = application.arguments(); // Validate slicingArguments https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Slicing-Arguments-Target for (const slicingArgumentName of slicingArguments) { const slicingArgument = parent.argument(slicingArgumentName); if (!slicingArgument?.type) { // Slicing arguments must be one of the field's arguments errorCollector.push(ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err( `Slicing argument "${slicingArgumentName}" is not an argument of "${parent.coordinate}"`, { nodes: sourceASTs(application, parent) } )); } else if (!isIntType(slicingArgument.type) && !isNonNullIntType(slicingArgument.type)) { // Slicing arguments must be Int or Int! errorCollector.push(ERRORS.LIST_SIZE_INVALID_SLICING_ARGUMENT.err( `Slicing argument "${slicingArgument.coordinate}" must be Int or Int!`, { nodes: sourceASTs(application, parent) } )); } } } function isNonNullListType(ty: Type): boolean { return isNonNullType(ty) && isListType(ty.ofType) } function validateSizedFieldsAreValidLists( application: Directive, ListSizeDirectiveArguments>, parent: FieldDefinition, errorCollector: GraphQLError[] ) { const { sizedFields = [] } = application.arguments(); // Validate sizedFields https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Sized-Fields-Target if (sizedFields.length && parent.type) { const baseParentType = baseType(parent.type); if (!isCompositeType(baseParentType)) { // The output type must have fields errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err( `Sized fields cannot be used because "${parent.type}" is not a composite type`, { nodes: sourceASTs(application, parent)} )); } else { for (const sizedFieldName of sizedFields) { const sizedField = baseParentType.field(sizedFieldName); if (!sizedField) { // Sized fields must be present on the output type errorCollector.push(ERRORS.LIST_SIZE_INVALID_SIZED_FIELD.err( `Sized field "${sizedFieldName}" is not a field on type "${baseParentType.coordinate}"`, { nodes: sourceASTs(application, parent) } )); } else if (!sizedField.type || !(isListType(sizedField.type) || isNonNullListType(sizedField.type))) { // Sized fields must be lists errorCollector.push(ERRORS.LIST_SIZE_APPLIED_TO_NON_LIST.err( `Sized field "${sizedField.coordinate}" is not a list`, { nodes: sourceASTs(application, parent) }, )); } } } } } export class FederationMetadata { private _externalTester?: ExternalTester; private _sharingPredicate?: (field: FieldDefinition) => boolean; private _fieldUsedPredicate?: (field: FieldDefinition) => boolean; private _isFed2Schema?: boolean; constructor(readonly schema: Schema) {} private onInvalidate() { this._externalTester = undefined; this._sharingPredicate = undefined; this._isFed2Schema = undefined; this._fieldUsedPredicate = undefined; } isFed2Schema(): boolean { if (!this._isFed2Schema) { const feature = this.federationFeature(); this._isFed2Schema = !!feature && feature.url.version.satisfies(new FeatureVersion(2, 0)) } return this._isFed2Schema; } federationFeature(): CoreFeature | undefined { return this.schema.coreFeatures?.getByIdentity(latestFederationSpec.identity); } private externalTester(): ExternalTester { if (!this._externalTester) { this._externalTester = new ExternalTester(this.schema, this.isFed2Schema()); } return this._externalTester; } private sharingPredicate(): (field: FieldDefinition) => boolean { if (!this._sharingPredicate) { this._sharingPredicate = computeShareables(this.schema); } return this._sharingPredicate; } private fieldUsedPredicate(): (field: FieldDefinition) => boolean { if (!this._fieldUsedPredicate) { const usedFields = collectUsedFields(this); this._fieldUsedPredicate = (field: FieldDefinition) => !!usedFields.has(field); } return this._fieldUsedPredicate; } isFieldUsed(field: FieldDefinition): boolean { return this.fieldUsedPredicate()(field); } isFieldExternal(field: FieldDefinition | InputFieldDefinition) { return this.externalTester().isExternal(field); } isFieldPartiallyExternal(field: FieldDefinition | InputFieldDefinition) { return this.externalTester().isPartiallyExternal(field); } isFieldFullyExternal(field: FieldDefinition | InputFieldDefinition) { return this.externalTester().isFullyExternal(field); } isFieldFakeExternal(field: FieldDefinition | InputFieldDefinition) { return this.externalTester().isFakeExternal(field); } selectionSelectsAnyExternalField(selectionSet: SelectionSet): boolean { return this.externalTester().selectsAnyExternalField(selectionSet); } isFieldShareable(field: FieldDefinition): boolean { return this.sharingPredicate()(field); } isInterfaceObjectType(type: NamedType): type is ObjectType { return isObjectType(type) && hasAppliedDirective(type, this.interfaceObjectDirective()); } federationDirectiveNameInSchema(name: string): string { if (this.isFed2Schema()) { const coreFeatures = this.schema.coreFeatures; assert(coreFeatures, 'Schema should be a core schema'); const federationFeature = coreFeatures.getByIdentity(latestFederationSpec.identity); assert(federationFeature, 'Schema should have the federation feature'); return federationFeature.directiveNameInSchema(name); } else { return name; } } federationTypeNameInSchema(name: string): string { // Currently, the types used to define the federation operations, that is _Any, _Entity and _Service, // are not considered part of the federation spec, and are instead hardcoded to the names above. // The reason being that there is no way to maintain backward compatbility with fed2 if we were to add // those to the federation spec without requiring users to add those types to their @link `import`, // and that wouldn't be a good user experience (because most users don't really know what those types // are/do). And so we special case it. if (name.charAt(0) === '_') { return name; } if (this.isFed2Schema()) { const coreFeatures = this.schema.coreFeatures; assert(coreFeatures, 'Schema should be a core schema'); const federationFeature = coreFeatures.getByIdentity(latestFederationSpec.identity); assert(federationFeature, 'Schema should have the federation feature'); return federationFeature.typeNameInSchema(name); } else { // The only type here so far is the the `FieldSet` one. And in fed1, it's called `_FieldSet`, so ... return '_' + name; } } // Should only be be called for "legacy" directives, those that existed in 2.0. This // allow to avoiding have to double-check the directive exists every time when we // know it will always exists (note that even though we accept fed1 schema as inputs, // those are almost immediately converted to fed2 ones by the `SchemaUpgrader`, so // we include @shareable or @override in those "legacy" directives). private getLegacyFederationDirective( name: FederationDirectiveName ): DirectiveDefinition { const directive = this.getFederationDirective(name); assert(directive, `The provided schema does not have federation directive @${name}`); return directive; } private getFederationDirective( name: FederationDirectiveName ): DirectiveDefinition | undefined { return this.schema.directive(this.federationDirectiveNameInSchema(name)) as DirectiveDefinition | undefined; } private getPost20FederationDirective( name: FederationDirectiveName ): Post20FederationDirectiveDefinition { return this.getFederationDirective(name) ?? { name, applications: () => new Set>(), }; } keyDirective(): DirectiveDefinition<{fields: any, resolvable?: boolean}> { return this.getLegacyFederationDirective(FederationDirectiveName.KEY); } overrideDirective(): DirectiveDefinition<{from: string, label?: string}> { return this.getLegacyFederationDirective(FederationDirectiveName.OVERRIDE); } extendsDirective(): DirectiveDefinition> { return this.getLegacyFederationDirective(FederationDirectiveName.EXTENDS); } externalDirective(): DirectiveDefinition<{reason: string}> { return this.getLegacyFederationDirective(FederationDirectiveName.EXTERNAL); } requiresDirective(): DirectiveDefinition<{fields: any}> { return this.getLegacyFederationDirective(FederationDirectiveName.REQUIRES); } providesDirective(): DirectiveDefinition<{fields: any}> { return this.getLegacyFederationDirective(FederationDirectiveName.PROVIDES); } shareableDirective(): DirectiveDefinition<{}> { return this.getLegacyFederationDirective(FederationDirectiveName.SHAREABLE); } tagDirective(): DirectiveDefinition<{name: string}> { return this.getLegacyFederationDirective(FederationDirectiveName.TAG); } composeDirective(): Post20FederationDirectiveDefinition<{name: string}> { return this.getPost20FederationDirective(FederationDirectiveName.COMPOSE_DIRECTIVE); } inaccessibleDirective(): DirectiveDefinition<{}> { return this.getLegacyFederationDirective(FederationDirectiveName.INACCESSIBLE); } interfaceObjectDirective(): Post20FederationDirectiveDefinition<{}> { return this.getPost20FederationDirective(FederationDirectiveName.INTERFACE_OBJECT); } authenticatedDirective(): Post20FederationDirectiveDefinition<{}> { return this.getPost20FederationDirective(FederationDirectiveName.AUTHENTICATED); } requiresScopesDirective(): Post20FederationDirectiveDefinition<{scopes: string[]}> { return this.getPost20FederationDirective(FederationDirectiveName.REQUIRES_SCOPES); } policyDirective(): Post20FederationDirectiveDefinition<{scopes: string[]}> { return this.getPost20FederationDirective(FederationDirectiveName.POLICY); } fromContextDirective(): Post20FederationDirectiveDefinition<{ field: string }> { return this.getPost20FederationDirective(FederationDirectiveName.FROM_CONTEXT); } contextDirective(): Post20FederationDirectiveDefinition<{ name: string }> { return this.getPost20FederationDirective(FederationDirectiveName.CONTEXT); } costDirective(): Post20FederationDirectiveDefinition { return this.getPost20FederationDirective(FederationDirectiveName.COST); } listSizeDirective(): Post20FederationDirectiveDefinition { return this.getPost20FederationDirective(FederationDirectiveName.LIST_SIZE); } cacheTagDirective(): Post20FederationDirectiveDefinition<{format: string}> { return this.getPost20FederationDirective(FederationDirectiveName.CACHE_TAG); } allFederationDirectives(): DirectiveDefinition[] { const baseDirectives: DirectiveDefinition[] = [ this.keyDirective(), this.externalDirective(), this.requiresDirective(), this.providesDirective(), this.tagDirective(), this.extendsDirective(), ]; if (!this.isFed2Schema()) { return baseDirectives; } baseDirectives.push(this.shareableDirective()); baseDirectives.push(this.inaccessibleDirective()); baseDirectives.push(this.overrideDirective()); const composeDirective = this.composeDirective(); if (isFederationDirectiveDefinedInSchema(composeDirective)) { baseDirectives.push(composeDirective); } const interfaceObjectDirective = this.interfaceObjectDirective(); if (isFederationDirectiveDefinedInSchema(interfaceObjectDirective)) { baseDirectives.push(interfaceObjectDirective); } const authenticatedDirective = this.authenticatedDirective(); if (isFederationDirectiveDefinedInSchema(authenticatedDirective)) { baseDirectives.push(authenticatedDirective); } const requiresScopesDirective = this.requiresScopesDirective(); if (isFederationDirectiveDefinedInSchema(requiresScopesDirective)) { baseDirectives.push(requiresScopesDirective); } const policyDirective = this.policyDirective(); if (isFederationDirectiveDefinedInSchema(policyDirective)) { baseDirectives.push(policyDirective); } const contextDirective = this.contextDirective(); if (isFederationDirectiveDefinedInSchema(contextDirective)) { baseDirectives.push(contextDirective); } const fromContextDirective = this.fromContextDirective(); if (isFederationDirectiveDefinedInSchema(fromContextDirective)) { baseDirectives.push(fromContextDirective); } const costDirective = this.costDirective(); if (isFederationDirectiveDefinedInSchema(costDirective)) { baseDirectives.push(costDirective); } const listSizeDirective = this.listSizeDirective(); if (isFederationDirectiveDefinedInSchema(listSizeDirective)) { baseDirectives.push(listSizeDirective); } const cacheTagDirective = this.cacheTagDirective(); if (isFederationDirectiveDefinedInSchema(cacheTagDirective)) { baseDirectives.push(cacheTagDirective); } return baseDirectives; } // Note that a subgraph may have no "entities" and so no _EntityType. entityType(): UnionType | undefined { return this.schema.type(this.federationTypeNameInSchema(entityTypeSpec.name)) as UnionType | undefined; } anyType(): ScalarType { return this.schema.type(this.federationTypeNameInSchema(anyTypeSpec.name)) as ScalarType; } serviceType(): ObjectType { return this.schema.type(this.federationTypeNameInSchema(serviceTypeSpec.name)) as ObjectType; } fieldSetType(): ScalarType { return this.schema.type(this.federationTypeNameInSchema(FederationTypeName.FIELD_SET)) as ScalarType; } allFederationTypes(): NamedType[] { // We manually include the `_Any`, `_Service` and `Entity` types because there are not strictly // speaking part of the federation @link spec. const fedTypes: NamedType[] = [ this.anyType(), this.serviceType(), ]; const fedFeature = this.federationFeature(); if (fedFeature) { const featureDef = FEDERATION_VERSIONS.find(fedFeature.url.version); assert(featureDef, () => `Federation spec should be known, but got ${fedFeature.url}`); for (const typeSpec of featureDef.typeSpecs()) { const type = this.schema.type(fedFeature.typeNameInSchema(typeSpec.name)); if (type) { fedTypes.push(type); } } } else { // Fed1: the only type we had was _FieldSet. fedTypes.push(this.fieldSetType()); } const entityType = this.entityType(); if (entityType) { fedTypes.push(entityType); } return fedTypes; } } export type FederationDirectiveNotDefinedInSchema = { name: string, applications: () => ReadonlySet>, } export type Post20FederationDirectiveDefinition = DirectiveDefinition | FederationDirectiveNotDefinedInSchema; export function isFederationDirectiveDefinedInSchema( definition: Post20FederationDirectiveDefinition ): definition is DirectiveDefinition { return definition instanceof DirectiveDefinition; } export function hasAppliedDirective(type: NamedType, definition: Post20FederationDirectiveDefinition): boolean { return isFederationDirectiveDefinedInSchema(definition) && type.hasAppliedDirective(definition); } export class FederationBlueprint extends SchemaBlueprint { constructor(private readonly withRootTypeRenaming: boolean) { super(); } onAddedCoreFeature(schema: Schema, feature: CoreFeature) { super.onAddedCoreFeature(schema, feature); if (feature.url.identity === federationIdentity) { const spec = FEDERATION_VERSIONS.find(feature.url.version); if (spec) { spec.addElementsToSchema(schema); } } } onMissingDirectiveDefinition(schema: Schema, directive: Directive): DirectiveDefinition | GraphQLError[] | undefined { if (directive.name === linkDirectiveDefaultName) { const args = directive.arguments(); const url = args && (args['url'] as string | undefined); let as: string | undefined = undefined; let imports: CoreImport[] = []; if (url && url.startsWith(linkSpec.identity)) { as = args['as'] as string | undefined; imports = extractCoreFeatureImports(linkSpec.url, directive as Directive); } const errors = linkSpec.addDefinitionsToSchema(schema, as, imports); return errors.length > 0 ? errors : schema.directive(directive.name); } return super.onMissingDirectiveDefinition(schema, directive); } ignoreParsedField(type: NamedType, fieldName: string): boolean { // Historically, federation 1 has accepted invalid schema, including some where the Query type included // the definition of `_entities` (so `_entities(representations: [_Any!]!): [_Entity]!`) but _without_ // defining the `_Any` or `_Entity` type. So while we want to be stricter for fed2 (so this kind of // really weird case can be fixed), we want fed2 to accept as much fed1 schema as possible. // // So, to avoid this problem, we ignore the _entities and _service fields if we parse them from // a fed1 input schema. Those will be added back anyway (along with the proper types) post-parsing. if (!FEDERATION_OPERATION_FIELDS.includes(fieldName)) { return false; } const metadata = federationMetadata(type.schema()); return !!metadata && !metadata.isFed2Schema(); } onConstructed(schema: Schema) { const existing = federationMetadata(schema); if (!existing) { (schema as any)['_federationMetadata'] = new FederationMetadata(schema); } } onDirectiveDefinitionAndSchemaParsed(schema: Schema): GraphQLError[] { const errors = completeSubgraphSchema(schema); schema.schemaDefinition.processUnappliedDirectives(); return errors; } onInvalidation(schema: Schema) { super.onInvalidation(schema); const metadata = federationMetadata(schema); assert(metadata, 'Federation schema should have had its metadata set on construction'); FederationMetadata.prototype['onInvalidate'].call(metadata); } onValidation(schema: Schema): GraphQLError[] { const errorCollector = super.onValidation(schema); // We rename all root type to their default names (we do here rather than in `prepareValidation` because // that can actually fail). if (this.withRootTypeRenaming) { for (const k of allSchemaRootKinds) { const type = schema.schemaDefinition.root(k)?.type; const defaultName = defaultRootName(k); if (type && type.name !== defaultName) { // We first ensure there is no other type using the default root name. If there is, this is a // composition error. const existing = schema.type(defaultName); if (existing) { errorCollector.push(ERROR_CATEGORIES.ROOT_TYPE_USED.get(k).err( `The schema has a type named "${defaultName}" but it is not set as the ${k} root type ("${type.name}" is instead): ` + 'this is not supported by federation. ' + 'If a root type does not use its default name, there should be no other type with that default name.', { nodes: sourceASTs(type, existing) }, )); } type.rename(defaultName); } } } const metadata = federationMetadata(schema); assert(metadata, 'Federation schema should have had its metadata set on construction'); // We skip the rest of validation for fed1 schema because there is a number of validation that is stricter than what fed 1 // accepted, and some of those issues are fixed by `SchemaUpgrader`. So insofar as any fed 1 scheam is ultimately converted // to a fed 2 one before composition, then skipping some validation on fed 1 schema is fine. if (!metadata.isFed2Schema()) { return errorCollector; } // We validate the @key, @requires and @provides. const keyDirective = metadata.keyDirective(); validateAllFieldSet({ definition: keyDirective, targetTypeExtractor: type => type, errorCollector, metadata, isOnParentType: true, allowOnNonExternalLeafFields: true, allowOnInterface: metadata.federationFeature()!.url.version.compareTo(new FeatureVersion(2, 3)) >= 0, onFields: field => { const type = baseType(field.type!); if (isUnionType(type) || isInterfaceType(type)) { let kind: string = type.kind; kind = kind.slice(0, kind.length - 'Type'.length); throw ERRORS.KEY_FIELDS_SELECT_INVALID_TYPE.err( `field "${field.coordinate}" is a ${kind} type which is not allowed in @key`, ); } } }); // Note that we currently reject @requires where a leaf field of the selection is not external, // because if it's provided by the current subgraph, why "requires" it? That said, it's not 100% // nonsensical if you wanted a local field to be part of the subgraph fetch even if it's not // truly queried _for some reason_. But it's unclear such reasons exists, so for now we prefer // rejecting it as it also make it less likely user misunderstand what @requires actually do. // But we could consider lifting that limitation if users comes with a good rational for allowing // it. validateAllFieldSet>({ definition: metadata.requiresDirective(), targetTypeExtractor: field => field.parent, errorCollector, metadata, allowFieldsWithArguments: true, }); // Note that like for @requires above, we error out if a leaf field of the selection is not // external in a @provides (we pass `false` for the `allowOnNonExternalLeafFields` parameter), // but contrarily to @requires, there is probably no reason to ever change this, as a @provides // of a field already provides is 100% nonsensical. validateAllFieldSet>({ definition: metadata.providesDirective(), targetTypeExtractor: field => { if (metadata.isFieldExternal(field)) { throw ERRORS.EXTERNAL_COLLISION_WITH_ANOTHER_DIRECTIVE.err( `Cannot have both @provides and @external on field "${field.coordinate}"`, { nodes: field.sourceAST }, ); } const type = baseType(field.type!); if (!isCompositeType(type)) { throw ERRORS.PROVIDES_ON_NON_OBJECT_FIELD.err( `Invalid @provides directive on field "${field.coordinate}": field has type "${field.type}" which is not a Composite Type`, { nodes: field.sourceAST }, ); } return type; }, errorCollector, metadata, }); // validate @context and @fromContext const contextDirective = metadata.contextDirective(); const contextToTypeMap = new Map(); for (const application of contextDirective.applications()) { const parent = application.parent; const name = application.arguments().name as string; const match = name.match(/^([A-Za-z]\w*)$/); if (name.includes('_')) { errorCollector.push(ERRORS.CONTEXT_NAME_INVALID.err( `Context name "${name}" may not contain an underscore.`, { nodes: sourceASTs(application) } )); } else if (!match) { errorCollector.push(ERRORS.CONTEXT_NAME_INVALID.err( `Context name "${name}" is invalid. It should have only alphanumeric characters.`, { nodes: sourceASTs(application) } )); } const types = contextToTypeMap.get(name); if (types) { types.push(parent); } else { contextToTypeMap.set(name, [parent]); } } const fromContextDirective = metadata.fromContextDirective(); for (const application of fromContextDirective.applications()) { const { field } = application.arguments(); const { context, selection } = parseContext(field); // error if parent's parent is a directive definition if (application.parent.parent.kind === 'DirectiveDefinition') { errorCollector.push(ERRORS.CONTEXT_NOT_SET.err( `@fromContext argument cannot be used on a directive definition "${application.parent.coordinate}".`, { nodes: sourceASTs(application) } )); continue; } const parent = application.parent as ArgumentDefinition>; // error if parent's parent is an interface if (parent?.parent?.parent?.kind !== 'ObjectType') { errorCollector.push(ERRORS.CONTEXT_NOT_SET.err( `@fromContext argument cannot be used on a field that exists on an abstract type "${application.parent.coordinate}".`, { nodes: sourceASTs(application) } )); continue; } // error if the parent's parent implements an interface containing the field const objectType = parent.parent.parent; for (const implementedInterfaceType of objectType.interfaces()) { const implementedInterfaceField = implementedInterfaceType.field(parent.parent.name); if (implementedInterfaceField) { errorCollector.push(ERRORS.CONTEXT_NOT_SET.err( `@fromContext argument cannot be used on a field implementing an interface field "${implementedInterfaceField.coordinate}".`, { nodes: sourceASTs(application) } )); } } if (parent.defaultValue !== undefined) { errorCollector.push(ERRORS.CONTEXT_NOT_SET.err( `@fromContext arguments may not have a default value: "${parent.coordinate}".`, { nodes: sourceASTs(application) } )); } if (!context || !selection) { errorCollector.push(ERRORS.NO_CONTEXT_IN_SELECTION.err( `@fromContext argument does not reference a context "${field}".`, { nodes: sourceASTs(application) } )); } else { const locations = contextToTypeMap.get(context); if (!locations) { errorCollector.push(ERRORS.CONTEXT_NOT_SET.err( `Context "${context}" is used at location "${parent.coordinate}" but is never set.`, { nodes: sourceASTs(application) } )); } else { validateFieldValue({ context, selection, fromContextParent: parent, setContextLocations: locations, errorCollector, metadata, }); } // validate that there is at least one resolvable key on the type const keyDirective = metadata.keyDirective(); const keyApplications = objectType.appliedDirectivesOf(keyDirective); if (!keyApplications.some(app => app.arguments().resolvable || app.arguments().resolvable === undefined)) { errorCollector.push(ERRORS.CONTEXT_NO_RESOLVABLE_KEY.err( `Object "${objectType.coordinate}" has no resolvable key but has a field with a contextual argument.`, { nodes: sourceASTs(objectType) } )); } } } validateNoExternalOnInterfaceFields(metadata, errorCollector); validateAllExternalFieldsUsed(metadata, errorCollector); validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector); validateInterfaceObjectsAreOnEntities(metadata, errorCollector); // If tag is redefined by the user, make sure the definition is compatible with what we expect const tagDirective = metadata.tagDirective(); if (tagDirective) { const error = tagSpec.checkCompatibleDirective(tagDirective); if (error) { errorCollector.push(error); } } // While @shareable is "repeatable", this is only so one can use it on both a main // type definition _and_ possible other type extensions. But putting 2 @shareable // on the same type definition or field is both useless, and suggest some miscomprehension, // so we reject it with an (hopefully helpful) error message. for (const objectType of schema.objectTypes()) { validateShareableNotRepeatedOnSameDeclaration(objectType, metadata, errorCollector); for (const field of objectType.fields()) { validateShareableNotRepeatedOnSameDeclaration(field, metadata, errorCollector); } } // Additionally, reject using @shareable on an interface field, as that does not actually // make sense. for (const shareableApplication of metadata.shareableDirective().applications()) { const element = shareableApplication.parent; if (element instanceof FieldDefinition && !isObjectType(element.parent)) { errorCollector.push(ERRORS.INVALID_SHAREABLE_USAGE.err( `Invalid use of @shareable on field "${element.coordinate}": only object type fields can be marked with @shareable`, { nodes: sourceASTs(shareableApplication, element.parent) }, )); } } const costDirective = metadata.costDirective(); const listSizeDirective = metadata.listSizeDirective(); // Validate @cost for (const application of costDirective.applications()) { validateCostNotAppliedToInterface(application, errorCollector); } // Validate @listSize for (const application of listSizeDirective.applications()) { const parent = application.parent; assert(parent instanceof FieldDefinition, "@listSize can only be applied to FIELD_DEFINITION"); validateListSizeAppliedToList(application, parent, errorCollector); validateAssumedSizeNotNegative(application, parent, errorCollector); validateSlicingArgumentsAreValidIntegers(application, parent, errorCollector); validateSizedFieldsAreValidLists(application, parent, errorCollector); } // Validate @authenticated, @requireScopes and @policy usage on interfaces and interface objects validateNoAuthenticationOnInterfaces(metadata, errorCollector); return errorCollector; } validationRules(): readonly SDLValidationRule[] { return FEDERATION_VALIDATION_RULES; } onUnknownDirectiveValidationError(schema: Schema, unknownDirectiveName: string, error: GraphQLError): GraphQLError { const metadata = federationMetadata(schema); assert(metadata, `This method should only have been called on a subgraph schema`) if (ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES.includes(unknownDirectiveName)) { // The directive name is "unknown" but it is a default federation directive name. So it means one of a few things // happened: // 1. it's a fed1 schema but the directive is a fed2 only one (only possible case for fed1 schema). // 2. the directive has not been imported at all (so needs to be prefixed for it to work). // 3. the directive has an `import`, but it's been aliased to another name. if (metadata.isFed2Schema()) { const federationFeature = metadata.federationFeature(); assert(federationFeature, 'Fed2 subgraph _must_ link to the federation feature') const directiveNameInSchema = federationFeature.directiveNameInSchema(unknownDirectiveName); if (directiveNameInSchema.startsWith(federationFeature.nameInSchema + '__')) { // There is no import for that directive return withModifiedErrorMessage( error, `${error.message} If you meant the "@${unknownDirectiveName}" federation directive, you should use fully-qualified name "@${directiveNameInSchema}" or add "@${unknownDirectiveName}" to the \`import\` argument of the @link to the federation specification.` ); } else { // There's an import, but it's renamed return withModifiedErrorMessage( error, `${error.message} If you meant the "@${unknownDirectiveName}" federation directive, you should use "@${directiveNameInSchema}" as it is imported under that name in the @link to the federation specification of this schema.` ); } } else { return withModifiedErrorMessage( error, `${error.message} If you meant the "@${unknownDirectiveName}" federation 2 directive, note that this schema is a federation 1 schema. To be a federation 2 schema, it needs to @link to the federation specifcation v2.` ); } } else if (!metadata.isFed2Schema()) { // We could get here in the case where a fed1 schema has tried to use a fed2 directive but mispelled it. const suggestions = suggestionList(unknownDirectiveName, ALL_DEFAULT_FEDERATION_DIRECTIVE_NAMES); if (suggestions.length > 0) { return withModifiedErrorMessage( error, `${error.message}${didYouMean(suggestions.map((s) => '@' + s))} If so, note that ${suggestions.length === 1 ? 'it is a federation 2 directive' : 'they are federation 2 directives'} but this schema is a federation 1 one. To be a federation 2 schema, it needs to @link to the federation specifcation v2.` ); } } return error; } applyDirectivesAfterParsing() { return true; } } function findUnusedNamedForLinkDirective(schema: Schema): string | undefined { if (!schema.directive(linkSpec.url.name)) { return undefined; } // The schema already defines a directive named `@link` so we need to use an alias. // To keep it simple, we add a number in the end (so we try `@link1`, and if that's taken `@link2`, ...) const baseName = linkSpec.url.name; const n = 1; for (;;) { const candidate = baseName + n; if (!schema.directive(candidate)) { return candidate; } } } export function setSchemaAsFed2Subgraph(schema: Schema, useLatest: boolean = false) { let core = schema.coreFeatures; let spec: CoreSpecDefinition; if (core) { spec = core.coreDefinition; // We don't accept pre-1.0 @core: this avoid having to care about what the name // of the argument below is, and why would be bother? assert(spec.url.version.satisfies(linkSpec.version), `Fed2 schema must use @link with version >= 1.0, but schema uses ${spec.url}`); } else { const alias = findUnusedNamedForLinkDirective(schema); const errors = linkSpec.addToSchema(schema, alias); if (errors.length > 0) { throw ErrGraphQLValidationFailed(errors); } spec = linkSpec; core = schema.coreFeatures; assert(core, 'Schema should now be a core schema'); } const fedSpec = useLatest ? latestFederationSpec : autoExpandedFederationSpec; assert(!core.getByIdentity(fedSpec.identity), 'Schema already set as a federation subgraph'); schema.schemaDefinition.applyDirective( core.coreItself.nameInSchema, { // note that there is a mismatch between url and directives that are imported. This is because // we want to maintain backward compatibility for those who have already upgraded and we had been upgrading the url to // latest, but we never automatically import directives that exist past 2.4 url: fedSpec.url.toString(), import: autoExpandedFederationSpec.directiveSpecs().map((spec) => `@${spec.name}`), } ); const errors = completeSubgraphSchema(schema); if (errors.length > 0) { throw ErrGraphQLValidationFailed(errors); } } // This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match // subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ... export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.14", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@context", "@fromContext", "@cost", "@listSize", "@cacheTag"])'; // This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests. export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.14", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])'; // This is the federation @link for tests that go through the SchemaUpgrader. export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])'; /** * Given a document that is assumed to _not_ be a fed2 schema (it does not have a `@link` to the federation spec), * returns an equivalent document that `@link` to the last known federation spec. * * @param document - the document to "augment". * @param options.addAsSchemaExtension - defines whether the added `@link` is added as a schema extension (`extend schema`) or * added to the schema definition. Defaults to `true` (added as an extension), as this mimics what we tends to write manually. * @param options.includeAllImports - defines whether we should auto import ALL latest federation v2 directive definitions or include * only limited set of directives (i.e. federation v2.4 definitions) */ export function asFed2SubgraphDocument(document: DocumentNode, options?: { addAsSchemaExtension?: boolean, includeAllImports?: boolean }): DocumentNode { const importedDirectives = options?.includeAllImports ? latestFederationSpec.directiveSpecs() : autoExpandedFederationSpec.directiveSpecs(); const directiveToAdd: ConstDirectiveNode = ({ kind: Kind.DIRECTIVE, name: { kind: Kind.NAME, value: linkDirectiveDefaultName }, arguments: [ { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: 'url' }, value: { kind: Kind.STRING, value: latestFederationSpec.url.toString() } }, { kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: 'import' }, value: { kind: Kind.LIST, values: importedDirectives.map((spec) => ({ kind: Kind.STRING, value: `@${spec.name}` })) } } ] }); if (options?.addAsSchemaExtension ?? true) { return { kind: Kind.DOCUMENT, loc: document.loc, definitions: document.definitions.concat({ kind: Kind.SCHEMA_EXTENSION, directives: [directiveToAdd] }), } } // We can't add a new schema definition if it already exists. If it doesn't we need to know if there is a mutation type or // not. const existingSchemaDefinition = document.definitions.find((d): d is SchemaDefinitionNode => d.kind == Kind.SCHEMA_DEFINITION); if (existingSchemaDefinition) { return { kind: Kind.DOCUMENT, loc: document.loc, definitions: document.definitions.filter((d) => d !== existingSchemaDefinition).concat([{ ...existingSchemaDefinition, directives: [directiveToAdd].concat(existingSchemaDefinition.directives ?? []), }]), } } else { const hasMutation = document.definitions.some((d) => d.kind === Kind.OBJECT_TYPE_DEFINITION && d.name.value === 'Mutation'); const makeOpType = (opType: OperationTypeNode, name: string): OperationTypeDefinitionNode => ({ kind: Kind.OPERATION_TYPE_DEFINITION, operation: opType, type: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: name, } }, }); return { kind: Kind.DOCUMENT, loc: document.loc, definitions: document.definitions.concat({ kind: Kind.SCHEMA_DEFINITION, directives: [directiveToAdd], operationTypes: [ makeOpType(OperationTypeNode.QUERY, 'Query') ].concat(hasMutation ? makeOpType(OperationTypeNode.MUTATION, 'Mutation') : []), }), } } } export function printSubgraphNames(names: string[]): string { return printHumanReadableList( names.map(n => `"${n}"`), { prefix: 'subgraph', prefixPlural: 'subgraphs', } ); } export function federationMetadata(schema: Schema): FederationMetadata | undefined { return (schema as any)['_federationMetadata']; } export function isFederationSubgraphSchema(schema: Schema): boolean { return !!federationMetadata(schema); } export function isFederationField(field: FieldDefinition): boolean { if (field.parent === field.schema().schemaDefinition.root("query")?.type) { return FEDERATION_OPERATION_FIELDS.includes(field.name); } return false; } export function isEntityType(type: NamedType): boolean { if (!isObjectType(type) && !isInterfaceType(type)) { return false; } const metadata = federationMetadata(type.schema()); return !!metadata && type.hasAppliedDirective(metadata.keyDirective()); } export function isInterfaceObjectType(type: NamedType): boolean { if (!isObjectType(type)) { return false; } const metadata = federationMetadata(type.schema()); return !!metadata && metadata.isInterfaceObjectType(type); } export function buildSubgraph( name: string, url: string, source: DocumentNode | string, withRootTypeRenaming: boolean = true, ): Subgraph { const buildOptions = { blueprint: new FederationBlueprint(withRootTypeRenaming), validate: false, }; let subgraph: Subgraph; try { const schema = typeof source === 'string' ? buildSchema(new Source(source, name), buildOptions) : buildSchemaFromAST(source, buildOptions) subgraph = new Subgraph(name, url, schema); } catch (e) { if (e instanceof GraphQLError && name !== FEDERATION_UNNAMED_SUBGRAPH_NAME) { throw addSubgraphToError(e, name, ERRORS.INVALID_GRAPHQL); } else { throw e; } } return subgraph.validate(); } export function newEmptyFederation2Schema(config?: SchemaConfig): Schema { const schema = new Schema(new FederationBlueprint(true), config); setSchemaAsFed2Subgraph(schema, true); return schema; } function completeSubgraphSchema(schema: Schema): GraphQLError[] { const coreFeatures = schema.coreFeatures; if (coreFeatures) { const fedFeature = coreFeatures.getByIdentity(federationIdentity); if (fedFeature) { return completeFed2SubgraphSchema(schema); } else { return completeFed1SubgraphSchema(schema); } } else { const fedLink = schema.schemaDefinition.appliedDirectivesOf(linkDirectiveDefaultName).find(isFedSpecLinkDirective); if (fedLink) { const errors = linkSpec.addToSchema(schema); if (errors.length > 0) { return errors; } return completeFed2SubgraphSchema(schema); } else { return completeFed1SubgraphSchema(schema); } } } function isFedSpecLinkDirective(directive: Directive): directive is Directive { const args = directive.arguments(); return directive.name === linkDirectiveDefaultName && args['url'] && (args['url'] as string).startsWith(federationIdentity); } function completeFed1SubgraphSchema(schema: Schema): GraphQLError[] { // We special case @key, @requires and @provides because we've seen existing user schema where those // have been defined in an invalid way, but in a way that fed1 wasn't rejecting. So for convenience, // if we detect one of those case, we just remove the definition and let the code afteward add the // proper definition back. // Note that, in a perfect world, we'd do this within the `SchemaUpgrader`. But the way the code // is organised, this method is called before we reach the `SchemaUpgrader`, and it doesn't seem // worth refactoring things drastically for that minor convenience. for (const name of [FederationDirectiveName.KEY, FederationDirectiveName.PROVIDES, FederationDirectiveName.REQUIRES]) { const directive = schema.directive(name); if (!directive) { continue; } // We shouldn't have applications at the time of this writing because `completeSubgraphSchema`, which calls this, // is only called: // 1. during schema parsing, by `FederationBluePrint.onDirectiveDefinitionAndSchemaParsed`, and that is called // before we process any directive applications. // 2. by `setSchemaAsFed2Subgraph`, but as the name imply, this trickles to `completeFed2SubgraphSchema`, not // this one method. // In other words, there is currently no way to create a full fed1 schema first, and get that method called // second. If that changes (no real reason but...), we'd have to modify this because when we remove the // definition to re-add the "correct" version, we'd have to re-attach existing applications (doable but not // done). This assert is so we notice it quickly if that ever happens (again, unlikely, because fed1 schema // is a backward compatibility thing and there is no reason to expand that too much in the future). assert(directive.applications().size === 0, `${directive} shouldn't have had validation at that places`); // The patterns we recognize and "correct" (by essentially ignoring the definition) // are: // 1. if the definition has no arguments at all. // 2. if the `fields` argument is declared as nullable. // 3. if the `fields` argument type is named "FieldSet" instead of "_FieldSet". // // Note that they all correspong to things we've seen in use schema. const fieldType = directive.argument('fields')?.type?.toString(); // Note that to be on the safe side, we check that `fields` is the only argument. That's // because while fed2 accepts the optional `resolvable` arg for @key, fed1 only ever // accepted that one argument for all those directives. But if the use had definited // more arguments _and_ provided value for such extra argument in some applications, // us removing the definition would create validation errors that would be hard to // understand for the user. const fieldTypeIsWrongInKnownWays = !!fieldType && directive.arguments().length === 1 && (fieldType === 'String' || fieldType === '_FieldSet' || fieldType === 'FieldSet'); if (directive.arguments().length === 0 || fieldTypeIsWrongInKnownWays) { directive.remove(); } } const errors = FEDERATION1_TYPES.map((spec) => spec.checkOrAdd(schema, FAKE_FED1_CORE_FEATURE_TO_RENAME_TYPES)) .concat(FEDERATION1_DIRECTIVES.map((spec) => spec.checkOrAdd(schema))) .flat(); return errors.length === 0 ? expandKnownFeatures(schema) : errors; } function completeFed2SubgraphSchema(schema: Schema): GraphQLError[] { const coreFeatures = schema.coreFeatures; assert(coreFeatures, 'This method should not have been called on a non-core schema'); const fedFeature = coreFeatures.getByIdentity(federationIdentity); assert(fedFeature, 'This method should not have been called on a schema with no @link for federation'); const spec = FEDERATION_VERSIONS.find(fedFeature.url.version); if (!spec) { return [ERRORS.UNKNOWN_FEDERATION_LINK_VERSION.err( `Invalid version ${fedFeature.url.version} for the federation feature in @link directive on schema`, { nodes: fedFeature.directive.sourceAST }, )]; } const errors = spec.addElementsToSchema(schema); return errors.length === 0 ? expandKnownFeatures(schema) : errors; } function expandKnownFeatures(schema: Schema): GraphQLError[] { const coreFeatures = schema.coreFeatures; if (!coreFeatures) { return []; } let errors: GraphQLError[] = []; for (const feature of coreFeatures.allFeatures()) { // We should already have dealt with the core/link spec and federation at this point. Also, we shouldn't have the `join` spec in subgraphs, // but some tests play with the idea and currently the join spec is implemented in a way that is not idempotent (it doesn't use // `DirectiveSpecification.checkAndAdd`; we should clean it up at some point, but not exactly urgent). if (feature === coreFeatures.coreItself || feature.url.identity === federationIdentity || feature.url.identity === joinIdentity) { continue; } const spec = coreFeatureDefinitionIfKnown(feature.url); if (!spec) { continue; } errors = errors.concat(spec.addElementsToSchema(schema)); } return errors; } export function parseFieldSetArgument({ parentType, directive, fieldAccessor, validate, decorateValidationErrors = true, normalize = false, }: { parentType: CompositeType, directive: Directive, {fields: any}>, fieldAccessor?: (type: CompositeType, fieldName: string) => FieldDefinition | undefined, validate?: boolean, decorateValidationErrors?: boolean, normalize?: boolean, }): SelectionSet { try { const selectionSet = parseSelectionSet({ parentType, source: validateFieldSetValue(directive), fieldAccessor, validate, }); if (validate ?? true) { selectionSet.forEachElement((elt) => { if (elt.kind === 'Field' && elt.alias) { // Note that this will be caught by the surrounding catch and "decorated". throw new GraphQLError(`Cannot use alias "${elt.alias}" in "${elt}": aliases are not currently supported in @${directive.name}`); } }); } return normalize ? selectionSet.normalize({ parentType, recursive: true }) : selectionSet; } catch (e) { if (!(e instanceof GraphQLError) || !decorateValidationErrors) { throw e; } throw handleFieldSetValidationError( directive, e, (msg: string) => { if (msg.startsWith('Cannot query field')) { if (msg.endsWith('.')) { msg = msg.slice(0, msg.length - 1); } if (directive.name === FederationDirectiveName.KEY) { msg = msg + ' (the field should either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).'; } else { msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).'; } } return msg; }, ); } } export function collectTargetFields({ parentType, directive, includeInterfaceFieldsImplementations, validate = true, }: { parentType: CompositeType, directive: Directive, {fields: any}>, includeInterfaceFieldsImplementations: boolean, validate?: boolean, }): FieldDefinition[] { const fields: FieldDefinition[] = []; try { parseFieldSetArgument({ parentType, directive, fieldAccessor: (t, f) => { const field = t.field(f); if (field) { fields.push(field); if (includeInterfaceFieldsImplementations && isInterfaceType(t)) { for (const implType of t.possibleRuntimeTypes()) { const implField = implType.field(f); if (implField) { fields.push(implField); } } } } return field; }, validate, }); } catch (e) { // If we explicitly requested no validation, then we shouldn't throw a (graphQL) error, but if we do, we swallow it // (returning a partial result, but we assume it is fine). const isGraphQLError = errorCauses(e) !== undefined if (!isGraphQLError || validate) { throw e; } } return fields; } function validateFieldSetValue(directive: Directive, {fields: any}>): string { const fields = directive.arguments().fields; const nodes = directive.sourceAST; if (typeof fields !== 'string') { throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err( `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`, { nodes }, ); } // While validating if the field is a string will work in most cases, this will not catch the case where the field argument was // unquoted but parsed as an enum value (see federation/issues/850 in particular). So if we have the AST (which we will usually // have in practice), use that to check that the argument was truly a string. if (nodes && nodes.kind === 'Directive') { for (const argNode of nodes.arguments ?? []) { if (argNode.name.value === 'fields') { if (argNode.value.kind !== 'StringValue') { throw ERROR_CATEGORIES.DIRECTIVE_INVALID_FIELDS_TYPE.get(directive.name).err( `Invalid value for argument "${directive.definition!.argument('fields')!.name}": must be a string.`, { nodes }, ); } break; } } } return fields; } export interface ServiceDefinition { typeDefs: DocumentNode; name: string; url?: string; } export function subgraphsFromServiceList(serviceList: ServiceDefinition[]): Subgraphs | GraphQLError[] { let errors: GraphQLError[] = []; const subgraphs = new Subgraphs(); for (const service of serviceList) { try { subgraphs.add(buildSubgraph(service.name, service.url ?? '', service.typeDefs)); } catch (e) { const causes = errorCauses(e); if (causes) { errors = errors.concat(causes); } else { throw e; } } } return errors.length === 0 ? subgraphs : errors; } // Simple wrapper around a Subgraph[] that ensures that 1) we never mistakenly get 2 subgraph with the same name, // 2) keep the subgraphs sorted by name (makes iteration more predictable). It also allow convenient access to // a subgraph by name so behave like a map in most ways (but with the previously mentioned benefits). export class Subgraphs { private readonly subgraphs = new OrderedMap(); add(subgraph: Subgraph): Subgraph { if (this.subgraphs.has(subgraph.name)) { throw new Error(`A subgraph named ${subgraph.name} already exists` + (subgraph.url ? ` (with url '${subgraph.url}')` : '')); } this.subgraphs.add(subgraph.name, subgraph); return subgraph; } get(name: string): Subgraph | undefined { return this.subgraphs.get(name); } size(): number { return this.subgraphs.size; } names(): readonly string[] { return this.subgraphs.keys(); } values(): readonly Subgraph[] { return this.subgraphs.values(); } *[Symbol.iterator]() { for (const subgraph of this.subgraphs) { yield subgraph; } } validate(): GraphQLError[] | undefined { let errors: GraphQLError[] = []; for (const subgraph of this.values()) { try { subgraph.validate(); } catch (e) { const causes = errorCauses(e); if (!causes) { throw e; } errors = errors.concat(causes); } } return errors.length === 0 ? undefined : errors; } toString(): string { return '[' + this.subgraphs.keys().join(', ') + ']' } } export const anyTypeSpec = createScalarTypeSpecification({ name: '_Any' }); export const serviceTypeSpec = createObjectTypeSpecification({ name: '_Service', fieldsFct: (schema) => [{ name: 'sdl', type: schema.stringType() }], }); export const entityTypeSpec = createUnionTypeSpecification({ name: '_Entity', membersFct: (schema) => { // Please note that `_Entity` cannot use "interface entities" since interface types cannot be in unions. // It is ok in practice because _Entity is only use as return type for `_entities`, and even when interfaces // are involve, the result of an `_entities` call will always be an object type anyway, and since we force // all implementations of an interface entity to be entity themselves in a subgraph, we're fine. return schema.objectTypes().filter(isEntityType).map((t) => t.name); }, }); export const FEDERATION_OPERATION_TYPES = [ anyTypeSpec, serviceTypeSpec, entityTypeSpec ]; export const serviceFieldName = '_service'; export const entitiesFieldName = '_entities'; export const FEDERATION_OPERATION_FIELDS: readonly string[] = [ serviceFieldName, entitiesFieldName ]; export class Subgraph { constructor( readonly name: string, readonly url: string, readonly schema: Schema, ) { if (name === FEDERATION_RESERVED_SUBGRAPH_NAME) { throw ERRORS.INVALID_SUBGRAPH_NAME.err(`Invalid name ${FEDERATION_RESERVED_SUBGRAPH_NAME} for a subgraph: this name is reserved`); } } metadata(): FederationMetadata { const metadata = federationMetadata(this.schema); assert(metadata, 'The subgraph schema should have built with the federation built-ins.'); return metadata; } isFed2Subgraph(): boolean { return this.metadata().isFed2Schema(); } // Adds the _entities and _service fields to the root query type. private addFederationOperations() { const metadata = this.metadata(); for (const type of FEDERATION_OPERATION_TYPES) { type.checkOrAdd(this.schema); } const queryRoot = this.schema.schemaDefinition.root("query"); const queryType = queryRoot ? queryRoot.type : this.schema.addType(new ObjectType("Query")); const entityField = queryType.field(entitiesFieldName); const entityType = metadata.entityType(); if (entityType) { const entityFieldType = new NonNullType(new ListType(entityType)); if (!entityField) { queryType.addField(entitiesFieldName, entityFieldType) .addArgument('representations', new NonNullType(new ListType(new NonNullType(metadata.anyType())))); } else if (!entityField.type) { // This can happen when the schema had an empty redefinition of _Entity as we've removed it in // that clear and that would have clear the type of the correspond field. Let's re-populate it // in that case. entityField.type = entityType; } } else if (entityField) { entityField.remove(); } if (!queryType.field(serviceFieldName)) { queryType.addField(serviceFieldName, new NonNullType(metadata.serviceType())); } } /** * Same as `Schema.assumeValid`. Use carefully. */ assumeValid(): Subgraph { this.addFederationOperations(); this.schema.assumeValid(); return this; } validate(): Subgraph { try { this.addFederationOperations(); this.schema.validate(); return this; } catch (e) { if (e instanceof GraphQLError) { // Note that `addSubgraphToError` only adds the provided code if the original error // didn't have one, and the only one that will not have a code are GraphQL errors // (since we assign specific codes to the federation errors). throw addSubgraphToError(e, this.name, ERRORS.INVALID_GRAPHQL); } else { throw e; } } } private isPrintedDirective(d: DirectiveDefinition): boolean { if (this.metadata().allFederationDirectives().includes(d)) { return false; } const core = this.schema.coreFeatures; return !core || core.sourceFeature(d)?.feature.url.identity !== linkIdentity; } private isPrintedType(t: NamedType): boolean { if (this.metadata().allFederationTypes().includes(t)) { return false; } // If the query type only have our federation specific fields, then that (almost surely) means the original subgraph // had no Query type and so we save printing it. if (isObjectType(t) && t.isQueryRootType() && t.fields().filter((f) => !isFederationField(f)).length === 0) { return false; } const core = this.schema.coreFeatures; return !core || core.sourceFeature(t)?.feature.url.identity !== linkIdentity; } private isPrintedDirectiveApplication(d: Directive): boolean { // We print almost all directive application, but the one we skip is the `@link` to the link spec itself. // The reason is that it is one of the things that usually not provided by users but is instead auto-added // and so this keep the output a tad "cleaner". // Do note that it is only auto-added if it uses the `@link` name. If it is renamed, we need to include // the application (and more generally, if there is more argument set than just the url, we print // the directive to make sure we're not hidding something relevant). if (!this.schema.coreFeatures || d.name !== linkSpec.url.name) { return true; } const args = d.arguments(); let urlArg: FeatureUrl | undefined = undefined; if ('url' in args) { try { urlArg = FeatureUrl.parse(args['url']); } catch (e) { // ignored on purpose: if the 'url' arg don't parse properly as a Feature url, then `urlArg` will // be `undefined` which we want. } } const isDefaultLinkToLink = urlArg?.identity === linkIdentity && Object.keys(args).length === 1; return !isDefaultLinkToLink; } /** * Returns a representation of the subgraph without any auto-imported directive definitions or "federation private" * types and fiels (`_service` et al.). * * In other words, this will correspond to what a user would usually write. * * Note that if one just want a representation of the full schema, then it can simply call `printSchema(this.schema)`. */ toString(basePrintOptions: PrintOptions = defaultPrintOptions) { return printSchema( this.schema, { ...basePrintOptions, directiveDefinitionFilter: (d) => this.isPrintedDirective(d), typeFilter: (t) => this.isPrintedType(t), fieldFilter: (f) => !isFederationField(f), directiveApplicationFilter: (d) => this.isPrintedDirectiveApplication(d), } ); } } export type SubgraphASTNode = ASTNode & { subgraph: string }; export function addSubgraphToASTNode(node: ASTNode, subgraph: string): SubgraphASTNode { // We won't override a existing subgraph info: it's not like the subgraph an ASTNode can come // from can ever change and this allow the provided to act as a "default" rather than a // hard setter, which is convenient in `addSubgraphToError` below if some of the AST of // the provided error already have a subgraph "origin". if ('subgraph' in (node as any)) { return node as SubgraphASTNode; } return { ...node, subgraph }; } export function addSubgraphToError(e: GraphQLError, subgraphName: string, errorCode?: ErrorCodeDefinition): GraphQLError { const updatedCauses = errorCauses(e)!.map(cause => { const message = `[${subgraphName}] ${cause.message}`; const nodes = cause.nodes ? cause.nodes.map(node => addSubgraphToASTNode(node, subgraphName)) : undefined; const code = errorCodeDef(cause) ?? errorCode; const options: GraphQLErrorOptions = { ...extractGraphQLErrorOptions(cause), nodes, originalError: cause, }; return code ? code.err(message, options) : new GraphQLError(message, options); }); return updatedCauses.length === 1 ? updatedCauses[0] : ErrGraphQLValidationFailed(updatedCauses); } class ExternalTester { private readonly fakeExternalFields = new Set(); private readonly providedFields = new Set(); private readonly externalDirective: DirectiveDefinition<{}>; private readonly externalFieldsOnType = new Set(); constructor(readonly schema: Schema, private readonly isFed2Schema: boolean) { this.externalDirective = this.metadata().externalDirective(); this.collectFakeExternals(); this.collectProvidedFields(); this.collectExternalsOnType(); } private metadata(): FederationMetadata { const metadata = federationMetadata(this.schema); assert(metadata, 'Schema should be a subgraphs schema'); return metadata; } private collectFakeExternals() { const metadata = this.metadata(); const extendsDirective = metadata.extendsDirective(); for (const key of metadata.keyDirective().applications()) { const parentType = key.parent as CompositeType; if (!(key.ofExtension() || parentType.hasAppliedDirective(extendsDirective))) { continue; } collectTargetFields({ parentType, directive: key as Directive, includeInterfaceFieldsImplementations: false, validate: false, }).filter((field) => field.hasAppliedDirective(this.externalDirective)) .forEach((field) => this.fakeExternalFields.add(field.coordinate)); } } private collectProvidedFields() { for (const provides of this.metadata().providesDirective().applications()) { const parent = provides.parent as FieldDefinition; const parentType = baseType(parent.type!); // If `parentType` is not a composite, that means an invalid @provides, but we ignore such errors // for now (also why we pass 'validate: false'). Proper errors will be thrown later during validation. if (isCompositeType(parentType)) { collectTargetFields({ parentType, directive: provides as Directive, includeInterfaceFieldsImplementations: true, validate: false, }).forEach((f) => this.providedFields.add(f.coordinate)); } } } private collectExternalsOnType() { // We do not collect @external on types for fed1 schema since those will be discarded by the schema upgrader. // The schema upgrader, through calls to `isExternal`, relies on the populated `externalFieldsOnType` object to // inform when @shareable should be automatically added. In the fed1 case, if the map is populated then @shareable won't // be added in places where it should have. if (!this.isFed2Schema) { return; } for (const type of this.schema.objectTypes()) { if (type.hasAppliedDirective(this.externalDirective)) { for (const field of type.fields()) { this.externalFieldsOnType.add(field.coordinate); } } } } isExternal(field: FieldDefinition | InputFieldDefinition) { return (field.hasAppliedDirective(this.externalDirective) || this.externalFieldsOnType.has(field.coordinate)) && !this.isFakeExternal(field); } isFakeExternal(field: FieldDefinition | InputFieldDefinition) { return this.fakeExternalFields.has(field.coordinate); } selectsAnyExternalField(selectionSet: SelectionSet): boolean { for (const selection of selectionSet.selections()) { if (selection.kind === 'FieldSelection' && this.isExternal(selection.element.definition)) { return true; } if (selection.selectionSet) { if (this.selectsAnyExternalField(selection.selectionSet)) { return true; } } } return false; } isPartiallyExternal(field: FieldDefinition | InputFieldDefinition) { return this.isExternal(field) && this.providedFields.has(field.coordinate); } isFullyExternal(field: FieldDefinition | InputFieldDefinition) { return this.isExternal(field) && !this.providedFields.has(field.coordinate); } } export type ProvidesOrRequiresApplication = Directive, {fields: any}> /* * It makes no sense to have a @provides/@requires on a non-external leaf field, and we usually reject it during schema * validation but this method allows to remove those for: * 1. when we extract subgraphs from a fed 1 supergraph, where such validation hadn't been run. * 2. for the fed 1 -> fed 2 upgader code. * * The reason we do this (and generally reject it) is that such provides/requires have a negative impact on later query * planning, because it sometimes make us to try type-exploding some interfaces unecessarily. Besides, if a use add * something useless, there is a change it hasn't fully understood something, and warning it about that fact through * an error is more helpful. */ export function removeInactiveProvidesAndRequires( schema: Schema, onModified: (field: FieldDefinition, original: ProvidesOrRequiresApplication, updated?: ProvidesOrRequiresApplication) => void = () => {}, ) { const metadata = federationMetadata(schema); if (!metadata) { return; } const providesDirective = metadata.providesDirective(); const requiresDirective = metadata.requiresDirective(); for (const type of schema.types()) { if (!isObjectType(type) && !isInterfaceType(type)) { continue; } for (const field of type.fields()) { const fieldBaseType = baseType(field.type!) as CompositeType; removeInactiveApplications(providesDirective, field, fieldBaseType, onModified); removeInactiveApplications(requiresDirective, field, type, onModified); } } } function removeInactiveApplications( directiveDefinition: DirectiveDefinition<{fields: any}>, field: FieldDefinition, parentType: CompositeType, onModified: (field: FieldDefinition, original: ProvidesOrRequiresApplication, updated?: ProvidesOrRequiresApplication) => void ) { for (const application of field.appliedDirectivesOf(directiveDefinition)) { let selection; try { selection = parseFieldSetArgument({parentType, directive: application}); } catch (e) { // This method is sometimes called on federation directives that haven't been validated, and so parsing the // `fields` argument may throw. In that case, we just silently ignore that particular directive application: // it's not the job of this method to do validation, but we will always validate things in other places // when needed so whatever error that directive has will be caught in a more appropriate place. continue; } if (selectsNonExternalLeafField(selection)) { application.remove(); const updated = withoutNonExternalLeafFields(selection); if (!updated.isEmpty()) { const updatedDirective = field.applyDirective(directiveDefinition, { fields: updated.toString(true, false) }); onModified(field, application, updatedDirective); } else { onModified(field, application); } } } } function isExternalOrHasExternalImplementations(field: FieldDefinition): boolean { const metadata = federationMetadata(field.schema()); if (!metadata) { return false; } if (field.hasAppliedDirective(metadata.externalDirective())) { return true; } const parentType = field.parent; if (isInterfaceType(parentType)) { for (const implem of parentType.possibleRuntimeTypes()) { const fieldInImplem = implem.field(field.name); if (fieldInImplem && fieldInImplem.hasAppliedDirective(metadata.externalDirective())) { return true; } } } return false; } function selectsNonExternalLeafField(selection: SelectionSet): boolean { return selection.selections().some(s => { if (s.kind === 'FieldSelection') { // If it's external, we're good and don't need to recurse. if (isExternalOrHasExternalImplementations(s.element.definition)) { return false; } // Otherwise, we select a non-external if it's a leaf, or the sub-selection does. return !s.selectionSet || selectsNonExternalLeafField(s.selectionSet); } else { return selectsNonExternalLeafField(s.selectionSet); } }); } function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet { return selectionSet.lazyMap((selection) => { if (selection.kind === 'FieldSelection') { if (isExternalOrHasExternalImplementations(selection.element.definition)) { // That field is external, so we can add the selection back entirely. return selection; } } if (selection.selectionSet) { // Note that for fragments this will always be true (and we just recurse), while // for fields, we'll only get here if the field is not external, and so // we want to add the selection only if it's not a leaf and even then, only // the part where we've recursed. const updated = withoutNonExternalLeafFields(selection.selectionSet); if (!updated.isEmpty()) { return selection.withUpdatedSelectionSet(updated); } } // We skip that selection. return undefined; }); } function validateNoAuthenticationOnInterfaces(metadata: FederationMetadata, errorCollector: GraphQLError[]) { const authenticatedDirective = metadata.authenticatedDirective(); const requiresScopesDirective = metadata.requiresScopesDirective(); const policyDirective = metadata.policyDirective(); [authenticatedDirective, requiresScopesDirective, policyDirective].forEach((directive) => { for (const application of directive.applications()) { const element: SchemaElement = application.parent; if ( // Is it applied on interface or interface object types? (isElementNamedType(element) && (isInterfaceType(element) || isInterfaceObjectType(element)) ) || // Is it applied on interface fields? (isFieldDefinition(element) && isInterfaceType(element.parent)) ) { let kind = ''; switch (element.kind) { case 'FieldDefinition': kind = 'field'; break; case 'InterfaceType': kind = 'interface'; break; case 'ObjectType': kind = 'interface object'; break; } errorCollector.push(ERRORS.AUTH_REQUIREMENTS_APPLIED_ON_INTERFACE.err( `Invalid use of @${directive.name} on ${kind} "${element.coordinate}": @${directive.name} cannot be applied on interfaces, interface fields and interface objects`, {nodes: sourceASTs(application, element.parent)}, )); } } }); }