Skip to content

Commit 183d698

Browse files
committed
federation support
1 parent 33dcd12 commit 183d698

File tree

7 files changed

+258
-104
lines changed

7 files changed

+258
-104
lines changed

package.json

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

src/TreeOperations/merge/arguments.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ParserField, Options } from '@/Models';
2+
import { MergeError } from '@/TreeOperations/merge/common';
3+
4+
export const mergeArguments = (parentName: string, args1: ParserField[], args2: ParserField[]) => {
5+
args2
6+
.filter((a) => a.type.fieldType.type === Options.required)
7+
.forEach((a2) => {
8+
if (!args1.find((a1) => a1.name === a2.name))
9+
throw new MergeError({
10+
conflictingNode: parentName,
11+
conflictingField: a2.name,
12+
message: 'Cannot merge when required argument does not exist in correlated node',
13+
});
14+
});
15+
return args1
16+
.map((a1) => {
17+
const equivalentA2 = args2.find((a2) => a2.name === a1.name);
18+
if (!equivalentA2 && a1.type.fieldType.type === Options.required)
19+
throw new MergeError({
20+
conflictingNode: parentName,
21+
conflictingField: a1.name,
22+
message: 'Cannot merge when required argument does not exist in correlated node',
23+
});
24+
if (!equivalentA2) return;
25+
if (a1.type.fieldType.type === Options.required) return a1;
26+
if (equivalentA2.type.fieldType.type === Options.required) return equivalentA2;
27+
})
28+
.filter(<T>(v: T | undefined): v is T => !!v);
29+
};

src/TreeOperations/merge/common.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class MergeError extends Error {
2+
constructor(
3+
public errorParams: {
4+
conflictingNode: string;
5+
conflictingField?: string;
6+
message?: string;
7+
},
8+
) {
9+
super('Merging error');
10+
}
11+
}
12+
13+
export type ErrorConflict = { conflictingNode: string; conflictingField?: string };

src/TreeOperations/merge.ts src/TreeOperations/merge/merge.ts

+64-23
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,67 @@
1-
import { ParserField, ParserTree, TypeSystemDefinition } from '@/Models';
1+
import { ParserField, ParserTree, TypeDefinition, TypeSystemDefinition } from '@/Models';
22
import { Parser } from '@/Parser';
3+
import { mergeArguments } from '@/TreeOperations/merge/arguments';
4+
import { MergeError, ErrorConflict } from '@/TreeOperations/merge/common';
35
import { isExtensionNode } from '@/TreeOperations/shared';
46
import { TreeToGraphQL } from '@/TreeToGraphQL';
5-
import { generateNodeId } from '@/shared';
7+
import { generateNodeId, getTypeName } from '@/shared';
68

9+
const detectConflictOnBaseNode = (n1: ParserField, n2: ParserField) => {
10+
if (n1.data.type !== n2.data.type)
11+
throw new MergeError({
12+
conflictingNode: n1.name,
13+
message: `Data type conflict of nodes ${n1.name} and ${n2.name}`,
14+
});
15+
if (JSON.stringify(n1.interfaces) !== JSON.stringify(n2.interfaces))
16+
throw new MergeError({
17+
conflictingNode: n1.name,
18+
message: `Data type conflict of nodes ${n1.name} and ${n2.name}`,
19+
});
20+
};
21+
22+
const detectConflictOnFieldNode = (parentName: string, f1: ParserField, f2: ParserField) => {
23+
const [f1Type, f2Type] = [getTypeName(f1.type.fieldType), getTypeName(f2.type.fieldType)];
24+
if (f1Type !== f2Type)
25+
throw new MergeError({
26+
conflictingNode: parentName,
27+
conflictingField: f1.name,
28+
message: `Data type conflict of node ${parentName} field ${f1.name} `,
29+
});
30+
};
731
const addFromLibrary = (n: ParserField): ParserField => ({ ...n, fromLibrary: true });
832

