Skip to content

Commit b534311

Browse files
committed
✨ Added tree ops for directives
1 parent a3b638a commit b534311

File tree

5 files changed

+177
-4
lines changed

5 files changed

+177
-4
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "graphql-js-tree",
3-
"version": "0.2.7",
3+
"version": "0.2.8",
44
"private": false,
55
"license": "MIT",
66
"description": "GraphQL Parser providing simplier structure",

src/TreeOperations/directive.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Options, ParserField } from '@/Models';
2+
import { checkValueType } from '@/TreeOperations/tree';
3+
4+
export const recursivelyDeleteDirectiveNodes = (nodes: ParserField[], directiveName: string) => {
5+
nodes.forEach((n) => {
6+
if (n.directives) {
7+
n.directives = n.directives.filter((d) => d.name !== directiveName);
8+
}
9+
recursivelyDeleteDirectiveNodes(n.args, directiveName);
10+
});
11+
};
12+
13+
export const recursivelyRenameDirectiveNodes = (
14+
nodes: ParserField[],
15+
oldDirectiveName: string,
16+
newDirectiveName: string,
17+
) => {
18+
nodes.forEach((n) => {
19+
if (n.directives) {
20+
n.directives = n.directives.map((d) =>
21+
d.name !== oldDirectiveName
22+
? d
23+
: {
24+
...d,
25+
name: newDirectiveName,
26+
type: {
27+
fieldType: {
28+
name: newDirectiveName,
29+
type: Options.name,
30+
},
31+
},
32+
},
33+
);
34+
}
35+
recursivelyRenameDirectiveNodes(n.args, oldDirectiveName, newDirectiveName);
36+
});
37+
};
38+
39+
export const recursivelyUpdateDirectiveArgument = (
40+
nodes: ParserField[],
41+
directiveName: string,
42+
oldField: ParserField,
43+
newField: ParserField,
44+
allNodes: ParserField[],
45+
) => {
46+
nodes.forEach((n) => {
47+
if (n.directives.length > 0) {
48+
const remappedDirectives = n.directives.map((d) => {
49+
if (d.name !== directiveName) return d;
50+
const remappedArgs = d.args.map((a) =>
51+
a.name === oldField.name
52+
? {
53+
...a,
54+
name: newField.name,
55+
value: {
56+
value: a.value?.value,
57+
type: checkValueType(newField, allNodes),
58+
},
59+
type: {
60+
fieldType: {
61+
...a.type.fieldType,
62+
name: newField.name,
63+
},
64+
},
65+
}
66+
: a,
67+
);
68+
return {
69+
...d,
70+
args: remappedArgs,
71+
};
72+
});
73+
n.directives = remappedDirectives;
74+
}
75+
recursivelyUpdateDirectiveArgument(n.args, directiveName, oldField, newField, allNodes);
76+
});
77+
};
78+
79+
export const recursivelyDeleteDirectiveArgument = (
80+
nodes: ParserField[],
81+
directiveName: string,
82+
argumentField: ParserField,
83+
) => {
84+
nodes.forEach((n) => {
85+
if (n.directives) {
86+
n.directives = n.directives.map((d) =>
87+
d.name !== directiveName
88+
? d
89+
: {
90+
...d,
91+
args: d.args.filter((a) => a.name !== argumentField.name),
92+
},
93+
);
94+
}
95+
recursivelyDeleteDirectiveArgument(n.args, directiveName, argumentField);
96+
});
97+
};

