UNPKG

@apollo/composition

Version:
163 lines (143 loc) 5.77 kB
import { printSchema, Schema, Subgraphs, defaultPrintOptions, shallowOrderPrintedDefinitions, PrintOptions, ServiceDefinition, subgraphsFromServiceList, upgradeSubgraphsIfNecessary, SubtypingRule, assert, Supergraph, } from "@apollo/federation-internals"; import { GraphQLError } from "graphql"; import { buildFederatedQueryGraph, buildSupergraphAPIQueryGraph } from "@apollo/query-graphs"; import { MergeResult, mergeSubgraphs } from "./merging"; import { validateGraphComposition } from "./validate"; import { CompositionHint } from "./hints"; export type CompositionResult = CompositionFailure | CompositionSuccess; export interface CompositionFailure { errors: GraphQLError[]; schema?: undefined; supergraphSdl?: undefined; hints?: undefined; } export interface CompositionSuccess { schema: Schema; supergraphSdl: string; hints: CompositionHint[]; errors?: undefined; } export interface CompositionOptions { sdlPrintOptions?: PrintOptions; allowedFieldTypeMergingSubtypingRules?: SubtypingRule[]; /// Flag to toggle if satisfiability should be performed during composition runSatisfiability?: boolean; /// Maximum allowable number of outstanding subgraph paths to validate maxValidationSubgraphPaths?: number; } function validateCompositionOptions(options: CompositionOptions) { // TODO: we currently cannot allow "list upgrades", meaning a subgraph returning `String` and another returning `[String]`. To support it, we would need the execution code to // recognize situation and "coerce" results from the first subgraph (the one returning `String`) into singleton lists. assert(!options?.allowedFieldTypeMergingSubtypingRules?.includes("list_upgrade"), "The `list_upgrade` field subtyping rule is currently not supported"); } /** * Used to compose a supergraph from subgraphs * `options.runSatisfiability` will default to `true` * * @param subgraphs Subgraphs * @param options CompositionOptions */ export function compose(subgraphs: Subgraphs, options: CompositionOptions = {}): CompositionResult { const { runSatisfiability = true, sdlPrintOptions, maxValidationSubgraphPaths } = options; validateCompositionOptions(options); const mergeResult = validateSubgraphsAndMerge(subgraphs); if (mergeResult.errors) { return { errors: mergeResult.errors }; } let satisfiabilityResult; if (runSatisfiability) { satisfiabilityResult = validateSatisfiability({ supergraphSchema: mergeResult.supergraph, }, { maxValidationSubgraphPaths }); if (satisfiabilityResult.errors) { return { errors: satisfiabilityResult.errors }; } } // printSchema calls validateOptions, which can throw let supergraphSdl; try { supergraphSdl = printSchema( mergeResult.supergraph, sdlPrintOptions ?? shallowOrderPrintedDefinitions(defaultPrintOptions), ); } catch (err) { return { errors: [err] }; } return { schema: mergeResult.supergraph, supergraphSdl, hints: [...mergeResult.hints, ...(satisfiabilityResult?.hints ?? [])], }; } /** * Method to validate and compose services * * @param services List of Service definitions * @param options CompositionOptions * @returns CompositionResult */ export function composeServices(services: ServiceDefinition[], options: CompositionOptions = {}): CompositionResult { const subgraphs = subgraphsFromServiceList(services); if (Array.isArray(subgraphs)) { // Errors in subgraphs are not truly "composition" errors, but it's probably still the best place // to surface them in this case. Not that `subgraphsFromServiceList` do ensure the errors will // include the subgraph name in their message. return { errors: subgraphs }; } return compose(subgraphs, options); } type SatisfiabilityArgs = { supergraphSchema: Schema supergraphSdl?: never } | { supergraphSdl: string, supergraphSchema?: never }; /** * Run satisfiability check for a supergraph * * Can pass either the supergraph's Schema or SDL to validate * @param args: SatisfiabilityArgs * @returns { errors? : GraphQLError[], hints? : CompositionHint[] } */ export function validateSatisfiability({ supergraphSchema, supergraphSdl} : SatisfiabilityArgs, options: CompositionOptions = {}) : { errors? : GraphQLError[], hints? : CompositionHint[], } { // We pass `null` for the `supportedFeatures` to disable the feature support validation. Validating feature support // is useful when executing/handling a supergraph, but here we're just validating the supergraph we've just created, // and there is no reason to error due to an unsupported feature. const supergraph = supergraphSchema ? new Supergraph(supergraphSchema, null) : Supergraph.build(supergraphSdl, { supportedFeatures: null }); const supergraphQueryGraph = buildSupergraphAPIQueryGraph(supergraph); const federatedQueryGraph = buildFederatedQueryGraph(supergraph, false); return validateGraphComposition(supergraph.schema, supergraph.subgraphNameToGraphEnumValue(), supergraphQueryGraph, federatedQueryGraph, options); } type ValidateSubgraphsAndMergeResult = MergeResult | { errors: GraphQLError[] }; /** * Upgrade subgraphs if necessary, then validates subgraphs before attempting to merge * * @param subgraphs * @returns ValidateSubgraphsAndMergeResult */ function validateSubgraphsAndMerge(subgraphs: Subgraphs) : ValidateSubgraphsAndMergeResult { const upgradeResult = upgradeSubgraphsIfNecessary(subgraphs); if (upgradeResult.errors) { return { errors: upgradeResult.errors }; } const toMerge = upgradeResult.subgraphs; const validationErrors = toMerge.validate(); if (validationErrors) { return { errors: validationErrors }; } return mergeSubgraphs(toMerge); }