33+
const mergeFields = (parentName: string, fields1: ParserField[], fields2: ParserField[]) => {
34+
const mergedCommonFieldsAndF1Fields = fields1
35+
.map((f1) => {
36+
const commonField = fields2.find((f2) => f2.name === f1.name);
37+
if (!commonField) return f1;
38+
detectConflictOnFieldNode(parentName, f1, commonField);
39+
const mergedField: ParserField = {
40+
...f1,
41+
args: mergeArguments(f1.name, f1.args, commonField.args),
42+
};
43+
return mergedField;
44+
})
45+
.filter(<T>(f: T | undefined): f is T => !!f);
46+
const otherF2Fields = fields2.filter((f2) => !fields1.find((f1) => f1.name === f2.name));
47+
return [...mergedCommonFieldsAndF1Fields, ...otherF2Fields];
48+
};
49+
950
const mergeNode = (n1: ParserField, n2: ParserField) => {
10-
const args = [...n1.args, ...n2.args.map(addFromLibrary)];
51+
detectConflictOnBaseNode(n1, n2);
52+
const args =
53+
n1.data.type === TypeDefinition.InputObjectTypeDefinition
54+
? mergeArguments(n1.name, n1.args, n2.args)
55+
: mergeFields(n1.name, n1.args, n2.args.map(addFromLibrary));
56+
1157
const mergedNode = {
1258
...n1,
1359
id: generateNodeId(n1.name, n1.data.type, args),
1460
args,
1561
directives: [...n1.directives, ...n2.directives.map(addFromLibrary)],
1662
interfaces: [...n1.interfaces, ...n2.interfaces],
1763
} as ParserField;
18-
//dedupe
19-
mergedNode.args = mergedNode.args.filter((a, i) => mergedNode.args.findIndex((aa) => aa.name === a.name) === i);
64+
2065
mergedNode.directives = mergedNode.directives.filter(
2166
(a, i) => mergedNode.directives.findIndex((aa) => aa.name === a.name) === i,
2267
);
@@ -30,7 +75,7 @@ export const mergeTrees = (tree1: ParserTree, tree2: ParserTree) => {
3075
const mergedNodesT1: ParserField[] = [];
3176
const mergedNodesT2: ParserField[] = [];
3277
const mergeResultNodes: ParserField[] = [];
33-
const errors: Array<{ conflictingNode: string; conflictingField: string }> = [];
78+
const errors: Array<ErrorConflict> = [];
3479
const filteredTree2Nodes = tree2.nodes.filter((t) => t.data.type !== TypeSystemDefinition.SchemaDefinition);
3580
// merge nodes
3681
tree1.nodes.forEach((t1n) => {
@@ -48,23 +93,19 @@ export const mergeTrees = (tree1: ParserTree, tree2: ParserTree) => {
4893
}
4994
}
5095
});
51-
} else {
52-
// Check if arg named same and different typings -> throw
53-
mergedNodesT1.push(t1n);
54-
mergedNodesT2.push(matchingNode);
55-
t1n.args.forEach((t1nA) => {
56-
const matchingArg = matchingNode.args.find((mNA) => mNA.name === t1nA.name);
57-
if (matchingArg) {
58-
if (JSON.stringify(matchingArg) !== JSON.stringify(t1nA)) {
59-
errors.push({
60-
conflictingField: t1nA.name,
61-
conflictingNode: t1n.name,
62-
});
63-
}
64-
}
65-
});
66-
if (!errors.length) {
67-
mergeResultNodes.push(mergeNode(t1n, matchingNode));
96+
return;
97+
}
98+
mergedNodesT1.push(t1n);
99+
mergedNodesT2.push(matchingNode);
100+
try {
101+
const mergeNodeResult = mergeNode(t1n, matchingNode);
102+
mergeResultNodes.push(mergeNodeResult);
103+
} catch (error) {
104+
if (error instanceof MergeError) {
105+
errors.push({
106+
conflictingNode: error.errorParams.conflictingNode,
107+
conflictingField: error.errorParams.conflictingField,
108+
});
68109
}
69110
}
70111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { mergeSDLs } from '@/TreeOperations/merge/merge';
2+
import { expectTrimmedEqual } from '@/__tests__/TestUtils';
3+
4+
describe('Merging GraphQL Inputs and field arguments', () => {
5+
it('Should merge inputs leaving only common fields.', () => {
6+
const baseSchema = `
7+
input UserInput {
8+
name: String!
9+
age: Int # Not in Subgraph B
10+
}
11+
`;
12+
13+
const mergingSchema = `
14+
input UserInput {
15+
name: String!
16+
email: String # Not in Subgraph A
17+
}
18+
`;
19+
const t1 = mergeSDLs(baseSchema, mergingSchema);
20+
if (t1.__typename === 'error') throw new Error('Invalid parse');
21+
expectTrimmedEqual(
22+
t1.sdl,
23+
`
24+
input UserInput{
25+
name: String!
26+
}`,
27+
);
28+
});
29+
it('Should merge inputs marking fields required.', () => {
30+
const baseSchema = `
31+
input UserInput {
32+
name: String!
33+
age: Int
34+
}
35+
`;
36+
37+
const mergingSchema = `
38+
input UserInput {
39+
name: String
40+
age: Int!
41+
}
42+
`;
43+
const t1 = mergeSDLs(baseSchema, mergingSchema);
44+
if (t1.__typename === 'error') throw new Error('Invalid parse');
45+
expectTrimmedEqual(
46+
t1.sdl,
47+
`
48+
input UserInput{
49+
name: String!
50+
age: Int!
51+
}`,
52+
);
53+
});
54+
it('Should not merge inputs', () => {
55+
const baseSchema = `
56+
input UserInput {
57+
name: String!
58+
}
59+
`;
60+
61+
const mergingSchema = `
62+
input UserInput {
63+
name: String!
64+
email: String!
65+
}
66+
`;
67+
const t1 = mergeSDLs(baseSchema, mergingSchema);
68+
if (t1.__typename === 'success') console.log(t1.sdl);
69+
expect(t1.__typename).toEqual('error');
70+
});
71+
it('Should merge field arguments marking them required.', () => {
72+
const baseSchema = `
73+
type Main{
74+
getUsers(funny: Boolean, premium: String!): String!
75+
}
76+
`;
77+
78+
const mergingSchema = `
79+
type Main{
80+
getUsers(funny: Boolean!, premium: String): String!
81+
}
82+
`;
83+
const t1 = mergeSDLs(baseSchema, mergingSchema);
84+
if (t1.__typename === 'error') throw new Error('Invalid parse');
85+
expectTrimmedEqual(
86+
t1.sdl,
87+
`
88+
type Main{
89+
getUsers(funny: Boolean! premium: String!): String!
90+
}`,
91+
);
92+
});
93+
it('Should merge field arguments leaving only common fields.', () => {
94+
const baseSchema = `
95+
type Main{
96+
getUsers(premium: String!): String!
97+
}
98+
`;
99+
100+
const mergingSchema = `
101+
type Main{
102+
getUsers(funny: Boolean, premium: String): 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+
type Main{
111+
getUsers(premium: String!): String!
112+
}`,
113+
);
114+
});
115+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { mergeSDLs } from '@/TreeOperations/merge/merge';
2+
3+
// const mergingErrorSchema = `
4+
// type Person{
5+
// lastName: String
6+
// }
7+
// `;
8+
9+
describe('Merging GraphQL Schemas', () => {
10+
it('should not merge interfaces and implementation of both nodes', () => {
11+
const baseSchema = `
12+
type Person implements Node{
13+
firstName: String
14+
health: String
15+
_id: String
16+
}
17+
interface Node {
18+
_id: String
19+
}
20+
`;
21+
22+
const mergingSchema = `
23+
type Person implements Dateable{
24+
lastName: String
25+
createdAt: String
26+
}
27+
interface Dateable {
28+
createdAt: String
29+
}
30+
`;
31+
const t1 = mergeSDLs(baseSchema, mergingSchema);
32+
expect(t1.__typename).toEqual('error');
33+
});
34+
});

0 commit comments

Comments
 (0)