Skip to content

Commit b739e21

Browse files
feat(gateway): Strip federation primitives during normalization (#4209)
This commit makes references to "federation primitives". This is just a way of saying all of the additions to a schema that federation requires as listed in the spec. This commit removes all federation primitives during the normalization step of composition. This simplifies the lives of all non-ApolloServer federation implementors. buildFederatedSchema goes to some lengths to provide a limited subset of SDL in the { _service { sdl } } resolver. In its current form, composition expects this format. This subset is fairly easy to achieve in JavaScript land, but isn't necessarily simple in other, non-JS graphql reference implementations. This has been an outstanding pain point for an endless number of users and can be quite simply normalized away during this step. This enables implementors to return a service's complete SDL from the { _service { sdl } } resolver without any errors. For unmanaged users, the gateway will now normalize the federation primitives away. For managed users, our backend will allow a service:push to contain federation primitives which will be normalized away in the same fashion. Fixes #3334
1 parent cc2972f commit b739e21

File tree

7 files changed

+255
-40
lines changed

7 files changed

+255
-40
lines changed

packages/apollo-federation/src/composition/__tests__/normalize.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
defaultRootOperationTypes,
44
replaceExtendedDefinitionsWithExtensions,
55
normalizeTypeDefs,
6+
stripFederationPrimitives,
67
} from '../normalize';
78
import { astSerializer } from '../../snapshotSerializers';
89

@@ -142,15 +143,160 @@ describe('SDL normalization and its respective parts', () => {
142143
});
143144
});
144145

