Skip to content

Commit e4654ad

Browse files
committed
✨ Added schema merging algorithm
1 parent 9426f61 commit e4654ad

File tree

4 files changed

+219
-1
lines changed

4 files changed

+219
-1
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-js-tree",
3-
"version": "0.3.6",
3+
"version": "0.3.7",
44
"private": false,
55
"license": "MIT",
66
"description": "GraphQL Parser providing simplier structure",

Diff for: src/TreeOperations/merge.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ParserField, ParserTree } from '@/Models';
2+
import { Parser } from '@/Parser';
3+
import { isExtensionNode } from '@/TreeOperations/shared';
4+
import { TreeToGraphQL } from '@/TreeToGraphQL';
5+
6+
const mergeNode = (n1: ParserField, n2: ParserField) => {
7+
const mergedNode = {
8+
...n1,
9+
args: [...n1.args, ...n2.args],
10+
directives: [...n1.directives, ...n2.directives],
11+
interfaces: [...n1.interfaces, ...n2.interfaces],
12+
} as ParserField;
13+
//dedupe
14+
mergedNode.args = mergedNode.args.filter((a, i) => mergedNode.args.findIndex((aa) => aa.name === a.name) === i);
15+
mergedNode.directives = mergedNode.directives.filter(
16+
(a, i) => mergedNode.directives.findIndex((aa) => aa.name === a.name) === i,
17+
);
18+
mergedNode.interfaces = mergedNode.interfaces.filter(
19+
(a, i) => mergedNode.interfaces.findIndex((aa) => aa === a) === i,
20+
);
21+
return mergedNode;
22+
};
23+
24+
export const mergeTrees = (tree1: ParserTree, tree2: ParserTree) => {
25+
const mergedNodesT1: ParserField[] = [];
26+
const mergedNodesT2: ParserField[] = [];
27+
const mergeResultNodes: ParserField[] = [];
28+
const errors: Array<{ conflictingNode: string; conflictingField: string }> = [];
29+
// merge nodes
30+
tree1.nodes.forEach((t1n) => {
31+
const matchingNode = tree2.nodes.find((t2n) => t2n.name === t1n.name && t1n.data.type === t2n.data.type);
32+
if (matchingNode) {
33+
if (isExtensionNode(matchingNode.data.type)) {
34+
t1n.args.forEach((t1nA) => {
35+
const matchingArg = matchingNode.args.find((mNA) => mNA.name === t1nA.name);
36+
if (matchingArg) {
37+
if (JSON.stringify(matchingArg) !== JSON.stringify(t1nA)) {
38+
errors.push({
39+
conflictingField: t1nA.name,
40+
conflictingNode: t1n.name,
41+
});
42+
}
43+
}
44+
});
45+
} else {
46+
// Check if arg named same and different typings -> throw
47+
mergedNodesT1.push(t1n);
48+
mergedNodesT2.push(matchingNode);
49+
t1n.args.forEach((t1nA) => {
50+
const matchingArg = matchingNode.args.find((mNA) => mNA.name === t1nA.name);
51+
if (matchingArg) {
52+
if (JSON.stringify(matchingArg) !== JSON.stringify(t1nA)) {
53+
errors.push({
54+
conflictingField: t1nA.name,
55+
conflictingNode: t1n.name,
56+
});
57+
}
58+
}
59+
});
60+
if (!errors.length) {
61+
mergeResultNodes.push(mergeNode(t1n, matchingNode));
62+
}
63+
}
64+
}
65+
});
66+
if (errors.length) {
67+
return {
68+
__typename: 'error' as const,
69+
errors,
70+
};
71+
}
72+
const t1Nodes = tree1.nodes.filter((t1n) => !mergedNodesT1.find((mtn1) => mtn1 === t1n));
73+
const t2Nodes = tree2.nodes.filter((t2n) => !mergedNodesT2.find((mtn2) => mtn2 === t2n));
74+
return {
75+
__typename: 'success' as const,
76+
nodes: [...t1Nodes, ...mergeResultNodes, ...t2Nodes],
77+
};
78+
};
79+
80+
export const mergeSDLs = (sdl1: string, sdl2: string) => {
81+
const t1 = Parser.parse(sdl1);
82+
const t2 = Parser.parse(sdl2);
83+
const mergeResult = mergeTrees(t1, t2);
84+
if (mergeResult.__typename === 'success') {
85+
const sdl = TreeToGraphQL.parse(mergeResult);
86+
return {
87+
...mergeResult,
88+
sdl,
89+
};
90+
}
91+
return mergeResult;
92+
};