src/TreeOperations/tree.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import {
1010
ValueDefinition,
1111
} from '@/Models';
1212
import { getTypeName } from '@/shared';
13+
import {
14+
recursivelyDeleteDirectiveArgument,
15+
recursivelyDeleteDirectiveNodes,
16+
recursivelyRenameDirectiveNodes,
17+
recursivelyUpdateDirectiveArgument,
18+
} from '@/TreeOperations/directive';
1319
import {
1420
changeInterfaceField,
1521
updateInterfaceNodeAddField,
@@ -59,6 +65,10 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => {
5965

6066
const updateFieldOnNode = (node: ParserField, i: number, updatedField: ParserField) => {
6167
regenerateId(updatedField);
68+
if (node.data.type === TypeSystemDefinition.DirectiveDefinition) {
69+
const oldField: ParserField = JSON.parse(JSON.stringify(node.args[i]));
70+
recursivelyUpdateDirectiveArgument(allNodes, node.name, oldField, updatedField, allNodes);
71+
}
6272
if (node.data.type === TypeDefinition.InterfaceTypeDefinition) {
6373
const oldField: ParserField = JSON.parse(JSON.stringify(node.args[i]));
6474
changeInterfaceField(tree.nodes, node, oldField, updatedField);
@@ -81,6 +91,9 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => {
8191
if (isError) {
8292
return;
8393
}
94+
if (node.data.type === TypeSystemDefinition.DirectiveDefinition) {
95+
recursivelyRenameDirectiveNodes(allNodes, node.name, newName);
96+
}
8497
if (node.data.type === TypeDefinition.InterfaceTypeDefinition) {
8598
const oldName = node.name;
8699
tree.nodes
@@ -121,6 +134,9 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => {
121134
if (node.data.type === ValueDefinition.InputValueDefinition) {
122135
const parent = allNodes.find((parentNode) => parentNode.args.includes(node));
123136
if (parent) {
137+
if (parent.data.type === TypeSystemDefinition.DirectiveDefinition) {
138+
recursivelyDeleteDirectiveArgument(allNodes, parent.name, node);
139+
}
124140
const index = parent.args.indexOf(node);
125141
deleteFieldFromNode(parent, index);
126142
} else {
@@ -142,10 +158,13 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => {
142158
return;
143159
}
144160
if (node.data.type === Instances.Directive) {
145-
throw new Error('Directives should be removed on node directly not using this function');
161+
throw new Error('Directive Instances should be removed on node directly not using this function');
146162
}
147163
if (node.data.type === Instances.Argument) {
148-
throw new Error('Directive Arguments should be removed on node directly not using this function');
164+
throw new Error('Directive Instance Arguments should be removed on node directly not using this function');
165+
}
166+
if (node.data.type === TypeSystemDefinition.DirectiveDefinition) {
167+
recursivelyDeleteDirectiveNodes(allNodes, node.name);
149168
}
150169
const deletedNode = tree.nodes.findIndex((n) => n === node);
151170
if (deletedNode === -1) throw new Error('Error deleting a node');
@@ -224,7 +243,7 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => {
224243
};
225244
};
226245

227-
const checkValueType = (node: ParserField, nodes: ParserField[]) => {
246+
export const checkValueType = (node: ParserField, nodes: ParserField[]) => {
228247
const isArray = isArrayType(node.type.fieldType);
229248
if (isArray) return Value.ListValue;
230249
const tName = getTypeName(node.type.fieldType);

src/__tests__/TreeOperations/tree.remove.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ describe('Tree Operations - node removal tests', () => {
1919
expect(treeMock.nodes[0].interfaces).not.toContainEqual('Node');
2020
expect(treeMock.nodes[0].args).not.toContainEqual(copyOfArg);
2121
});
22+
test('Delete directive node from tree', () => {
23+
const treeMock = createMock();
24+
const directiveNode = JSON.parse(JSON.stringify(treeMock.nodes[5]));
25+
mutate(treeMock, treeMock.nodes).removeNode(treeMock.nodes[5]);
26+
expect(treeMock.nodes).not.toContainEqual(directiveNode);
27+
expect(treeMock.nodes[0].directives).toHaveLength(0);
28+
});
2229
test('Delete field from root node', () => {
2330
const treeMock = createMock();
2431
const oldPersonNodeId = treeMock.nodes[0].id;
@@ -68,4 +75,11 @@ describe('Tree Operations - node removal tests', () => {
6875
mutate(treeMock, treeMock.nodes).removeNode(treeMock.nodes[3].args[0]);
6976
expect(treeMock.nodes[3].args).not.toContainEqual(oldArgument);
7077
});
78+
test('Delete directive field from directive definition node', () => {
79+
const treeMock = createMock();
80+
const oldArgument = JSON.parse(JSON.stringify(treeMock.nodes[5].args[0]));
81+
mutate(treeMock, treeMock.nodes).removeNode(treeMock.nodes[5].args[0]);
82+
expect(treeMock.nodes[5].args).not.toContainEqual(oldArgument);
83+
expect(treeMock.nodes[0].directives[0].args).toHaveLength(0);
84+
});
7185
});

src/__tests__/TreeOperations/tree.rename.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Options, ParserField, ScalarTypes, Value } from '@/Models';
12
import { createParserField, getTypeName } from '@/shared';
23
import { mutate } from '@/TreeOperations/tree';
34
import { createMock, createInterfaceMock } from '@/__tests__/TreeOperations/mocks';
@@ -19,6 +20,15 @@ describe('Tree Operations tests - rename operations', () => {
1920
expect(treeMock.nodes[1].interfaces).toContainEqual('Alien');
2021
expect(treeMock.nodes[1].args[0].fromInterface).toContainEqual('Alien');
2122
});
23+
test(`Change directive node name`, () => {
24+
const treeMock = createMock();
25+
const oldDirectiveNodeId = treeMock.nodes[5].id;
26+
mutate(treeMock, treeMock.nodes).renameRootNode(treeMock.nodes[5], 'alien');
27+
expect(treeMock.nodes[5].id).not.toEqual(oldDirectiveNodeId);
28+
expect(treeMock.nodes[5].name).toEqual('alien');
29+
expect(getTypeName(treeMock.nodes[0].directives[0].type.fieldType)).toEqual('alien');
30+
expect(treeMock.nodes[0].directives[0].name).toEqual('alien');
31+
});
2232
test('Update field name on field node', () => {
2333
const treeMock = createMock();
2434
const oldPersonNodeId = treeMock.nodes[0].id;
@@ -47,6 +57,39 @@ describe('Tree Operations tests - rename operations', () => {
4757
fromInterface: [treeMock.nodes[0].name],
4858
});
4959
});
60+
test('Update field name on directive definition', () => {
61+
const treeMock = createMock();
62+
const updatedField = createParserField({
63+
...treeMock.nodes[5].args[0],
64+
name: 'firstName',
65+
type: {
66+
fieldType: {
67+
type: Options.name,
68+
name: ScalarTypes.String,
69+
},
70+
},
71+
});
72+
const argumentCopy = JSON.parse(JSON.stringify(treeMock.nodes[0].directives[0].args[0])) as ParserField;
73+
74+
mutate(treeMock, treeMock.nodes).updateFieldOnNode(treeMock.nodes[5], 0, updatedField);
75+
expect(treeMock.nodes[5].args).toContainEqual({
76+
...updatedField,
77+
});
78+
expect(treeMock.nodes[0].directives[0].args).toContainEqual({
79+
...argumentCopy,
80+
name: 'firstName',
81+
value: {
82+
type: Value.StringValue,
83+
value: argumentCopy.value?.value,
84+
},
85+
type: {
86+
fieldType: {
87+
name: 'firstName',
88+
type: Options.name,
89+
},
90+
},
91+
});
92+
});
5093
test('Update input value name on input value node', () => {
5194
const treeMock = createMock();
5295
const oldQueryId = treeMock.nodes[1].id;

0 commit comments

Comments
 (0)