146+
describe('stripFederationPrimitives', () => {
147+
it(`removes all federation directive definitions`, () => {
148+
const typeDefs = gql`
149+
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
150+
directive @external on FIELD_DEFINITION
151+
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
152+
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
153+
directive @extends on OBJECT | INTERFACE
154+
155+
type Query {
156+
thing: String
157+
}
158+
`;
159+
160+
expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(`
161+
type Query {
162+
thing: String
163+
}
164+
`);
165+
});
166+
167+
it(`doesn't remove custom directive definitions`, () => {
168+
const typeDefs = gql`
169+
directive @custom on OBJECT
170+
171+
type Query {
172+
thing: String
173+
}
174+
`;
175+
176+
expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(`
177+
directive @custom on OBJECT
178+
179+
type Query {
180+
thing: String
181+
}
182+
`);
183+
});
184+
185+
it(`removes all federation type definitions (scalars, unions, object types)`, () => {
186+
const typeDefs = gql`
187+
scalar _Any
188+
scalar _FieldSet
189+
190+
union _Entity
191+
192+
type _Service {
193+
sdl: String
194+
}
195+
196+
type Query {
197+
thing: String
198+
}
199+
`;
200+
201+
expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(`
202+
type Query {
203+
thing: String
204+
}
205+
`);
206+
});
207+
208+
it(`doesn't remove custom scalar, union, or object type definitions`, () => {
209+
const typeDefs = gql`
210+
scalar CustomScalar
211+
212+
type CustomType {
213+
field: String!
214+
}
215+
216+
union CustomUnion
217+
218+
type Query {
219+
thing: String
220+
}
221+
`;
222+
223+
expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(`
224+
scalar CustomScalar
225+
226+
type CustomType {
227+
field: String!
228+
}
229+
230+
union CustomUnion
231+
232+
type Query {
233+
thing: String
234+
}
235+
`);
236+
});
237+
238+
it(`removes all federation field definitions (_service, _entities)`, () => {
239+
const typeDefs = gql`
240+
type Query {
241+
_service: _Service!
242+
_entities(representations: [_Any!]!): [_Entity]!
243+
thing: String
244+
}
245+
`;
246+
247+
expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(`
248+
type Query {
249+
thing: String
250+
}
251+
`);
252+
});
253+
254+
it(`removes the Query type altogether if it has no fields left after normalization`, () => {
255+
const typeDefs = gql`
256+
type Query {
257+
_service: _Service!
258+
_entities(representations: [_Any!]!): [_Entity]!
259+
}
260+
261+
type Custom {
262+
field: String
263+
}
264+
`;
265+
266+
expect(stripFederationPrimitives(typeDefs)).toMatchInlineSnapshot(`
267+
type Custom {
268+
field: String
269+
}
270+
`);
271+
});
272+
});
273+
145274
describe('normalizeTypeDefs', () => {
146275
it('integration', () => {
147276
const typeDefsToNormalize = gql`
277+
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
278+
directive @external on FIELD_DEFINITION
279+
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
280+
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
281+
directive @extends on OBJECT | INTERFACE
282+
283+
scalar _Any
284+
scalar _FieldSet
285+
286+
union _Entity
287+
288+
type _Service {
289+
sdl: String
290+
}
291+
148292
schema {
149293
query: RootQuery
150294
mutation: RootMutation
151295
}
152296
153297
type RootQuery {
298+
_service: _Service!
299+
_entities(representations: [_Any!]!): [_Entity]!
154300
product: Product
155301
}
156302

packages/apollo-federation/src/composition/compose.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
isFederationDirective,
3131
executableDirectiveLocations,
3232
stripTypeSystemDirectivesFromTypeDefs,
33+
defaultRootOperationNameLookup,
3334
} from './utils';
3435
import {
3536
ServiceDefinition,
@@ -41,13 +42,13 @@ import { compositionRules } from './rules';
4142

4243
const EmptyQueryDefinition = {
4344
kind: Kind.OBJECT_TYPE_DEFINITION,
44-
name: { kind: Kind.NAME, value: 'Query' },
45+
name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.query },
4546
fields: [],
4647
serviceName: null,
4748
};
4849
const EmptyMutationDefinition = {
4950
kind: Kind.OBJECT_TYPE_DEFINITION,
50-
name: { kind: Kind.NAME, value: 'Mutation' },
51+
name: { kind: Kind.NAME, value: defaultRootOperationNameLookup.mutation },
5152
fields: [],
5253
serviceName: null,
5354
};
@@ -531,16 +532,9 @@ export function composeServices(services: ServiceDefinition[]) {
531532

532533
// TODO: We should fix this to take non-default operation root types in
533534
// implementing services into account.
534-
535-
const operationTypeMap = {
536-
query: 'Query',
537-
mutation: 'Mutation',
538-
subscription: 'Subscription',
539-
};
540-
541535
schema = new GraphQLSchema({
542536
...schema.toConfig(),
543-
...mapValues(operationTypeMap, typeName =>
537+
...mapValues(defaultRootOperationNameLookup, typeName =>
544538
typeName
545539
? (schema.getType(typeName) as GraphQLObjectType<any, any>)
546540
: undefined,

packages/apollo-federation/src/composition/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export * from './compose';
22
export * from './composeAndValidate';
33
export * from './types';
44
export { compositionRules } from './rules';
5-
export { defaultRootOperationNameLookup, normalizeTypeDefs } from './normalize';
5+
export { normalizeTypeDefs } from './normalize';
6+
export { defaultRootOperationNameLookup } from './utils';

packages/apollo-federation/src/composition/normalize.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@ import {
33
DocumentNode,
44
visit,
55
ObjectTypeDefinitionNode,
6+
ObjectTypeExtensionNode,
67
Kind,
7-
OperationTypeNode,
88
InterfaceTypeDefinitionNode,
9+
VisitFn,
910
} from 'graphql';
10-
import { findDirectivesOnTypeOrField, defKindToExtKind } from './utils';
11+
import {
12+
findDirectivesOnTypeOrField,
13+
defKindToExtKind,
14+
reservedRootFields,
15+
defaultRootOperationNameLookup
16+
} from './utils';
17+
import federationDirectives from '../directives';
1118

1219
export function normalizeTypeDefs(typeDefs: DocumentNode) {
13-
return defaultRootOperationTypes(
14-
replaceExtendedDefinitionsWithExtensions(typeDefs),
20+
// The order of this is important - `stripFederationPrimitives` must come after
21+
// `defaultRootOperationTypes` because it depends on the `Query` type being named
22+
// its default: `Query`.
23+
return stripFederationPrimitives(
24+
defaultRootOperationTypes(
25+
replaceExtendedDefinitionsWithExtensions(typeDefs),
26+
),
1527
);
1628
}
1729

18-
// Map of OperationTypeNode to its respective default root operation type name
19-
export const defaultRootOperationNameLookup: {
20-
[node in OperationTypeNode]: DefaultRootOperationTypeName;
21-
} = {
22-
query: 'Query',
23-
mutation: 'Mutation',
24-
subscription: 'Subscription',
25-
};
26-
2730
export function defaultRootOperationTypes(
2831
typeDefs: DocumentNode,
2932
): DocumentNode {
@@ -251,3 +254,67 @@ export function replaceExtendedDefinitionsWithExtensions(
251254

252255
return typeDefsWithExtendedTypesReplaced;
253256
}
257+
258+
// For non-ApolloServer libraries that support federation, this allows a
259+
// library to report the entire schema's SDL rather than an awkward, stripped out
260+
// subset of the schema. Generally there's no need to include the federation
261+
// primitives, but in many cases it's more difficult to exclude them.
262+
//
263+
// This removes the following from a GraphQL Document:
264+
// directives: @external, @key, @requires, @provides, @extends
265+
// scalars: _Any, _FieldSet
266+
// union: _Entity
267+
// object type: _Service
268+
// Query fields: _service, _entities
269+
export function stripFederationPrimitives(document: DocumentNode) {
270+
const typeDefinitionVisitor: VisitFn<
271+
any,
272+
ObjectTypeDefinitionNode | ObjectTypeExtensionNode
273+
> = (node) => {
274+
// Remove the `_entities` and `_service` fields from the `Query` type
275+
if (node.name.value === defaultRootOperationNameLookup.query) {
276+
const filteredFieldDefinitions = node.fields?.filter(
277+
(fieldDefinition) =>
278+
!reservedRootFields.includes(fieldDefinition.name.value),
279+
);
280+
281+
// If the 'Query' type is now empty just remove it
282+
if (!filteredFieldDefinitions || filteredFieldDefinitions.length === 0) {
283+
return null;
284+
}
285+
286+
return {
287+
...node,
288+
fields: filteredFieldDefinitions,
289+
};
290+
}
291+
292+
// Remove the _Service type from the document
293+
const isFederationType = node.name.value === '_Service';
294+
return isFederationType ? null : node;
295+
};
296+
297+
return visit(document, {
298+
// Remove all federation directive definitions from the document
299+
DirectiveDefinition(node) {
300+
const isFederationDirective = federationDirectives.some(
301+
(directive) => directive.name === node.name.value,
302+
);
303+
return isFederationDirective ? null : node;
304+
},
305+
// Remove all federation scalar definitions from the document
306+
ScalarTypeDefinition(node) {
307+
const isFederationScalar = ['_Any', '_FieldSet'].includes(
308+
node.name.value,
309+
);
310+
return isFederationScalar ? null : node;
311+
},
312+
// Remove all federation union definitions from the document
313+
UnionTypeDefinition(node) {
314+
const isFederationUnion = node.name.value === "_Entity";
315+
return isFederationUnion ? null : node;
316+
},
317+
ObjectTypeDefinition: typeDefinitionVisitor,
318+
ObjectTypeExtension: typeDefinitionVisitor,
319+
});
320+
}

packages/apollo-federation/src/composition/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ import {
2929
ASTNode,
3030
DirectiveDefinitionNode,
3131
GraphQLDirective,
32+
OperationTypeNode,
3233
} from 'graphql';
3334
import Maybe from 'graphql/tsutils/Maybe';
34-
import { ExternalFieldDefinition } from './types';
35+
import { ExternalFieldDefinition, DefaultRootOperationTypeName } from './types';
3536
import federationDirectives from '../directives';
3637

3738
export function isStringValueNode(node: any): node is StringValueNode {
@@ -549,3 +550,14 @@ export const executableDirectiveLocations = [
549550
export function isFederationDirective(directive: GraphQLDirective): boolean {
550551
return federationDirectives.some(({ name }) => name === directive.name);
551552
}
553+
554+
export const reservedRootFields = ['_service', '_entities'];
555+
556+
// Map of OperationTypeNode to its respective default root operation type name
557+
export const defaultRootOperationNameLookup: {
558+
[node in OperationTypeNode]: DefaultRootOperationTypeName;
559+
} = {
560+
query: 'Query',
561+
mutation: 'Mutation',
562+
subscription: 'Subscription',
563+
};

packages/apollo-federation/src/composition/validate/preComposition/reservedFieldUsed.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { GraphQLError, visit } from 'graphql';
22
import { ServiceDefinition } from '../../types';
3-
import { logServiceAndType, errorWithCode } from '../../utils';
4-
5-
const reservedRootFields = ['_service', '_entities'];
3+
import {
4+
logServiceAndType,
5+
errorWithCode,
6+
reservedRootFields
7+
} from '../../utils';
68

79
/**
810
* - Schemas should not define the _service or _entitites fields on the query root

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy