Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Gatsby-style directives in extensions #3185

Merged
merged 2 commits into from
Jul 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/quick-hotels-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@graphql-tools/stitch': major
'@graphql-tools/stitching-directives': major
'@graphql-tools/utils': major
'@graphql-tools/wrap': major
---

fix(getDirectives): preserve order around repeatable directives

BREAKING CHANGE: getDirectives now always return an array of individual DirectiveAnnotation objects consisting of `name` and `args` properties.

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).

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!

When specifying directives in extensions, one can use either the old or new format.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDirectives, MapperKind, mapSchema } from '@graphql-tools/utils';
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { cloneSubschemaConfig, SubschemaConfig } from '@graphql-tools/delegate';

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

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

if (computed == null) {
return undefined;
}

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

if (selectionSet == null) {
return undefined;
Expand Down
38 changes: 19 additions & 19 deletions packages/stitch/tests/mergeDefinitions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { makeExecutableSchema } from '@graphql-tools/schema';
import { stitchSchemas } from '@graphql-tools/stitch';
import { getDirectives } from '@graphql-tools/utils';
import { getDirective } from '@graphql-tools/utils';
import { stitchingDirectives } from '@graphql-tools/stitching-directives';
import {
GraphQLObjectType,
Expand Down Expand Up @@ -282,22 +282,22 @@ describe('merge canonical types', () => {
const scalarType = gatewaySchema.getType('ProductScalar');
assertGraphQLScalerType(scalarType)

expect(getDirectives(firstSchema, queryType.toConfig())['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, objectType.toConfig())['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, interfaceType.toConfig())['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, inputType.toConfig())['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, enumType.toConfig())['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, unionType.toConfig())['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, scalarType.toConfig())['mydir'].value).toEqual('first');

expect(getDirectives(firstSchema, queryType.getFields()['field1'])['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, queryType.getFields()['field2'])['mydir'].value).toEqual('second');
expect(getDirectives(firstSchema, objectType.getFields()['id'])['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, objectType.getFields()['url'])['mydir'].value).toEqual('second');
expect(getDirectives(firstSchema, interfaceType.getFields()['id'])['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, interfaceType.getFields()['url'])['mydir'].value).toEqual('second');
expect(getDirectives(firstSchema, inputType.getFields()['id'])['mydir'].value).toEqual('first');
expect(getDirectives(firstSchema, inputType.getFields()['url'])['mydir'].value).toEqual('second');
expect(getDirective(firstSchema, queryType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, objectType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, interfaceType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, inputType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, enumType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, unionType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, scalarType.toConfig(), 'mydir')?.[0]['value']).toEqual('first');

expect(getDirective(firstSchema, queryType.getFields()['field1'], 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, queryType.getFields()['field2'], 'mydir')?.[0]['value']).toEqual('second');
expect(getDirective(firstSchema, objectType.getFields()['id'], 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, objectType.getFields()['url'], 'mydir')?.[0]['value']).toEqual('second');
expect(getDirective(firstSchema, interfaceType.getFields()['id'], 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, interfaceType.getFields()['url'], 'mydir')?.[0]['value']).toEqual('second');
expect(getDirective(firstSchema, inputType.getFields()['id'], 'mydir')?.[0]['value']).toEqual('first');
expect(getDirective(firstSchema, inputType.getFields()['url'], 'mydir')?.[0]['value']).toEqual('second');

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

it('promotes canonical root field definitions', async () => {
Expand Down
17 changes: 9 additions & 8 deletions packages/stitch/tests/typeMergingWithExtensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ describe('merging using type merging', () => {
},
resolve: (_root, { keys }) => keys.map((key: Record<string, any>) => users.find(u => u.id === key['id'])),
extensions: {
directives: {
merge: {},
},
directives: [{
name: 'merge',
}],
},
}
}),
Expand All @@ -68,12 +68,13 @@ describe('merging using type merging', () => {
username: { type: GraphQLString }
}),
extensions: {
directives: {
key: {
directives: [{
name: 'key',
args: {
selectionSet: '{ id }',
}
}
}
},
}],
},
});

const accountsSchema = stitchingDirectivesValidator(new GraphQLSchema({
Expand Down
148 changes: 95 additions & 53 deletions packages/stitching-directives/src/stitchingDirectivesTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {

import { cloneSubschemaConfig, SubschemaConfig, MergedTypeConfig, MergedFieldConfig } from '@graphql-tools/delegate';
import {
getDirectives,
getDirective,
getImplementingTypes,
MapperKind,
mapSchema,
Expand Down Expand Up @@ -73,45 +73,48 @@ export function stitchingDirectivesTransformer(

mapSchema(schema, {
[MapperKind.OBJECT_TYPE]: type => {
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);

if (keyDirectiveName != null && directives[keyDirectiveName] != null) {
const keyDirective = directives[keyDirectiveName];
const selectionSet = parseSelectionSet(keyDirective.selectionSet, { noLocation: true });
const keyDirective = getDirective(schema, type, keyDirectiveName, pathToDirectivesInExtensions)?.[0];
if (keyDirective != null) {
const selectionSet = parseSelectionSet(keyDirective['selectionSet'], { noLocation: true });
selectionSetsByType[type.name] = selectionSet;
}

if (canonicalDirectiveName != null && directives[canonicalDirectiveName]) {
const canonicalDirective = getDirective(
schema,
type,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];
if (canonicalDirective != null) {
setCanonicalDefinition(type.name);
}

return undefined;
},
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions);

if (computedDirectiveName != null && directives[computedDirectiveName] != null) {
const computedDirective = directives[computedDirectiveName];
const selectionSet = parseSelectionSet(computedDirective.selectionSet, { noLocation: true });
const computedDirective = getDirective(
schema,
fieldConfig,
computedDirectiveName,
pathToDirectivesInExtensions
)?.[0];
if (computedDirective != null) {
const selectionSet = parseSelectionSet(computedDirective['selectionSet'], { noLocation: true });
if (!computedFieldSelectionSets[typeName]) {
computedFieldSelectionSets[typeName] = Object.create(null);
}
computedFieldSelectionSets[typeName][fieldName] = selectionSet;
}

if (
mergeDirectiveName != null &&
directives[mergeDirectiveName] != null &&
directives[mergeDirectiveName].keyField
) {
const mergeDirectiveKeyField = directives[mergeDirectiveName].keyField;
const mergeDirective = getDirective(schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions)?.[0];
if (mergeDirective?.['keyField'] != null) {
const mergeDirectiveKeyField = mergeDirective['keyField'];
const selectionSet = parseSelectionSet(`{ ${mergeDirectiveKeyField}}`, { noLocation: true });

const typeNames: Array<string> = directives[mergeDirectiveName].types;
const typeNames: Array<string> = mergeDirective['types'];

const returnType = getNamedType(fieldConfig.type);

forEachConcreteType(schema, returnType, directives[mergeDirectiveName]?.types, typeName => {
forEachConcreteType(schema, returnType, typeNames, typeName => {
if (typeNames == null || typeNames.includes(typeName)) {
const existingSelectionSet = selectionSetsByType[typeName];
selectionSetsByType[typeName] = existingSelectionSet
Expand All @@ -121,70 +124,111 @@ export function stitchingDirectivesTransformer(
});
}

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
fieldConfig,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];
if (canonicalDirective != null) {
setCanonicalDefinition(typeName, fieldName);
}

return undefined;
},
[MapperKind.INTERFACE_TYPE]: type => {
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
type,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective) {
setCanonicalDefinition(type.name);
}

return undefined;
},
[MapperKind.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => {
const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName]) {
const canonicalDirective = getDirective(
schema,
fieldConfig,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective) {
setCanonicalDefinition(typeName, fieldName);
}

return undefined;
},
[MapperKind.INPUT_OBJECT_TYPE]: type => {
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
type,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective) {
setCanonicalDefinition(type.name);
}

return undefined;
},
[MapperKind.INPUT_OBJECT_FIELD]: (inputFieldConfig, fieldName, typeName) => {
const directives = getDirectives(schema, inputFieldConfig, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
inputFieldConfig,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective != null) {
setCanonicalDefinition(typeName, fieldName);
}

return undefined;
},
[MapperKind.UNION_TYPE]: type => {
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
type,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective != null) {
setCanonicalDefinition(type.name);
}

return undefined;
},
[MapperKind.ENUM_TYPE]: type => {
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
type,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective != null) {
setCanonicalDefinition(type.name);
}

return undefined;
},
[MapperKind.SCALAR_TYPE]: type => {
const directives = getDirectives(schema, type, pathToDirectivesInExtensions);

if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) {
const canonicalDirective = getDirective(
schema,
type,
canonicalDirectiveName,
pathToDirectivesInExtensions
)?.[0];

if (canonicalDirective != null) {
setCanonicalDefinition(type.name);
}

Expand Down Expand Up @@ -248,23 +292,21 @@ export function stitchingDirectivesTransformer(

mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName) => {
const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions);

if (mergeDirectiveName != null && directives[mergeDirectiveName] != null) {
const directiveArgumentMap = directives[mergeDirectiveName];
const mergeDirective = getDirective(schema, fieldConfig, mergeDirectiveName, pathToDirectivesInExtensions)?.[0];

if (mergeDirective != null) {
const returnType = getNullableType(fieldConfig.type);
const returnsList = isListType(returnType);
const namedType = getNamedType(returnType);

let mergeArgsExpr: string = directiveArgumentMap.argsExpr;
let mergeArgsExpr: string = mergeDirective['argsExpr'];

if (mergeArgsExpr == null) {
const key: Array<string> = directiveArgumentMap.key;
const keyField: string = directiveArgumentMap.keyField;
const key: Array<string> = mergeDirective['key'];
const keyField: string = mergeDirective['keyField'];
const keyExpr = key != null ? buildKeyExpr(key) : keyField != null ? `$key.${keyField}` : '$key';

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

const lastArgName = argNames.pop();
Expand All @@ -275,7 +317,7 @@ export function stitchingDirectivesTransformer(
}
}

const typeNames: Array<string> = directiveArgumentMap.types;
const typeNames: Array<string> = mergeDirective['types'];

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

const additionalArgs = directiveArgumentMap.additionalArgs;
const additionalArgs = mergeDirective['additionalArgs'];
if (additionalArgs != null) {
parsedMergeArgsExpr.args = mergeDeep(
parsedMergeArgsExpr.args,
Expand Down
Loading