From 9c8991c4e73371aa32866fee59f0edbbd1cd842a Mon Sep 17 00:00:00 2001 From: Chris Lenfest Date: Mon, 8 Jan 2024 14:05:22 -0600 Subject: [PATCH 01/17] Ideas for some basic callbacks for allowing for custom validation on a per directive basis --- internals-js/src/federation.ts | 23 +++++++++++++++++++++++ internals-js/src/specs/sourceSpec.ts | 13 +++++++++++++ 2 files changed, 36 insertions(+) diff --git a/internals-js/src/federation.ts b/internals-js/src/federation.ts index d5af52634..dfe2d3313 100644 --- a/internals-js/src/federation.ts +++ b/internals-js/src/federation.ts @@ -80,6 +80,7 @@ import { FEDERATION1_TYPES, FEDERATION1_DIRECTIVES, } from "./specs/federationSpec"; +import { validateSourceAPIDirective, validateSourceFieldDirective, validateSourceTypeDirective } from './specs/sourceSpec'; import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print"; import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification"; import { didYouMean, suggestionList } from "./suggestions"; @@ -582,8 +583,14 @@ export class FederationMetadata { private _sharingPredicate?: (field: FieldDefinition) => boolean; private _fieldUsedPredicate?: (field: FieldDefinition) => boolean; private _isFed2Schema?: boolean; + private preMergeValidationDirectiveMap: { [key: string]: (schema: Schema, directive: Directive, errorCollector: GraphQLError[]) => void }; constructor(readonly schema: Schema) { + this.preMergeValidationDirectiveMap = { + [this.sourceAPIDirective().name]: validateSourceAPIDirective, + [this.sourceTypeDirective().name]: validateSourceTypeDirective, + [this.sourceFieldDirective().name]: validateSourceFieldDirective, + } } private onInvalidate() { @@ -605,6 +612,10 @@ export class FederationMetadata { return this.schema.coreFeatures?.getByIdentity(federationSpec.identity); } + getPreMergeValidationDirectiveMap(): { [key: string]: (schema: Schema, directive: Directive, errorCollector: GraphQLError[]) => void } { + return this.preMergeValidationDirectiveMap; + } + private externalTester(): ExternalTester { if (!this._externalTester) { this._externalTester = new ExternalTester(this.schema, this.isFed2Schema()); @@ -1081,6 +1092,18 @@ export class FederationBlueprint extends SchemaBlueprint { validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector); validateInterfaceObjectsAreOnEntities(metadata, errorCollector); + // for any individual directives that have their own validation, go ahead and perform that + const validationDirectiveMap = metadata.getPreMergeValidationDirectiveMap(); + for (const [directive, validator] of Object.entries(validationDirectiveMap)) { + const directiveDefinition = schema.directive(directive); + if (directiveDefinition) { + const usages = directiveDefinition.applications(); + for (const usage of usages) { + validator(schema, usage, errorCollector); + } + } + } + // If tag is redefined by the user, make sure the definition is compatible with what we expect const tagDirective = metadata.tagDirective(); if (tagDirective) { diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 78c807318..077365cfa 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -6,6 +6,7 @@ import { InputObjectType, InputFieldDefinition, ListType, + Directive, } from '../definitions'; import { registerKnownFeature } from '../knownCoreFeatures'; import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; @@ -142,6 +143,18 @@ export class SourceSpecDefinition extends FeatureDefinition { } } +export function validateSourceAPIDirective(_schema: Schema, _directive: Directive, _errorCollector: GraphQLError[]) { + // TODO +} + +export function validateSourceTypeDirective(_schema: Schema, _directive: Directive, _errorCollector: GraphQLError[]) { + // TODO +} + +export function validateSourceFieldDirective(_schema: Schema, _directive: Directive, _errorCollector: GraphQLError[]) { + // TODO +} + export type SourceAPIDirectiveArgs = { name: string; http?: HTTPSourceAPI; From 7a8b8bb24f7f408363d6597dd44679a02336985d Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 9 Jan 2024 15:24:36 -0500 Subject: [PATCH 02/17] Enable FeatureDefinition schema validation via validateKnownFeatures. --- internals-js/src/federation.ts | 31 ++--------- internals-js/src/knownCoreFeatures.ts | 17 +++++- internals-js/src/specs/coreSpec.ts | 5 ++ internals-js/src/specs/sourceSpec.ts | 80 +++++++++++++++++++++++---- 4 files changed, 95 insertions(+), 38 deletions(-) diff --git a/internals-js/src/federation.ts b/internals-js/src/federation.ts index dfe2d3313..1ab5b0d11 100644 --- a/internals-js/src/federation.ts +++ b/internals-js/src/federation.ts @@ -80,11 +80,10 @@ import { FEDERATION1_TYPES, FEDERATION1_DIRECTIVES, } from "./specs/federationSpec"; -import { validateSourceAPIDirective, validateSourceFieldDirective, validateSourceTypeDirective } from './specs/sourceSpec'; import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print"; import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification"; import { didYouMean, suggestionList } from "./suggestions"; -import { coreFeatureDefinitionIfKnown } from "./knownCoreFeatures"; +import { coreFeatureDefinitionIfKnown, validateKnownFeatures } from "./knownCoreFeatures"; import { joinIdentity } from "./specs/joinSpec"; import { SourceAPIDirectiveArgs, @@ -583,15 +582,8 @@ export class FederationMetadata { private _sharingPredicate?: (field: FieldDefinition) => boolean; private _fieldUsedPredicate?: (field: FieldDefinition) => boolean; private _isFed2Schema?: boolean; - private preMergeValidationDirectiveMap: { [key: string]: (schema: Schema, directive: Directive, errorCollector: GraphQLError[]) => void }; - constructor(readonly schema: Schema) { - this.preMergeValidationDirectiveMap = { - [this.sourceAPIDirective().name]: validateSourceAPIDirective, - [this.sourceTypeDirective().name]: validateSourceTypeDirective, - [this.sourceFieldDirective().name]: validateSourceFieldDirective, - } - } + constructor(readonly schema: Schema) {} private onInvalidate() { this._externalTester = undefined; @@ -612,10 +604,6 @@ export class FederationMetadata { return this.schema.coreFeatures?.getByIdentity(federationSpec.identity); } - getPreMergeValidationDirectiveMap(): { [key: string]: (schema: Schema, directive: Directive, errorCollector: GraphQLError[]) => void } { - return this.preMergeValidationDirectiveMap; - } - private externalTester(): ExternalTester { if (!this._externalTester) { this._externalTester = new ExternalTester(this.schema, this.isFed2Schema()); @@ -1092,17 +1080,10 @@ export class FederationBlueprint extends SchemaBlueprint { validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector); validateInterfaceObjectsAreOnEntities(metadata, errorCollector); - // for any individual directives that have their own validation, go ahead and perform that - const validationDirectiveMap = metadata.getPreMergeValidationDirectiveMap(); - for (const [directive, validator] of Object.entries(validationDirectiveMap)) { - const directiveDefinition = schema.directive(directive); - if (directiveDefinition) { - const usages = directiveDefinition.applications(); - for (const usage of usages) { - validator(schema, usage, errorCollector); - } - } - } + // FeatureDefinition objects passed to registerKnownFeature can register + // validation functions for subgraph schemas by overriding the + // validateSubgraphSchema method. + validateKnownFeatures(schema, errorCollector); // If tag is redefined by the user, make sure the definition is compatible with what we expect const tagDirective = metadata.tagDirective(); diff --git a/internals-js/src/knownCoreFeatures.ts b/internals-js/src/knownCoreFeatures.ts index 55faef616..83bb3ceaf 100644 --- a/internals-js/src/knownCoreFeatures.ts +++ b/internals-js/src/knownCoreFeatures.ts @@ -1,6 +1,8 @@ +import { GraphQLError } from "graphql"; +import { Schema } from "./definitions"; import { FeatureDefinition, FeatureDefinitions, FeatureUrl } from "./specs/coreSpec"; -const registeredFeatures: Map = new Map(); +const registeredFeatures = new Map(); export function registerKnownFeature(definitions: FeatureDefinitions) { if (!registeredFeatures.has(definitions.identity)) { @@ -12,6 +14,19 @@ export function coreFeatureDefinitionIfKnown(url: FeatureUrl): FeatureDefinition return registeredFeatures.get(url.identity)?.find(url.version); } +export function validateKnownFeatures( + schema: Schema, + errorCollector: GraphQLError[] = [], +): GraphQLError[] { + registeredFeatures.forEach(definitions => { + const feature = definitions.latest(); + if (feature.validateSubgraphSchema !== FeatureDefinition.prototype.validateSubgraphSchema) { + errorCollector.push(...feature.validateSubgraphSchema(schema)); + } + }); + return errorCollector; +} + /** * Removes a feature from the set of known features. * diff --git a/internals-js/src/specs/coreSpec.ts b/internals-js/src/specs/coreSpec.ts index 95ac988da..3c2efef96 100644 --- a/internals-js/src/specs/coreSpec.ts +++ b/internals-js/src/specs/coreSpec.ts @@ -117,6 +117,11 @@ export abstract class FeatureDefinition { .concat(this.typeSpecs().map((spec) => spec.name)); } + // No-op implementation that can be overridden by subclasses. + validateSubgraphSchema(_schema: Schema): GraphQLError[] { + return []; + } + protected nameInSchema(schema: Schema): string | undefined { const feature = this.featureInSchema(schema); return feature?.nameInSchema; diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 077365cfa..5bfc96d13 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -6,13 +6,15 @@ import { InputObjectType, InputFieldDefinition, ListType, - Directive, + DirectiveDefinition, } from '../definitions'; import { registerKnownFeature } from '../knownCoreFeatures'; import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; export const sourceIdentity = 'https://specs.apollo.dev/source'; +const KNOWN_SOURCE_PROTOCOLS = ["http"]; + export class SourceSpecDefinition extends FeatureDefinition { constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) { super(new FeatureUrl(sourceIdentity, 'source', version), minimumFederationVersion); @@ -141,18 +143,72 @@ export class SourceSpecDefinition extends FeatureDefinition { sourceFieldDirective(schema: Schema) { return this.directive(schema, 'sourceField')!; } -} - -export function validateSourceAPIDirective(_schema: Schema, _directive: Directive, _errorCollector: GraphQLError[]) { - // TODO -} -export function validateSourceTypeDirective(_schema: Schema, _directive: Directive, _errorCollector: GraphQLError[]) { - // TODO -} - -export function validateSourceFieldDirective(_schema: Schema, _directive: Directive, _errorCollector: GraphQLError[]) { - // TODO + override validateSubgraphSchema(schema: Schema): GraphQLError[] { + const sourceAPIInSchema = schema.directive('sourceAPI') as DirectiveDefinition | undefined; + const sourceTypeInSchema = schema.directive('sourceType') as DirectiveDefinition | undefined; + const sourceFieldInSchema = schema.directive('sourceField') as DirectiveDefinition | undefined; + + if (!(sourceAPIInSchema || sourceTypeInSchema || sourceFieldInSchema)) { + // If none of the @source* directives are present, nothing needs + // validating. + return []; + } + + const errors: GraphQLError[] = []; + const apiNameToProtocol = new Map(); + + if (sourceAPIInSchema) { + sourceAPIInSchema.applications().forEach(application => { + // Each @sourceAPI application must use one of the known protocols. + const { name, ...rest } = application.arguments(); + let protocol: string | undefined; + + Object.keys(rest).forEach(key => { + if (KNOWN_SOURCE_PROTOCOLS.includes(key)) { + if (protocol) { + errors.push(new GraphQLError( + `@${sourceAPIInSchema.name} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + )); + } + protocol = key; + } + }); + + if (apiNameToProtocol.has(name)) { + errors.push(new GraphQLError(`@${sourceAPIInSchema.name} must specify a unique name`)); + } + + if (protocol) { + apiNameToProtocol.set(name, protocol); + } else { + errors.push(new GraphQLError( + `@${sourceAPIInSchema.name} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + )); + } + }); + } + + if (sourceTypeInSchema) { + sourceTypeInSchema.applications().forEach(application => { + const { api } = application.arguments(); + if (!api || !apiNameToProtocol.has(api)) { + errors.push(new GraphQLError(`@${sourceTypeInSchema.name} must specify a known api`)); + } + }); + } + + if (sourceFieldInSchema) { + sourceFieldInSchema.applications().forEach(application => { + const { api } = application.arguments(); + if (!api || !apiNameToProtocol.has(api)) { + errors.push(new GraphQLError(`@${sourceFieldInSchema.name} must specify a known api`)); + } + }); + } + + return errors; + } } export type SourceAPIDirectiveArgs = { From 235dddbba91d1806beda7f9ff94e147ab99b1e3e Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 12 Jan 2024 18:45:32 -0500 Subject: [PATCH 03/17] Implement easy validation rules, leaving some trickier TODOs. --- internals-js/src/specs/sourceSpec.ts | 234 ++++++++++++++++++++++++--- 1 file changed, 216 insertions(+), 18 deletions(-) diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 5bfc96d13..689af3b55 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -7,14 +7,15 @@ import { InputFieldDefinition, ListType, DirectiveDefinition, + SchemaElement, } from '../definitions'; import { registerKnownFeature } from '../knownCoreFeatures'; import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; +import { parseJSONSelection } from '../parsing/JSONSelection'; +import { parseURLPathTemplate } from '../parsing/URLPathTemplate'; export const sourceIdentity = 'https://specs.apollo.dev/source'; -const KNOWN_SOURCE_PROTOCOLS = ["http"]; - export class SourceSpecDefinition extends FeatureDefinition { constructor(version: FeatureVersion, minimumFederationVersion?: FeatureVersion) { super(new FeatureUrl(sourceIdentity, 'source', version), minimumFederationVersion); @@ -156,34 +157,55 @@ export class SourceSpecDefinition extends FeatureDefinition { } const errors: GraphQLError[] = []; - const apiNameToProtocol = new Map(); + const apiNameToProtocol = new Map(); if (sourceAPIInSchema) { sourceAPIInSchema.applications().forEach(application => { - // Each @sourceAPI application must use one of the known protocols. const { name, ...rest } = application.arguments(); - let protocol: string | undefined; - Object.keys(rest).forEach(key => { - if (KNOWN_SOURCE_PROTOCOLS.includes(key)) { + if (apiNameToProtocol.has(name)) { + errors.push(new GraphQLError(`${sourceAPIInSchema.name} must specify unique name`)); + } + + // Ensure name is a valid GraphQL identifier. + if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { + errors.push(new GraphQLError(`${sourceAPIInSchema}(name: ${ + JSON.stringify(name) + }) must be valid GraphQL identifier`)); + } + + let protocol: ProtocolName | undefined; + KNOWN_SOURCE_PROTOCOLS.forEach(knownProtocol => { + if (rest[knownProtocol]) { if (protocol) { errors.push(new GraphQLError( - `@${sourceAPIInSchema.name} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceAPIInSchema.name} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, )); } - protocol = key; + protocol = knownProtocol; } }); - if (apiNameToProtocol.has(name)) { - errors.push(new GraphQLError(`@${sourceAPIInSchema.name} must specify a unique name`)); - } - if (protocol) { apiNameToProtocol.set(name, protocol); + + const protocolValue = rest[protocol]; + if (protocolValue && protocol === "http") { + const { baseURL, headers } = protocolValue as HTTPSourceAPI; + + try { + new URL(baseURL); + } catch (e) { + errors.push(new GraphQLError(`${sourceAPIInSchema.name} http.baseURL ${ + JSON.stringify(baseURL) + } must be valid URL`)); + } + + validateHTTPHeaders(headers, errors, sourceAPIInSchema.name); + } } else { errors.push(new GraphQLError( - `@${sourceAPIInSchema.name} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceAPIInSchema.name} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, )); } }); @@ -191,18 +213,168 @@ export class SourceSpecDefinition extends FeatureDefinition { if (sourceTypeInSchema) { sourceTypeInSchema.applications().forEach(application => { - const { api } = application.arguments(); + const { api, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`@${sourceTypeInSchema.name} must specify a known api`)); + errors.push(new GraphQLError(`${sourceTypeInSchema.name} specifies unknown api ${api}`)); + } else { + const expectedProtocol = apiNameToProtocol.get(api); + const protocolValue = expectedProtocol && rest[expectedProtocol]; + if (expectedProtocol && !protocolValue) { + errors.push(new GraphQLError( + `${sourceTypeInSchema.name} must specify same ${ + expectedProtocol + } argument as corresponding ${ + sourceAPIInSchema!.name + } for api ${api}`, + )); + } + + if (protocolValue && expectedProtocol === "http") { + const { GET, POST, headers, body } = protocolValue as HTTPSourceType; + + if ([GET, POST].filter(Boolean).length !== 1) { + errors.push(new GraphQLError( + `${sourceTypeInSchema.name} must specify exactly one of http.GET or http.POST`, + )); + } else { + const urlPathTemplate = (GET || POST)!; + try { + // TODO Validate URL path template uses only available fields of + // the type. + parseURLPathTemplate(urlPathTemplate); + } catch (e) { + errors.push(new GraphQLError( + `${sourceTypeInSchema.name} http.GET or http.POST must be valid URL path template`, + )); + } + } + + validateHTTPHeaders(headers, errors, sourceTypeInSchema.name); + + if (body) { + try { + parseJSONSelection(body); + // TODO Validate body selection matches the available fields. + } catch (e) { + errors.push(new GraphQLError( + `${sourceTypeInSchema.name} http.body not valid JSONSelection: ${ + e.message + }`, + )); + } + } + } + } + + const ast = application.parent.sourceAST; + switch (ast?.kind) { + case "ObjectTypeDefinition": + case "InterfaceTypeDefinition": + if (!ast.directives?.some(directive => directive.name.value === "key")) { + errors.push(new GraphQLError( + `${sourceTypeInSchema.name} must be applied to an entity type that also has a @key directive`, + )); + } + // TODO Validate selection is valid JSONSelection for type. + break; + default: + errors.push(new GraphQLError( + `${sourceTypeInSchema.name} must be applied to object or interface type`, + )); } }); } if (sourceFieldInSchema) { sourceFieldInSchema.applications().forEach(application => { - const { api } = application.arguments(); + const { api, selection, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`@${sourceFieldInSchema.name} must specify a known api`)); + errors.push(new GraphQLError(`${sourceFieldInSchema.name} specifies unknown api ${api}`)); + } else { + const expectedProtocol = apiNameToProtocol.get(api); + const protocolValue = expectedProtocol && rest[expectedProtocol]; + if (protocolValue && expectedProtocol === "http") { + const { + GET, POST, PUT, PATCH, DELETE, + headers, + body, + } = protocolValue as HTTPSourceField; + + const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean); + if (usedMethods.length > 1) { + errors.push(new GraphQLError(`${ + sourceFieldInSchema.name + } allows at most one of http.GET, http.POST, http.PUT, http.PATCH, and http.DELETE`)); + } else if (usedMethods.length === 1) { + const urlPathTemplate = usedMethods[0]!; + try { + // TODO Validate URL path template uses only available fields of + // the type and/or argument names of the field. + parseURLPathTemplate(urlPathTemplate); + } catch (e) { + errors.push(new GraphQLError( + `${sourceFieldInSchema.name} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, + )); + } + } + + validateHTTPHeaders(headers, errors, sourceFieldInSchema.name); + + if (body) { + try { + parseJSONSelection(body); + // TODO Validate body string matches the available fields of the + // parent type and/or argument names of the field. + } catch (e) { + errors.push(new GraphQLError( + `${sourceFieldInSchema.name} http.body not valid JSONSelection: ${ + e.message + }`, + )); + } + } + } + } + + if (selection) { + try { + parseJSONSelection(selection); + // TODO Validate selection string matches the available fields of + // the parent type and/or argument names of the field. + } catch (e) { + errors.push(new GraphQLError( + `${sourceFieldInSchema.name} selection not valid JSONSelection: ${ + e.message + }`, + )); + } + } + + // @sourceField is allowed only on root Query and Mutation fields or + // fields of entity object types. + const fieldParent = application.parent; + if (fieldParent.sourceAST?.kind !== "FieldDefinition") { + errors.push(new GraphQLError( + `${sourceFieldInSchema.name} must be applied to field`, + )); + } else { + const typeGrandparent = fieldParent.parent as SchemaElement; + if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") { + errors.push(new GraphQLError( + `${sourceFieldInSchema.name} must be applied to field of object type`, + )); + } else { + const typeGrandparentName = typeGrandparent.sourceAST?.name.value; + if ( + typeGrandparentName !== "Query" && + typeGrandparentName !== "Mutation" && + typeGrandparent.appliedDirectivesOf("key").length === 0 + ) { + errors.push(new GraphQLError( + `${sourceFieldInSchema.name} must be applied to root Query or Mutation field or field of entity type`, + )); + } + } } }); } @@ -211,6 +383,32 @@ export class SourceSpecDefinition extends FeatureDefinition { } } +function validateHTTPHeaders( + headers: HTTPHeaderMapping[] | undefined, + errors: GraphQLError[], + directiveName: string, +) { + if (headers) { + headers.forEach(({ name, as, value }, i) => { + // Ensure name is a valid HTTP header name. + if (!/^[a-zA-Z0-9-_]+$/.test(name)) { + errors.push(new GraphQLError(`${directiveName} headers[${i}].name == ${ + JSON.stringify(name) + } is not valid HTTP header name`)); + } + + if (!as === !value) { + errors.push(new GraphQLError(`${directiveName} headers[${i}] must specify exactly one of as or value`)); + } + + // TODO Validate value is valid HTTP header value? + }); + } +} + +const KNOWN_SOURCE_PROTOCOLS = ["http"] as const; +type ProtocolName = (typeof KNOWN_SOURCE_PROTOCOLS)[number]; + export type SourceAPIDirectiveArgs = { name: string; http?: HTTPSourceAPI; From 30b7713942f7bf224fc48021ef31343050ad2217 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 16 Jan 2024 15:49:55 -0500 Subject: [PATCH 04/17] Basic initial validation error tests. --- composition-js/src/__tests__/compose.test.ts | 97 ++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index 61c6f53b4..ccfd8726a 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -4961,4 +4961,101 @@ describe('@source* directives', () => { }` ) }); + + describe('validation errors', () => { + const goodSchema = gql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) + @link(url: "https://specs.apollo.dev/source/v0.1", import: [ + "@sourceAPI" + "@sourceType" + "@sourceField" + ]) + @sourceAPI( + name: "A" + http: { baseURL: "https://api.a.com/v1" } + ) + { + query: Query + } + + type Query { + resources: [Resource!]! @sourceField( + api: "A" + http: { GET: "/resources" } + ) + } + + type Resource @key(fields: "id") @sourceType( + api: "A" + http: { GET: "/resources/{id}" } + selection: "id description" + ) { + id: ID! + description: String! + } + `; + + const badSchema = gql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) + @link(url: "https://specs.apollo.dev/source/v0.1", import: [ + "@sourceAPI" + "@sourceType" + "@sourceField" + ]) + @sourceAPI( + name: "A?!" # Should be valid GraphQL identifier + http: { baseURL: "https://api.a.com/v1" } + ) + { + query: Query + } + + type Query { + resources: [Resource!]! @sourceField( + api: "A" + http: { GET: "/resources" } + ) + } + + type Resource @key(fields: "id") @sourceType( + api: "A" + http: { GET: "/resources/{id}" } + selection: "id description" + ) { + id: ID! + description: String! + } + `; + + it('good schema composes without validation errors', () => { + const result = composeServices([{ + name: 'good', + typeDefs: goodSchema, + }]); + expect(result.errors ?? []).toEqual([]); + }); + + it('bad schema composes with validation errors', () => { + const result = composeServices([{ + name: 'bad', + typeDefs: badSchema, + }]); + + const messages = result.errors!.map(e => e.message); + + expect(messages).toContain( + '[bad] @sourceAPI(name: "A?!") must be valid GraphQL identifier' + ); + + expect(messages).toContain( + '[bad] sourceType specifies unknown api A' + ); + + expect(messages).toContain( + '[bad] sourceField specifies unknown api A' + ); + }); + }); }); From c942d0dcc2f587352a41f022e82b4fc0bdcb9cb4 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 17 Jan 2024 13:56:33 -0500 Subject: [PATCH 05/17] Let `@source*` directives be renamed when imported. https://github.com/apollographql/federation/pull/2910#discussion_r1455684417 --- composition-js/src/__tests__/compose.test.ts | 46 +++++++++ internals-js/src/specs/sourceSpec.ts | 102 ++++++++++++------- 2 files changed, 113 insertions(+), 35 deletions(-) diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index ccfd8726a..a5ad5870d 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -5057,5 +5057,51 @@ describe('@source* directives', () => { '[bad] sourceField specifies unknown api A' ); }); + + const renamedSchema = gql` + extend schema + @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"]) + @link(url: "https://specs.apollo.dev/source/v0.1", import: [ + { name: "@sourceAPI", as: "@api" } + { name: "@sourceType", as: "@type" } + { name: "@sourceField", as: "@field" } + ]) + @api( + name: "not an identifier" + http: { baseURL: "https://api.a.com/v1" } + ) + { + query: Query + } + + type Query { + resources: [Resource!]! @field( + api: "not an identifier" + http: { GET: "/resources" } + ) + } + + type Resource @key(fields: "id") @type( + api: "not an identifier" + http: { GET: "/resources/{id}" } + selection: "id description" + ) { + id: ID! + description: String! + } + `; + + it('can handle the @source* directives being renamed', () => { + const result = composeServices([{ + name: 'renamed', + typeDefs: renamedSchema, + }]); + + const messages = result.errors!.map(e => e.message); + + expect(messages).toContain( + '[renamed] @api(name: "not an identifier") must be valid GraphQL identifier' + ); + }); }); }); diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 689af3b55..30b439665 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -1,5 +1,5 @@ import { DirectiveLocation, GraphQLError } from 'graphql'; -import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec"; +import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion, LinkDirectiveArgs } from "./coreSpec"; import { Schema, NonNullType, @@ -145,12 +145,44 @@ export class SourceSpecDefinition extends FeatureDefinition { return this.directive(schema, 'sourceField')!; } + private getSourceDirectives(schema: Schema) { + const result: { + sourceAPI?: DirectiveDefinition; + sourceType?: DirectiveDefinition; + sourceField?: DirectiveDefinition; + } = {}; + + schema.schemaDefinition.appliedDirectivesOf('link') + .forEach(linkDirective => { + const { url, import: imports } = linkDirective.arguments(); + if (imports && FeatureUrl.parse(url).identity === sourceIdentity) { + imports.forEach(nameOrRename => { + const originalName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.name; + const importedName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.as || originalName; + const importedNameWithoutAt = importedName.replace(/^@/, ''); + + if (originalName === '@sourceAPI') { + result.sourceAPI = schema.directive(importedNameWithoutAt) as DirectiveDefinition; + } else if (originalName === '@sourceType') { + result.sourceType = schema.directive(importedNameWithoutAt) as DirectiveDefinition; + } else if (originalName === '@sourceField') { + result.sourceField = schema.directive(importedNameWithoutAt) as DirectiveDefinition; + } + }); + } + }); + + return result; + } + override validateSubgraphSchema(schema: Schema): GraphQLError[] { - const sourceAPIInSchema = schema.directive('sourceAPI') as DirectiveDefinition | undefined; - const sourceTypeInSchema = schema.directive('sourceType') as DirectiveDefinition | undefined; - const sourceFieldInSchema = schema.directive('sourceField') as DirectiveDefinition | undefined; + const { + sourceAPI, + sourceType, + sourceField, + } = this.getSourceDirectives(schema); - if (!(sourceAPIInSchema || sourceTypeInSchema || sourceFieldInSchema)) { + if (!(sourceAPI || sourceType || sourceField)) { // If none of the @source* directives are present, nothing needs // validating. return []; @@ -159,17 +191,17 @@ export class SourceSpecDefinition extends FeatureDefinition { const errors: GraphQLError[] = []; const apiNameToProtocol = new Map(); - if (sourceAPIInSchema) { - sourceAPIInSchema.applications().forEach(application => { + if (sourceAPI) { + sourceAPI.applications().forEach(application => { const { name, ...rest } = application.arguments(); if (apiNameToProtocol.has(name)) { - errors.push(new GraphQLError(`${sourceAPIInSchema.name} must specify unique name`)); + errors.push(new GraphQLError(`${sourceAPI.name} must specify unique name`)); } // Ensure name is a valid GraphQL identifier. if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { - errors.push(new GraphQLError(`${sourceAPIInSchema}(name: ${ + errors.push(new GraphQLError(`${sourceAPI.name}(name: ${ JSON.stringify(name) }) must be valid GraphQL identifier`)); } @@ -179,7 +211,7 @@ export class SourceSpecDefinition extends FeatureDefinition { if (rest[knownProtocol]) { if (protocol) { errors.push(new GraphQLError( - `${sourceAPIInSchema.name} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceAPI.name} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, )); } protocol = knownProtocol; @@ -196,35 +228,35 @@ export class SourceSpecDefinition extends FeatureDefinition { try { new URL(baseURL); } catch (e) { - errors.push(new GraphQLError(`${sourceAPIInSchema.name} http.baseURL ${ + errors.push(new GraphQLError(`${sourceAPI.name} http.baseURL ${ JSON.stringify(baseURL) } must be valid URL`)); } - validateHTTPHeaders(headers, errors, sourceAPIInSchema.name); + validateHTTPHeaders(headers, errors, sourceAPI.name); } } else { errors.push(new GraphQLError( - `${sourceAPIInSchema.name} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceAPI.name} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, )); } }); } - if (sourceTypeInSchema) { - sourceTypeInSchema.applications().forEach(application => { + if (sourceType) { + sourceType.applications().forEach(application => { const { api, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceTypeInSchema.name} specifies unknown api ${api}`)); + errors.push(new GraphQLError(`${sourceType.name} specifies unknown api ${api}`)); } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; if (expectedProtocol && !protocolValue) { errors.push(new GraphQLError( - `${sourceTypeInSchema.name} must specify same ${ + `${sourceType.name} must specify same ${ expectedProtocol } argument as corresponding ${ - sourceAPIInSchema!.name + sourceAPI!.name } for api ${api}`, )); } @@ -234,7 +266,7 @@ export class SourceSpecDefinition extends FeatureDefinition { if ([GET, POST].filter(Boolean).length !== 1) { errors.push(new GraphQLError( - `${sourceTypeInSchema.name} must specify exactly one of http.GET or http.POST`, + `${sourceType.name} must specify exactly one of http.GET or http.POST`, )); } else { const urlPathTemplate = (GET || POST)!; @@ -244,12 +276,12 @@ export class SourceSpecDefinition extends FeatureDefinition { parseURLPathTemplate(urlPathTemplate); } catch (e) { errors.push(new GraphQLError( - `${sourceTypeInSchema.name} http.GET or http.POST must be valid URL path template`, + `${sourceType.name} http.GET or http.POST must be valid URL path template`, )); } } - validateHTTPHeaders(headers, errors, sourceTypeInSchema.name); + validateHTTPHeaders(headers, errors, sourceType.name); if (body) { try { @@ -257,7 +289,7 @@ export class SourceSpecDefinition extends FeatureDefinition { // TODO Validate body selection matches the available fields. } catch (e) { errors.push(new GraphQLError( - `${sourceTypeInSchema.name} http.body not valid JSONSelection: ${ + `${sourceType.name} http.body not valid JSONSelection: ${ e.message }`, )); @@ -272,24 +304,24 @@ export class SourceSpecDefinition extends FeatureDefinition { case "InterfaceTypeDefinition": if (!ast.directives?.some(directive => directive.name.value === "key")) { errors.push(new GraphQLError( - `${sourceTypeInSchema.name} must be applied to an entity type that also has a @key directive`, + `${sourceType.name} must be applied to an entity type that also has a @key directive`, )); } // TODO Validate selection is valid JSONSelection for type. break; default: errors.push(new GraphQLError( - `${sourceTypeInSchema.name} must be applied to object or interface type`, + `${sourceType.name} must be applied to object or interface type`, )); } }); } - if (sourceFieldInSchema) { - sourceFieldInSchema.applications().forEach(application => { + if (sourceField) { + sourceField.applications().forEach(application => { const { api, selection, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceFieldInSchema.name} specifies unknown api ${api}`)); + errors.push(new GraphQLError(`${sourceField.name} specifies unknown api ${api}`)); } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; @@ -303,7 +335,7 @@ export class SourceSpecDefinition extends FeatureDefinition { const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean); if (usedMethods.length > 1) { errors.push(new GraphQLError(`${ - sourceFieldInSchema.name + sourceField.name } allows at most one of http.GET, http.POST, http.PUT, http.PATCH, and http.DELETE`)); } else if (usedMethods.length === 1) { const urlPathTemplate = usedMethods[0]!; @@ -313,12 +345,12 @@ export class SourceSpecDefinition extends FeatureDefinition { parseURLPathTemplate(urlPathTemplate); } catch (e) { errors.push(new GraphQLError( - `${sourceFieldInSchema.name} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, + `${sourceField.name} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, )); } } - validateHTTPHeaders(headers, errors, sourceFieldInSchema.name); + validateHTTPHeaders(headers, errors, sourceField.name); if (body) { try { @@ -327,7 +359,7 @@ export class SourceSpecDefinition extends FeatureDefinition { // parent type and/or argument names of the field. } catch (e) { errors.push(new GraphQLError( - `${sourceFieldInSchema.name} http.body not valid JSONSelection: ${ + `${sourceField.name} http.body not valid JSONSelection: ${ e.message }`, )); @@ -343,7 +375,7 @@ export class SourceSpecDefinition extends FeatureDefinition { // the parent type and/or argument names of the field. } catch (e) { errors.push(new GraphQLError( - `${sourceFieldInSchema.name} selection not valid JSONSelection: ${ + `${sourceField.name} selection not valid JSONSelection: ${ e.message }`, )); @@ -355,13 +387,13 @@ export class SourceSpecDefinition extends FeatureDefinition { const fieldParent = application.parent; if (fieldParent.sourceAST?.kind !== "FieldDefinition") { errors.push(new GraphQLError( - `${sourceFieldInSchema.name} must be applied to field`, + `${sourceField.name} must be applied to field`, )); } else { const typeGrandparent = fieldParent.parent as SchemaElement; if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") { errors.push(new GraphQLError( - `${sourceFieldInSchema.name} must be applied to field of object type`, + `${sourceField.name} must be applied to field of object type`, )); } else { const typeGrandparentName = typeGrandparent.sourceAST?.name.value; @@ -371,7 +403,7 @@ export class SourceSpecDefinition extends FeatureDefinition { typeGrandparent.appliedDirectivesOf("key").length === 0 ) { errors.push(new GraphQLError( - `${sourceFieldInSchema.name} must be applied to root Query or Mutation field or field of entity type`, + `${sourceField.name} must be applied to root Query or Mutation field or field of entity type`, )); } } From 633e78787b2b98b03647e23d2a2c1744aecdbb2e Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 17 Jan 2024 13:59:00 -0500 Subject: [PATCH 06/17] Use HTTP_PROTOCOL constant instead of string literal. https://github.com/apollographql/federation/pull/2910#discussion_r1455710190 --- internals-js/src/specs/sourceSpec.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 30b439665..3a0d04711 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -222,7 +222,7 @@ export class SourceSpecDefinition extends FeatureDefinition { apiNameToProtocol.set(name, protocol); const protocolValue = rest[protocol]; - if (protocolValue && protocol === "http") { + if (protocolValue && protocol === HTTP_PROTOCOL) { const { baseURL, headers } = protocolValue as HTTPSourceAPI; try { @@ -261,7 +261,7 @@ export class SourceSpecDefinition extends FeatureDefinition { )); } - if (protocolValue && expectedProtocol === "http") { + if (protocolValue && expectedProtocol === HTTP_PROTOCOL) { const { GET, POST, headers, body } = protocolValue as HTTPSourceType; if ([GET, POST].filter(Boolean).length !== 1) { @@ -325,7 +325,7 @@ export class SourceSpecDefinition extends FeatureDefinition { } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; - if (protocolValue && expectedProtocol === "http") { + if (protocolValue && expectedProtocol === HTTP_PROTOCOL) { const { GET, POST, PUT, PATCH, DELETE, headers, @@ -438,7 +438,10 @@ function validateHTTPHeaders( } } -const KNOWN_SOURCE_PROTOCOLS = ["http"] as const; +const HTTP_PROTOCOL = "http"; +const KNOWN_SOURCE_PROTOCOLS = [ + HTTP_PROTOCOL, +] as const; type ProtocolName = (typeof KNOWN_SOURCE_PROTOCOLS)[number]; export type SourceAPIDirectiveArgs = { From 168b83b12c68e55ce5c45ed543bcefc23cb1ea9e Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 17 Jan 2024 14:11:37 -0500 Subject: [PATCH 07/17] Use @ more consistently in `@source*` directive validation errors. --- composition-js/src/__tests__/compose.test.ts | 4 +- internals-js/src/specs/sourceSpec.ts | 49 ++++++++++---------- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index a5ad5870d..79c62524c 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -5050,11 +5050,11 @@ describe('@source* directives', () => { ); expect(messages).toContain( - '[bad] sourceType specifies unknown api A' + '[bad] @sourceType specifies unknown api A' ); expect(messages).toContain( - '[bad] sourceField specifies unknown api A' + '[bad] @sourceField specifies unknown api A' ); }); diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 3a0d04711..14f1ad3ea 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -196,12 +196,12 @@ export class SourceSpecDefinition extends FeatureDefinition { const { name, ...rest } = application.arguments(); if (apiNameToProtocol.has(name)) { - errors.push(new GraphQLError(`${sourceAPI.name} must specify unique name`)); + errors.push(new GraphQLError(`${sourceAPI} must specify unique name`)); } // Ensure name is a valid GraphQL identifier. if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { - errors.push(new GraphQLError(`${sourceAPI.name}(name: ${ + errors.push(new GraphQLError(`${sourceAPI}(name: ${ JSON.stringify(name) }) must be valid GraphQL identifier`)); } @@ -211,7 +211,7 @@ export class SourceSpecDefinition extends FeatureDefinition { if (rest[knownProtocol]) { if (protocol) { errors.push(new GraphQLError( - `${sourceAPI.name} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceAPI} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, )); } protocol = knownProtocol; @@ -228,7 +228,7 @@ export class SourceSpecDefinition extends FeatureDefinition { try { new URL(baseURL); } catch (e) { - errors.push(new GraphQLError(`${sourceAPI.name} http.baseURL ${ + errors.push(new GraphQLError(`${sourceAPI} http.baseURL ${ JSON.stringify(baseURL) } must be valid URL`)); } @@ -237,7 +237,7 @@ export class SourceSpecDefinition extends FeatureDefinition { } } else { errors.push(new GraphQLError( - `${sourceAPI.name} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceAPI} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, )); } }); @@ -247,16 +247,16 @@ export class SourceSpecDefinition extends FeatureDefinition { sourceType.applications().forEach(application => { const { api, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceType.name} specifies unknown api ${api}`)); + errors.push(new GraphQLError(`${sourceType} specifies unknown api ${api}`)); } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; if (expectedProtocol && !protocolValue) { errors.push(new GraphQLError( - `${sourceType.name} must specify same ${ + `${sourceType} must specify same ${ expectedProtocol } argument as corresponding ${ - sourceAPI!.name + sourceAPI } for api ${api}`, )); } @@ -266,7 +266,7 @@ export class SourceSpecDefinition extends FeatureDefinition { if ([GET, POST].filter(Boolean).length !== 1) { errors.push(new GraphQLError( - `${sourceType.name} must specify exactly one of http.GET or http.POST`, + `${sourceType} must specify exactly one of http.GET or http.POST`, )); } else { const urlPathTemplate = (GET || POST)!; @@ -276,7 +276,7 @@ export class SourceSpecDefinition extends FeatureDefinition { parseURLPathTemplate(urlPathTemplate); } catch (e) { errors.push(new GraphQLError( - `${sourceType.name} http.GET or http.POST must be valid URL path template`, + `${sourceType} http.GET or http.POST must be valid URL path template`, )); } } @@ -289,9 +289,7 @@ export class SourceSpecDefinition extends FeatureDefinition { // TODO Validate body selection matches the available fields. } catch (e) { errors.push(new GraphQLError( - `${sourceType.name} http.body not valid JSONSelection: ${ - e.message - }`, + `${sourceType} http.body not valid JSONSelection: ${e.message}`, )); } } @@ -304,14 +302,14 @@ export class SourceSpecDefinition extends FeatureDefinition { case "InterfaceTypeDefinition": if (!ast.directives?.some(directive => directive.name.value === "key")) { errors.push(new GraphQLError( - `${sourceType.name} must be applied to an entity type that also has a @key directive`, + `${sourceType} must be applied to an entity type that also has a @key directive`, )); } // TODO Validate selection is valid JSONSelection for type. break; default: errors.push(new GraphQLError( - `${sourceType.name} must be applied to object or interface type`, + `${sourceType} must be applied to object or interface type`, )); } }); @@ -321,7 +319,7 @@ export class SourceSpecDefinition extends FeatureDefinition { sourceField.applications().forEach(application => { const { api, selection, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceField.name} specifies unknown api ${api}`)); + errors.push(new GraphQLError(`${sourceField} specifies unknown api ${api}`)); } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; @@ -335,7 +333,7 @@ export class SourceSpecDefinition extends FeatureDefinition { const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean); if (usedMethods.length > 1) { errors.push(new GraphQLError(`${ - sourceField.name + sourceField } allows at most one of http.GET, http.POST, http.PUT, http.PATCH, and http.DELETE`)); } else if (usedMethods.length === 1) { const urlPathTemplate = usedMethods[0]!; @@ -345,7 +343,7 @@ export class SourceSpecDefinition extends FeatureDefinition { parseURLPathTemplate(urlPathTemplate); } catch (e) { errors.push(new GraphQLError( - `${sourceField.name} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, + `${sourceField} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, )); } } @@ -359,9 +357,7 @@ export class SourceSpecDefinition extends FeatureDefinition { // parent type and/or argument names of the field. } catch (e) { errors.push(new GraphQLError( - `${sourceField.name} http.body not valid JSONSelection: ${ - e.message - }`, + `${sourceField} http.body not valid JSONSelection: ${e.message}`, )); } } @@ -375,7 +371,7 @@ export class SourceSpecDefinition extends FeatureDefinition { // the parent type and/or argument names of the field. } catch (e) { errors.push(new GraphQLError( - `${sourceField.name} selection not valid JSONSelection: ${ + `${sourceField} selection not valid JSONSelection: ${ e.message }`, )); @@ -387,13 +383,13 @@ export class SourceSpecDefinition extends FeatureDefinition { const fieldParent = application.parent; if (fieldParent.sourceAST?.kind !== "FieldDefinition") { errors.push(new GraphQLError( - `${sourceField.name} must be applied to field`, + `${sourceField} must be applied to field`, )); } else { const typeGrandparent = fieldParent.parent as SchemaElement; if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") { errors.push(new GraphQLError( - `${sourceField.name} must be applied to field of object type`, + `${sourceField} must be applied to field of object type`, )); } else { const typeGrandparentName = typeGrandparent.sourceAST?.name.value; @@ -403,7 +399,7 @@ export class SourceSpecDefinition extends FeatureDefinition { typeGrandparent.appliedDirectivesOf("key").length === 0 ) { errors.push(new GraphQLError( - `${sourceField.name} must be applied to root Query or Mutation field or field of entity type`, + `${sourceField} must be applied to root Query or Mutation field or field of entity type`, )); } } @@ -420,6 +416,9 @@ function validateHTTPHeaders( errors: GraphQLError[], directiveName: string, ) { + if (!directiveName.startsWith('@')) { + directiveName = '@' + directiveName; + } if (headers) { headers.forEach(({ name, as, value }, i) => { // Ensure name is a valid HTTP header name. From 34fb5dceb253911342ca431306e29df04803899b Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 17 Jan 2024 15:53:16 -0500 Subject: [PATCH 08/17] Forbid body argument for GET and DELETE requests. --- internals-js/src/specs/sourceSpec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 14f1ad3ea..5323ece7f 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -284,6 +284,12 @@ export class SourceSpecDefinition extends FeatureDefinition { validateHTTPHeaders(headers, errors, sourceType.name); if (body) { + if (GET) { + errors.push(new GraphQLError( + `${sourceType} http.GET cannot specify http.body`, + )); + } + try { parseJSONSelection(body); // TODO Validate body selection matches the available fields. @@ -351,6 +357,16 @@ export class SourceSpecDefinition extends FeatureDefinition { validateHTTPHeaders(headers, errors, sourceField.name); if (body) { + if (GET) { + errors.push(new GraphQLError( + `${sourceType} http.GET cannot specify http.body`, + )); + } else if (DELETE) { + errors.push(new GraphQLError( + `${sourceType} http.DELETE cannot specify http.body`, + )); + } + try { parseJSONSelection(body); // TODO Validate body string matches the available fields of the From 746fc4d5d16a50ab6486dec80cc0631921d09f12 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 11:44:07 -0500 Subject: [PATCH 09/17] Decompose separate validation method for each `@source*` directive. https://github.com/apollographql/federation/pull/2910#discussion_r1455688439 --- internals-js/src/specs/sourceSpec.ts | 419 +++++++++++++++------------ 1 file changed, 228 insertions(+), 191 deletions(-) diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 5323ece7f..8a743ef40 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -11,8 +11,6 @@ import { } from '../definitions'; import { registerKnownFeature } from '../knownCoreFeatures'; import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; -import { parseJSONSelection } from '../parsing/JSONSelection'; -import { parseURLPathTemplate } from '../parsing/URLPathTemplate'; export const sourceIdentity = 'https://specs.apollo.dev/source'; @@ -83,6 +81,8 @@ export class SourceSpecDefinition extends FeatureDefinition { HTTPSourceType.addField(new InputFieldDefinition('POST')).type = URLPathTemplate; HTTPSourceType.addField(new InputFieldDefinition('headers')).type = new ListType(new NonNullType(HTTPHeaderMapping)); + // Note that this body selection can only use @key fields of the type, + // because there are no field arguments to consume with @sourceType. HTTPSourceType.addField(new InputFieldDefinition('body')).type = JSONSelection; sourceType.addArgument('http', HTTPSourceType); @@ -188,242 +188,271 @@ export class SourceSpecDefinition extends FeatureDefinition { return []; } - const errors: GraphQLError[] = []; const apiNameToProtocol = new Map(); + const errors: GraphQLError[] = []; if (sourceAPI) { - sourceAPI.applications().forEach(application => { - const { name, ...rest } = application.arguments(); + this.validateSourceAPI(sourceAPI, apiNameToProtocol, errors); + } - if (apiNameToProtocol.has(name)) { - errors.push(new GraphQLError(`${sourceAPI} must specify unique name`)); - } + if (sourceType) { + this.validateSourceType(sourceType, apiNameToProtocol, errors); + } - // Ensure name is a valid GraphQL identifier. - if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { - errors.push(new GraphQLError(`${sourceAPI}(name: ${ - JSON.stringify(name) - }) must be valid GraphQL identifier`)); - } + if (sourceField) { + this.validateSourceField(sourceField, apiNameToProtocol, errors); + } - let protocol: ProtocolName | undefined; - KNOWN_SOURCE_PROTOCOLS.forEach(knownProtocol => { - if (rest[knownProtocol]) { - if (protocol) { - errors.push(new GraphQLError( - `${sourceAPI} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, - )); - } - protocol = knownProtocol; - } - }); + return errors; + } - if (protocol) { - apiNameToProtocol.set(name, protocol); + private validateSourceAPI( + sourceAPI: DirectiveDefinition, + apiNameToProtocol: Map, + errors: GraphQLError[], + ) { + sourceAPI.applications().forEach(application => { + const { name, ...rest } = application.arguments(); - const protocolValue = rest[protocol]; - if (protocolValue && protocol === HTTP_PROTOCOL) { - const { baseURL, headers } = protocolValue as HTTPSourceAPI; + if (apiNameToProtocol.has(name)) { + errors.push(new GraphQLError(`${sourceAPI} must specify unique name`)); + } - try { - new URL(baseURL); - } catch (e) { - errors.push(new GraphQLError(`${sourceAPI} http.baseURL ${ - JSON.stringify(baseURL) - } must be valid URL`)); - } + // Ensure name is a valid GraphQL identifier. + if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { + errors.push(new GraphQLError(`${sourceAPI}(name: ${ + JSON.stringify(name) + }) must be valid GraphQL identifier`)); + } - validateHTTPHeaders(headers, errors, sourceAPI.name); + let protocol: ProtocolName | undefined; + KNOWN_SOURCE_PROTOCOLS.forEach(knownProtocol => { + if (rest[knownProtocol]) { + if (protocol) { + errors.push(new GraphQLError( + `${sourceAPI} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + )); } - } else { + protocol = knownProtocol; + } + }); + + if (protocol) { + apiNameToProtocol.set(name, protocol); + + const protocolValue = rest[protocol]; + if (protocolValue && protocol === HTTP_PROTOCOL) { + const { baseURL, headers } = protocolValue as HTTPSourceAPI; + + try { + new URL(baseURL); + } catch (e) { + errors.push(new GraphQLError(`${sourceAPI} http.baseURL ${ + JSON.stringify(baseURL) + } must be valid URL`)); + } + + validateHTTPHeaders(headers, errors, sourceAPI.name); + } + } else { + errors.push(new GraphQLError( + `${sourceAPI} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + )); + } + }); + } + + private validateSourceType( + sourceType: DirectiveDefinition, + apiNameToProtocol: Map, + errors: GraphQLError[], + ) { + sourceType.applications().forEach(application => { + const { api, selection, ...rest } = application.arguments(); + if (!api || !apiNameToProtocol.has(api)) { + errors.push(new GraphQLError(`${sourceType} specifies unknown api ${api}`)); + } else { + const expectedProtocol = apiNameToProtocol.get(api); + const protocolValue = expectedProtocol && rest[expectedProtocol]; + if (expectedProtocol && !protocolValue) { errors.push(new GraphQLError( - `${sourceAPI} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + `${sourceType} must specify same ${ + expectedProtocol + } argument as corresponding @sourceAPI for api ${api}`, )); } - }); - } - if (sourceType) { - sourceType.applications().forEach(application => { - const { api, ...rest } = application.arguments(); - if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceType} specifies unknown api ${api}`)); - } else { - const expectedProtocol = apiNameToProtocol.get(api); - const protocolValue = expectedProtocol && rest[expectedProtocol]; - if (expectedProtocol && !protocolValue) { + if (protocolValue && expectedProtocol === HTTP_PROTOCOL) { + const { GET, POST, headers, body } = protocolValue as HTTPSourceType; + + if ([GET, POST].filter(Boolean).length !== 1) { errors.push(new GraphQLError( - `${sourceType} must specify same ${ - expectedProtocol - } argument as corresponding ${ - sourceAPI - } for api ${api}`, + `${sourceType} must specify exactly one of http.GET or http.POST`, )); + } else { + const urlPathTemplate = (GET || POST)!; + try { + // TODO Validate URL path template uses only available @key fields + // of the type. + parseURLPathTemplate(urlPathTemplate); + } catch (e) { + errors.push(new GraphQLError( + `${sourceType} http.GET or http.POST must be valid URL path template`, + )); + } } - if (protocolValue && expectedProtocol === HTTP_PROTOCOL) { - const { GET, POST, headers, body } = protocolValue as HTTPSourceType; + validateHTTPHeaders(headers, errors, sourceType.name); - if ([GET, POST].filter(Boolean).length !== 1) { + if (body) { + if (GET) { errors.push(new GraphQLError( - `${sourceType} must specify exactly one of http.GET or http.POST`, + `${sourceType} http.GET cannot specify http.body`, )); - } else { - const urlPathTemplate = (GET || POST)!; - try { - // TODO Validate URL path template uses only available fields of - // the type. - parseURLPathTemplate(urlPathTemplate); - } catch (e) { - errors.push(new GraphQLError( - `${sourceType} http.GET or http.POST must be valid URL path template`, - )); - } } - validateHTTPHeaders(headers, errors, sourceType.name); - - if (body) { - if (GET) { - errors.push(new GraphQLError( - `${sourceType} http.GET cannot specify http.body`, - )); - } - - try { - parseJSONSelection(body); - // TODO Validate body selection matches the available fields. - } catch (e) { - errors.push(new GraphQLError( - `${sourceType} http.body not valid JSONSelection: ${e.message}`, - )); - } + try { + parseJSONSelection(body); + // TODO Validate body selection matches the available fields. + } catch (e) { + errors.push(new GraphQLError( + `${sourceType} http.body not valid JSONSelection: ${e.message}`, + )); } } } + } - const ast = application.parent.sourceAST; - switch (ast?.kind) { - case "ObjectTypeDefinition": - case "InterfaceTypeDefinition": - if (!ast.directives?.some(directive => directive.name.value === "key")) { - errors.push(new GraphQLError( - `${sourceType} must be applied to an entity type that also has a @key directive`, - )); - } + const ast = application.parent.sourceAST; + switch (ast?.kind) { + case "ObjectTypeDefinition": + case "InterfaceTypeDefinition": + if (!ast.directives?.some(directive => directive.name.value === "key")) { + errors.push(new GraphQLError( + `${sourceType} must be applied to an entity type that also has a @key directive`, + )); + } + try { + parseJSONSelection(selection); // TODO Validate selection is valid JSONSelection for type. - break; - default: + } catch (e) { errors.push(new GraphQLError( - `${sourceType} must be applied to object or interface type`, + `${sourceType} selection not valid JSONSelection: ${e.message}`, )); - } - }); - } + } + break; + default: + errors.push(new GraphQLError( + `${sourceType} must be applied to object or interface type`, + )); + } + }); + } - if (sourceField) { - sourceField.applications().forEach(application => { - const { api, selection, ...rest } = application.arguments(); - if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceField} specifies unknown api ${api}`)); - } else { - const expectedProtocol = apiNameToProtocol.get(api); - const protocolValue = expectedProtocol && rest[expectedProtocol]; - if (protocolValue && expectedProtocol === HTTP_PROTOCOL) { - const { - GET, POST, PUT, PATCH, DELETE, - headers, - body, - } = protocolValue as HTTPSourceField; - - const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean); - if (usedMethods.length > 1) { - errors.push(new GraphQLError(`${ - sourceField - } allows at most one of http.GET, http.POST, http.PUT, http.PATCH, and http.DELETE`)); - } else if (usedMethods.length === 1) { - const urlPathTemplate = usedMethods[0]!; - try { - // TODO Validate URL path template uses only available fields of - // the type and/or argument names of the field. - parseURLPathTemplate(urlPathTemplate); - } catch (e) { - errors.push(new GraphQLError( - `${sourceField} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, - )); - } + private validateSourceField( + sourceField: DirectiveDefinition, + apiNameToProtocol: Map, + errors: GraphQLError[], + ) { + sourceField.applications().forEach(application => { + const { api, selection, ...rest } = application.arguments(); + if (!api || !apiNameToProtocol.has(api)) { + errors.push(new GraphQLError(`${sourceField} specifies unknown api ${api}`)); + } else { + const expectedProtocol = apiNameToProtocol.get(api); + const protocolValue = expectedProtocol && rest[expectedProtocol]; + if (protocolValue && expectedProtocol === HTTP_PROTOCOL) { + const { + GET, POST, PUT, PATCH, DELETE, + headers, + body, + } = protocolValue as HTTPSourceField; + + const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean); + if (usedMethods.length > 1) { + errors.push(new GraphQLError(`${ + sourceField + } allows at most one of http.GET, http.POST, http.PUT, http.PATCH, and http.DELETE`)); + } else if (usedMethods.length === 1) { + const urlPathTemplate = usedMethods[0]!; + try { + // TODO Validate URL path template uses only available fields of + // the type and/or argument names of the field. + parseURLPathTemplate(urlPathTemplate); + } catch (e) { + errors.push(new GraphQLError( + `${sourceField} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, + )); } + } - validateHTTPHeaders(headers, errors, sourceField.name); - - if (body) { - if (GET) { - errors.push(new GraphQLError( - `${sourceType} http.GET cannot specify http.body`, - )); - } else if (DELETE) { - errors.push(new GraphQLError( - `${sourceType} http.DELETE cannot specify http.body`, - )); - } - - try { - parseJSONSelection(body); - // TODO Validate body string matches the available fields of the - // parent type and/or argument names of the field. - } catch (e) { - errors.push(new GraphQLError( - `${sourceField} http.body not valid JSONSelection: ${e.message}`, - )); - } + validateHTTPHeaders(headers, errors, sourceField.name); + + if (body) { + if (GET) { + errors.push(new GraphQLError( + `${sourceField} http.GET cannot specify http.body`, + )); + } else if (DELETE) { + errors.push(new GraphQLError( + `${sourceField} http.DELETE cannot specify http.body`, + )); + } + + try { + parseJSONSelection(body); + // TODO Validate body string matches the available fields of the + // parent type and/or argument names of the field. + } catch (e) { + errors.push(new GraphQLError( + `${sourceField} http.body not valid JSONSelection: ${e.message}`, + )); } } } + } - if (selection) { - try { - parseJSONSelection(selection); - // TODO Validate selection string matches the available fields of - // the parent type and/or argument names of the field. - } catch (e) { - errors.push(new GraphQLError( - `${sourceField} selection not valid JSONSelection: ${ - e.message - }`, - )); - } + if (selection) { + try { + parseJSONSelection(selection); + // TODO Validate selection string matches the available fields of + // the parent type and/or argument names of the field. + } catch (e) { + errors.push(new GraphQLError( + `${sourceField} selection not valid JSONSelection: ${ + e.message + }`, + )); } + } - // @sourceField is allowed only on root Query and Mutation fields or - // fields of entity object types. - const fieldParent = application.parent; - if (fieldParent.sourceAST?.kind !== "FieldDefinition") { + // @sourceField is allowed only on root Query and Mutation fields or + // fields of entity object types. + const fieldParent = application.parent; + if (fieldParent.sourceAST?.kind !== "FieldDefinition") { + errors.push(new GraphQLError( + `${sourceField} must be applied to field`, + )); + } else { + const typeGrandparent = fieldParent.parent as SchemaElement; + if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") { errors.push(new GraphQLError( - `${sourceField} must be applied to field`, + `${sourceField} must be applied to field of object type`, )); } else { - const typeGrandparent = fieldParent.parent as SchemaElement; - if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") { + const typeGrandparentName = typeGrandparent.sourceAST?.name.value; + if ( + typeGrandparentName !== "Query" && + typeGrandparentName !== "Mutation" && + typeGrandparent.appliedDirectivesOf("key").length === 0 + ) { errors.push(new GraphQLError( - `${sourceField} must be applied to field of object type`, + `${sourceField} must be applied to root Query or Mutation field or field of entity type`, )); - } else { - const typeGrandparentName = typeGrandparent.sourceAST?.name.value; - if ( - typeGrandparentName !== "Query" && - typeGrandparentName !== "Mutation" && - typeGrandparent.appliedDirectivesOf("key").length === 0 - ) { - errors.push(new GraphQLError( - `${sourceField} must be applied to root Query or Mutation field or field of entity type`, - )); - } } } - }); - } - - return errors; + } + }); } } @@ -453,6 +482,14 @@ function validateHTTPHeaders( } } +function parseJSONSelection(_selection: string): any { + // TODO +} + +function parseURLPathTemplate(_template: string): any { + // TODO +} + const HTTP_PROTOCOL = "http"; const KNOWN_SOURCE_PROTOCOLS = [ HTTP_PROTOCOL, From ccbdd249b54110607e2ac349f1b2e0b0676a9c3c Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 13:44:50 -0500 Subject: [PATCH 10/17] Add `keyTypeMap` argument to `@sourceField`. --- internals-js/src/specs/sourceSpec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 8a743ef40..c22e4213b 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -102,6 +102,7 @@ export class SourceSpecDefinition extends FeatureDefinition { sourceField.repeatable = true; sourceField.addArgument('api', new NonNullType(schema.stringType())); sourceField.addArgument('selection', JSONSelection); + sourceField.addArgument('keyTypeMap', KeyTypeMap); const HTTPSourceField = schema.addType(new InputObjectType('HTTPSourceField')); HTTPSourceField.addField(new InputFieldDefinition('GET')).type = URLPathTemplate; @@ -540,6 +541,7 @@ export type SourceFieldDirectiveArgs = { api: string; http?: HTTPSourceField; selection?: JSONSelection; + keyTypeMap?: KeyTypeMap; }; export type HTTPSourceField = { From d35bc4e21890de36d982d3a3c749126717710d17 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 13:45:16 -0500 Subject: [PATCH 11/17] Use `assertName` from `graphql` package instead of hard-coded `RegExp`. https://github.com/apollographql/federation/pull/2910#discussion_r1455699763 --- composition-js/src/__tests__/compose.test.ts | 4 ++-- internals-js/src/specs/sourceSpec.ts | 16 +++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index 79c62524c..71dc542f2 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -5046,7 +5046,7 @@ describe('@source* directives', () => { const messages = result.errors!.map(e => e.message); expect(messages).toContain( - '[bad] @sourceAPI(name: "A?!") must be valid GraphQL identifier' + '[bad] @sourceAPI(name: "A?!") must be valid GraphQL name' ); expect(messages).toContain( @@ -5100,7 +5100,7 @@ describe('@source* directives', () => { const messages = result.errors!.map(e => e.message); expect(messages).toContain( - '[renamed] @api(name: "not an identifier") must be valid GraphQL identifier' + '[renamed] @api(name: "not an identifier") must be valid GraphQL name' ); }); }); diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index c22e4213b..2dbb719b3 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -1,4 +1,4 @@ -import { DirectiveLocation, GraphQLError } from 'graphql'; +import { DirectiveLocation, GraphQLError, assertName } from 'graphql'; import { FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion, LinkDirectiveArgs } from "./coreSpec"; import { Schema, @@ -219,11 +219,12 @@ export class SourceSpecDefinition extends FeatureDefinition { errors.push(new GraphQLError(`${sourceAPI} must specify unique name`)); } - // Ensure name is a valid GraphQL identifier. - if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { + try { + assertName(name); + } catch (e) { errors.push(new GraphQLError(`${sourceAPI}(name: ${ JSON.stringify(name) - }) must be valid GraphQL identifier`)); + }) must be valid GraphQL name: ${e.message}`)); } let protocol: ProtocolName | undefined; @@ -457,6 +458,11 @@ export class SourceSpecDefinition extends FeatureDefinition { } } +function isValidHTTPHeaderName(name: string): boolean { + // https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/ + return /^[a-zA-Z0-9-_]+$/.test(name); +} + function validateHTTPHeaders( headers: HTTPHeaderMapping[] | undefined, errors: GraphQLError[], @@ -468,7 +474,7 @@ function validateHTTPHeaders( if (headers) { headers.forEach(({ name, as, value }, i) => { // Ensure name is a valid HTTP header name. - if (!/^[a-zA-Z0-9-_]+$/.test(name)) { + if (!isValidHTTPHeaderName(name)) { errors.push(new GraphQLError(`${directiveName} headers[${i}].name == ${ JSON.stringify(name) } is not valid HTTP header name`)); From 5ffb16f9bc34f965189c383e5175f4880a7419c5 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 14:44:38 -0500 Subject: [PATCH 12/17] Establish known error codes for `@source*` directives. https://github.com/apollographql/federation/pull/2910#discussion_r1455698430 --- composition-js/src/__tests__/compose.test.ts | 4 +- internals-js/src/error.ts | 108 ++++++++++++++++++ internals-js/src/specs/sourceSpec.ts | 110 ++++++++++++------- 3 files changed, 181 insertions(+), 41 deletions(-) diff --git a/composition-js/src/__tests__/compose.test.ts b/composition-js/src/__tests__/compose.test.ts index 71dc542f2..94477630c 100644 --- a/composition-js/src/__tests__/compose.test.ts +++ b/composition-js/src/__tests__/compose.test.ts @@ -5046,7 +5046,7 @@ describe('@source* directives', () => { const messages = result.errors!.map(e => e.message); expect(messages).toContain( - '[bad] @sourceAPI(name: "A?!") must be valid GraphQL name' + '[bad] @sourceAPI(name: "A?!") must specify valid GraphQL name' ); expect(messages).toContain( @@ -5100,7 +5100,7 @@ describe('@source* directives', () => { const messages = result.errors!.map(e => e.message); expect(messages).toContain( - '[renamed] @api(name: "not an identifier") must be valid GraphQL name' + '[renamed] @api(name: "not an identifier") must specify valid GraphQL name' ); }); }); diff --git a/internals-js/src/error.ts b/internals-js/src/error.ts index 43e9be7c1..204ed4b14 100644 --- a/internals-js/src/error.ts +++ b/internals-js/src/error.ts @@ -555,6 +555,95 @@ const INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE = makeCodeDefinition( { addedIn: '2.3.0' }, ) +const SOURCE_API_NAME_INVALID = makeCodeDefinition( + 'SOURCE_API_NAME_INVALID', + 'Each `@sourceAPI` directive must take a unique and valid name as an argument', +); + +const SOURCE_API_PROTOCOL_INVALID = makeCodeDefinition( + 'SOURCE_API_PROTOCOL_INVALID', + 'Each `@sourceAPI` directive must specify exactly one of the known protocols', +); + +const SOURCE_API_HTTP_BASE_URL_INVALID = makeCodeDefinition( + 'SOURCE_API_HTTP_BASE_URL_INVALID', + 'The `@sourceAPI` directive must specify a valid http.baseURL', +); + +const SOURCE_HTTP_HEADERS_INVALID = makeCodeDefinition( + 'SOURCE_HTTP_HEADERS_INVALID', + 'The http.headers argument of `@source*` directives must specify valid HTTP headers', +); + +const SOURCE_TYPE_API_ERROR = makeCodeDefinition( + 'SOURCE_TYPE_API_ERROR', + 'The api argument of the @sourceType directive must match a valid @sourceAPI name', +); + +const SOURCE_TYPE_PROTOCOL_INVALID = makeCodeDefinition( + 'SOURCE_TYPE_PROTOCOL_INVALID', + 'The @sourceType directive must specify the same protocol as its corresponding @sourceAPI', +); + +const SOURCE_TYPE_HTTP_METHOD_INVALID = makeCodeDefinition( + 'SOURCE_TYPE_HTTP_METHOD_INVALID', + 'The @sourceType directive must specify exactly one of http.GET or http.POST', +); + +const SOURCE_TYPE_HTTP_PATH_INVALID = makeCodeDefinition( + 'SOURCE_TYPE_HTTP_PATH_INVALID', + 'The @sourceType directive must specify a valid URL template for http.GET or http.POST', +); + +const SOURCE_TYPE_HTTP_BODY_INVALID = makeCodeDefinition( + 'SOURCE_TYPE_HTTP_BODY_INVALID', + 'If the @sourceType specifies http.body, it must be a valid JSONSelection', +); + +const SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY = makeCodeDefinition( + 'SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY', + 'The @sourceType directive must be applied to an object or interface type that also has @key', +); + +const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition( + 'SOURCE_TYPE_SELECTION_INVALID', + 'The selection argument of the @sourceType directive must be a valid JSONSelection that outputs fields of the GraphQL type', +); + +const SOURCE_FIELD_API_ERROR = makeCodeDefinition( + 'SOURCE_FIELD_API_ERROR', + 'The api argument of the @sourceField directive must match a valid @sourceAPI name', +); + +const SOURCE_FIELD_PROTOCOL_INVALID = makeCodeDefinition( + 'SOURCE_FIELD_PROTOCOL_INVALID', + 'If @sourceField specifies a protocol, it must match the corresponding @sourceAPI protocol', +); + +const SOURCE_FIELD_HTTP_METHOD_INVALID = makeCodeDefinition( + 'SOURCE_FIELD_HTTP_METHOD_INVALID', + 'The @sourceField directive must specify at most one of http.{GET,POST,PUT,PATCH,DELETE}', +); + +const SOURCE_FIELD_HTTP_PATH_INVALID = makeCodeDefinition( + 'SOURCE_FIELD_HTTP_PATH_INVALID', + 'The @sourceField directive must specify a valid URL template for http.{GET,POST,PUT,PATCH,DELETE}', +); + +const SOURCE_FIELD_HTTP_BODY_INVALID = makeCodeDefinition( + 'SOURCE_FIELD_HTTP_BODY_INVALID', + 'If @sourceField specifies http.body, it must be a valid JSONSelection matching available arguments and fields', +); + +const SOURCE_FIELD_SELECTION_INVALID = makeCodeDefinition( + 'SOURCE_FIELD_SELECTION_INVALID', + 'The selection argument of the @sourceField directive must be a valid JSONSelection that outputs fields of the GraphQL type', +); + +const SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD = makeCodeDefinition( + 'SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD', + 'The @sourceField directive must be applied to a field of the Query or Mutation types, or of an entity type' +); export const ERROR_CATEGORIES = { DIRECTIVE_FIELDS_MISSING_EXTERNAL, @@ -643,6 +732,25 @@ export const ERRORS = { INTERFACE_OBJECT_USAGE_ERROR, INTERFACE_KEY_NOT_ON_IMPLEMENTATION, INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE, + // Errors related to @sourceAPI, @sourceType, and/or @sourceField + SOURCE_API_NAME_INVALID, + SOURCE_API_PROTOCOL_INVALID, + SOURCE_API_HTTP_BASE_URL_INVALID, + SOURCE_HTTP_HEADERS_INVALID, + SOURCE_TYPE_API_ERROR, + SOURCE_TYPE_PROTOCOL_INVALID, + SOURCE_TYPE_HTTP_METHOD_INVALID, + SOURCE_TYPE_HTTP_PATH_INVALID, + SOURCE_TYPE_HTTP_BODY_INVALID, + SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY, + SOURCE_TYPE_SELECTION_INVALID, + SOURCE_FIELD_API_ERROR, + SOURCE_FIELD_PROTOCOL_INVALID, + SOURCE_FIELD_HTTP_METHOD_INVALID, + SOURCE_FIELD_HTTP_PATH_INVALID, + SOURCE_FIELD_HTTP_BODY_INVALID, + SOURCE_FIELD_SELECTION_INVALID, + SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD, }; const codeDefByCode = Object.values(ERRORS).reduce((obj: {[code: string]: ErrorCodeDefinition}, codeDef: ErrorCodeDefinition) => { obj[codeDef.code] = codeDef; return obj; }, {}); diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index 2dbb719b3..d6f5b8789 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -11,6 +11,7 @@ import { } from '../definitions'; import { registerKnownFeature } from '../knownCoreFeatures'; import { createDirectiveSpecification } from '../directiveAndTypeSpecification'; +import { ERRORS } from '../error'; export const sourceIdentity = 'https://specs.apollo.dev/source'; @@ -216,23 +217,30 @@ export class SourceSpecDefinition extends FeatureDefinition { const { name, ...rest } = application.arguments(); if (apiNameToProtocol.has(name)) { - errors.push(new GraphQLError(`${sourceAPI} must specify unique name`)); + errors.push(ERRORS.SOURCE_API_NAME_INVALID.err( + `${sourceAPI} must specify unique name`, + { nodes: application.sourceAST }, + )); } try { assertName(name); } catch (e) { - errors.push(new GraphQLError(`${sourceAPI}(name: ${ - JSON.stringify(name) - }) must be valid GraphQL name: ${e.message}`)); + errors.push(ERRORS.SOURCE_API_NAME_INVALID.err( + `${sourceAPI}(name: ${ + JSON.stringify(name) + }) must specify valid GraphQL name`, + { nodes: application.sourceAST }, + )); } let protocol: ProtocolName | undefined; KNOWN_SOURCE_PROTOCOLS.forEach(knownProtocol => { if (rest[knownProtocol]) { if (protocol) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err( `${sourceAPI} must specify only one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + { nodes: application.sourceAST }, )); } protocol = knownProtocol; @@ -249,16 +257,18 @@ export class SourceSpecDefinition extends FeatureDefinition { try { new URL(baseURL); } catch (e) { - errors.push(new GraphQLError(`${sourceAPI} http.baseURL ${ - JSON.stringify(baseURL) - } must be valid URL`)); + errors.push(ERRORS.SOURCE_API_HTTP_BASE_URL_INVALID.err( + `${sourceAPI} http.baseURL ${JSON.stringify(baseURL)} must be valid URL`, + { nodes: application.sourceAST }, + )); } validateHTTPHeaders(headers, errors, sourceAPI.name); } } else { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err( `${sourceAPI} must specify one of ${KNOWN_SOURCE_PROTOCOLS.join(', ')}`, + { nodes: application.sourceAST }, )); } }); @@ -272,15 +282,19 @@ export class SourceSpecDefinition extends FeatureDefinition { sourceType.applications().forEach(application => { const { api, selection, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceType} specifies unknown api ${api}`)); + errors.push(ERRORS.SOURCE_TYPE_API_ERROR.err( + `${sourceType} specifies unknown api ${api}`, + { nodes: application.sourceAST }, + )); } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; if (expectedProtocol && !protocolValue) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_API_ERROR.err( `${sourceType} must specify same ${ expectedProtocol } argument as corresponding @sourceAPI for api ${api}`, + { nodes: application.sourceAST }, )); } @@ -288,8 +302,9 @@ export class SourceSpecDefinition extends FeatureDefinition { const { GET, POST, headers, body } = protocolValue as HTTPSourceType; if ([GET, POST].filter(Boolean).length !== 1) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_HTTP_METHOD_INVALID.err( `${sourceType} must specify exactly one of http.GET or http.POST`, + { nodes: application.sourceAST }, )); } else { const urlPathTemplate = (GET || POST)!; @@ -298,8 +313,8 @@ export class SourceSpecDefinition extends FeatureDefinition { // of the type. parseURLPathTemplate(urlPathTemplate); } catch (e) { - errors.push(new GraphQLError( - `${sourceType} http.GET or http.POST must be valid URL path template`, + errors.push(ERRORS.SOURCE_TYPE_HTTP_PATH_INVALID.err( + `${sourceType} http.GET or http.POST must be valid URL path template` )); } } @@ -308,8 +323,9 @@ export class SourceSpecDefinition extends FeatureDefinition { if (body) { if (GET) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err( `${sourceType} http.GET cannot specify http.body`, + { nodes: application.sourceAST }, )); } @@ -317,8 +333,9 @@ export class SourceSpecDefinition extends FeatureDefinition { parseJSONSelection(body); // TODO Validate body selection matches the available fields. } catch (e) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err( `${sourceType} http.body not valid JSONSelection: ${e.message}`, + { nodes: application.sourceAST }, )); } } @@ -330,22 +347,25 @@ export class SourceSpecDefinition extends FeatureDefinition { case "ObjectTypeDefinition": case "InterfaceTypeDefinition": if (!ast.directives?.some(directive => directive.name.value === "key")) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY.err( `${sourceType} must be applied to an entity type that also has a @key directive`, + { nodes: application.sourceAST }, )); } try { parseJSONSelection(selection); // TODO Validate selection is valid JSONSelection for type. } catch (e) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_SELECTION_INVALID.err( `${sourceType} selection not valid JSONSelection: ${e.message}`, + { nodes: application.sourceAST }, )); } break; default: - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY.err( `${sourceType} must be applied to object or interface type`, + { nodes: application.sourceAST }, )); } }); @@ -359,7 +379,10 @@ export class SourceSpecDefinition extends FeatureDefinition { sourceField.applications().forEach(application => { const { api, selection, ...rest } = application.arguments(); if (!api || !apiNameToProtocol.has(api)) { - errors.push(new GraphQLError(`${sourceField} specifies unknown api ${api}`)); + errors.push(ERRORS.SOURCE_FIELD_API_ERROR.err( + `${sourceField} specifies unknown api ${api}`, + { nodes: application.sourceAST }, + )); } else { const expectedProtocol = apiNameToProtocol.get(api); const protocolValue = expectedProtocol && rest[expectedProtocol]; @@ -372,9 +395,9 @@ export class SourceSpecDefinition extends FeatureDefinition { const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean); if (usedMethods.length > 1) { - errors.push(new GraphQLError(`${ - sourceField - } allows at most one of http.GET, http.POST, http.PUT, http.PATCH, and http.DELETE`)); + errors.push(ERRORS.SOURCE_FIELD_HTTP_METHOD_INVALID.err( + `${sourceField} allows at most one of http.{GET,POST,PUT,PATCH,DELETE}`, + )); } else if (usedMethods.length === 1) { const urlPathTemplate = usedMethods[0]!; try { @@ -382,8 +405,8 @@ export class SourceSpecDefinition extends FeatureDefinition { // the type and/or argument names of the field. parseURLPathTemplate(urlPathTemplate); } catch (e) { - errors.push(new GraphQLError( - `${sourceField} http.GET, http.POST, http.PUT, http.PATCH, or http.DELETE must be valid URL path template`, + errors.push(ERRORS.SOURCE_FIELD_HTTP_PATH_INVALID.err( + `${sourceField} http.{GET,POST,PUT,PATCH,DELETE} must be valid URL path template` )); } } @@ -392,12 +415,14 @@ export class SourceSpecDefinition extends FeatureDefinition { if (body) { if (GET) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err( `${sourceField} http.GET cannot specify http.body`, + { nodes: application.sourceAST }, )); } else if (DELETE) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err( `${sourceField} http.DELETE cannot specify http.body`, + { nodes: application.sourceAST }, )); } @@ -406,8 +431,9 @@ export class SourceSpecDefinition extends FeatureDefinition { // TODO Validate body string matches the available fields of the // parent type and/or argument names of the field. } catch (e) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err( `${sourceField} http.body not valid JSONSelection: ${e.message}`, + { nodes: application.sourceAST }, )); } } @@ -420,10 +446,9 @@ export class SourceSpecDefinition extends FeatureDefinition { // TODO Validate selection string matches the available fields of // the parent type and/or argument names of the field. } catch (e) { - errors.push(new GraphQLError( - `${sourceField} selection not valid JSONSelection: ${ - e.message - }`, + errors.push(ERRORS.SOURCE_FIELD_SELECTION_INVALID.err( + `${sourceField} selection not valid JSONSelection: ${e.message}`, + { nodes: application.sourceAST }, )); } } @@ -432,14 +457,16 @@ export class SourceSpecDefinition extends FeatureDefinition { // fields of entity object types. const fieldParent = application.parent; if (fieldParent.sourceAST?.kind !== "FieldDefinition") { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err( `${sourceField} must be applied to field`, + { nodes: application.sourceAST }, )); } else { const typeGrandparent = fieldParent.parent as SchemaElement; if (typeGrandparent.sourceAST?.kind !== "ObjectTypeDefinition") { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err( `${sourceField} must be applied to field of object type`, + { nodes: application.sourceAST }, )); } else { const typeGrandparentName = typeGrandparent.sourceAST?.name.value; @@ -448,8 +475,9 @@ export class SourceSpecDefinition extends FeatureDefinition { typeGrandparentName !== "Mutation" && typeGrandparent.appliedDirectivesOf("key").length === 0 ) { - errors.push(new GraphQLError( + errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err( `${sourceField} must be applied to root Query or Mutation field or field of entity type`, + { nodes: application.sourceAST }, )); } } @@ -475,13 +503,17 @@ function validateHTTPHeaders( headers.forEach(({ name, as, value }, i) => { // Ensure name is a valid HTTP header name. if (!isValidHTTPHeaderName(name)) { - errors.push(new GraphQLError(`${directiveName} headers[${i}].name == ${ - JSON.stringify(name) - } is not valid HTTP header name`)); + errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err( + `${directiveName} headers[${i}].name == ${ + JSON.stringify(name) + } is not valid HTTP header name`, + )); } if (!as === !value) { - errors.push(new GraphQLError(`${directiveName} headers[${i}] must specify exactly one of as or value`)); + errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err( + `${directiveName} headers[${i}] must specify exactly one of as or value`, + )); } // TODO Validate value is valid HTTP header value? From a5f9650b9d41dfcc8f2a7e7de0313025b645e246 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 15:04:03 -0500 Subject: [PATCH 13/17] Add changeset. --- .changeset/popular-mirrors-move.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/popular-mirrors-move.md diff --git a/.changeset/popular-mirrors-move.md b/.changeset/popular-mirrors-move.md new file mode 100644 index 000000000..84207ecb4 --- /dev/null +++ b/.changeset/popular-mirrors-move.md @@ -0,0 +1,6 @@ +--- +"@apollo/federation-internals": minor +"@apollo/composition": patch +--- + +Allow known `FeatureDefinition` subclasses to define custom subgraph schema validation rules From 8d6fbd72027fc1041ffb514b68102a77d41c484f Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 15:10:38 -0500 Subject: [PATCH 14/17] Add new errors to errors.md. https://github.com/apollographql/federation/pull/2910#discussion_r1455698430 --- docs/source/errors.md | 18 ++++++++++++++++++ internals-js/src/error.ts | 19 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/source/errors.md b/docs/source/errors.md index 65af3446d..f528e3aec 100644 --- a/docs/source/errors.md +++ b/docs/source/errors.md @@ -89,6 +89,24 @@ The following errors might be raised during composition: | `ROOT_SUBSCRIPTION_USED` | A subgraph's schema defines a type with the name `subscription`, while also specifying a _different_ type name as the root query object. This is not allowed. | 0.x | | | `SATISFIABILITY_ERROR` | Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs. | 2.0.0 | | | `SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES` | A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake. | 2.0.0 | | +| `SOURCE_API_HTTP_BASE_URL_INVALID` | The `@sourceAPI` directive must specify a valid http.baseURL | 2.6.0 | | +| `SOURCE_API_NAME_INVALID` | Each `@sourceAPI` directive must take a unique and valid name as an argument | 2.6.0 | | +| `SOURCE_API_PROTOCOL_INVALID` | Each `@sourceAPI` directive must specify exactly one of the known protocols | 2.6.0 | | +| `SOURCE_FIELD_API_ERROR` | The api argument of the @sourceField directive must match a valid @sourceAPI name | 2.6.0 | | +| `SOURCE_FIELD_HTTP_BODY_INVALID` | If @sourceField specifies http.body, it must be a valid JSONSelection matching available arguments and fields | 2.6.0 | | +| `SOURCE_FIELD_HTTP_METHOD_INVALID` | The @sourceField directive must specify at most one of http.{GET,POST,PUT,PATCH,DELETE} | 2.6.0 | | +| `SOURCE_FIELD_HTTP_PATH_INVALID` | The @sourceField directive must specify a valid URL template for http.{GET,POST,PUT,PATCH,DELETE} | 2.6.0 | | +| `SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD` | The @sourceField directive must be applied to a field of the Query or Mutation types, or of an entity type | 2.6.0 | | +| `SOURCE_FIELD_PROTOCOL_INVALID` | If @sourceField specifies a protocol, it must match the corresponding @sourceAPI protocol | 2.6.0 | | +| `SOURCE_FIELD_SELECTION_INVALID` | The selection argument of the @sourceField directive must be a valid JSONSelection that outputs fields of the GraphQL type | 2.6.0 | | +| `SOURCE_HTTP_HEADERS_INVALID` | The http.headers argument of `@source*` directives must specify valid HTTP headers | 2.6.0 | | +| `SOURCE_TYPE_API_ERROR` | The api argument of the @sourceType directive must match a valid @sourceAPI name | 2.6.0 | | +| `SOURCE_TYPE_HTTP_BODY_INVALID` | If the @sourceType specifies http.body, it must be a valid JSONSelection | 2.6.0 | | +| `SOURCE_TYPE_HTTP_METHOD_INVALID` | The @sourceType directive must specify exactly one of http.GET or http.POST | 2.6.0 | | +| `SOURCE_TYPE_HTTP_PATH_INVALID` | The @sourceType directive must specify a valid URL template for http.GET or http.POST | 2.6.0 | | +| `SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY` | The @sourceType directive must be applied to an object or interface type that also has @key | 2.6.0 | | +| `SOURCE_TYPE_PROTOCOL_INVALID` | The @sourceType directive must specify the same protocol as its corresponding @sourceAPI | 2.6.0 | | +| `SOURCE_TYPE_SELECTION_INVALID` | The selection argument of the @sourceType directive must be a valid JSONSelection that outputs fields of the GraphQL type | 2.0.0 | | | `TYPE_DEFINITION_INVALID` | A built-in or federation type has an invalid definition in the schema. | 2.0.0 | | | `TYPE_KIND_MISMATCH` | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface. | 2.0.0 | Replaces: `VALUE_TYPE_KIND_MISMATCH`, `EXTENSION_OF_WRONG_KIND`, `ENUM_MISMATCH_TYPE` | | `TYPE_WITH_ONLY_UNUSED_EXTERNAL` | A federation 1 schema has a composite type comprised only of unused external fields. Note that this error can _only_ be raised for federation 1 schema as federation 2 schema do not allow unused external fields (and errors with code EXTERNAL_UNUSED will be raised in that case). But when federation 1 schema are automatically migrated to federation 2 ones, unused external fields are automatically removed, and in rare case this can leave a type empty. If that happens, an error with this code will be raised | 2.0.0 | | diff --git a/internals-js/src/error.ts b/internals-js/src/error.ts index 204ed4b14..884e37648 100644 --- a/internals-js/src/error.ts +++ b/internals-js/src/error.ts @@ -558,51 +558,61 @@ const INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE = makeCodeDefinition( const SOURCE_API_NAME_INVALID = makeCodeDefinition( 'SOURCE_API_NAME_INVALID', 'Each `@sourceAPI` directive must take a unique and valid name as an argument', + { addedIn: '2.6.0' }, ); const SOURCE_API_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_API_PROTOCOL_INVALID', 'Each `@sourceAPI` directive must specify exactly one of the known protocols', + { addedIn: '2.6.0' }, ); const SOURCE_API_HTTP_BASE_URL_INVALID = makeCodeDefinition( 'SOURCE_API_HTTP_BASE_URL_INVALID', 'The `@sourceAPI` directive must specify a valid http.baseURL', + { addedIn: '2.6.0' }, ); const SOURCE_HTTP_HEADERS_INVALID = makeCodeDefinition( 'SOURCE_HTTP_HEADERS_INVALID', 'The http.headers argument of `@source*` directives must specify valid HTTP headers', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_API_ERROR = makeCodeDefinition( 'SOURCE_TYPE_API_ERROR', 'The api argument of the @sourceType directive must match a valid @sourceAPI name', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_TYPE_PROTOCOL_INVALID', 'The @sourceType directive must specify the same protocol as its corresponding @sourceAPI', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_HTTP_METHOD_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_METHOD_INVALID', 'The @sourceType directive must specify exactly one of http.GET or http.POST', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_HTTP_PATH_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_PATH_INVALID', 'The @sourceType directive must specify a valid URL template for http.GET or http.POST', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_HTTP_BODY_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_BODY_INVALID', 'If the @sourceType specifies http.body, it must be a valid JSONSelection', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY = makeCodeDefinition( 'SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY', 'The @sourceType directive must be applied to an object or interface type that also has @key', + { addedIn: '2.6.0' }, ); const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition( @@ -613,36 +623,43 @@ const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition( const SOURCE_FIELD_API_ERROR = makeCodeDefinition( 'SOURCE_FIELD_API_ERROR', 'The api argument of the @sourceField directive must match a valid @sourceAPI name', + { addedIn: '2.6.0' }, ); const SOURCE_FIELD_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_FIELD_PROTOCOL_INVALID', 'If @sourceField specifies a protocol, it must match the corresponding @sourceAPI protocol', + { addedIn: '2.6.0' }, ); const SOURCE_FIELD_HTTP_METHOD_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_METHOD_INVALID', 'The @sourceField directive must specify at most one of http.{GET,POST,PUT,PATCH,DELETE}', + { addedIn: '2.6.0' }, ); const SOURCE_FIELD_HTTP_PATH_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_PATH_INVALID', 'The @sourceField directive must specify a valid URL template for http.{GET,POST,PUT,PATCH,DELETE}', + { addedIn: '2.6.0' }, ); const SOURCE_FIELD_HTTP_BODY_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_BODY_INVALID', 'If @sourceField specifies http.body, it must be a valid JSONSelection matching available arguments and fields', + { addedIn: '2.6.0' }, ); const SOURCE_FIELD_SELECTION_INVALID = makeCodeDefinition( 'SOURCE_FIELD_SELECTION_INVALID', 'The selection argument of the @sourceField directive must be a valid JSONSelection that outputs fields of the GraphQL type', + { addedIn: '2.6.0' }, ); const SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD = makeCodeDefinition( 'SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD', - 'The @sourceField directive must be applied to a field of the Query or Mutation types, or of an entity type' + 'The @sourceField directive must be applied to a field of the Query or Mutation types, or of an entity type', + { addedIn: '2.6.0' }, ); export const ERROR_CATEGORIES = { From 768b9cad79263dea60fd754ffb3857ca5d5dcf3f Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 15:30:28 -0500 Subject: [PATCH 15/17] Improve formatting of `@source*`-related errors. --- docs/source/errors.md | 30 +++++++++++++++--------------- internals-js/src/error.ts | 30 +++++++++++++++--------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/source/errors.md b/docs/source/errors.md index f528e3aec..06d36de71 100644 --- a/docs/source/errors.md +++ b/docs/source/errors.md @@ -92,21 +92,21 @@ The following errors might be raised during composition: | `SOURCE_API_HTTP_BASE_URL_INVALID` | The `@sourceAPI` directive must specify a valid http.baseURL | 2.6.0 | | | `SOURCE_API_NAME_INVALID` | Each `@sourceAPI` directive must take a unique and valid name as an argument | 2.6.0 | | | `SOURCE_API_PROTOCOL_INVALID` | Each `@sourceAPI` directive must specify exactly one of the known protocols | 2.6.0 | | -| `SOURCE_FIELD_API_ERROR` | The api argument of the @sourceField directive must match a valid @sourceAPI name | 2.6.0 | | -| `SOURCE_FIELD_HTTP_BODY_INVALID` | If @sourceField specifies http.body, it must be a valid JSONSelection matching available arguments and fields | 2.6.0 | | -| `SOURCE_FIELD_HTTP_METHOD_INVALID` | The @sourceField directive must specify at most one of http.{GET,POST,PUT,PATCH,DELETE} | 2.6.0 | | -| `SOURCE_FIELD_HTTP_PATH_INVALID` | The @sourceField directive must specify a valid URL template for http.{GET,POST,PUT,PATCH,DELETE} | 2.6.0 | | -| `SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD` | The @sourceField directive must be applied to a field of the Query or Mutation types, or of an entity type | 2.6.0 | | -| `SOURCE_FIELD_PROTOCOL_INVALID` | If @sourceField specifies a protocol, it must match the corresponding @sourceAPI protocol | 2.6.0 | | -| `SOURCE_FIELD_SELECTION_INVALID` | The selection argument of the @sourceField directive must be a valid JSONSelection that outputs fields of the GraphQL type | 2.6.0 | | -| `SOURCE_HTTP_HEADERS_INVALID` | The http.headers argument of `@source*` directives must specify valid HTTP headers | 2.6.0 | | -| `SOURCE_TYPE_API_ERROR` | The api argument of the @sourceType directive must match a valid @sourceAPI name | 2.6.0 | | -| `SOURCE_TYPE_HTTP_BODY_INVALID` | If the @sourceType specifies http.body, it must be a valid JSONSelection | 2.6.0 | | -| `SOURCE_TYPE_HTTP_METHOD_INVALID` | The @sourceType directive must specify exactly one of http.GET or http.POST | 2.6.0 | | -| `SOURCE_TYPE_HTTP_PATH_INVALID` | The @sourceType directive must specify a valid URL template for http.GET or http.POST | 2.6.0 | | -| `SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY` | The @sourceType directive must be applied to an object or interface type that also has @key | 2.6.0 | | -| `SOURCE_TYPE_PROTOCOL_INVALID` | The @sourceType directive must specify the same protocol as its corresponding @sourceAPI | 2.6.0 | | -| `SOURCE_TYPE_SELECTION_INVALID` | The selection argument of the @sourceType directive must be a valid JSONSelection that outputs fields of the GraphQL type | 2.0.0 | | +| `SOURCE_FIELD_API_ERROR` | The `api` argument of the `@sourceField` directive must match a valid `@sourceAPI` name | 2.6.0 | | +| `SOURCE_FIELD_HTTP_BODY_INVALID` | If `@sourceField` specifies http.body, it must be a valid `JSONSelection` matching available arguments and fields | 2.6.0 | | +| `SOURCE_FIELD_HTTP_METHOD_INVALID` | The `@sourceField` directive must specify at most one of `http.{GET,POST,PUT,PATCH,DELETE}` | 2.6.0 | | +| `SOURCE_FIELD_HTTP_PATH_INVALID` | The `@sourceField` directive must specify a valid URL template for `http.{GET,POST,PUT,PATCH,DELETE}` | 2.6.0 | | +| `SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD` | The `@sourceField` directive must be applied to a field of the `Query` or `Mutation` types, or of an entity type | 2.6.0 | | +| `SOURCE_FIELD_PROTOCOL_INVALID` | If `@sourceField` specifies a protocol, it must match the corresponding `@sourceAPI` protocol | 2.6.0 | | +| `SOURCE_FIELD_SELECTION_INVALID` | The `selection` argument of the `@sourceField` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type | 2.6.0 | | +| `SOURCE_HTTP_HEADERS_INVALID` | The `http.headers` argument of `@source*` directives must specify valid HTTP headers | 2.6.0 | | +| `SOURCE_TYPE_API_ERROR` | The `api` argument of the `@sourceType` directive must match a valid `@sourceAPI` name | 2.6.0 | | +| `SOURCE_TYPE_HTTP_BODY_INVALID` | If the `@sourceType` specifies `http.body`, it must be a valid `JSONSelection` | 2.6.0 | | +| `SOURCE_TYPE_HTTP_METHOD_INVALID` | The `@sourceType` directive must specify exactly one of `http.GET` or `http.POST` | 2.6.0 | | +| `SOURCE_TYPE_HTTP_PATH_INVALID` | The `@sourceType` directive must specify a valid URL template for `http.GET` or `http.POST` | 2.6.0 | | +| `SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY` | The `@sourceType` directive must be applied to an object or interface type that also has `@key` | 2.6.0 | | +| `SOURCE_TYPE_PROTOCOL_INVALID` | The `@sourceType` directive must specify the same protocol as its corresponding `@sourceAPI` | 2.6.0 | | +| `SOURCE_TYPE_SELECTION_INVALID` | The `selection` argument of the `@sourceType` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type | 2.0.0 | | | `TYPE_DEFINITION_INVALID` | A built-in or federation type has an invalid definition in the schema. | 2.0.0 | | | `TYPE_KIND_MISMATCH` | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface. | 2.0.0 | Replaces: `VALUE_TYPE_KIND_MISMATCH`, `EXTENSION_OF_WRONG_KIND`, `ENUM_MISMATCH_TYPE` | | `TYPE_WITH_ONLY_UNUSED_EXTERNAL` | A federation 1 schema has a composite type comprised only of unused external fields. Note that this error can _only_ be raised for federation 1 schema as federation 2 schema do not allow unused external fields (and errors with code EXTERNAL_UNUSED will be raised in that case). But when federation 1 schema are automatically migrated to federation 2 ones, unused external fields are automatically removed, and in rare case this can leave a type empty. If that happens, an error with this code will be raised | 2.0.0 | | diff --git a/internals-js/src/error.ts b/internals-js/src/error.ts index 884e37648..c94578957 100644 --- a/internals-js/src/error.ts +++ b/internals-js/src/error.ts @@ -575,90 +575,90 @@ const SOURCE_API_HTTP_BASE_URL_INVALID = makeCodeDefinition( const SOURCE_HTTP_HEADERS_INVALID = makeCodeDefinition( 'SOURCE_HTTP_HEADERS_INVALID', - 'The http.headers argument of `@source*` directives must specify valid HTTP headers', + 'The `http.headers` argument of `@source*` directives must specify valid HTTP headers', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_API_ERROR = makeCodeDefinition( 'SOURCE_TYPE_API_ERROR', - 'The api argument of the @sourceType directive must match a valid @sourceAPI name', + 'The `api` argument of the `@sourceType` directive must match a valid `@sourceAPI` name', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_TYPE_PROTOCOL_INVALID', - 'The @sourceType directive must specify the same protocol as its corresponding @sourceAPI', + 'The `@sourceType` directive must specify the same protocol as its corresponding `@sourceAPI`', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_HTTP_METHOD_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_METHOD_INVALID', - 'The @sourceType directive must specify exactly one of http.GET or http.POST', + 'The `@sourceType` directive must specify exactly one of `http.GET` or `http.POST`', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_HTTP_PATH_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_PATH_INVALID', - 'The @sourceType directive must specify a valid URL template for http.GET or http.POST', + 'The `@sourceType` directive must specify a valid URL template for `http.GET` or `http.POST`', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_HTTP_BODY_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_BODY_INVALID', - 'If the @sourceType specifies http.body, it must be a valid JSONSelection', + 'If the `@sourceType` specifies `http.body`, it must be a valid `JSONSelection`', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY = makeCodeDefinition( 'SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY', - 'The @sourceType directive must be applied to an object or interface type that also has @key', + 'The `@sourceType` directive must be applied to an object or interface type that also has `@key`', { addedIn: '2.6.0' }, ); const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition( 'SOURCE_TYPE_SELECTION_INVALID', - 'The selection argument of the @sourceType directive must be a valid JSONSelection that outputs fields of the GraphQL type', + 'The `selection` argument of the `@sourceType` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type', ); const SOURCE_FIELD_API_ERROR = makeCodeDefinition( 'SOURCE_FIELD_API_ERROR', - 'The api argument of the @sourceField directive must match a valid @sourceAPI name', + 'The `api` argument of the `@sourceField` directive must match a valid `@sourceAPI` name', { addedIn: '2.6.0' }, ); const SOURCE_FIELD_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_FIELD_PROTOCOL_INVALID', - 'If @sourceField specifies a protocol, it must match the corresponding @sourceAPI protocol', + 'If `@sourceField` specifies a protocol, it must match the corresponding `@sourceAPI` protocol', { addedIn: '2.6.0' }, ); const SOURCE_FIELD_HTTP_METHOD_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_METHOD_INVALID', - 'The @sourceField directive must specify at most one of http.{GET,POST,PUT,PATCH,DELETE}', + 'The `@sourceField` directive must specify at most one of `http.{GET,POST,PUT,PATCH,DELETE}`', { addedIn: '2.6.0' }, ); const SOURCE_FIELD_HTTP_PATH_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_PATH_INVALID', - 'The @sourceField directive must specify a valid URL template for http.{GET,POST,PUT,PATCH,DELETE}', + 'The `@sourceField` directive must specify a valid URL template for `http.{GET,POST,PUT,PATCH,DELETE}`', { addedIn: '2.6.0' }, ); const SOURCE_FIELD_HTTP_BODY_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_BODY_INVALID', - 'If @sourceField specifies http.body, it must be a valid JSONSelection matching available arguments and fields', + 'If `@sourceField` specifies http.body, it must be a valid `JSONSelection` matching available arguments and fields', { addedIn: '2.6.0' }, ); const SOURCE_FIELD_SELECTION_INVALID = makeCodeDefinition( 'SOURCE_FIELD_SELECTION_INVALID', - 'The selection argument of the @sourceField directive must be a valid JSONSelection that outputs fields of the GraphQL type', + 'The `selection` argument of the `@sourceField` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type', { addedIn: '2.6.0' }, ); const SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD = makeCodeDefinition( 'SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD', - 'The @sourceField directive must be applied to a field of the Query or Mutation types, or of an entity type', + 'The `@sourceField` directive must be applied to a field of the `Query` or `Mutation` types, or of an entity type', { addedIn: '2.6.0' }, ); From a84fc2ec72eb7a7b227e450cc6c9c66aca8bb12d Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 16:06:29 -0500 Subject: [PATCH 16/17] Use `FeatureUrl.maybeParse` to avoid throwing. Analogous to 56a78160b5738aa1a78cbc0ec81484437e9d9195. --- internals-js/src/specs/sourceSpec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals-js/src/specs/sourceSpec.ts b/internals-js/src/specs/sourceSpec.ts index d6f5b8789..31765a5fd 100644 --- a/internals-js/src/specs/sourceSpec.ts +++ b/internals-js/src/specs/sourceSpec.ts @@ -157,7 +157,7 @@ export class SourceSpecDefinition extends FeatureDefinition { schema.schemaDefinition.appliedDirectivesOf('link') .forEach(linkDirective => { const { url, import: imports } = linkDirective.arguments(); - if (imports && FeatureUrl.parse(url).identity === sourceIdentity) { + if (imports && FeatureUrl.maybeParse(url)?.identity === sourceIdentity) { imports.forEach(nameOrRename => { const originalName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.name; const importedName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.as || originalName; From 0736da204b9b236c36233d9d330395b6c9a17c79 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 19 Jan 2024 16:09:20 -0500 Subject: [PATCH 17/17] Use addedIn:2.7.0 for new errors. https://github.com/apollographql/federation/pull/2910#discussion_r1459772302 --- docs/source/errors.md | 34 +++++++++++++++++----------------- internals-js/src/error.ts | 34 +++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/source/errors.md b/docs/source/errors.md index 06d36de71..87b1e8ad5 100644 --- a/docs/source/errors.md +++ b/docs/source/errors.md @@ -89,23 +89,23 @@ The following errors might be raised during composition: | `ROOT_SUBSCRIPTION_USED` | A subgraph's schema defines a type with the name `subscription`, while also specifying a _different_ type name as the root query object. This is not allowed. | 0.x | | | `SATISFIABILITY_ERROR` | Subgraphs can be merged, but the resulting supergraph API would have queries that cannot be satisfied by those subgraphs. | 2.0.0 | | | `SHAREABLE_HAS_MISMATCHED_RUNTIME_TYPES` | A shareable field return type has mismatched possible runtime types in the subgraphs in which the field is declared. As shared fields must resolve the same way in all subgraphs, this is almost surely a mistake. | 2.0.0 | | -| `SOURCE_API_HTTP_BASE_URL_INVALID` | The `@sourceAPI` directive must specify a valid http.baseURL | 2.6.0 | | -| `SOURCE_API_NAME_INVALID` | Each `@sourceAPI` directive must take a unique and valid name as an argument | 2.6.0 | | -| `SOURCE_API_PROTOCOL_INVALID` | Each `@sourceAPI` directive must specify exactly one of the known protocols | 2.6.0 | | -| `SOURCE_FIELD_API_ERROR` | The `api` argument of the `@sourceField` directive must match a valid `@sourceAPI` name | 2.6.0 | | -| `SOURCE_FIELD_HTTP_BODY_INVALID` | If `@sourceField` specifies http.body, it must be a valid `JSONSelection` matching available arguments and fields | 2.6.0 | | -| `SOURCE_FIELD_HTTP_METHOD_INVALID` | The `@sourceField` directive must specify at most one of `http.{GET,POST,PUT,PATCH,DELETE}` | 2.6.0 | | -| `SOURCE_FIELD_HTTP_PATH_INVALID` | The `@sourceField` directive must specify a valid URL template for `http.{GET,POST,PUT,PATCH,DELETE}` | 2.6.0 | | -| `SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD` | The `@sourceField` directive must be applied to a field of the `Query` or `Mutation` types, or of an entity type | 2.6.0 | | -| `SOURCE_FIELD_PROTOCOL_INVALID` | If `@sourceField` specifies a protocol, it must match the corresponding `@sourceAPI` protocol | 2.6.0 | | -| `SOURCE_FIELD_SELECTION_INVALID` | The `selection` argument of the `@sourceField` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type | 2.6.0 | | -| `SOURCE_HTTP_HEADERS_INVALID` | The `http.headers` argument of `@source*` directives must specify valid HTTP headers | 2.6.0 | | -| `SOURCE_TYPE_API_ERROR` | The `api` argument of the `@sourceType` directive must match a valid `@sourceAPI` name | 2.6.0 | | -| `SOURCE_TYPE_HTTP_BODY_INVALID` | If the `@sourceType` specifies `http.body`, it must be a valid `JSONSelection` | 2.6.0 | | -| `SOURCE_TYPE_HTTP_METHOD_INVALID` | The `@sourceType` directive must specify exactly one of `http.GET` or `http.POST` | 2.6.0 | | -| `SOURCE_TYPE_HTTP_PATH_INVALID` | The `@sourceType` directive must specify a valid URL template for `http.GET` or `http.POST` | 2.6.0 | | -| `SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY` | The `@sourceType` directive must be applied to an object or interface type that also has `@key` | 2.6.0 | | -| `SOURCE_TYPE_PROTOCOL_INVALID` | The `@sourceType` directive must specify the same protocol as its corresponding `@sourceAPI` | 2.6.0 | | +| `SOURCE_API_HTTP_BASE_URL_INVALID` | The `@sourceAPI` directive must specify a valid http.baseURL | 2.7.0 | | +| `SOURCE_API_NAME_INVALID` | Each `@sourceAPI` directive must take a unique and valid name as an argument | 2.7.0 | | +| `SOURCE_API_PROTOCOL_INVALID` | Each `@sourceAPI` directive must specify exactly one of the known protocols | 2.7.0 | | +| `SOURCE_FIELD_API_ERROR` | The `api` argument of the `@sourceField` directive must match a valid `@sourceAPI` name | 2.7.0 | | +| `SOURCE_FIELD_HTTP_BODY_INVALID` | If `@sourceField` specifies http.body, it must be a valid `JSONSelection` matching available arguments and fields | 2.7.0 | | +| `SOURCE_FIELD_HTTP_METHOD_INVALID` | The `@sourceField` directive must specify at most one of `http.{GET,POST,PUT,PATCH,DELETE}` | 2.7.0 | | +| `SOURCE_FIELD_HTTP_PATH_INVALID` | The `@sourceField` directive must specify a valid URL template for `http.{GET,POST,PUT,PATCH,DELETE}` | 2.7.0 | | +| `SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD` | The `@sourceField` directive must be applied to a field of the `Query` or `Mutation` types, or of an entity type | 2.7.0 | | +| `SOURCE_FIELD_PROTOCOL_INVALID` | If `@sourceField` specifies a protocol, it must match the corresponding `@sourceAPI` protocol | 2.7.0 | | +| `SOURCE_FIELD_SELECTION_INVALID` | The `selection` argument of the `@sourceField` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type | 2.7.0 | | +| `SOURCE_HTTP_HEADERS_INVALID` | The `http.headers` argument of `@source*` directives must specify valid HTTP headers | 2.7.0 | | +| `SOURCE_TYPE_API_ERROR` | The `api` argument of the `@sourceType` directive must match a valid `@sourceAPI` name | 2.7.0 | | +| `SOURCE_TYPE_HTTP_BODY_INVALID` | If the `@sourceType` specifies `http.body`, it must be a valid `JSONSelection` | 2.7.0 | | +| `SOURCE_TYPE_HTTP_METHOD_INVALID` | The `@sourceType` directive must specify exactly one of `http.GET` or `http.POST` | 2.7.0 | | +| `SOURCE_TYPE_HTTP_PATH_INVALID` | The `@sourceType` directive must specify a valid URL template for `http.GET` or `http.POST` | 2.7.0 | | +| `SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY` | The `@sourceType` directive must be applied to an object or interface type that also has `@key` | 2.7.0 | | +| `SOURCE_TYPE_PROTOCOL_INVALID` | The `@sourceType` directive must specify the same protocol as its corresponding `@sourceAPI` | 2.7.0 | | | `SOURCE_TYPE_SELECTION_INVALID` | The `selection` argument of the `@sourceType` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type | 2.0.0 | | | `TYPE_DEFINITION_INVALID` | A built-in or federation type has an invalid definition in the schema. | 2.0.0 | | | `TYPE_KIND_MISMATCH` | A type has the same name in different subgraphs, but a different kind. For instance, one definition is an object type but another is an interface. | 2.0.0 | Replaces: `VALUE_TYPE_KIND_MISMATCH`, `EXTENSION_OF_WRONG_KIND`, `ENUM_MISMATCH_TYPE` | diff --git a/internals-js/src/error.ts b/internals-js/src/error.ts index c94578957..25897c14b 100644 --- a/internals-js/src/error.ts +++ b/internals-js/src/error.ts @@ -558,61 +558,61 @@ const INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE = makeCodeDefinition( const SOURCE_API_NAME_INVALID = makeCodeDefinition( 'SOURCE_API_NAME_INVALID', 'Each `@sourceAPI` directive must take a unique and valid name as an argument', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_API_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_API_PROTOCOL_INVALID', 'Each `@sourceAPI` directive must specify exactly one of the known protocols', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_API_HTTP_BASE_URL_INVALID = makeCodeDefinition( 'SOURCE_API_HTTP_BASE_URL_INVALID', 'The `@sourceAPI` directive must specify a valid http.baseURL', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_HTTP_HEADERS_INVALID = makeCodeDefinition( 'SOURCE_HTTP_HEADERS_INVALID', 'The `http.headers` argument of `@source*` directives must specify valid HTTP headers', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_API_ERROR = makeCodeDefinition( 'SOURCE_TYPE_API_ERROR', 'The `api` argument of the `@sourceType` directive must match a valid `@sourceAPI` name', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_TYPE_PROTOCOL_INVALID', 'The `@sourceType` directive must specify the same protocol as its corresponding `@sourceAPI`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_HTTP_METHOD_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_METHOD_INVALID', 'The `@sourceType` directive must specify exactly one of `http.GET` or `http.POST`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_HTTP_PATH_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_PATH_INVALID', 'The `@sourceType` directive must specify a valid URL template for `http.GET` or `http.POST`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_HTTP_BODY_INVALID = makeCodeDefinition( 'SOURCE_TYPE_HTTP_BODY_INVALID', 'If the `@sourceType` specifies `http.body`, it must be a valid `JSONSelection`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY = makeCodeDefinition( 'SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY', 'The `@sourceType` directive must be applied to an object or interface type that also has `@key`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition( @@ -623,43 +623,43 @@ const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition( const SOURCE_FIELD_API_ERROR = makeCodeDefinition( 'SOURCE_FIELD_API_ERROR', 'The `api` argument of the `@sourceField` directive must match a valid `@sourceAPI` name', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_FIELD_PROTOCOL_INVALID = makeCodeDefinition( 'SOURCE_FIELD_PROTOCOL_INVALID', 'If `@sourceField` specifies a protocol, it must match the corresponding `@sourceAPI` protocol', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_FIELD_HTTP_METHOD_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_METHOD_INVALID', 'The `@sourceField` directive must specify at most one of `http.{GET,POST,PUT,PATCH,DELETE}`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_FIELD_HTTP_PATH_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_PATH_INVALID', 'The `@sourceField` directive must specify a valid URL template for `http.{GET,POST,PUT,PATCH,DELETE}`', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_FIELD_HTTP_BODY_INVALID = makeCodeDefinition( 'SOURCE_FIELD_HTTP_BODY_INVALID', 'If `@sourceField` specifies http.body, it must be a valid `JSONSelection` matching available arguments and fields', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_FIELD_SELECTION_INVALID = makeCodeDefinition( 'SOURCE_FIELD_SELECTION_INVALID', 'The `selection` argument of the `@sourceField` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); const SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD = makeCodeDefinition( 'SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD', 'The `@sourceField` directive must be applied to a field of the `Query` or `Mutation` types, or of an entity type', - { addedIn: '2.6.0' }, + { addedIn: '2.7.0' }, ); export const ERROR_CATEGORIES = {