Skip to content

Commit 74581cf

Browse files
authored
feat: support Gatsby-style directives in extensions (#3185)
* feat: support Gatsby-style directives in extensions BREAKING CHANGE: getDirectives now always return an array of DirectiveAnnotation objects New function getDirective returns an array of args records for each use of the directive. Note: this is true even when the directive is non-repeatable. This is because one use of this function is to throw an error if more than one directive annotation is used for a non repeatable directive! * add changeset
1 parent c5342de commit 74581cf

File tree

11 files changed

+482
-343
lines changed

11 files changed

+482
-343
lines changed

.changeset/quick-hotels-beam.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@graphql-tools/stitch': major
3+
'@graphql-tools/stitching-directives': major
4+
'@graphql-tools/utils': major
5+
'@graphql-tools/wrap': major
6+
---
7+
8+
fix(getDirectives): preserve order around repeatable directives
9+
10+
BREAKING CHANGE: getDirectives now always return an array of individual DirectiveAnnotation objects consisting of `name` and `args` properties.
11+
12+
New useful function `getDirective` returns an array of objects representing any args for each use of a single directive (returning the empty object `{}` when a directive is used without arguments).
13+
14+
Note: The `getDirective` function returns an array even when the specified directive is non-repeatable. This is because one use of this function is to throw an error if more than one directive annotation is used for a non repeatable directive!
15+
16+
When specifying directives in extensions, one can use either the old or new format.

packages/stitch/src/subschemaConfigTransforms/computedDirectiveTransformer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getDirectives, MapperKind, mapSchema } from '@graphql-tools/utils';
1+
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
22
import { cloneSubschemaConfig, SubschemaConfig } from '@graphql-tools/delegate';
33

44
import { SubschemaConfigTransform } from '../types';
@@ -15,13 +15,13 @@ export function computedDirectiveTransformer(computedDirectiveName: string): Sub
1515
return undefined;
1616
}
1717

18-
const computed = getDirectives(schema, fieldConfig)[computedDirectiveName];
18+
const computed = getDirective(schema, fieldConfig, computedDirectiveName)?.[0];
1919

2020
if (computed == null) {
2121
return undefined;
2222
}
2323

24-
const selectionSet = computed.fields != null ? `{ ${computed.fields} }` : computed.selectionSet;
24+
const selectionSet = computed['fields'] != null ? `{ ${computed['fields']} }` : computed['selectionSet'];
2525

