@apollo/federation
Advanced tools
Comparing version 0.32.0 to 0.33.0
@@ -7,3 +7,3 @@ # CHANGELOG for `@apollo/federation` | ||
- _Nothing yet! Stay tuned!_ | ||
- Add flexibility for @tag directive definition validation in subgraphs. @tag definitions are now permitted to be a subset of the spec's definition. This means that within the definition, `repeatable` is optional as are each of the directive locations. [PR #1022](https://github.com/apollographql/federation/pull/1022) | ||
@@ -10,0 +10,0 @@ ## v0.32.0 |
@@ -1,2 +0,2 @@ | ||
import { StringValueNode, NameNode, DocumentNode, DirectiveNode, GraphQLNamedType, GraphQLError, GraphQLSchema, GraphQLObjectType, GraphQLField, SelectionNode, TypeDefinitionNode, TypeExtensionNode, ASTNode, DirectiveDefinitionNode, GraphQLDirective, OperationTypeNode } from 'graphql'; | ||
import { StringValueNode, NameNode, DocumentNode, DirectiveNode, GraphQLNamedType, GraphQLError, GraphQLSchema, GraphQLObjectType, GraphQLField, SelectionNode, TypeDefinitionNode, TypeExtensionNode, ASTNode, DirectiveDefinitionNode, GraphQLDirective, OperationTypeNode, NonNullTypeNode, NamedTypeNode } from 'graphql'; | ||
import { ExternalFieldDefinition, DefaultRootOperationTypeName, Maybe, FederationType, FederationDirective, FederationField, ServiceDefinition } from './types'; | ||
@@ -6,2 +6,4 @@ import { ASTNodeWithDirectives } from '../directives'; | ||
export declare function isDirectiveDefinitionNode(node: any): node is DirectiveDefinitionNode; | ||
export declare function isNonNullTypeNode(node: any): node is NonNullTypeNode; | ||
export declare function isNamedTypeNode(node: any): node is NamedTypeNode; | ||
export declare function mapFieldNamesToServiceName<Node extends { | ||
@@ -17,3 +19,2 @@ name: NameNode; | ||
}; | ||
export declare function stripDescriptions(astNode: ASTNode): any; | ||
export declare function stripTypeSystemDirectivesFromTypeDefs(typeDefs: DocumentNode): DocumentNode; | ||
@@ -20,0 +21,0 @@ export declare function parseSelections(source: string): ReadonlyArray<SelectionNode>; |
@@ -22,3 +22,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.getFederationMetadata = exports.assertCompositionFailure = exports.assertCompositionSuccess = exports.compositionHasErrors = exports.defaultRootOperationNameLookup = exports.reservedRootFields = exports.isFederationDirective = exports.isApolloTypeSystemDirective = exports.executableDirectiveLocations = exports.defKindToExtKind = exports.findTypeNodeInServiceList = exports.typeNodesAreEquivalent = exports.diffTypeNodes = exports.isTypeNodeAnEntity = exports.selectionIncludesField = exports.findFieldsThatReturnType = exports.findTypesContainingFieldWithReturnType = exports.errorWithCode = exports.logDirective = exports.logServiceAndType = exports.hasMatchingFieldInDirectives = exports.parseSelections = exports.stripTypeSystemDirectivesFromTypeDefs = exports.stripDescriptions = exports.stripExternalFieldsFromTypeDefs = exports.findSelectionSetOnNode = exports.printFieldSet = exports.findDirectivesOnNode = exports.mapFieldNamesToServiceName = exports.isDirectiveDefinitionNode = exports.isStringValueNode = void 0; | ||
exports.getFederationMetadata = exports.assertCompositionFailure = exports.assertCompositionSuccess = exports.compositionHasErrors = exports.defaultRootOperationNameLookup = exports.reservedRootFields = exports.isFederationDirective = exports.isApolloTypeSystemDirective = exports.executableDirectiveLocations = exports.defKindToExtKind = exports.findTypeNodeInServiceList = exports.typeNodesAreEquivalent = exports.diffTypeNodes = exports.isTypeNodeAnEntity = exports.selectionIncludesField = exports.findFieldsThatReturnType = exports.findTypesContainingFieldWithReturnType = exports.errorWithCode = exports.logDirective = exports.logServiceAndType = exports.hasMatchingFieldInDirectives = exports.parseSelections = exports.stripTypeSystemDirectivesFromTypeDefs = exports.stripExternalFieldsFromTypeDefs = exports.findSelectionSetOnNode = exports.printFieldSet = exports.findDirectivesOnNode = exports.mapFieldNamesToServiceName = exports.isNamedTypeNode = exports.isNonNullTypeNode = exports.isDirectiveDefinitionNode = exports.isStringValueNode = void 0; | ||
const graphql_1 = require("graphql"); | ||
@@ -35,2 +35,10 @@ const directives_1 = __importStar(require("../directives")); | ||
exports.isDirectiveDefinitionNode = isDirectiveDefinitionNode; | ||
function isNonNullTypeNode(node) { | ||
return node.kind === graphql_1.Kind.NON_NULL_TYPE; | ||
} | ||
exports.isNonNullTypeNode = isNonNullTypeNode; | ||
function isNamedTypeNode(node) { | ||
return node.kind === graphql_1.Kind.NAMED_TYPE; | ||
} | ||
exports.isNamedTypeNode = isNamedTypeNode; | ||
function mapFieldNamesToServiceName(fields, serviceName) { | ||
@@ -72,10 +80,2 @@ return fields.reduce((prev, next) => { | ||
exports.stripExternalFieldsFromTypeDefs = stripExternalFieldsFromTypeDefs; | ||
function stripDescriptions(astNode) { | ||
return (0, graphql_1.visit)(astNode, { | ||
enter(node) { | ||
return 'description' in node ? { ...node, description: undefined } : node; | ||
}, | ||
}); | ||
} | ||
exports.stripDescriptions = stripDescriptions; | ||
function stripTypeSystemDirectivesFromTypeDefs(typeDefs) { | ||
@@ -82,0 +82,0 @@ const typeDefsWithoutTypeSystemDirectives = (0, graphql_1.visit)(typeDefs, { |
@@ -27,9 +27,9 @@ "use strict"; | ||
}); | ||
if (tagDirectiveDefinition) { | ||
const printedTagDefinition = 'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'; | ||
if ((0, graphql_1.print)(normalizeDirectiveDefinitionNode(tagDirectiveDefinition)) !== | ||
printedTagDefinition) { | ||
errors.push((0, utils_1.errorWithCode)('TAG_DIRECTIVE_DEFINITION_INVALID', (0, utils_1.logDirective)('tag') + | ||
`Found @tag definition in service ${serviceName}, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions matches the following:\n\t${printedTagDefinition}`, tagDirectiveDefinition)); | ||
} | ||
const printedTagDefinition = 'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'; | ||
const parsedTagDefinition = (0, graphql_1.parse)(printedTagDefinition) | ||
.definitions[0]; | ||
if (tagDirectiveDefinition && | ||
!(0, directives_1.directiveDefinitionsAreCompatible)(parsedTagDefinition, tagDirectiveDefinition)) { | ||
errors.push((0, utils_1.errorWithCode)('TAG_DIRECTIVE_DEFINITION_INVALID', (0, utils_1.logDirective)('tag') + | ||
`Found @tag definition in service ${serviceName}, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions is compatible with the following:\n\t${printedTagDefinition}`, tagDirectiveDefinition)); | ||
} | ||
@@ -39,7 +39,2 @@ return errors.filter(({ message }) => !errorsMessagesToFilter.some((keyWord) => message === keyWord)); | ||
exports.tagDirective = tagDirective; | ||
function normalizeDirectiveDefinitionNode(node) { | ||
node = (0, utils_1.stripDescriptions)(node); | ||
node.locations.sort((a, b) => a.value.localeCompare(b.value)); | ||
return node; | ||
} | ||
//# sourceMappingURL=tagDirective.js.map |
@@ -1,2 +0,2 @@ | ||
import { GraphQLDirective, GraphQLNamedType, GraphQLInputObjectType, DirectiveNode, GraphQLField, FieldDefinitionNode, InputValueDefinitionNode, SchemaDefinitionNode, TypeSystemExtensionNode, TypeDefinitionNode, ExecutableDefinitionNode } from 'graphql'; | ||
import { GraphQLDirective, GraphQLNamedType, GraphQLInputObjectType, DirectiveNode, GraphQLField, FieldDefinitionNode, InputValueDefinitionNode, SchemaDefinitionNode, TypeSystemExtensionNode, TypeDefinitionNode, ExecutableDefinitionNode, DirectiveDefinitionNode } from 'graphql'; | ||
export declare const KeyDirective: GraphQLDirective; | ||
@@ -16,2 +16,3 @@ export declare const ExtendsDirective: GraphQLDirective; | ||
export declare function typeIncludesDirective(type: GraphQLNamedType, directiveName: string): boolean; | ||
export declare function directiveDefinitionsAreCompatible(baseDefinition: DirectiveDefinitionNode, toCompare: DirectiveDefinitionNode): boolean; | ||
//# sourceMappingURL=directives.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.typeIncludesDirective = exports.gatherDirectives = exports.otherKnownDirectiveDefinitions = exports.federationDirectives = exports.TagDirective = exports.ProvidesDirective = exports.RequiresDirective = exports.ExternalDirective = exports.ExtendsDirective = exports.KeyDirective = void 0; | ||
exports.directiveDefinitionsAreCompatible = exports.typeIncludesDirective = exports.gatherDirectives = exports.otherKnownDirectiveDefinitions = exports.federationDirectives = exports.TagDirective = exports.ProvidesDirective = exports.RequiresDirective = exports.ExternalDirective = exports.ExtendsDirective = exports.KeyDirective = void 0; | ||
const graphql_1 = require("graphql"); | ||
@@ -89,5 +89,33 @@ exports.KeyDirective = new graphql_1.GraphQLDirective({ | ||
const directives = gatherDirectives(type); | ||
return directives.some(directive => directive.name.value === directiveName); | ||
return directives.some((directive) => directive.name.value === directiveName); | ||
} | ||
exports.typeIncludesDirective = typeIncludesDirective; | ||
function directiveDefinitionsAreCompatible(baseDefinition, toCompare) { | ||
var _a, _b, _c, _d; | ||
if (baseDefinition.name.value !== toCompare.name.value) | ||
return false; | ||
if (((_a = baseDefinition.arguments) === null || _a === void 0 ? void 0 : _a.length) !== ((_b = toCompare.arguments) === null || _b === void 0 ? void 0 : _b.length)) { | ||
return false; | ||
} | ||
for (const arg of (_c = baseDefinition.arguments) !== null && _c !== void 0 ? _c : []) { | ||
const toCompareArg = (_d = toCompare.arguments) === null || _d === void 0 ? void 0 : _d.find((a) => a.name.value === arg.name.value); | ||
if (!toCompareArg) | ||
return false; | ||
if ((0, graphql_1.print)(stripDescriptions(arg)) !== (0, graphql_1.print)(stripDescriptions(toCompareArg))) { | ||
return false; | ||
} | ||
} | ||
if (toCompare.locations.some((location) => !baseDefinition.locations.find((baseLocation) => baseLocation.value === location.value))) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
exports.directiveDefinitionsAreCompatible = directiveDefinitionsAreCompatible; | ||
function stripDescriptions(astNode) { | ||
return (0, graphql_1.visit)(astNode, { | ||
enter(node) { | ||
return 'description' in node ? { ...node, description: undefined } : node; | ||
}, | ||
}); | ||
} | ||
//# sourceMappingURL=directives.js.map |
{ | ||
"name": "@apollo/federation", | ||
"version": "0.32.0", | ||
"version": "0.33.0", | ||
"description": "Apollo Federation Utilities", | ||
@@ -33,3 +33,3 @@ "main": "dist/index.js", | ||
}, | ||
"gitHead": "ef410068ac1925c370e79c3d1b94e4217e6e21a6" | ||
"gitHead": "1074f2af34afd1a59ad59fbc7965d926f8b42dbc" | ||
} |
@@ -35,2 +35,4 @@ import { | ||
stripIgnoredCharacters, | ||
NonNullTypeNode, | ||
NamedTypeNode, | ||
} from 'graphql'; | ||
@@ -60,2 +62,10 @@ import { | ||
export function isNonNullTypeNode(node: any): node is NonNullTypeNode { | ||
return node.kind === Kind.NON_NULL_TYPE; | ||
} | ||
export function isNamedTypeNode(node: any): node is NamedTypeNode { | ||
return node.kind === Kind.NAMED_TYPE; | ||
} | ||
// Create a map of { fieldName: serviceName } for each field. | ||
@@ -144,10 +154,2 @@ export function mapFieldNamesToServiceName<Node extends { name: NameNode }>( | ||
export function stripDescriptions(astNode: ASTNode) { | ||
return visit(astNode, { | ||
enter(node) { | ||
return 'description' in node ? { ...node, description: undefined } : node; | ||
}, | ||
}); | ||
} | ||
export function stripTypeSystemDirectivesFromTypeDefs(typeDefs: DocumentNode) { | ||
@@ -154,0 +156,0 @@ const typeDefsWithoutTypeSystemDirectives = visit(typeDefs, { |
@@ -68,2 +68,17 @@ import { tagDirective } from '..'; | ||
}); | ||
it('permits alternative, compatible @tag definitions', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
directive @tag(name: String!) on FIELD_DEFINITION | INTERFACE | ||
type Query { | ||
hello: String @tag(name: "hello") | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
const errors = tagDirective(serviceA); | ||
expect(errors).toHaveLength(0); | ||
}); | ||
}); | ||
@@ -103,31 +118,126 @@ | ||
it('when @tag usage and definition exist, but definition is incorrect', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
directive @tag(name: String!) on FIELD_DEFINITION | ||
describe('incompatible definition', () => { | ||
it('locations incompatible', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
directive @tag(name: String!) on FIELD_DEFINITION | SCHEMA | ||
type Query { | ||
hello: String @tag(name: "hello") | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
type Query { | ||
hello: String @tag(name: "hello") | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
const errors = tagDirective(serviceA); | ||
const errors = tagDirective(serviceA); | ||
expect(errors).toMatchInlineSnapshot(` | ||
Array [ | ||
Object { | ||
"code": "TAG_DIRECTIVE_DEFINITION_INVALID", | ||
"locations": Array [ | ||
Object { | ||
"column": 1, | ||
"line": 2, | ||
}, | ||
], | ||
"message": "[@tag] -> Found @tag definition in service serviceA, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions matches the following: | ||
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION", | ||
}, | ||
] | ||
`); | ||
expect(errors).toMatchInlineSnapshot(` | ||
Array [ | ||
Object { | ||
"code": "TAG_DIRECTIVE_DEFINITION_INVALID", | ||
"locations": Array [ | ||
Object { | ||
"column": 1, | ||
"line": 2, | ||
}, | ||
], | ||
"message": "[@tag] -> Found @tag definition in service serviceA, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions is compatible with the following: | ||
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION", | ||
}, | ||
] | ||
`); | ||
}); | ||
it('name arg missing', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
directive @tag on FIELD_DEFINITION | ||
type Query { | ||
hello: String @tag | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
const errors = tagDirective(serviceA); | ||
expect(errors).toMatchInlineSnapshot(` | ||
Array [ | ||
Object { | ||
"code": "TAG_DIRECTIVE_DEFINITION_INVALID", | ||
"locations": Array [ | ||
Object { | ||
"column": 1, | ||
"line": 2, | ||
}, | ||
], | ||
"message": "[@tag] -> Found @tag definition in service serviceA, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions is compatible with the following: | ||
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION", | ||
}, | ||
] | ||
`); | ||
}); | ||
it('name arg incompatible', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
directive @tag(name: String) on FIELD_DEFINITION | ||
type Query { | ||
hello: String @tag(name: "hello") | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
const errors = tagDirective(serviceA); | ||
expect(errors).toMatchInlineSnapshot(` | ||
Array [ | ||
Object { | ||
"code": "TAG_DIRECTIVE_DEFINITION_INVALID", | ||
"locations": Array [ | ||
Object { | ||
"column": 1, | ||
"line": 2, | ||
}, | ||
], | ||
"message": "[@tag] -> Found @tag definition in service serviceA, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions is compatible with the following: | ||
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION", | ||
}, | ||
] | ||
`); | ||
}); | ||
it('additional args', () => { | ||
const serviceA = { | ||
typeDefs: gql` | ||
directive @tag(name: String!, additional: String) on FIELD_DEFINITION | ||
type Query { | ||
hello: String @tag(name: "hello") | ||
} | ||
`, | ||
name: 'serviceA', | ||
}; | ||
const errors = tagDirective(serviceA); | ||
expect(errors).toMatchInlineSnapshot(` | ||
Array [ | ||
Object { | ||
"code": "TAG_DIRECTIVE_DEFINITION_INVALID", | ||
"locations": Array [ | ||
Object { | ||
"column": 1, | ||
"line": 2, | ||
}, | ||
], | ||
"message": "[@tag] -> Found @tag definition in service serviceA, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions is compatible with the following: | ||
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION", | ||
}, | ||
] | ||
`); | ||
}); | ||
}); | ||
@@ -134,0 +244,0 @@ |
@@ -1,3 +0,6 @@ | ||
import { federationDirectives } from '../../../directives'; | ||
import { | ||
directiveDefinitionsAreCompatible, | ||
federationDirectives, | ||
} from '../../../directives'; | ||
import { | ||
DirectiveDefinitionNode, | ||
@@ -7,4 +10,3 @@ KnownDirectivesRule, | ||
BREAK, | ||
print, | ||
NameNode, | ||
parse, | ||
} from 'graphql'; | ||
@@ -15,3 +17,3 @@ import { KnownArgumentNamesOnDirectivesRule } from 'graphql/validation/rules/KnownArgumentNamesRule'; | ||
import { ServiceDefinition } from '../../types'; | ||
import { errorWithCode, logDirective, stripDescriptions } from '../../utils'; | ||
import { errorWithCode, logDirective } from '../../utils'; | ||
@@ -52,20 +54,22 @@ // Likely brittle but also will be very obvious if this breaks. Based on the | ||
// Ensure the tag directive definition is correct | ||
if (tagDirectiveDefinition) { | ||
const printedTagDefinition = | ||
'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'; | ||
const printedTagDefinition = | ||
'directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION'; | ||
const parsedTagDefinition = parse(printedTagDefinition) | ||
.definitions[0] as DirectiveDefinitionNode; | ||
if ( | ||
print(normalizeDirectiveDefinitionNode(tagDirectiveDefinition)) !== | ||
printedTagDefinition | ||
) { | ||
errors.push( | ||
errorWithCode( | ||
'TAG_DIRECTIVE_DEFINITION_INVALID', | ||
logDirective('tag') + | ||
`Found @tag definition in service ${serviceName}, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions matches the following:\n\t${printedTagDefinition}`, | ||
tagDirectiveDefinition, | ||
), | ||
); | ||
} | ||
if ( | ||
tagDirectiveDefinition && | ||
!directiveDefinitionsAreCompatible( | ||
parsedTagDefinition, | ||
tagDirectiveDefinition, | ||
) | ||
) { | ||
errors.push( | ||
errorWithCode( | ||
'TAG_DIRECTIVE_DEFINITION_INVALID', | ||
logDirective('tag') + | ||
`Found @tag definition in service ${serviceName}, but the @tag directive definition was invalid. Please ensure the directive definition in your schema's type definitions is compatible with the following:\n\t${printedTagDefinition}`, | ||
tagDirectiveDefinition, | ||
), | ||
); | ||
} | ||
@@ -78,9 +82,1 @@ | ||
}; | ||
function normalizeDirectiveDefinitionNode(node: DirectiveDefinitionNode) { | ||
// Remove descriptions from the AST | ||
node = stripDescriptions(node); | ||
// Sort locations alphabetically | ||
(node.locations as NameNode[]).sort((a, b) => a.value.localeCompare(b.value)); | ||
return node; | ||
} |
@@ -17,2 +17,6 @@ import { | ||
ExecutableDefinitionNode, | ||
DirectiveDefinitionNode, | ||
print, | ||
ASTNode, | ||
visit, | ||
} from 'graphql'; | ||
@@ -139,3 +143,46 @@ | ||
const directives = gatherDirectives(type as GraphQLNamedTypeWithDirectives); | ||
return directives.some(directive => directive.name.value === directiveName); | ||
return directives.some((directive) => directive.name.value === directiveName); | ||
} | ||
export function directiveDefinitionsAreCompatible( | ||
baseDefinition: DirectiveDefinitionNode, | ||
toCompare: DirectiveDefinitionNode, | ||
) { | ||
if (baseDefinition.name.value !== toCompare.name.value) return false; | ||
// arguments must be equal in length | ||
if (baseDefinition.arguments?.length !== toCompare.arguments?.length) { | ||
return false; | ||
} | ||
// arguments must be equal in type | ||
for (const arg of baseDefinition.arguments ?? []) { | ||
const toCompareArg = toCompare.arguments?.find( | ||
(a) => a.name.value === arg.name.value, | ||
); | ||
if (!toCompareArg) return false; | ||
if ( | ||
print(stripDescriptions(arg)) !== print(stripDescriptions(toCompareArg)) | ||
) { | ||
return false; | ||
} | ||
} | ||
// toCompare's locations must exist in baseDefinition's locations | ||
if ( | ||
toCompare.locations.some( | ||
(location) => | ||
!baseDefinition.locations.find( | ||
(baseLocation) => baseLocation.value === location.value, | ||
), | ||
) | ||
) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
function stripDescriptions(astNode: ASTNode) { | ||
return visit(astNode, { | ||
enter(node) { | ||
return 'description' in node ? { ...node, description: undefined } : node; | ||
}, | ||
}); | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
853352
18530