Diff for: src/__tests__/TreeOperations/merge.spec.ts

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { mergeSDLs } from '@/TreeOperations/merge';
2+
import { expectTrimmedEqual } from '@/__tests__/TestUtils';
3+
4+
// const mergingErrorSchema = `
5+
// type Person{
6+
// lastName: String
7+
// }
8+
// `;
9+
10+
describe('Merging GraphQL Schemas', () => {
11+
it('Should merge fields of both nodes', () => {
12+
const baseSchema = `
13+
type Person{
14+
firstName: String
15+
health: String
16+
}
17+
`;
18+
19+
const mergingSchema = `
20+
type Person{
21+
lastName: String
22+
}
23+
`;
24+
const t1 = mergeSDLs(baseSchema, mergingSchema);
25+
if (t1.__typename === 'error') throw new Error('Invalid parse');
26+
expectTrimmedEqual(
27+
t1.sdl,
28+
`
29+
type Person{
30+
firstName: String
31+
health: String
32+
lastName: String
33+
}`,
34+
);
35+
});
36+
it('Should generate Conflict', () => {
37+
const baseSchema = `
38+
type Person{
39+
firstName: String
40+
health: String
41+
}
42+
`;
43+
44+
const mergingSchema = `
45+
type Person{
46+
lastName: String
47+
health: Int
48+
}
49+
`;
50+
const t1 = mergeSDLs(baseSchema, mergingSchema);
51+
expect(t1.__typename).toEqual('error');
52+
});
53+
it('Should not merge extension nodes', () => {
54+
const baseSchema = `
55+
type Person{
56+
firstName: String
57+
}
58+
extend type Person{
59+
lastName: String
60+
}
61+
`;
62+
63+
const mergingSchema = `
64+
extend type Person{
65+
age: String
66+
}
67+
`;
68+
const t1 = mergeSDLs(baseSchema, mergingSchema);
69+
if (t1.__typename === 'error') throw new Error('Invalid parse');
70+
expectTrimmedEqual(
71+
t1.sdl,
72+
`
73+
type Person{
74+
firstName: String
75+
}
76+
extend type Person{
77+
lastName: String
78+
}
79+
extend type Person{
80+
age: String
81+
}`,
82+
);
83+
});
84+
it('Should merge interfaces and implementation of both nodes', () => {
85+
const baseSchema = `
86+
type Person implements Node{
87+
firstName: String
88+
health: String
89+
_id: String
90+
}
91+
interface Node {
92+
_id: String
93+
}
94+
`;
95+
96+
const mergingSchema = `
97+
type Person implements Dateable{
98+
lastName: String
99+
createdAt: String
100+
}
101+
interface Dateable {
102+
createdAt: String
103+
}
104+
`;
105+
const t1 = mergeSDLs(baseSchema, mergingSchema);
106+
if (t1.__typename === 'error') throw new Error('Invalid parse');
107+
expectTrimmedEqual(
108+
t1.sdl,
109+
`
110+
interface Node {
111+
_id: String
112+
}
113+
type Person implements Node & Dateable{
114+
firstName: String
115+
health: String
116+
_id: String
117+
lastName: String
118+
createdAt: String
119+
}
120+
interface Dateable {
121+
createdAt: String
122+
}`,
123+
);
124+
});
125+
});

Diff for: src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './Parser';
33
export * from './Models';
44
export * from './shared/index';
55
export * from './TreeOperations/tree';
6+
export * from './TreeOperations/merge';
67
export * from './GqlParser/index';
78
export * from './GqlParser/GqlParserTreeToGql';
89
export * from './GqlParser/valueNode';

0 commit comments

Comments
 (0)