2626
if (selectionSet == null) {
2727
return undefined;

packages/stitch/tests/mergeDefinitions.test.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { makeExecutableSchema } from '@graphql-tools/schema';
22
import { stitchSchemas } from '@graphql-tools/stitch';
3-
import { getDirectives } from '@graphql-tools/utils';
3+
import { getDirective } from '@graphql-tools/utils';
44
import { stitchingDirectives } from '@graphql-tools/stitching-directives';
55
import {
66
GraphQLObjectType,
@@ -282,22 +282,22 @@ describe('merge canonical types', () => {
282282
const scalarType = gatewaySchema.getType('ProductScalar');
283283
assertGraphQLScalerType(scalarType)
284284

285-
expect(getDirectives(firstSchema, queryType.toConfig())['mydir'].value).toEqual('first');
286-
expect(getDirectives(firstSchema, objectType.toConfig())['mydir'].value).toEqual('first');
287-
expect(getDirectives(firstSchema, interfaceType.toConfig())['mydir'].value).toEqual('first');
288-
expect(getDirectives(firstSchema, inputType.toConfig())['mydir'].value).toEqual('first');
289-
expect(getDirectives(firstSchema, enumType.toConfig())['mydir'].value).toEqual('first');
290-
expect(getDirectives(firstSchema, unionType.toConfig())['mydir'].value).toEqual('first');
291-
expect(getDirectives(firstSchema, scalarType.toConfig())['mydir'].value).toEqual('first');
292-
293-
expect(getDirectives(firstSchema, queryType.getFields()['field1'])['mydir'].value).toEqual('first');
294-
expect(getDirectives(firstSchema, queryType.getFields()['field2'])['mydir'].value).toEqual('second');
295-
expect(getDirectives(firstSchema, objectType.getFields()['id'])['mydir'].value).toEqual('first');
296-
expect(getDirectives(firstSchema, objectType.getFields()['url'])['mydir'].value).toEqual('second');
297-
expect(getDirectives(firstSchema, interfaceType.getFields()['id'])['mydir'].value).toEqual('first');
298-
expect(getDirectives(firstSchema, interfaceType.getFields()['url'])['mydir'].value).toEqual('second');
299-
expect(getDirectives(firstSchema, inputType.getFields()['id'])['mydir'].value).toEqual('first');
300-
expect(getDirectives(firstSchema, inputType.getFields()['url'])['mydir'].value).toEqual('second');
285+
expect(getDirective(firstSchema, queryType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
286+
expect(getDirective(firstSchema, objectType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
287+
expect(getDirective(firstSchema, interfaceType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
288+
expect(getDirective(firstSchema, inputType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
289+
expect(getDirective(firstSchema, enumType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
290+
expect(getDirective(firstSchema, unionType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
291+
expect(getDirective(firstSchema, scalarType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
292+
293+
expect(getDirective(firstSchema, queryType.getFields()['field1'], 'mydir')?.[0]['value']).toEqual('first');
294+
expect(getDirective(firstSchema, queryType.getFields()['field2'], 'mydir')?.[0]['value']).toEqual('second');
295+
expect(getDirective(firstSchema, objectType.getFields()['id'], 'mydir')?.[0]['value']).toEqual('first');
296+
expect(getDirective(firstSchema, objectType.getFields()['url'], 'mydir')?.[0]['value']).toEqual('second');
297+
expect(getDirective(firstSchema, interfaceType.getFields()['id'], 'mydir')?.[0]['value']).toEqual('first');
298+
expect(getDirective(firstSchema, interfaceType.getFields()['url'], 'mydir')?.[0]['value']).toEqual('second');
299+
expect(getDirective(firstSchema, inputType.getFields()['id'], 'mydir')?.[0]['value']).toEqual('first');
300+
expect(getDirective(firstSchema, inputType.getFields()['url'], 'mydir')?.[0]['value']).toEqual('second');
301301

302302
expect(enumType.toConfig().astNode?.values?.map(v => v.description?.value)).toEqual(['first', 'first', 'second']);
303303
expect(enumType.toConfig().values['YES'].astNode?.description?.value).toEqual('first');
@@ -309,8 +309,8 @@ describe('merge canonical types', () => {
309309
const objectType = gatewaySchema.getType('Product') as GraphQLObjectType;
310310
expect(objectType.getFields()['id'].deprecationReason).toEqual('first');
311311
expect(objectType.getFields()['url'].deprecationReason).toEqual('second');
312-
expect(getDirectives(firstSchema, objectType.getFields()['id'])['deprecated'].reason).toEqual('first');
313-
expect(getDirectives(firstSchema, objectType.getFields()['url'])['deprecated'].reason).toEqual('second');
312+
expect(getDirective(firstSchema, objectType.getFields()['id'], 'deprecated')?.[0]['reason']).toEqual('first');
313+
expect(getDirective(firstSchema, objectType.getFields()['url'], 'deprecated')?.[0]['reason']).toEqual('second');
314314
});
315315

316316
it('promotes canonical root field definitions', async () => {

packages/stitch/tests/typeMergingWithExtensions.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ describe('merging using type merging', () => {
5252
},
5353
resolve: (_root, { keys }) => keys.map((key: Record<string, any>) => users.find(u => u.id === key['id'])),
5454
extensions: {
55-
directives: {
56-
merge: {},
57-
},
55+
directives: [{
56+
name: 'merge',
57+
}],
5858
},
5959
}
6060
}),
@@ -68,12 +68,13 @@ describe('merging using type merging', () => {
6868
username: { type: GraphQLString }
6969
}),
7070
extensions: {
71-
directives: {
72-
key: {
71+
directives: [{
72+
name: 'key',
73+
args: {
7374
selectionSet: '{ id }',
74-
}
75-
}
76-
}
75+
},
76+
}],
77+
},
7778
});
7879

7980
const accountsSchema = stitchingDirectivesValidator(new GraphQLSchema({

packages/stitching-directives/src/stitchingDirectivesTransformer.ts

Lines changed: 95 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717

1818
import { cloneSubschemaConfig, SubschemaConfig, MergedTypeConfig, MergedFieldConfig } from '@graphql-tools/delegate';
1919
import {
20-
getDirectives,
20+
getDirective,
2121
getImplementingTypes,
2222
MapperKind,
2323
mapSchema,
@@ -73,45 +73,48 @@ export function stitchingDirectivesTransformer(
7373

7474
mapSchema(schema, {
7575
[MapperKind.OBJECT_TYPE]: type => {
76-
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);
77-
78-
if (keyDirectiveName != null && directives[keyDirectiveName] != null) {
79-
const keyDirective = directives[keyDirectiveName];
80-
const selectionSet = parseSelectionSet(keyDirective.selectionSet, { noLocation: true });
76+
const keyDirective = getDirective(schema, type, keyDirectiveName, pathToDirectivesInExtensions)?.[0];
77+
if (keyDirective != null) {
78+
const selectionSet = parseSelectionSet(keyDirective['selectionSet'], { noLocation: true });
8179
selectionSetsByType[type.name] = selectionSet;
8280
}
8381

84-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName]) {
82+
const canonicalDirective = getDirective(
83+
schema,
84+
type,
85+
canonicalDirectiveName,
86+
pathToDirectivesInExtensions
87+
)?.[0];
88+
if (canonicalDirective != null) {
8589
setCanonicalDefinition(type.name);
8690
}
87-
8891
return undefined;
8992
},
9093
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
91-
const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions);
92-
93-
if (computedDirectiveName != null && directives[computedDirectiveName] != null) {
94-
const computedDirective = directives[computedDirectiveName];
95-
const selectionSet = parseSelectionSet(computedDirective.selectionSet, { noLocation: true });
94+
const computedDirective = getDirective(
95+
schema,
96+
fieldConfig,
97+
computedDirectiveName,
98+
pathToDirectivesInExtensions
99+
)?.[0];
100+
if (computedDirective != null) {
101+
const selectionSet = parseSelectionSet(computedDirective['selectionSet'], { noLocation: true });
96102
if (!computedFieldSelectionSets[typeName]) {
97103
computedFieldSelectionSets[typeName] = Object.create(null);
98104
}
99105
computedFieldSelectionSets[typeName][fieldName] = selectionSet;
100106
}
101107

102-
if (
103-
mergeDirectiveName != null &&
104-
directives[mergeDirectiveName] != null &&
105-
directives[mergeDirectiveName].keyField
106-
) {
107-
const mergeDirectiveKeyField = directives[mergeDirectiveName].keyField;
108+
const mergeDirective = getDirective(schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions)?.[0];
109+
if (mergeDirective?.['keyField'] != null) {
110+
const mergeDirectiveKeyField = mergeDirective['keyField'];
108111
const selectionSet = parseSelectionSet(`{ ${mergeDirectiveKeyField}}`, { noLocation: true });
109112

110-
const typeNames: Array<string> = directives[mergeDirectiveName].types;
113+
const typeNames: Array<string> = mergeDirective['types'];
111114

112115
const returnType = getNamedType(fieldConfig.type);
113116

114-
forEachConcreteType(schema, returnType, directives[mergeDirectiveName]?.types, typeName => {
117+
forEachConcreteType(schema, returnType, typeNames, typeName => {
115118
if (typeNames == null || typeNames.includes(typeName)) {
116119
const existingSelectionSet = selectionSetsByType[typeName];
117120
selectionSetsByType[typeName] = existingSelectionSet
@@ -121,70 +124,111 @@ export function stitchingDirectivesTransformer(
121124
});
122125
}
123126

124-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
127+
const canonicalDirective = getDirective(
128+
schema,
129+
fieldConfig,
130+
canonicalDirectiveName,
131+
pathToDirectivesInExtensions
132+
)?.[0];
133+
if (canonicalDirective != null) {
125134
setCanonicalDefinition(typeName, fieldName);
126135
}
127136

128137
return undefined;
129138
},
130139
[MapperKind.INTERFACE_TYPE]: type => {
131-
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);
132-
133-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
140+
const canonicalDirective = getDirective(
141+
schema,
142+
type,
143+
canonicalDirectiveName,
144+
pathToDirectivesInExtensions
145+
)?.[0];
146+
147+
if (canonicalDirective) {
134148
setCanonicalDefinition(type.name);
135149
}
136150

137151
return undefined;
138152
},
139153
[MapperKind.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => {
140-
const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions);
141-
142-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName]) {
154+
const canonicalDirective = getDirective(
155+
schema,
156+
fieldConfig,
157+
canonicalDirectiveName,
158+
pathToDirectivesInExtensions
159+
)?.[0];
160+
161+
if (canonicalDirective) {
143162
setCanonicalDefinition(typeName, fieldName);
144163
}
145164

146165
return undefined;
147166
},
148167
[MapperKind.INPUT_OBJECT_TYPE]: type => {
149-
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);
150-
151-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
168+
const canonicalDirective = getDirective(
169+
schema,
170+
type,
171+
canonicalDirectiveName,
172+
pathToDirectivesInExtensions
173+
)?.[0];
174+
175+
if (canonicalDirective) {
152176
setCanonicalDefinition(type.name);
153177
}
154178

155179
return undefined;
156180
},
157181
[MapperKind.INPUT_OBJECT_FIELD]: (inputFieldConfig, fieldName, typeName) => {
158-
const directives = getDirectives(schema, inputFieldConfig, pathToDirectivesInExtensions);
159-
160-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
182+
const canonicalDirective = getDirective(
183+
schema,
184+
inputFieldConfig,
185+
canonicalDirectiveName,
186+
pathToDirectivesInExtensions
187+
)?.[0];
188+
189+
if (canonicalDirective != null) {
161190
setCanonicalDefinition(typeName, fieldName);
162191
}
163192

164193
return undefined;
165194
},
166195
[MapperKind.UNION_TYPE]: type => {
167-
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);
168-
169-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
196+
const canonicalDirective = getDirective(
197+
schema,
198+
type,
199+
canonicalDirectiveName,
200+
pathToDirectivesInExtensions
201+
)?.[0];
202+
203+
if (canonicalDirective != null) {
170204
setCanonicalDefinition(type.name);
171205
}
172206

173207
return undefined;
174208
},
175209
[MapperKind.ENUM_TYPE]: type => {
176-
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);
177-
178-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
210+
const canonicalDirective = getDirective(
211+
schema,
212+
type,
213+
canonicalDirectiveName,
214+
pathToDirectivesInExtensions
215+
)?.[0];
216+
217+
if (canonicalDirective != null) {
179218
setCanonicalDefinition(type.name);
180219
}
181220

182221
return undefined;
183222
},
184223
[MapperKind.SCALAR_TYPE]: type => {
185-
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);
186-
187-
if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
224+
const canonicalDirective = getDirective(
225+
schema,
226+
type,
227+
canonicalDirectiveName,
228+
pathToDirectivesInExtensions
229+
)?.[0];
230+
231+
if (canonicalDirective != null) {
188232
setCanonicalDefinition(type.name);
189233
}
190234

@@ -248,23 +292,21 @@ export function stitchingDirectivesTransformer(
248292

249293
mapSchema(schema, {
250294
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName) => {
251-
const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions);
252-
253-
if (mergeDirectiveName != null && directives[mergeDirectiveName] != null) {
254-
const directiveArgumentMap = directives[mergeDirectiveName];
295+
const mergeDirective = getDirective(schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions)?.[0];
255296

297+
if (mergeDirective != null) {
256298
const returnType = getNullableType(fieldConfig.type);
257299
const returnsList = isListType(returnType);
258300
const namedType = getNamedType(returnType);
259301

260-
let mergeArgsExpr: string = directiveArgumentMap.argsExpr;
302+
let mergeArgsExpr: string = mergeDirective['argsExpr'];
261303

262304
if (mergeArgsExpr == null) {
263-
const key: Array<string> = directiveArgumentMap.key;
264-
const keyField: string = directiveArgumentMap.keyField;
305+
const key: Array<string> = mergeDirective['key'];
306+
const keyField: string = mergeDirective['keyField'];
265307
const keyExpr = key != null ? buildKeyExpr(key) : keyField != null ? `$key.${keyField}` : '$key';
266308

267-
const keyArg: string = directiveArgumentMap.keyArg;
309+
const keyArg: string = mergeDirective['keyArg'];
268310
const argNames = keyArg == null ? [Object.keys(fieldConfig.args ?? {})[0]] : keyArg.split('.');
269311

270312
const lastArgName = argNames.pop();
@@ -275,7 +317,7 @@ export function stitchingDirectivesTransformer(
275317
}
276318
}
277319

278-
const typeNames: Array<string> = directiveArgumentMap.types;
320+
const typeNames: Array<string> = mergeDirective['types'];
279321

280322
forEachConcreteTypeName(namedType, schema, typeNames, typeName => {
281323
const parsedMergeArgsExpr = parseMergeArgsExpr(
@@ -285,7 +327,7 @@ export function stitchingDirectivesTransformer(
285327
: mergeSelectionSets(...allSelectionSetsByType[typeName])
286328
);
287329

288-
const additionalArgs = directiveArgumentMap.additionalArgs;
330+
const additionalArgs = mergeDirective['additionalArgs'];
289331
if (additionalArgs != null) {
290332
parsedMergeArgsExpr.args = mergeDeep(
291333
parsedMergeArgsExpr.args,

0 commit comments

Comments
 (0)