UNPKG

@apollo/federation-internals

Version:
1,118 lines (1,034 loc) 54.9 kB
import { baseType, CompositeType, copyDirectiveDefinitionToSchema, Directive, FieldDefinition, InputFieldDefinition, InputObjectType, InterfaceType, isExecutableDirectiveLocation, isEnumType, isInterfaceType, isObjectType, isUnionType, ListType, NamedType, newNamedType, NonNullType, NullableType, ObjectType, Schema, Type, EnumType, UnionType, } from "./definitions"; import { newEmptyFederation2Schema, parseFieldSetArgument, removeInactiveProvidesAndRequires, } from "./federation"; import { CoreSpecDefinition, FeatureVersion } from "./specs/coreSpec"; import { JoinFieldDirectiveArguments, JoinSpecDefinition, JoinTypeDirectiveArguments } from "./specs/joinSpec"; import { FederationMetadata, Subgraph, Subgraphs } from "./federation"; import { assert } from "./utils"; import { validateSupergraph } from "./supergraphs"; import { builtTypeReference } from "./buildSchema"; import { isSubtype } from "./types"; import { printSchema } from "./print"; import { parseSelectionSet } from "./operations"; import fs from 'fs'; import path from 'path'; import { validateStringContainsBoolean } from "./utils"; import { ContextSpecDefinition, CostSpecDefinition, SchemaElement, errorCauses, isFederationDirectiveDefinedInSchema, printErrors } from "."; function filteredTypes( supergraph: Schema, joinSpec: JoinSpecDefinition, coreSpec: CoreSpecDefinition ): NamedType[] { // Note: we skip coreSpec to avoid having core__Purpose since we don't create core schema subgraph. // But once we support core schema subgraphs and start shipping federation core features, we may need // to revisit this. return supergraph.types().filter(t => !joinSpec.isSpecType(t) && !coreSpec.isSpecType(t)); } export function extractSubgraphsNamesAndUrlsFromSupergraph(supergraph: Schema): {name: string, url: string}[] { const [_, joinSpec] = validateSupergraph(supergraph); const [subgraphs] = collectEmptySubgraphs(supergraph, joinSpec); return subgraphs.values().map(subgraph => {return { name: subgraph.name, url: subgraph.url }}); } function collectEmptySubgraphs(supergraph: Schema, joinSpec: JoinSpecDefinition): [Subgraphs, Map<string, string>] { const subgraphs = new Subgraphs(); const graphDirective = joinSpec.graphDirective(supergraph); const graphEnum = joinSpec.graphEnum(supergraph); const graphEnumNameToSubgraphName = new Map<string, string>(); for (const value of graphEnum.values) { const graphApplications = value.appliedDirectivesOf(graphDirective); if (!graphApplications.length) { throw new Error(`Value ${value} of join__Graph enum has no @join__graph directive`); } const info = graphApplications[0].arguments(); const subgraph = new Subgraph(info.name, info.url, newEmptyFederation2Schema()); subgraphs.add(subgraph); graphEnumNameToSubgraphName.set(value.name, info.name); } return [subgraphs, graphEnumNameToSubgraphName]; } class SubgraphExtractionError { constructor( readonly originalError: any, readonly subgraph: Subgraph, ) { } } function collectFieldReachableTypesForSubgraph( supergraph: Schema, subgraphName: string, addReachableType: (t: NamedType) => void, fieldInfoInSubgraph: (f: FieldDefinition<any> | InputFieldDefinition, subgraphName: string) => { isInSubgraph: boolean, typesInFederationDirectives: NamedType[] }, typeInfoInSubgraph: (t: NamedType, subgraphName: string) => { isEntityWithKeyInSubgraph: boolean, typesInFederationDirectives: NamedType[] }, ): void { const seenTypes = new Set<string>(); // The types reachable at "top-level" are both the root types, plus any entity type with a key in this subgraph. const stack = supergraph.schemaDefinition.roots().map((root) => root.type as NamedType) for (const type of supergraph.types()) { const { isEntityWithKeyInSubgraph, typesInFederationDirectives } = typeInfoInSubgraph(type, subgraphName); if (isEntityWithKeyInSubgraph) { stack.push(type); } typesInFederationDirectives.forEach((t) => stack.push(t)); } while (stack.length > 0) { const type = stack.pop()!; addReachableType(type); if (seenTypes.has(type.name)) { continue; } seenTypes.add(type.name); switch (type.kind) { // @ts-expect-error: we fall-through to ObjectType for fields and implemented interfaces. case 'InterfaceType': // If an interface if reachable, then all of its implementation are too (a field returning the interface could return any of the // implementation at runtime typically). type.allImplementations().forEach((t) => stack.push(t)); case 'ObjectType': type.interfaces().forEach((t) => stack.push(t)); for (const field of type.fields()) { const { isInSubgraph, typesInFederationDirectives } = fieldInfoInSubgraph(field, subgraphName); if (isInSubgraph) { field.arguments().forEach((arg) => stack.push(baseType(arg.type!))); stack.push(baseType(field.type!)); typesInFederationDirectives.forEach((t) => stack.push(t)); } } break; case 'InputObjectType': for (const field of type.fields()) { const { isInSubgraph, typesInFederationDirectives } = fieldInfoInSubgraph(field, subgraphName); if (isInSubgraph) { stack.push(baseType(field.type!)); typesInFederationDirectives.forEach((t) => stack.push(t)); } } break; case 'UnionType': type.members().forEach((m) => stack.push(m.type)); break; } } for (const directive of supergraph.directives()) { // In fed1 supergraphs, which is the only place this is called, only executable directive from subgraph only ever made // it to the supergraph. Skipping anything else saves us from worrying about supergraph-specific directives too. if (!directive.hasExecutableLocations()) { continue; } directive.arguments().forEach((arg) => stack.push(baseType(arg.type!))); } } function collectFieldReachableTypesForAllSubgraphs( supergraph: Schema, allSubgraphs: readonly string[], fieldInfoInSubgraph: (f: FieldDefinition<any> | InputFieldDefinition, subgraphName: string) => { isInSubgraph: boolean, typesInFederationDirectives: NamedType[] }, typeInfoInSubgraph: (t: NamedType, subgraphName: string) => { isEntityWithKeyInSubgraph: boolean, typesInFederationDirectives: NamedType[] }, ): Map<string, Set<string>> { const reachableTypesBySubgraphs = new Map<string, Set<string>>(); for (const subgraphName of allSubgraphs) { const reachableTypes = new Set<string>(); collectFieldReachableTypesForSubgraph( supergraph, subgraphName, (t) => reachableTypes.add(t.name), fieldInfoInSubgraph, typeInfoInSubgraph, ); reachableTypesBySubgraphs.set(subgraphName, reachableTypes); } return reachableTypesBySubgraphs; } function typesUsedInFederationDirective(fieldSet: string | undefined, parentType: CompositeType): NamedType[] { if (!fieldSet) { return []; } const usedTypes: NamedType[] = []; parseSelectionSet({ parentType, source: fieldSet, fieldAccessor: (type, fieldName) => { const field = type.field(fieldName); if (field) { usedTypes.push(baseType(field.type!)); } return field; }, validate: false, }); return usedTypes; } export function extractSubgraphsFromSupergraph(supergraph: Schema, validateExtractedSubgraphs: boolean = true): [Subgraphs, Map<string, string>] { const [coreFeatures, joinSpec, contextSpec, costSpec] = validateSupergraph(supergraph); const isFed1 = joinSpec.version.equals(new FeatureVersion(0, 1)); try { // We first collect the subgraphs (creating an empty schema that we'll populate next for each). const [subgraphs, graphEnumNameToSubgraphName] = collectEmptySubgraphs(supergraph, joinSpec); const getSubgraph = (application: Directive<any, { graph?: string }>): Subgraph | undefined => { const graph = application.arguments().graph; if (!graph) { return undefined; } const subgraphName = graphEnumNameToSubgraphName.get(graph); assert(subgraphName, () => `Invalid graph name ${graph} found in ${application} on ${application.parent}: does not match a graph defined in the @join__Graph enum`); const subgraph = subgraphs.get(subgraphName); assert(subgraph, 'All subgraphs should have been created by `collectEmptySubgraphs`'); return subgraph; }; const subgraphNameToGraphEnumValue = new Map<string, string>(); for (const [k, v] of graphEnumNameToSubgraphName.entries()) { subgraphNameToGraphEnumValue.set(v, k); } const getSubgraphEnumValue = (subgraphName: string): string => { const enumValue = subgraphNameToGraphEnumValue.get(subgraphName); assert(enumValue, () => `Invalid subgraph name ${subgraphName} found: does not match a subgraph defined in the @join__Graph enum`); return enumValue; } const types = filteredTypes(supergraph, joinSpec, coreFeatures.coreDefinition); const args: ExtractArguments = { supergraph, subgraphs, joinSpec, contextSpec, costSpec, filteredTypes: types, getSubgraph, getSubgraphEnumValue, }; if (isFed1) { extractSubgraphsFromFed1Supergraph(args); } else { extractSubgraphsFromFed2Supergraph(args); } // We're done with the subgraphs, so call validate (which, amongst other things, sets up the _entities query field, which ensures // all entities in all subgraphs are reachable from a query and so are properly included in the "query graph" later). for (const subgraph of subgraphs) { if (validateExtractedSubgraphs) { try { subgraph.validate(); } catch (e) { // This is going to be caught directly by the enclosing try-catch, but this is so we indicate the subgraph having the issue. throw new SubgraphExtractionError(e, subgraph); } } else { subgraph.assumeValid(); } } return [subgraphs, subgraphNameToGraphEnumValue]; } catch (e) { let error = e; let subgraph: Subgraph | undefined = undefined; // We want this catch to capture all errors happening during extraction, but the most common // case is likely going to be fed2 validation that fed1 didn't enforced, and those will be // throw when validating the extracted subgraphs, and n that case we use // `SubgraphExtractionError` to pass the subgraph that errored out, which allows us // to provide a bit more context in those cases. if (e instanceof SubgraphExtractionError) { error = e.originalError; subgraph = e.subgraph; } // There is 2 reasons this could happen: // 1. if the supergraph is a Fed1 one, because fed2 has stricter validations than fed1, this could be due to the supergraph // containing something invalid that fed1 accepted and fed2 didn't (for instance, an invalid `@provides` selection). // 2. otherwise, this would be a bug (because fed1 compatibility excluded, we shouldn't extract invalid subgraphs from valid supergraphs). // We throw essentially the same thing in both cases, but adapt the message slightly. const impacted = subgraph ? `subgraph "${subgraph.name}"` : 'subgraphs'; if (isFed1) { // Note that this could be a bug with the code handling fed1 as well, but it's more helpful to ask users to recompose their subgraphs with fed2 as either // it'll solve the issue and that's good, or we'll hit the other message anyway. const msg = `Error extracting ${impacted} from the supergraph: this might be due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n` + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the problems and, once fixed, generate a correct federation 2 supergraph'; throw new Error(`${msg}.\n\nDetails:\n${errorToString(error)}`); } else { const msg = `Unexpected error extracting ${impacted} from the supergraph: this is either a bug, or the supergraph has been corrupted`; const dumpMsg = subgraph ? '\n\n' + maybeDumpSubgraphSchema(subgraph) : ''; throw new Error(`${msg}.\n\nDetails:\n${errorToString(error)}${dumpMsg}`); } } } type ExtractArguments = { supergraph: Schema, subgraphs: Subgraphs, joinSpec: JoinSpecDefinition, contextSpec: ContextSpecDefinition | undefined, costSpec: CostSpecDefinition | undefined, filteredTypes: NamedType[], getSubgraph: (application: Directive<any, { graph?: string }>) => Subgraph | undefined, getSubgraphEnumValue: (subgraphName: string) => string } type SubgraphTypeInfo<T extends NamedType> = Map<string, { type: T, subgraph: Subgraph }>; type TypeInfo<T extends NamedType> = { type: T, subgraphsInfo: SubgraphTypeInfo<T>, }; type TypesInfo = { objOrItfTypes: TypeInfo<ObjectType | InterfaceType>[], inputObjTypes: TypeInfo<InputObjectType>[], enumTypes: TypeInfo<EnumType>[], unionTypes: TypeInfo<UnionType>[], }; function addAllEmptySubgraphTypes(args: ExtractArguments): TypesInfo { const { supergraph, joinSpec, filteredTypes, getSubgraph, } = args; const typeDirective = joinSpec.typeDirective(supergraph); const objOrItfTypes: TypeInfo<ObjectType | InterfaceType>[] = []; const inputObjTypes: TypeInfo<InputObjectType>[] = []; const enumTypes: TypeInfo<EnumType>[] = []; const unionTypes: TypeInfo<UnionType>[] = []; for (const type of filteredTypes) { const typeApplications = type.appliedDirectivesOf(typeDirective); switch (type.kind) { // See comment in `addEmptyType` for why it actually matters that object and interface are handled together. // (on top of it making sense code-wise since both type behave exactly the same for most of what we're doing here). case 'InterfaceType': case 'ObjectType': objOrItfTypes.push({ type, subgraphsInfo: addEmptyType(type, type.appliedDirectivesOf(typeDirective), args) }); break; case 'InputObjectType': inputObjTypes.push({ type, subgraphsInfo: addEmptyType(type, type.appliedDirectivesOf(typeDirective), args) }); break; case 'EnumType': enumTypes.push({ type, subgraphsInfo: addEmptyType(type, type.appliedDirectivesOf(typeDirective), args) }); break; case 'UnionType': unionTypes.push({ type, subgraphsInfo: addEmptyType(type, type.appliedDirectivesOf(typeDirective), args) }); break; case 'ScalarType': // Scalar are a bit special in that they don't have any sub-component, so we don't track them beyond adding them to the // proper subgraphs. It's also simple because there is no possible key so there is exactly on @join__type application for // each subgraph having the scalar (and most arg cannot be present). for (const application of typeApplications) { const subgraph = getSubgraph(application); assert(subgraph, () => `Should have found the subgraph for ${application}`); const subgraphType = subgraph.schema.addType(newNamedType(type.kind, type.name)); if (args.costSpec) { propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec); } } break; } } return { objOrItfTypes, inputObjTypes, enumTypes, unionTypes, } } function addEmptyType<T extends NamedType>( type: T, typeApplications: Directive<T, JoinTypeDirectiveArguments>[], args: ExtractArguments, ): SubgraphTypeInfo<T> { const { supergraph, getSubgraph, getSubgraphEnumValue } = args; // In fed2, we always mark all types with `@join__type` but making sure. assert(typeApplications.length > 0, `Missing @join__type on ${type}`) const subgraphsInfo: SubgraphTypeInfo<T> = new Map<string, { type: T, subgraph: Subgraph }>(); for (const application of typeApplications) { const { graph, key, extension, resolvable, isInterfaceObject } = application.arguments(); let subgraphInfo = subgraphsInfo.get(graph); if (!subgraphInfo) { const subgraph = getSubgraph(application); assert(subgraph, () => `Should have found the subgraph for ${application}`); const kind = isInterfaceObject ? 'ObjectType' : type.kind; // Note that we have to cast to `T` below. First because going through `type.kind` and `newNamedType` // does not give a `T`. But even if we were to bend the type-system to work for that, there is the // case of interface objects where an interface in the supergraph ends up being an object in the // subgraph. But this is ok because we the object and interface type cases are lumped together (and // this also means we "need" it to be this way). const subgraphType = subgraph.schema.addType(newNamedType(kind, type.name)) as T; if (isInterfaceObject) { subgraphType.applyDirective('interfaceObject'); } subgraphInfo = { type: subgraphType, subgraph }; subgraphsInfo.set(graph, subgraphInfo); } if (key) { const directive = subgraphInfo.type.applyDirective('key', {'fields': key, resolvable}); if (extension) { directive.setOfExtension(subgraphInfo.type.newExtension()); } } } const supergraphContextDirective = args.contextSpec?.contextDirective(supergraph); if (supergraphContextDirective) { const contextApplications = type.appliedDirectivesOf(supergraphContextDirective); // for every application, apply the context directive to the correct subgraph for (const application of contextApplications) { const { name } = application.arguments(); const match = name.match(/^(.*)__([A-Za-z]\w*)$/); const graph = match ? match[1] : undefined; const context = match ? match[2] : undefined; assert(graph, `Invalid context name ${name} found in ${application} on ${application.parent}: does not match the expected pattern`); const subgraphInfo = subgraphsInfo.get(getSubgraphEnumValue(graph)); const contextDirective = subgraphInfo?.subgraph.metadata().contextDirective(); if (subgraphInfo && contextDirective && isFederationDirectiveDefinedInSchema(contextDirective)) { subgraphInfo.type.applyDirective(contextDirective, {name: context}); } } } return subgraphsInfo; } function extractObjOrItfContent(args: ExtractArguments, info: TypeInfo<ObjectType | InterfaceType>[]) { const fieldDirective = args.joinSpec.fieldDirective(args.supergraph); // join_implements was added in join 0.2, and this method does not run for join 0.1, so it should be defined. const implementsDirective = args.joinSpec.implementsDirective(args.supergraph); assert(implementsDirective, '@join__implements should existing for a fed2 supergraph'); for (const { type, subgraphsInfo } of info) { const implementsApplications = type.appliedDirectivesOf(implementsDirective); for (const application of implementsApplications) { const args = application.arguments(); // Note that if we have a `@join__implements` for a subgraph, then we must have a `@join__type` too, so // the `get` below is guaranteed to not be undefined. const subgraphInfo = subgraphsInfo.get(args.graph)!; subgraphInfo.type.addImplementedInterface(args.interface); } if (args.costSpec) { for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec); } } for (const field of type.fields()) { const fieldApplications = field.appliedDirectivesOf(fieldDirective); if (fieldApplications.length === 0) { // In fed2 subgraph, no @join__field means that the field is in all the subgraphs in which the type is. const isShareable = isObjectType(type) && subgraphsInfo.size > 1; for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { addSubgraphField({ field, type: subgraphType, subgraph, isShareable, costSpec: args.costSpec }); } } else { const isShareable = isObjectType(type) && (fieldApplications as Directive<any, { external?: boolean, usedOverridden?: boolean }>[]).filter((application) => { const args = application.arguments(); return !args.external && !args.usedOverridden; }).length > 1; for (const application of fieldApplications) { const joinFieldArgs = application.arguments(); // We use a @join__field with no graph to indicates when a field in the supergraph does not come // directly from any subgraph and there is thus nothing to do to "extract" it. if (!joinFieldArgs.graph) { continue; } const { type: subgraphType, subgraph } = subgraphsInfo.get(joinFieldArgs.graph)!; addSubgraphField({ field, type: subgraphType, subgraph, isShareable, joinFieldArgs, costSpec: args.costSpec }); } } } } } function extractInputObjContent(args: ExtractArguments, info: TypeInfo<InputObjectType>[]) { const fieldDirective = args.joinSpec.fieldDirective(args.supergraph); for (const { type, subgraphsInfo } of info) { for (const field of type.fields()) { const fieldApplications = field.appliedDirectivesOf(fieldDirective); if (fieldApplications.length === 0) { // In fed2 subgraph, no @join__field means that the field is in all the subgraphs in which the type is. for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { addSubgraphInputField({ field, type: subgraphType, subgraph, costSpec: args.costSpec }); } } else { for (const application of fieldApplications) { const joinFieldArgs = application.arguments(); // We use a @join__field with no graph to indicates when a field in the supergraph does not come // directly from any subgraph and there is thus nothing to do to "extract" it. if (!joinFieldArgs.graph) { continue; } const { type: subgraphType, subgraph } = subgraphsInfo.get(joinFieldArgs.graph)!; addSubgraphInputField({ field, type: subgraphType, subgraph, joinFieldArgs, costSpec: args.costSpec }); } } } } } function extractEnumTypeContent(args: ExtractArguments, info: TypeInfo<EnumType>[]) { // This was added in join 0.3, so it can genuinely be undefined. const enumValueDirective = args.joinSpec.enumValueDirective(args.supergraph); for (const { type, subgraphsInfo } of info) { if (args.costSpec) { for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { propagateDemandControlDirectives(type, subgraphType, subgraph, args.costSpec); } } for (const value of type.values) { const enumValueApplications = enumValueDirective ? value.appliedDirectivesOf(enumValueDirective) : []; if (enumValueApplications.length === 0) { for (const { type: subgraphType } of subgraphsInfo.values()) { subgraphType.addValue(value.name); } } else { for (const application of enumValueApplications) { const args = application.arguments(); const { type: subgraphType } = subgraphsInfo.get(args.graph)!; subgraphType.addValue(value.name); } } } } } function extractUnionTypeContent(args: ExtractArguments, info: TypeInfo<UnionType>[]) { // This was added in join 0.3, so it can genuinely be undefined. const unionMemberDirective = args.joinSpec.unionMemberDirective(args.supergraph); // Note that union members works a bit differently from fields or enum values, and this because we cannot have // directive applications on type members. So the `unionMemberDirective` applications are on the type itself, // and they mention the member that they target. for (const { type, subgraphsInfo } of info) { const unionMemberApplications = unionMemberDirective ? type.appliedDirectivesOf(unionMemberDirective) : []; if (unionMemberApplications.length === 0) { // No @join__unionMember; every member should be added to every subgraph having the union (at least as long // as the subgraph has the member itself). for (const { type: subgraphType, subgraph } of subgraphsInfo.values()) { for (const member of type.types()) { const subgraphMember = subgraph.schema.type(member.name); if (subgraphMember) { // Note that object types in the supergraph are guaranteed to be object types in subgraphs. subgraphType.addType(subgraphMember as ObjectType); } } } } else { for (const application of unionMemberApplications) { const args = application.arguments(); const { type: subgraphType, subgraph } = subgraphsInfo.get(args.graph)!; // Note that object types in the supergraph are guaranteed to be object types in subgraphs. // We also know that the type must exist in this case (we don't generate broken @join_unionMember). subgraphType.addType(subgraph.schema.type(args.member) as ObjectType); } } } } function extractSubgraphsFromFed2Supergraph(args: ExtractArguments) { const { objOrItfTypes, inputObjTypes, enumTypes, unionTypes, } = addAllEmptySubgraphTypes(args); extractObjOrItfContent(args, objOrItfTypes); extractInputObjContent(args, inputObjTypes); extractEnumTypeContent(args, enumTypes); extractUnionTypeContent(args, unionTypes); // We add all the "executable" directives from the supergraph to each subgraphs, as those may be part // of a query and end up in any subgraph fetches. We do this "last" to make sure that if one of the directive // use a type for an argument, that argument exists. // Note that we don't bother with non-executable directives at the moment since we've don't extract their // applications. It might become something we need later, but we don't so far. const allExecutableDirectives = args.supergraph.directives().filter((def) => def.hasExecutableLocations()); for (const subgraph of args.subgraphs) { removeInactiveProvidesAndRequires(subgraph.schema); removeUnusedTypesFromSubgraph(subgraph.schema); for (const definition of allExecutableDirectives) { // Note that we skip any potentially applied directives in the argument of the copied definition, because as said // in the comment above, we haven't copied type-system directives. And so far, we really don't care about those // applications. copyDirectiveDefinitionToSchema({ definition, schema: subgraph.schema, copyDirectiveApplicationsInArguments: false, locationFilter: (loc) => isExecutableDirectiveLocation(loc), }); } } } const DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME = 'APOLLO_FEDERATION_DEBUG_SUBGRAPHS'; function maybeDumpSubgraphSchema(subgraph: Subgraph): string { const shouldDump = !!validateStringContainsBoolean(process.env[DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME]); if (!shouldDump) { return `Re-run with environment variable '${DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME}' set to 'true' to extract the invalid subgraph`; } try { const filename = `extracted-subgraph-${subgraph.name}-${Date.now()}.graphql`; const file = path.resolve(filename); if (fs.existsSync(file)) { // Note that this is caught directly by the surrounded catch. throw new Error(`candidate file ${filename} already existed`); } fs.writeFileSync(file, printSchema(subgraph.schema)); return `The (invalid) extracted subgraph has been written in: ${file}.`; } catch (e2) { return `Was not able to print generated subgraph for "${subgraph.name}" because: ${errorToString(e2)}`; } } function propagateDemandControlDirectives(source: SchemaElement<any, any>, dest: SchemaElement<any, any>, subgraph: Subgraph, costSpec: CostSpecDefinition) { const costDirective = costSpec.costDirective(source.schema()); if (costDirective) { const application = source.appliedDirectivesOf(costDirective)[0]; if (application) { dest.applyDirective(subgraph.metadata().costDirective().name, application.arguments()); } } const listSizeDirective = costSpec.listSizeDirective(source.schema()); if (listSizeDirective) { const application = source.appliedDirectivesOf(listSizeDirective)[0]; if (application) { dest.applyDirective(subgraph.metadata().listSizeDirective().name, application.arguments()); } } } function errorToString(e: any,): string { const causes = errorCauses(e); return causes ? printErrors(causes) : String(e); } function addSubgraphField({ field, type, subgraph, isShareable, joinFieldArgs, costSpec, }: { field: FieldDefinition<ObjectType | InterfaceType>, type: ObjectType | InterfaceType, subgraph: Subgraph, isShareable: boolean, joinFieldArgs?: JoinFieldDirectiveArguments, costSpec?: CostSpecDefinition, }): FieldDefinition<ObjectType | InterfaceType> { const copiedFieldType = joinFieldArgs?.type ? decodeType(joinFieldArgs.type, subgraph.schema, subgraph.name) : copyType(field.type!, subgraph.schema, subgraph.name); const subgraphField = type.addField(field.name, copiedFieldType); for (const arg of field.arguments()) { const argDef = subgraphField.addArgument(arg.name, copyType(arg.type!, subgraph.schema, subgraph.name), arg.defaultValue); if (costSpec) { propagateDemandControlDirectives(arg, argDef, subgraph, costSpec); } } if (joinFieldArgs?.requires) { subgraphField.applyDirective(subgraph.metadata().requiresDirective(), {'fields': joinFieldArgs.requires}); } if (joinFieldArgs?.provides) { subgraphField.applyDirective(subgraph.metadata().providesDirective(), {'fields': joinFieldArgs.provides}); } if (joinFieldArgs?.contextArguments) { const fromContextDirective = subgraph.metadata().fromContextDirective(); if (!isFederationDirectiveDefinedInSchema(fromContextDirective)) { throw new Error(`@fromContext directive is not defined in the subgraph schema: ${subgraph.name}`); } else { for (const arg of joinFieldArgs.contextArguments) { // this code will remove the subgraph name from the context const match = arg.context.match(/^.*__([A-Za-z]\w*)$/); if (!match) { throw new Error(`Invalid context argument: ${arg.context}`); } subgraphField.addArgument(arg.name, decodeType(arg.type, subgraph.schema, subgraph.name)); const argOnField = subgraphField.argument(arg.name); argOnField?.applyDirective(fromContextDirective, { field: `\$${match[1]} ${arg.selection}`, }); } } } const external = !!joinFieldArgs?.external; if (external) { subgraphField.applyDirective(subgraph.metadata().externalDirective()); } const usedOverridden = !!joinFieldArgs?.usedOverridden; if (usedOverridden && !joinFieldArgs?.overrideLabel) { subgraphField.applyDirective(subgraph.metadata().externalDirective(), {'reason': '[overridden]'}); } if (joinFieldArgs?.override) { subgraphField.applyDirective(subgraph.metadata().overrideDirective(), { from: joinFieldArgs.override, ...(joinFieldArgs.overrideLabel ? { label: joinFieldArgs.overrideLabel } : {}) }); } if (isShareable && !external && !usedOverridden) { subgraphField.applyDirective(subgraph.metadata().shareableDirective()); } if (costSpec) { propagateDemandControlDirectives(field, subgraphField, subgraph, costSpec); } return subgraphField; } function addSubgraphInputField({ field, type, subgraph, joinFieldArgs, costSpec, }: { field: InputFieldDefinition, type: InputObjectType, subgraph: Subgraph, joinFieldArgs?: JoinFieldDirectiveArguments, costSpec?: CostSpecDefinition, }): InputFieldDefinition { const copiedType = joinFieldArgs?.type ? decodeType(joinFieldArgs?.type, subgraph.schema, subgraph.name) : copyType(field.type!, subgraph.schema, subgraph.name); const inputField = type.addField(field.name, copiedType); inputField.defaultValue = field.defaultValue if (costSpec) { propagateDemandControlDirectives(field, inputField, subgraph, costSpec); } return inputField; } function extractSubgraphsFromFed1Supergraph({ supergraph, subgraphs, joinSpec, filteredTypes, getSubgraph, }: ExtractArguments): Subgraphs { const typeDirective = joinSpec.typeDirective(supergraph); const ownerDirective = joinSpec.ownerDirective(supergraph); const fieldDirective = joinSpec.fieldDirective(supergraph); /* * For fed1 supergraph, only entity types are marked with `@join__type` and `@join__field`. Which mean that for value types, * we cannot directly know in which subgraphs they were initially defined. One strategy consists in "extracting" value types into * all subgraphs blindly: functionally, having some unused types in an extracted subgraph schema does not matter much. However, adding * those useless types increases memory usage, and we've seen some case with lots of subgraphs and lots of value types where those * unused types balloon up memory usage (from 100MB to 1GB in one example; obviously, this is made worst by the fact that javascript * is pretty memory heavy in the first place). So to avoid that problem, for fed1 supergraph, we do a first pass where we collect * for all the subgraphs the set of types that are actually reachable in that subgraph. As we extract do the actual type extraction, * we use this to ignore non-reachable types for any given subgraph. */ const reachableTypesBySubgraph = collectFieldReachableTypesForAllSubgraphs( supergraph, subgraphs.names(), (f, name) => { const fieldApplications: Directive<any, { graph?: string, requires?: string, provides?: string }>[] = f.appliedDirectivesOf(fieldDirective); if (fieldApplications.length) { const application = fieldApplications.find((application) => getSubgraph(application)?.name === name); if (application) { const args = application.arguments(); const typesInFederationDirectives = typesUsedInFederationDirective(args.provides, baseType(f.type!) as CompositeType) .concat(typesUsedInFederationDirective(args.requires, f.parent)); return { isInSubgraph: true, typesInFederationDirectives }; } else { return { isInSubgraph: false, typesInFederationDirectives: [] }; } } else { // No field application depends on the "owner" directive on the type. If we have no owner, then the // field is in all subgraph and we return true. Otherwise, the field is only in the owner subgraph. // In any case, the field cannot have a requires or provides const ownerApplications = ownerDirective ? f.parent.appliedDirectivesOf(ownerDirective) : []; return { isInSubgraph: !ownerApplications.length || getSubgraph(ownerApplications[0])?.name == name, typesInFederationDirectives: [] }; } }, (t, name) => { const typeApplications: Directive<any, { graph: string, key?: string}>[] = t.appliedDirectivesOf(typeDirective); const application = typeApplications.find((application) => (application.arguments().key && (getSubgraph(application)?.name === name))); if (application) { const typesInFederationDirectives = typesUsedInFederationDirective(application.arguments().key, t as CompositeType); return { isEntityWithKeyInSubgraph: true, typesInFederationDirectives }; } else { return { isEntityWithKeyInSubgraph: false, typesInFederationDirectives: [] }; } }, ); const includeTypeInSubgraph = (t: NamedType, name: string) => reachableTypesBySubgraph.get(name)?.has(t.name) ?? false; // Next, we iterate on all types and add it to the proper subgraphs (along with any @key). // Note that we first add all types empty and populate the types next. This avoids having to care about the iteration // order if we have fields than depends on other types. for (const type of filteredTypes) { const typeApplications = type.appliedDirectivesOf(typeDirective); if (!typeApplications.length) { // Imply we don't know in which subgraph the type is, so we had it in all subgraph in which the type is reachable. for (const subgraph of subgraphs) { if (includeTypeInSubgraph(type, subgraph.name)) { subgraph.schema.addType(newNamedType(type.kind, type.name)); } } } else { for (const application of typeApplications) { const args = application.arguments(); const subgraph = getSubgraph(application)!; assert(subgraph, () => `Should have found the subgraph for ${application}`); const schema = subgraph.schema; // We can have more than one type directive for a given subgraph let subgraphType = schema.type(type.name); if (!subgraphType) { const kind = args.isInterfaceObject ? 'ObjectType' : type.kind; subgraphType = schema.addType(newNamedType(kind, type.name)); if (args.isInterfaceObject) { subgraphType.applyDirective('interfaceObject'); } } if (args.key) { const { resolvable } = args; const directive = subgraphType.applyDirective('key', {'fields': args.key, resolvable}); if (args.extension) { directive.setOfExtension(subgraphType.newExtension()); } } } } } // We can now populate all those types (with relevant @provides and @requires on fields). for (const type of filteredTypes) { switch (type.kind) { case 'ObjectType': // @ts-expect-error: we fall-through the inputObjectType for fields. case 'InterfaceType': for (const implementations of type.interfaceImplementations()) { // There is no `@join__implements` in fed1 supergraphs, so we have no choice but to mark the // object/interface as implementing the interface in all subgraphs (at least those that contains // both types). const name = implementations.interface.name; for (const subgraph of subgraphs) { const subgraphType = subgraph.schema.type(type.name); const subgraphItf = subgraph.schema.type(name); if (subgraphType && subgraphItf) { (subgraphType as (ObjectType | InterfaceType)).addImplementedInterface(name); } } } // Fall-through on purpose. case 'InputObjectType': for (const field of type.fields()) { const fieldApplications = field.appliedDirectivesOf(fieldDirective); if (!fieldApplications.length) { // In fed1 supergraphs, the meaning of having no join__field depends on whether the parent type has a // `@join__owner`. If it does, it means the field is only on that owner subgraph. Otherwise, we kind of // don't know, so we add it to all subgraphs that have the parent type and, if the field base type // is a named type, know that field type. const ownerApplications = ownerDirective ? type.appliedDirectivesOf(ownerDirective) : []; if (ownerApplications.length > 0) { assert(ownerApplications.length == 1, () => `Found multiple join__owner directives on type ${type}`) const subgraph = getSubgraph(ownerApplications[0]); assert(subgraph, () => `Should have found the subgraph for ${ownerApplications[0]}`); addSubgraphFieldForFed1(field, subgraph, false); } else { const fieldBaseType = baseType(field.type!); const isShareable = isObjectType(type) && subgraphs.values().filter((s) => s.schema.type(type.name)).length > 1; for (const subgraph of subgraphs) { if (subgraph.schema.type(fieldBaseType.name)) { addSubgraphFieldForFed1(field, subgraph, isShareable); } } } } else { // Note that fed1 supergraphs only include `@join__field` for non-external fields, so it needs shareable as soon // as it has more than one `@join__field`. const isShareable = isObjectType(type) && fieldApplications.length > 1; for (const application of fieldApplications) { const subgraph = getSubgraph(application); // We use a @join__field with no graph to indicates when a field in the supergraph does not come // directly from any subgraph and there is thus nothing to do to "extract" it. if (!subgraph) { continue; } const args = application.arguments(); addSubgraphFieldForFed1(field, subgraph, isShareable, args); } } } break; case 'EnumType': for (const subgraph of subgraphs) { const subgraphEnum = subgraph.schema.type(type.name); if (!subgraphEnum) { continue; } assert(isEnumType(subgraphEnum), () => `${subgraphEnum} should be an enum but found a ${subgraphEnum.kind}`); // There is not `@join__enumValue` in fed1, so we add to all graphs regardless. for (const value of type.values) { subgraphEnum.addValue(value.name); } } break; case 'UnionType': for (const subgraph of subgraphs) { const subgraphUnion = subgraph.schema.type(type.name); if (!subgraphUnion) { continue; } assert(isUnionType(subgraphUnion), () => `${subgraphUnion} should be an enum but found a ${subgraphUnion.kind}`); // There is not `@join__unionMember` in fed1, so we add to all graphs regardless. for (const memberTypeName of type.types().map((t) => t.name)) { const subgraphType = subgraph.schema.type(memberTypeName); if (subgraphType) { subgraphUnion.addType(subgraphType as ObjectType); } } } break; } } const allExecutableDirectives = supergraph.directives().filter((def) => def.hasExecutableLocations()); for (const subgraph of subgraphs) { // The join spec in fed1 was not including external fields. Let's make sure we had them or we'll get validation // errors later. addExternalFields(subgraph, supergraph, true); removeInactiveProvidesAndRequires(subgraph.schema); removeUnusedTypesFromSubgraph(subgraph.schema); // Lastly, we add all the "executable" directives from the supergraph to each subgraphs, as those may be part // of a query and end up in any subgraph fetches. We do this "last" to make sure that if one of the directive // use a type for an argument, that argument exists. // Note that we don't bother with non-executable directives at the moment since we've don't extract their // applications. It might become something we need later, but we don't so far. for (const definition of allExecutableDirectives) { // Note that we skip any potentially applied directives in the argument of the copied definition, because as said // in the comment above, we haven't copied type-system directives. And so far, we really don't care about those // applications. copyDirectiveDefinitionToSchema({ definition, schema: subgraph.schema, copyDirectiveApplicationsInArguments: false, locationFilter: (loc) => isExecutableDirectiveLocation(loc), }); } } return subgraphs; } type AnyField = FieldDefinition<ObjectType | InterfaceType> | InputFieldDefinition; function addSubgraphFieldForFed1(field: AnyField, subgraph: Subgraph, isShareable: boolean, joinFieldArgs?: JoinFieldDirectiveArguments): void { const subgraphType = subgraph.schema.type(field.parent.name); if (!subgraphType) { return; } if (field instanceof FieldDefinition) { addSubgraphField({ field, subgraph, type: subgraphType as ObjectType | InterfaceType, isShareable, joinFieldArgs, }); } else { addSubgraphInputField({ field, subgraph, type: subgraphType as InputObjectType, joinFieldArgs, }); } } function decodeType(encodedType: string, subgraph: Schema, subgraphName: string): Type { try { return builtTypeReference(encodedType, subgraph); } catch (e) { assert(false, () => `Cannot parse type "${encodedType}" in subgraph ${subgraphName}: ${e}`); } } function copyType(type: Type, subgraph: Schema, subgraphName: string): Type { switch (type.kind) { case 'ListType': return new ListType(copyType(type.ofType, subgraph, subgraphName)); case 'NonNullType': return new NonNullType(copyType(type.ofType, subgraph, subgraphName) as NullableType); default: const subgraphType = subgraph.type(type.name); assert(subgraphType, () => `Cannot find type "${type.name}" in subgraph "${subgraphName}"`); return subgraphType; } } function addExternalFields(subgraph: Subgraph, supergraph: Schema, isFed1: boolean) { const metadata = subgraph.metadata(); for (const type of subgraph.schema.types()) { if (!isObjectType(type) && !isInterfaceType(type)) { continue; } // First, handle @key for (const keyApplication of type.appliedDirectivesOf(metadata.keyDirective())) { // Historically, the federation code for keys, when applied _to a type extension_: // 1) required @external on any field of the key // 2) but required the subgraph to resolve any field of that key // despite the combination of those being arguably illogical (@external is supposed to signify the field is _not_ resolve // by the subgraph). // To maintain backward compatibility, we have to preserve that behavior. The way this is done is that during merging, // if a key is on an extension, we remember it in the corresponding @join__type. And when reading @join__type directive // in `extractSubgraphsFromSupergraph`, we mark the generated key directive as applied to an extension (note that only // the key directive is marked that way, not the rest of the type; this is because we actually don't know if the rest // what part of an extension or not and we prefer not presuming). So, now, if we look at the fields in a key and // that key was on an extension, we know that we should not mark it @external, because it _is_ resolved by the subgraph. // If the key is on a type definition however, then we don't have that historical legacy, and so if the field is // not part of the subgraph, then it means that it is truly external (and composition validation will ensure that this // is fine). // Note that this is called `forceNonExternal` because an extension key field might well be part of a @provides somewhere // else (it's not useful to do so, kind of imply an incomprehension and we'll remove those in `removeNeedlessProvides`, // but it's not forbidden and has been seen) which has already added the field as @external, and we want to _remove_ the // @external in that case. Also note that for fed 1 supergraphs, the 'ofExtension' information is not available so we // have to default of forcing non-external on all key fields. Which is ok because "true" external on key fields was not // supported anyway. const forceNonExternal = isFed1 || !!keyApplication.ofExtension(); addExternalFieldsFromDirectiveFieldSet(subgraph, type, keyApplication, supergraph, forceNonExternal); } // Then any @requires or @provides on fields for (const field of type.fields()) { for (const requiresApplication of field.appliedDirectivesOf(metadata.requiresDirective())) { addExternalFieldsFromDirectiveFieldSet(subgraph, type, requiresApplication, supergraph); } const fieldBaseType = baseType(field.type!); for (const providesApplication of field.appliedDirectivesOf(metadata.providesDirective())) { assert(isObjectType(fieldBaseType) || isInterfaceType(fieldBaseType), () => `Found @provides on field ${field.coordinate} whose type ${field.type!} (${fieldBaseType.kind}) is not an object or interface `); addExternalFieldsFromDirectiveFieldSet(subgraph, fieldBaseType, providesApplication, supergraph); } } // And then any constraint due to implemented interfaces. addExternalFieldsFromInterface(metadata, type); } } function addExternalFieldsFromDirectiveFieldSet( subgraph: Subgraph, parentType: ObjectType | InterfaceType, directive: Directive<NamedType | FieldDefinition<CompositeType>, {fields: any}>, supergraph: Schema, forceNonExternal: boolean = false, ) { const external = subgraph.metadata().externalDirective(); const fieldAccessor = function (type: CompositeType, fieldName: string): FieldDefinition<any> { const field = type.field(fieldName); if (field) { if (forceNonExternal && field.hasAppliedDirective(external)) { field.appliedDirectivesOf(external).forEach(d => d.remove()); } return field; } assert(!isU