Skip to content

Commit 43da6b5

Browse files
committedMar 23, 2021
enhance(merge): reduce number of iterations
1 parent 3eb72fd commit 43da6b5

File tree

8 files changed

+217
-238
lines changed

8 files changed

+217
-238
lines changed
 

‎.changeset/orange-lies-begin.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-tools/merge': patch
3+
'@graphql-tools/utils': patch
4+
---
5+
6+
enhance(merge): reduce number of iterations

‎packages/merge/src/typedefs-mergers/comments.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
getDescription,
33
StringValueNode,
4-
TypeDefinitionNode,
54
FieldDefinitionNode,
65
InputValueDefinitionNode,
76
ASTNode,
@@ -10,6 +9,7 @@ import {
109
visit,
1110
VisitFn,
1211
} from 'graphql';
12+
import { NamedDefinitionNode } from './merge-nodes';
1313

1414
let commentsRegistry: {
1515
[path: string]: string[];
@@ -19,7 +19,7 @@ export function resetComments(): void {
1919
commentsRegistry = {};
2020
}
2121

22-
export function collectComment(node: TypeDefinitionNode): void {
22+
export function collectComment(node: NamedDefinitionNode): void {
2323
const entityName = node.name.value;
2424
pushComment(node, entityName);
2525

@@ -48,12 +48,7 @@ export function collectComment(node: TypeDefinitionNode): void {
4848
}
4949
}
5050

51-
export function pushComment(
52-
node: { readonly description?: StringValueNode },
53-
entity: string,
54-
field?: string,
55-
argument?: string
56-
): void {
51+
export function pushComment(node: any, entity: string, field?: string, argument?: string): void {
5752
const comment = getDescription(node, { commentDescriptions: true });
5853

5954
if (typeof comment !== 'string' || comment.length === 0) {
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,5 @@
11
import { Config } from './merge-typedefs';
2-
import { DefinitionNode } from 'graphql';
3-
import {
4-
isGraphQLEnum,
5-
isGraphQLInputType,
6-
isGraphQLInterface,
7-
isGraphQLScalar,
8-
isGraphQLType,
9-
isGraphQLUnion,
10-
isGraphQLDirective,
11-
isGraphQLTypeExtension,
12-
isGraphQLInputTypeExtension,
13-
isGraphQLEnumExtension,
14-
isGraphQLUnionExtension,
15-
isGraphQLScalarExtension,
16-
isGraphQLInterfaceExtension,
17-
} from './utils';
2+
import { DefinitionNode, Kind, NameNode, SchemaDefinitionNode, SchemaExtensionNode } from 'graphql';
183
import { mergeType } from './type';
194
import { mergeEnum } from './enum';
205
import { mergeScalar } from './scalar';
@@ -23,43 +8,64 @@ import { mergeInputType } from './input-type';
238
import { mergeInterface } from './interface';
249
import { mergeDirective } from './directives';
2510
import { collectComment } from './comments';
11+
import { mergeSchemaDefs } from './schema-def';
2612

27-
export type MergedResultMap = { [name: string]: DefinitionNode };
13+
export type MergedResultMap = Record<string, NamedDefinitionNode> & {
14+
[schemaDefSymbol]: SchemaDefinitionNode | SchemaExtensionNode;
15+
};
16+
export type NamedDefinitionNode = DefinitionNode & { name?: NameNode };
2817

29-
export function mergeGraphQLNodes(nodes: ReadonlyArray<DefinitionNode>, config?: Config): MergedResultMap {
30-
return nodes.reduce<MergedResultMap>((prev: MergedResultMap, nodeDefinition: DefinitionNode) => {
31-
const node = nodeDefinition as any;
18+
export function isNamedDefinitionNode(definitionNode: DefinitionNode): definitionNode is NamedDefinitionNode {
19+
return 'name' in definitionNode;
20+
}
3221

33-
if (node && node.name && node.name.value) {
34-
const name = node.name.value;
22+
export const schemaDefSymbol = Symbol('schemaDefSymbol');
3523

36-
if (config && config.commentDescriptions) {
37-
collectComment(node);
24+
export function mergeGraphQLNodes(nodes: ReadonlyArray<DefinitionNode>, config?: Config): MergedResultMap {
25+
const mergedResultMap = {} as MergedResultMap;
26+
for (const nodeDefinition of nodes) {
27+
if (isNamedDefinitionNode(nodeDefinition)) {
28+
const name = nodeDefinition.name.value;
29+
if (config?.commentDescriptions) {
30+
collectComment(nodeDefinition);
3831
}
3932

40-
if (
41-
config &&
42-
config.exclusions &&
43-
(config.exclusions.includes(name + '.*') || config.exclusions.includes(name))
44-
) {
45-
delete prev[name];
46-
} else if (isGraphQLType(nodeDefinition) || isGraphQLTypeExtension(nodeDefinition)) {
47-
prev[name] = mergeType(nodeDefinition, prev[name] as any, config);
48-
} else if (isGraphQLEnum(nodeDefinition) || isGraphQLEnumExtension(nodeDefinition)) {
49-
prev[name] = mergeEnum(nodeDefinition, prev[name] as any, config);
50-
} else if (isGraphQLUnion(nodeDefinition) || isGraphQLUnionExtension(nodeDefinition)) {
51-
prev[name] = mergeUnion(nodeDefinition, prev[name] as any, config);
52-
} else if (isGraphQLScalar(nodeDefinition) || isGraphQLScalarExtension(nodeDefinition)) {
53-
prev[name] = mergeScalar(nodeDefinition, prev[name] as any, config);
54-
} else if (isGraphQLInputType(nodeDefinition) || isGraphQLInputTypeExtension(nodeDefinition)) {
55-
prev[name] = mergeInputType(nodeDefinition, prev[name] as any, config);
56-
} else if (isGraphQLInterface(nodeDefinition) || isGraphQLInterfaceExtension(nodeDefinition)) {
57-
prev[name] = mergeInterface(nodeDefinition, prev[name] as any, config);
58-
} else if (isGraphQLDirective(nodeDefinition)) {
59-
prev[name] = mergeDirective(nodeDefinition, prev[name] as any);
33+
if (config?.exclusions?.includes(name + '.*') || config?.exclusions?.includes(name)) {
34+
delete mergedResultMap[name];
35+
} else {
36+
switch (nodeDefinition.kind) {
37+
case Kind.OBJECT_TYPE_DEFINITION:
38+
case Kind.OBJECT_TYPE_EXTENSION:
39+
mergedResultMap[name] = mergeType(nodeDefinition, mergedResultMap[name] as any, config);
40+
break;
41+
case Kind.ENUM_TYPE_DEFINITION:
42+
case Kind.ENUM_TYPE_EXTENSION:
43+
mergedResultMap[name] = mergeEnum(nodeDefinition, mergedResultMap[name] as any, config);
44+
break;
45+
case Kind.UNION_TYPE_DEFINITION:
46+
case Kind.UNION_TYPE_EXTENSION:
47+
mergedResultMap[name] = mergeUnion(nodeDefinition, mergedResultMap[name] as any, config);
48+
break;
49+
case Kind.SCALAR_TYPE_DEFINITION:
50+
case Kind.SCALAR_TYPE_EXTENSION:
51+
mergedResultMap[name] = mergeScalar(nodeDefinition, mergedResultMap[name] as any, config);
52+
break;
53+
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
54+
case Kind.INPUT_OBJECT_TYPE_EXTENSION:
55+
mergedResultMap[name] = mergeInputType(nodeDefinition, mergedResultMap[name] as any, config);
56+
break;
57+
case Kind.INTERFACE_TYPE_DEFINITION:
58+
case Kind.INTERFACE_TYPE_EXTENSION:
59+
mergedResultMap[name] = mergeInterface(nodeDefinition, mergedResultMap[name] as any, config);
60+
break;
61+
case Kind.DIRECTIVE_DEFINITION:
62+
mergedResultMap[name] = mergeDirective(nodeDefinition, mergedResultMap[name] as any);
63+
break;
64+
}
6065
}
66+
} else if (nodeDefinition.kind === Kind.SCHEMA_DEFINITION || nodeDefinition.kind === Kind.SCHEMA_EXTENSION) {
67+
mergedResultMap[schemaDefSymbol] = mergeSchemaDefs(nodeDefinition, mergedResultMap[schemaDefSymbol], config);
6168
}
62-
63-
return prev;
64-
}, {});
69+
}
70+
return mergedResultMap;
6571
}

‎packages/merge/src/typedefs-mergers/merge-typedefs.ts

+88-57
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
import { DefinitionNode, DocumentNode, GraphQLSchema, parse, Source, Kind, isSchema } from 'graphql';
2-
import { isSourceTypes, isStringTypes, isSchemaDefinition } from './utils';
3-
import { MergedResultMap, mergeGraphQLNodes } from './merge-nodes';
1+
import {
2+
DefinitionNode,
3+
DocumentNode,
4+
GraphQLSchema,
5+
parse,
6+
Source,
7+
Kind,
8+
isSchema,
9+
OperationTypeDefinitionNode,
10+
OperationTypeNode,
11+
isDefinitionNode,
12+
} from 'graphql';
13+
import { CompareFn, defaultStringComparator, isSourceTypes, isStringTypes } from './utils';
14+
import { MergedResultMap, mergeGraphQLNodes, schemaDefSymbol } from './merge-nodes';
415
import { resetComments, printWithComments } from './comments';
5-
import { createSchemaDefinition, getDocumentNodeFromSchema } from '@graphql-tools/utils';
16+
import { getDocumentNodeFromSchema } from '@graphql-tools/utils';
17+
import { operationTypeDefinitionNodeTypeRootTypeMap } from './schema-def';
618

719
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
8-
type CompareFn<T> = (a: T, b: T) => number;
920

1021
export interface Config {
1122
/**
@@ -104,75 +115,95 @@ export function mergeTypeDefs(
104115
return result;
105116
}
106117

118+
function visitTypeSources(
119+
types: Array<string | Source | DocumentNode | GraphQLSchema | DefinitionNode>,
120+
allNodes: DefinitionNode[]
121+
) {
122+
for (const type of types) {
123+
if (type) {
124+
if (Array.isArray(type)) {
125+
visitTypeSources(types, allNodes);
126+
} else if (isSchema(type)) {
127+
const documentNode = getDocumentNodeFromSchema(type);
128+
visitTypeSources(documentNode.definitions as DefinitionNode[], allNodes);
129+
} else if (isStringTypes(type) || isSourceTypes(type)) {
130+
const documentNode = parse(type);
131+
visitTypeSources(documentNode.definitions as DefinitionNode[], allNodes);
132+
} else if (isDefinitionNode(type)) {
133+
allNodes.push(type);
134+
} else {
135+
visitTypeSources(type.definitions as DefinitionNode[], allNodes);
136+
}
137+
}
138+
}
139+
}
140+
107141
export function mergeGraphQLTypes(
108142
types: Array<string | Source | DocumentNode | GraphQLSchema>,
109143
config: Config
110144
): DefinitionNode[] {
111145
resetComments();
112146

113-
const allNodes: ReadonlyArray<DefinitionNode> = types
114-
.map<DocumentNode>(type => {
115-
if (Array.isArray(type)) {
116-
type = mergeTypeDefs(type);
117-
}
118-
if (isSchema(type)) {
119-
return getDocumentNodeFromSchema(type);
120-
} else if (isStringTypes(type) || isSourceTypes(type)) {
121-
return parse(type);
122-
}
147+
const allNodes: DefinitionNode[] = [];
148+
visitTypeSources(types, allNodes);
123149

124-
return type;
125-
})
126-
.map(ast => ast.definitions)
127-
.reduce((defs, newDef = []) => [...defs, ...newDef], []);
150+
const mergedNodes: MergedResultMap = mergeGraphQLNodes(allNodes, config);
128151

129152
// XXX: right now we don't handle multiple schema definitions
130-
let schemaDef: {
131-
query: string | null;
132-
mutation: string | null;
133-
subscription: string | null;
134-
} = allNodes.filter(isSchemaDefinition).reduce<any>(
135-
(def, node) => {
136-
node.operationTypes
137-
.filter(op => op.type.name.value)
138-
.forEach(op => {
139-
def[op.operation] = op.type.name.value;
140-
});
141-
142-
return def;
143-
},
144-
{
145-
query: null,
146-
mutation: null,
147-
subscription: null,
148-
}
149-
);
150-
151-
const mergedNodes: MergedResultMap = mergeGraphQLNodes(allNodes, config);
152-
const allTypes = Object.keys(mergedNodes);
153+
let schemaDef = mergedNodes[schemaDefSymbol] || {
154+
kind: Kind.SCHEMA_DEFINITION,
155+
operationTypes: [],
156+
};
153157

154-
if (config && config.sort) {
155-
allTypes.sort(typeof config.sort === 'function' ? config.sort : undefined);
158+
if (config?.useSchemaDefinition) {
159+
const operationTypes = schemaDef.operationTypes as OperationTypeDefinitionNode[];
160+
for (const opTypeDefNodeType in operationTypeDefinitionNodeTypeRootTypeMap) {
161+
const opTypeDefNode = operationTypes.find(operationType => operationType.operation === opTypeDefNodeType);
162+
if (!opTypeDefNode) {
163+
const existingPossibleRootType = mergedNodes[operationTypeDefinitionNodeTypeRootTypeMap[opTypeDefNodeType]];
164+
if (existingPossibleRootType) {
165+
operationTypes.push({
166+
kind: Kind.OPERATION_TYPE_DEFINITION,
167+
type: {
168+
kind: Kind.NAMED_TYPE,
169+
name: existingPossibleRootType.name,
170+
},
171+
operation: opTypeDefNodeType as OperationTypeNode,
172+
});
173+
}
174+
}
175+
}
156176
}
157177

158-
if (config && config.useSchemaDefinition) {
159-
const queryType = schemaDef.query ? schemaDef.query : allTypes.find(t => t === 'Query');
160-
const mutationType = schemaDef.mutation ? schemaDef.mutation : allTypes.find(t => t === 'Mutation');
161-
const subscriptionType = schemaDef.subscription ? schemaDef.subscription : allTypes.find(t => t === 'Subscription');
178+
if (config?.forceSchemaDefinition && !schemaDef?.operationTypes?.length) {
162179
schemaDef = {
163-
query: queryType,
164-
mutation: mutationType,
165-
subscription: subscriptionType,
180+
kind: Kind.SCHEMA_DEFINITION,
181+
operationTypes: [
182+
{
183+
kind: Kind.OPERATION_TYPE_DEFINITION,
184+
operation: 'query',
185+
type: {
186+
kind: Kind.NAMED_TYPE,
187+
name: {
188+
kind: Kind.NAME,
189+
value: 'Query',
190+
},
191+
},
192+
},
193+
],
166194
};
167195
}
168196

169-
const schemaDefinition = createSchemaDefinition(schemaDef, {
170-
force: config.forceSchemaDefinition,
171-
});
197+
const mergedNodeDefinitions = Object.values(mergedNodes);
198+
199+
if (schemaDef.operationTypes?.length) {
200+
mergedNodeDefinitions.push(schemaDef);
201+
}
172202

173-
if (!schemaDefinition) {
174-
return Object.values(mergedNodes);
203+
if (config?.sort) {
204+
const sortFn = typeof config.sort === 'function' ? config.sort : defaultStringComparator;
205+
mergedNodeDefinitions.sort((a, b) => sortFn(a.name?.value, b.name?.value));
175206
}
176207

177-
return [...Object.values(mergedNodes), parse(schemaDefinition).definitions[0]];
208+
return mergedNodeDefinitions;
178209
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Kind, OperationTypeDefinitionNode, SchemaDefinitionNode, SchemaExtensionNode } from 'graphql';
2+
import { mergeDirectives } from './directives';
3+
import { Config } from './merge-typedefs';
4+
5+
export const operationTypeDefinitionNodeTypeRootTypeMap = {
6+
query: 'Query',
7+
mutation: 'Mutation',
8+
subscription: 'Subscription',
9+
} as const;
10+
11+
function mergeOperationTypes(
12+
opNodeList: ReadonlyArray<OperationTypeDefinitionNode> = [],
13+
existingOpNodeList: ReadonlyArray<OperationTypeDefinitionNode> = []
14+
): OperationTypeDefinitionNode[] {
15+
const finalOpNodeList: OperationTypeDefinitionNode[] = [];
16+
for (const opNodeType in operationTypeDefinitionNodeTypeRootTypeMap) {
17+
const opNode =
18+
opNodeList.find(n => n.operation === opNodeType) || existingOpNodeList.find(n => n.operation === opNodeType);
19+
if (opNode) {
20+
finalOpNodeList.push(opNode);
21+
}
22+
}
23+
return finalOpNodeList;
24+
}
25+
26+
export function mergeSchemaDefs(
27+
node: SchemaDefinitionNode | SchemaExtensionNode,
28+
existingNode: SchemaDefinitionNode | SchemaExtensionNode,
29+
config?: Config
30+
): SchemaDefinitionNode | SchemaExtensionNode {
31+
if (existingNode) {
32+
return {
33+
kind:
34+
config?.convertExtensions ||
35+
node.kind === Kind.SCHEMA_DEFINITION ||
36+
existingNode.kind === Kind.SCHEMA_DEFINITION
37+
? Kind.SCHEMA_DEFINITION
38+
: Kind.SCHEMA_EXTENSION,
39+
description: node['description'] || existingNode['description'],
40+
directives: mergeDirectives(node.directives, existingNode.directives, config),
41+
operationTypes: mergeOperationTypes(node.operationTypes, existingNode.operationTypes),
42+
} as any;
43+
}
44+
45+
return (config?.convertExtensions
46+
? {
47+
...node,
48+
kind: Kind.SCHEMA_EXTENSION,
49+
}
50+
: node) as any;
51+
}
+16-81
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,4 @@
1-
import {
2-
TypeNode,
3-
DefinitionNode,
4-
EnumTypeDefinitionNode,
5-
NamedTypeNode,
6-
ListTypeNode,
7-
NonNullTypeNode,
8-
ObjectTypeDefinitionNode,
9-
Source,
10-
UnionTypeDefinitionNode,
11-
ScalarTypeDefinitionNode,
12-
InputObjectTypeDefinitionNode,
13-
InterfaceTypeDefinitionNode,
14-
DirectiveDefinitionNode,
15-
SchemaDefinitionNode,
16-
ObjectTypeExtensionNode,
17-
InputObjectTypeExtensionNode,
18-
EnumTypeExtensionNode,
19-
UnionTypeExtensionNode,
20-
ScalarTypeExtensionNode,
21-
InterfaceTypeExtensionNode,
22-
Kind,
23-
} from 'graphql';
1+
import { TypeNode, NamedTypeNode, ListTypeNode, NonNullTypeNode, Source, Kind } from 'graphql';
242

253
export function isStringTypes(types: any): types is string {
264
return typeof types === 'string';
@@ -30,68 +8,12 @@ export function isSourceTypes(types: any): types is Source {
308
return types instanceof Source;
319
}
3210

33-
export function isGraphQLType(definition: DefinitionNode): definition is ObjectTypeDefinitionNode {
34-
return definition.kind === 'ObjectTypeDefinition';
35-
}
36-
37-
export function isGraphQLTypeExtension(definition: DefinitionNode): definition is ObjectTypeExtensionNode {
38-
return definition.kind === 'ObjectTypeExtension';
39-
}
40-
41-
export function isGraphQLEnum(definition: DefinitionNode): definition is EnumTypeDefinitionNode {
42-
return definition.kind === 'EnumTypeDefinition';
43-
}
44-
45-
export function isGraphQLEnumExtension(definition: DefinitionNode): definition is EnumTypeExtensionNode {
46-
return definition.kind === 'EnumTypeExtension';
47-
}
48-
49-
export function isGraphQLUnion(definition: DefinitionNode): definition is UnionTypeDefinitionNode {
50-
return definition.kind === 'UnionTypeDefinition';
51-
}
52-
53-
export function isGraphQLUnionExtension(definition: DefinitionNode): definition is UnionTypeExtensionNode {
54-
return definition.kind === 'UnionTypeExtension';
55-
}
56-
57-
export function isGraphQLScalar(definition: DefinitionNode): definition is ScalarTypeDefinitionNode {
58-
return definition.kind === 'ScalarTypeDefinition';
59-
}
60-
61-
export function isGraphQLScalarExtension(definition: DefinitionNode): definition is ScalarTypeExtensionNode {
62-
return definition.kind === 'ScalarTypeExtension';
63-
}
64-
65-
export function isGraphQLInputType(definition: DefinitionNode): definition is InputObjectTypeDefinitionNode {
66-
return definition.kind === 'InputObjectTypeDefinition';
67-
}
68-
69-
export function isGraphQLInputTypeExtension(definition: DefinitionNode): definition is InputObjectTypeExtensionNode {
70-
return definition.kind === 'InputObjectTypeExtension';
71-
}
72-
73-
export function isGraphQLInterface(definition: DefinitionNode): definition is InterfaceTypeDefinitionNode {
74-
return definition.kind === 'InterfaceTypeDefinition';
75-
}
76-
77-
export function isGraphQLInterfaceExtension(definition: DefinitionNode): definition is InterfaceTypeExtensionNode {
78-
return definition.kind === 'InterfaceTypeExtension';
79-
}
80-
81-
export function isGraphQLDirective(definition: DefinitionNode): definition is DirectiveDefinitionNode {
82-
return definition.kind === 'DirectiveDefinition';
83-
}
84-
8511
export function extractType(type: TypeNode): NamedTypeNode {
8612
let visitedType = type;
87-
while (visitedType.kind === 'ListType' || visitedType.kind === 'NonNullType') {
13+
while (visitedType.kind === Kind.LIST_TYPE || visitedType.kind === 'NonNullType') {
8814
visitedType = visitedType.type;
8915
}
90-
return visitedType as any;
91-
}
92-
93-
export function isSchemaDefinition(node: DefinitionNode): node is SchemaDefinitionNode {
94-
return node.kind === 'SchemaDefinition';
16+
return visitedType;
9517
}
9618

9719
export function isWrappingTypeNode(type: TypeNode): type is ListTypeNode | NonNullTypeNode {
@@ -117,3 +39,16 @@ export function printTypeNode(type: TypeNode): string {
11739

11840
return type.name.value;
11941
}
42+
43+
export enum CompareVal {
44+
A_SMALLER_THAN_B = -1,
45+
A_EQUALS_B = 0,
46+
A_GREATER_THAN_B = 1,
47+
}
48+
export type CompareFn<T> = (a: T, b: T) => -1 | 0 | 1;
49+
50+
export function defaultStringComparator(a: string, b: string): CompareVal {
51+
if (a < b) return CompareVal.A_SMALLER_THAN_B;
52+
if (a > b) return CompareVal.A_GREATER_THAN_B;
53+
return CompareVal.A_EQUALS_B;
54+
}

‎packages/utils/src/create-schema-definition.ts

-44
This file was deleted.

‎packages/utils/src/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export * from './fix-schema-ast';
1313
export * from './parse-graphql-json';
1414
export * from './parse-graphql-sdl';
1515
export * from './get-user-types-from-schema';
16-
export * from './create-schema-definition';
1716
export * from './build-operation-for-field';
1817
export * from './types';
1918
export * from './filterSchema';

0 commit comments

Comments
 (0)
Please sign in to comment.