Skip to content

Commit

Permalink
Modify ast nodes when transforming schemas (#1762)
Browse files Browse the repository at this point in the history
Each transform should also modify the underlying astnode and extensionASTNodes, if they exist.

This PR also changes mapSchema to automatically update a type's astNode list of field definitions to match the actual field definitions includes within the type's new config map after mapping.

There is likely space for making sure to update additional astNodes within types to match the enclosed graphql type system objects, for example, enum types with the enclosed enum values. That may be included in a separate PR at this time.

Addresses #1747
  • Loading branch information
yaacovCR committed Jul 13, 2020
1 parent 428a80b commit 81e668d
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 14 deletions.
72 changes: 67 additions & 5 deletions packages/stitch/tests/alternateStitchSchemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -969,8 +969,8 @@ describe('rename nested object fields with interfaces', () => {
});
});

describe('WrapType query transform', () => {
test('should work', async () => {
describe('WrapType', () => {
test('Query transform should work', async () => {
const transformedBookingSchema = wrapSchema(bookingSchema, [
new WrapType('Query', 'Namespace_Query', 'namespace'),
]);
Expand Down Expand Up @@ -1022,10 +1022,8 @@ describe('WrapType query transform', () => {

expect(result).toEqual(expectedResult);
});
});

describe('WrapType mutation transform', () => {
test('should work', async () => {
test('Mutation transform should work', async () => {
const transformedBookingSchema = wrapSchema(bookingSchema, [
new WrapType('Mutation', 'Namespace_Mutation', 'namespace'),
]);
Expand Down Expand Up @@ -1084,6 +1082,70 @@ describe('WrapType mutation transform', () => {

expect(result).toEqual(expectedResult);
});

test('namespacing different subschemas with overlapping root field names', async () => {
const typeDefGen = (i: number) => `
type Query {
test: Test${i}Response
}
type Test${i}Response {
aString: String!
}
`;

const resolverGen = (i: number) => ({
Query: {
test: () => ({
aString: `test${i}`,
}),
},
});

const subschemaGen = (i: number) => ({
schema: makeExecutableSchema({
typeDefs: typeDefGen(i),
resolvers: resolverGen(i)
}),
transforms: [new WrapType(`Query`, `Test${i}_Query`, `test${i}`)]
});

const stitchedSchema = stitchSchemas({
subschemas: [subschemaGen(1), subschemaGen(2)]
});

const query = `{
test1 {
test {
aString
}
}
test2 {
test {
aString
}
}
}`;

const result = await graphql(stitchedSchema, query);

const expectedResult = {
data: {
test1: {
test: {
aString: 'test1',
},
},
test2: {
test: {
aString: 'test2',
},
},
},
};

expect(result).toEqual(expectedResult);
});
});

describe('schema transformation with extraction of nested fields', () => {
Expand Down
55 changes: 54 additions & 1 deletion packages/utils/src/fields.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { GraphQLFieldConfigMap, GraphQLObjectType, GraphQLFieldConfig, GraphQLSchema } from 'graphql';
import {
GraphQLFieldConfigMap,
GraphQLObjectType,
GraphQLFieldConfig,
GraphQLSchema,
ObjectTypeDefinitionNode,
ObjectTypeExtensionNode,
FieldDefinitionNode,
} from 'graphql';
import { MapperKind } from './Interfaces';
import { mapSchema } from './mapSchema';
import { addTypes } from './addTypes';
Expand Down Expand Up @@ -34,6 +42,8 @@ export function appendObjectFields(
return new GraphQLObjectType({
...config,
fields: newFieldConfigMap,
astNode: rebuildAstNode(config.astNode, newFieldConfigMap),
extensionASTNodes: rebuildExtensionAstNodes(config.extensionASTNodes),
});
}
},
Expand Down Expand Up @@ -65,6 +75,8 @@ export function removeObjectFields(
return new GraphQLObjectType({
...config,
fields: newFieldConfigMap,
astNode: rebuildAstNode(config.astNode, newFieldConfigMap),
extensionASTNodes: rebuildExtensionAstNodes(config.extensionASTNodes),
});
}
},
Expand Down Expand Up @@ -131,10 +143,51 @@ export function modifyObjectFields(
return new GraphQLObjectType({
...config,
fields: newFieldConfigMap,
astNode: rebuildAstNode(config.astNode, newFieldConfigMap),
extensionASTNodes: rebuildExtensionAstNodes(config.extensionASTNodes),
});
}
},
});

return [newSchema, removedFields];
}

function rebuildAstNode(
astNode: ObjectTypeDefinitionNode,
fieldConfigMap: Record<string, GraphQLFieldConfig<any, any>>
): ObjectTypeDefinitionNode {
if (astNode == null) {
return undefined;
}

const newAstNode: ObjectTypeDefinitionNode = {
...astNode,
fields: undefined,
};

const fields: Array<FieldDefinitionNode> = [];
Object.values(fieldConfigMap).forEach(fieldConfig => {
if (fieldConfig.astNode != null) {
fields.push(fieldConfig.astNode);
}
});

return {
...newAstNode,
fields,
};
}

function rebuildExtensionAstNodes(
extensionASTNodes: ReadonlyArray<ObjectTypeExtensionNode>
): Array<ObjectTypeExtensionNode> {
if (!extensionASTNodes?.length) {
return [];
}

return extensionASTNodes.map(node => ({
...node,
fields: undefined,
}));
}
77 changes: 74 additions & 3 deletions packages/utils/src/mapSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ import {
GraphQLList,
GraphQLNonNull,
GraphQLEnumType,
ObjectTypeDefinitionNode,
InterfaceTypeDefinitionNode,
InputObjectTypeDefinitionNode,
InputObjectTypeExtensionNode,
InterfaceTypeExtensionNode,
ObjectTypeExtensionNode,
InputValueDefinitionNode,
FieldDefinitionNode,
} from 'graphql';

import {
Expand Down Expand Up @@ -239,6 +247,15 @@ function mapFields(originalTypeMap: TypeMap, schema: GraphQLSchema, schemaMapper
newFieldConfigMap[fieldName] = originalFieldConfig;
} else if (Array.isArray(mappedField)) {
const [newFieldName, newFieldConfig] = mappedField;
if (newFieldConfig.astNode != null) {
newFieldConfig.astNode = {
...newFieldConfig.astNode,
name: {
...newFieldConfig.astNode.name,
value: newFieldName,
},
};
}
newFieldConfigMap[newFieldName] = newFieldConfig;
} else if (mappedField !== null) {
newFieldConfigMap[fieldName] = mappedField;
Expand All @@ -247,18 +264,26 @@ function mapFields(originalTypeMap: TypeMap, schema: GraphQLSchema, schemaMapper

if (isObjectType(originalType)) {
newTypeMap[typeName] = new GraphQLObjectType({
...((config as unknown) as GraphQLObjectTypeConfig<any, any>),
...(config as GraphQLObjectTypeConfig<any, any>),
fields: newFieldConfigMap,
astNode: rebuildAstNode((config as GraphQLObjectTypeConfig<any, any>).astNode, newFieldConfigMap),
extensionASTNodes: rebuildExtensionAstNodes((config as GraphQLObjectTypeConfig<any, any>).extensionASTNodes),
});
} else if (isInterfaceType(originalType)) {
newTypeMap[typeName] = new GraphQLInterfaceType({
...((config as unknown) as GraphQLInterfaceTypeConfig<any, any>),
...(config as GraphQLInterfaceTypeConfig<any, any>),
fields: newFieldConfigMap,
astNode: rebuildAstNode((config as GraphQLInterfaceTypeConfig<any, any>).astNode, newFieldConfigMap),
extensionASTNodes: rebuildExtensionAstNodes(
(config as GraphQLInterfaceTypeConfig<any, any>).extensionASTNodes
),
});
} else {
newTypeMap[typeName] = new GraphQLInputObjectType({
...((config as unknown) as GraphQLInputObjectTypeConfig),
...(config as GraphQLInputObjectTypeConfig),
fields: newFieldConfigMap,
astNode: rebuildAstNode((config as GraphQLInputObjectTypeConfig).astNode, newFieldConfigMap),
extensionASTNodes: rebuildExtensionAstNodes((config as GraphQLInputObjectTypeConfig).extensionASTNodes),
});
}
}
Expand Down Expand Up @@ -471,3 +496,49 @@ function getEnumValueMapper(schemaMapper: SchemaMapper): EnumValueMapper | null
const enumValueMapper = schemaMapper[MapperKind.ENUM_VALUE];
return enumValueMapper != null ? enumValueMapper : null;
}

function rebuildAstNode<
TypeDefinitionNode extends ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode | InputObjectTypeDefinitionNode
>(
astNode: TypeDefinitionNode,
fieldOrInputFieldConfigMap: Record<
string,
TypeDefinitionNode extends ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode
? GraphQLFieldConfig<any, any>
: GraphQLInputFieldConfig
>
): TypeDefinitionNode {
if (astNode == null) {
return undefined;
}

const newAstNode: TypeDefinitionNode = {
...astNode,
fields: undefined,
};

const fields: Array<FieldDefinitionNode | InputValueDefinitionNode> = [];
Object.values(fieldOrInputFieldConfigMap).forEach(fieldOrInputFieldConfig => {
if (fieldOrInputFieldConfig.astNode != null) {
fields.push(fieldOrInputFieldConfig.astNode);
}
});

return {
...newAstNode,
fields,
};
}

function rebuildExtensionAstNodes<
TypeExtensionNode extends ObjectTypeExtensionNode | InterfaceTypeExtensionNode | InputObjectTypeExtensionNode
>(extensionASTNodes: ReadonlyArray<TypeExtensionNode>): Array<TypeExtensionNode> {
if (!extensionASTNodes?.length) {
return [];
}

return extensionASTNodes.map(node => ({
...node,
fields: undefined,
}));
}
2 changes: 1 addition & 1 deletion packages/wrap/src/transforms/MapFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export default class MapFields implements Transform {
errorsTransformer?: ErrorsTransformer
) {
this.transformer = new TransformCompositeFields(
(_typeName, _fieldName, fieldConfig) => fieldConfig,
() => undefined,
(typeName, fieldName, fieldNode, fragments, transformationContext) => {
const typeTransformers = fieldNodeTransformerMap[typeName];
if (typeTransformers == null) {
Expand Down

0 comments on commit 81e668d

Please sign in to comment.