diff --git a/package.json b/package.json index f2b0d22..efe0254 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-js-tree", - "version": "2.0.0", + "version": "3.0.2", "private": false, "license": "MIT", "description": "GraphQL Parser providing simplier structure", @@ -29,16 +29,17 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.2", + "graphql": "^16.8.1", "husky": "^8.0.3", "jest": "^29.7.0", "prettier": "^3.1.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", - "ts-patch": "^3.1.1", - "typescript": "^5.3.3", + "ts-patch": "^3.2.1", + "typescript": "5.5", "typescript-transform-paths": "^3.4.6" }, - "dependencies": { - "graphql": "^16.8.1" + "peerDependencies": { + "graphql": "^16.0.0 || ^17.0.0" } } diff --git a/src/TreeOperations/merge/arguments.ts b/src/TreeOperations/merge/arguments.ts new file mode 100644 index 0000000..277b49c --- /dev/null +++ b/src/TreeOperations/merge/arguments.ts @@ -0,0 +1,30 @@ +import { ParserField, Options } from '@/Models'; +import { MergeError } from '@/TreeOperations/merge/common'; + +export const mergeArguments = (parentName: string, args1: ParserField[], args2: ParserField[]) => { + args2 + .filter((a) => a.type.fieldType.type === Options.required) + .forEach((a2) => { + if (!args1.find((a1) => a1.name === a2.name)) + throw new MergeError({ + conflictingNode: parentName, + conflictingField: a2.name, + message: 'Cannot merge when required argument does not exist in correlated node', + }); + }); + return args1 + .map((a1) => { + const equivalentA2 = args2.find((a2) => a2.name === a1.name); + if (!equivalentA2 && a1.type.fieldType.type === Options.required) + throw new MergeError({ + conflictingNode: parentName, + conflictingField: a1.name, + message: 'Cannot merge when required argument does not exist in correlated node', + }); + if (!equivalentA2) return; + if (a1.type.fieldType.type === Options.required) return a1; + if (equivalentA2.type.fieldType.type === Options.required) return equivalentA2; + if (a1.type.fieldType.type === equivalentA2.type.fieldType.type) return a1; + }) + .filter((v: T | undefined): v is T => !!v); +}; diff --git a/src/TreeOperations/merge/common.ts b/src/TreeOperations/merge/common.ts new file mode 100644 index 0000000..77ce61c --- /dev/null +++ b/src/TreeOperations/merge/common.ts @@ -0,0 +1,13 @@ +export class MergeError extends Error { + constructor( + public errorParams: { + conflictingNode: string; + conflictingField?: string; + message?: string; + }, + ) { + super('Merging error'); + } +} + +export type ErrorConflict = { conflictingNode: string; conflictingField?: string }; diff --git a/src/TreeOperations/merge.ts b/src/TreeOperations/merge/merge.ts similarity index 51% rename from src/TreeOperations/merge.ts rename to src/TreeOperations/merge/merge.ts index 7143686..d472f61 100644 --- a/src/TreeOperations/merge.ts +++ b/src/TreeOperations/merge/merge.ts @@ -1,13 +1,59 @@ -import { ParserField, ParserTree, TypeSystemDefinition } from '@/Models'; +import { ParserField, ParserTree, TypeDefinition, TypeSystemDefinition } from '@/Models'; import { Parser } from '@/Parser'; +import { mergeArguments } from '@/TreeOperations/merge/arguments'; +import { MergeError, ErrorConflict } from '@/TreeOperations/merge/common'; import { isExtensionNode } from '@/TreeOperations/shared'; import { TreeToGraphQL } from '@/TreeToGraphQL'; -import { generateNodeId } from '@/shared'; +import { generateNodeId, getTypeName } from '@/shared'; +const detectConflictOnBaseNode = (n1: ParserField, n2: ParserField) => { + if (n1.data.type !== n2.data.type) + throw new MergeError({ + conflictingNode: n1.name, + message: `Data type conflict of nodes ${n1.name} and ${n2.name}`, + }); + if (JSON.stringify(n1.interfaces) !== JSON.stringify(n2.interfaces)) + throw new MergeError({ + conflictingNode: n1.name, + message: `Data type conflict of nodes ${n1.name} and ${n2.name}`, + }); +}; + +const detectConflictOnFieldNode = (parentName: string, f1: ParserField, f2: ParserField) => { + const [f1Type, f2Type] = [getTypeName(f1.type.fieldType), getTypeName(f2.type.fieldType)]; + if (f1Type !== f2Type) + throw new MergeError({ + conflictingNode: parentName, + conflictingField: f1.name, + message: `Data type conflict of node ${parentName} field ${f1.name} `, + }); +}; const addFromLibrary = (n: ParserField): ParserField => ({ ...n, fromLibrary: true }); +const mergeFields = (parentName: string, fields1: ParserField[], fields2: ParserField[]) => { + const mergedCommonFieldsAndF1Fields = fields1 + .map((f1) => { + const commonField = fields2.find((f2) => f2.name === f1.name); + if (!commonField) return f1; + detectConflictOnFieldNode(parentName, f1, commonField); + const mergedField: ParserField = { + ...f1, + args: mergeArguments(f1.name, f1.args, commonField.args), + }; + return mergedField; + }) + .filter((f: T | undefined): f is T => !!f); + const otherF2Fields = fields2.filter((f2) => !fields1.find((f1) => f1.name === f2.name)); + return [...mergedCommonFieldsAndF1Fields, ...otherF2Fields]; +}; + const mergeNode = (n1: ParserField, n2: ParserField) => { - const args = [...n1.args, ...n2.args.map(addFromLibrary)]; + detectConflictOnBaseNode(n1, n2); + const args = + n1.data.type === TypeDefinition.InputObjectTypeDefinition + ? mergeArguments(n1.name, n1.args, n2.args) + : mergeFields(n1.name, n1.args, n2.args.map(addFromLibrary)); + const mergedNode = { ...n1, id: generateNodeId(n1.name, n1.data.type, args), @@ -15,8 +61,7 @@ const mergeNode = (n1: ParserField, n2: ParserField) => { directives: [...n1.directives, ...n2.directives.map(addFromLibrary)], interfaces: [...n1.interfaces, ...n2.interfaces], } as ParserField; - //dedupe - mergedNode.args = mergedNode.args.filter((a, i) => mergedNode.args.findIndex((aa) => aa.name === a.name) === i); + mergedNode.directives = mergedNode.directives.filter( (a, i) => mergedNode.directives.findIndex((aa) => aa.name === a.name) === i, ); @@ -30,7 +75,7 @@ export const mergeTrees = (tree1: ParserTree, tree2: ParserTree) => { const mergedNodesT1: ParserField[] = []; const mergedNodesT2: ParserField[] = []; const mergeResultNodes: ParserField[] = []; - const errors: Array<{ conflictingNode: string; conflictingField: string }> = []; + const errors: Array = []; const filteredTree2Nodes = tree2.nodes.filter((t) => t.data.type !== TypeSystemDefinition.SchemaDefinition); // merge nodes tree1.nodes.forEach((t1n) => { @@ -48,23 +93,19 @@ export const mergeTrees = (tree1: ParserTree, tree2: ParserTree) => { } } }); - } else { - // Check if arg named same and different typings -> throw - mergedNodesT1.push(t1n); - mergedNodesT2.push(matchingNode); - t1n.args.forEach((t1nA) => { - const matchingArg = matchingNode.args.find((mNA) => mNA.name === t1nA.name); - if (matchingArg) { - if (JSON.stringify(matchingArg) !== JSON.stringify(t1nA)) { - errors.push({ - conflictingField: t1nA.name, - conflictingNode: t1n.name, - }); - } - } - }); - if (!errors.length) { - mergeResultNodes.push(mergeNode(t1n, matchingNode)); + return; + } + mergedNodesT1.push(t1n); + mergedNodesT2.push(matchingNode); + try { + const mergeNodeResult = mergeNode(t1n, matchingNode); + mergeResultNodes.push(mergeNodeResult); + } catch (error) { + if (error instanceof MergeError) { + errors.push({ + conflictingNode: error.errorParams.conflictingNode, + conflictingField: error.errorParams.conflictingField, + }); } } } diff --git a/src/TreeOperations/tree.ts b/src/TreeOperations/tree.ts index ca99819..e931f55 100644 --- a/src/TreeOperations/tree.ts +++ b/src/TreeOperations/tree.ts @@ -44,7 +44,7 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { return; } }; - const deleteFieldFromNode = (n: ParserField, i: number) => { + const deleteFieldFromNode = (n: ParserField, i: number, parentNode?: string) => { if (n.data.type === TypeDefinition.InterfaceTypeDefinition) { const argName = n.args[i].name; tree.nodes @@ -64,7 +64,7 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { } n.args.splice(i, 1); regenerateId(n); - mutateParentIfField(n); + mutateParentIfField(n, parentNode); }; const updateFieldOnNode = (node: ParserField, i: number, updatedField: ParserField, parentNode?: string) => { @@ -82,13 +82,13 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { mutateParentIfField(node, parentNode); }; - const addFieldToNode = (node: ParserField, f: ParserField) => { + const addFieldToNode = (node: ParserField, f: ParserField, parentNode?: string) => { node.args?.push({ ...f }); if (node.data.type === TypeDefinition.InterfaceTypeDefinition) { updateInterfaceNodeAddField(tree.nodes, node); } regenerateId(node); - mutateParentIfField(node); + mutateParentIfField(node, parentNode); }; const renameRootNode = (node: ParserField, newName: string) => { const isError = allNodes.map((n) => n.name).includes(newName); @@ -118,12 +118,12 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { node.name = newName; regenerateId(node); }; - const removeNode = (node: ParserField) => { + const removeNode = (node: ParserField, parentNode?: string) => { if (node.data.type === TypeSystemDefinition.FieldDefinition) { const parent = allNodes.find((parentNode) => parentNode.args.includes(node)); if (parent) { const index = parent.args.indexOf(node); - deleteFieldFromNode(parent, index); + deleteFieldFromNode(parent, index, parentNode); } return; } @@ -131,7 +131,7 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { const parent = allNodes.find((parentNode) => parentNode.args.includes(node)); if (parent) { const index = parent.args.indexOf(node); - deleteFieldFromNode(parent, index); + deleteFieldFromNode(parent, index, parentNode); } return; } @@ -142,13 +142,13 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { recursivelyDeleteDirectiveArgument(allNodes, parent.name, node); } const index = parent.args.indexOf(node); - deleteFieldFromNode(parent, index); + deleteFieldFromNode(parent, index, parentNode); } else { const parent = allNodes.find((p) => p.args.some((a) => a.args.includes(node))); const field = parent?.args.find((a) => a.args.includes(node)); if (field) { const fieldIndex = field.args.findIndex((f) => f === node); - deleteFieldFromNode(field, fieldIndex); + deleteFieldFromNode(field, fieldIndex, parentNode); } } return; @@ -157,7 +157,7 @@ export const mutate = (tree: ParserTree, allNodes: ParserField[]) => { const parent = allNodes.find((parentNode) => parentNode.args.includes(node)); if (parent) { const index = parent.args.indexOf(node); - deleteFieldFromNode(parent, index); + deleteFieldFromNode(parent, index, parentNode); } return; } diff --git a/src/__tests__/TreeOperations/merge/merge.input.spec.ts b/src/__tests__/TreeOperations/merge/merge.input.spec.ts new file mode 100644 index 0000000..ea88470 --- /dev/null +++ b/src/__tests__/TreeOperations/merge/merge.input.spec.ts @@ -0,0 +1,142 @@ +import { mergeSDLs } from '@/TreeOperations/merge/merge'; +import { expectTrimmedEqual } from '@/__tests__/TestUtils'; + +describe('Merging GraphQL Inputs and field arguments', () => { + it('Should merge inputs leaving only common fields.', () => { + const baseSchema = ` + input UserInput { + name: String! + age: Int # Not in Subgraph B + } + `; + + const mergingSchema = ` + input UserInput { + name: String! + email: String # Not in Subgraph A + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'error') throw new Error('Invalid parse'); + expectTrimmedEqual( + t1.sdl, + ` + input UserInput{ + name: String! + }`, + ); + }); + it('Should merge inputs leaving common fields both required and not', () => { + const baseSchema = ` + input UserInput { + name: String! + list: Boolean + age: Int # Not in Subgraph B + } + `; + + const mergingSchema = ` + input UserInput { + name: String! + list: Boolean + email: String # Not in Subgraph A + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'error') throw new Error('Invalid parse'); + expectTrimmedEqual( + t1.sdl, + ` + input UserInput{ + name: String! + list: Boolean + }`, + ); + }); + it('Should merge inputs marking fields required.', () => { + const baseSchema = ` + input UserInput { + name: String! + age: Int + } + `; + + const mergingSchema = ` + input UserInput { + name: String + age: Int! + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'error') throw new Error('Invalid parse'); + expectTrimmedEqual( + t1.sdl, + ` + input UserInput{ + name: String! + age: Int! + }`, + ); + }); + it('Should not merge inputs', () => { + const baseSchema = ` + input UserInput { + name: String! + } + `; + + const mergingSchema = ` + input UserInput { + name: String! + email: String! + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'success') console.log(t1.sdl); + expect(t1.__typename).toEqual('error'); + }); + it('Should merge field arguments marking them required.', () => { + const baseSchema = ` + type Main{ + getUsers(funny: Boolean, premium: String!): String! + } + `; + + const mergingSchema = ` + type Main{ + getUsers(funny: Boolean!, premium: String): String! + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'error') throw new Error('Invalid parse'); + expectTrimmedEqual( + t1.sdl, + ` + type Main{ + getUsers(funny: Boolean! premium: String!): String! + }`, + ); + }); + it('Should merge field arguments leaving only common fields.', () => { + const baseSchema = ` + type Main{ + getUsers(premium: String!): String! + } + `; + + const mergingSchema = ` + type Main{ + getUsers(funny: Boolean, premium: String): String! + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'error') throw new Error('Invalid parse'); + expectTrimmedEqual( + t1.sdl, + ` + type Main{ + getUsers(premium: String!): String! + }`, + ); + }); +}); diff --git a/src/__tests__/TreeOperations/merge/merge.interfaces.spec.ts b/src/__tests__/TreeOperations/merge/merge.interfaces.spec.ts new file mode 100644 index 0000000..6b64a2d --- /dev/null +++ b/src/__tests__/TreeOperations/merge/merge.interfaces.spec.ts @@ -0,0 +1,34 @@ +import { mergeSDLs } from '@/TreeOperations/merge/merge'; + +// const mergingErrorSchema = ` +// type Person{ +// lastName: String +// } +// `; + +describe('Merging GraphQL Schemas', () => { + it('should not merge interfaces and implementation of both nodes', () => { + const baseSchema = ` + type Person implements Node{ + firstName: String + health: String + _id: String + } + interface Node { + _id: String + } + `; + + const mergingSchema = ` + type Person implements Dateable{ + lastName: String + createdAt: String + } + interface Dateable { + createdAt: String + } + `; + const t1 = mergeSDLs(baseSchema, mergingSchema); + expect(t1.__typename).toEqual('error'); + }); +}); diff --git a/src/__tests__/TreeOperations/merge.spec.ts b/src/__tests__/TreeOperations/merge/merge.spec.ts similarity index 60% rename from src/__tests__/TreeOperations/merge.spec.ts rename to src/__tests__/TreeOperations/merge/merge.spec.ts index 4348fbf..7e08620 100644 --- a/src/__tests__/TreeOperations/merge.spec.ts +++ b/src/__tests__/TreeOperations/merge/merge.spec.ts @@ -1,5 +1,4 @@ -import { Parser } from '@/Parser'; -import { mergeSDLs, mergeTrees } from '@/TreeOperations/merge'; +import { mergeSDLs } from '@/TreeOperations/merge/merge'; import { expectTrimmedEqual } from '@/__tests__/TestUtils'; // const mergingErrorSchema = ` @@ -69,6 +68,7 @@ describe('Merging GraphQL Schemas', () => { } `; const t1 = mergeSDLs(baseSchema, mergingSchema); + if (t1.__typename === 'success') console.log(t1.sdl); expect(t1.__typename).toEqual('error'); }); it('Should not merge extension nodes', () => { @@ -102,84 +102,6 @@ describe('Merging GraphQL Schemas', () => { }`, ); }); - it('Tree test - Should merge interfaces and implementation of both nodes matching library fields.', () => { - const baseSchema = ` - type Person implements Node{ - firstName: String - health: String - _id: String - } - interface Node { - _id: String - } - `; - - const mergingSchema = ` - type Person implements Dateable{ - lastName: String - createdAt: String - } - interface Dateable { - createdAt: String - } - `; - const baseTree = Parser.parse(baseSchema); - const libraryTree = Parser.parse(mergingSchema); - const mergedTree = mergeTrees(baseTree, libraryTree); - if (mergedTree.__typename === 'error') throw new Error('Invalid parse'); - const PersonNode = mergedTree.nodes.find((n) => n.name === 'Person'); - const lastNameField = PersonNode?.args.find((a) => a.name === 'lastName'); - const createdAtField = PersonNode?.args.find((a) => a.name === 'createdAt'); - expect(lastNameField?.fromLibrary).toBeTruthy(); - expect(createdAtField?.fromLibrary).toBeTruthy(); - }); - it('Should merge interfaces and implementation of both nodes', () => { - const baseSchema = ` - type Person implements Node{ - firstName: String - health: String - _id: String - } - interface Node { - _id: String - } - `; - - const mergingSchema = ` - type Person implements Dateable{ - lastName: String - createdAt: String - } - interface Dateable { - createdAt: String - } - `; - const t1 = mergeSDLs(baseSchema, mergingSchema); - if (t1.__typename === 'error') throw new Error('Invalid parse'); - expect( - t1.nodes.every((n, i) => { - return i === t1.nodes.findIndex((t1n) => t1n.id === n.id); - }), - ).toBeTruthy(); - expectTrimmedEqual( - t1.sdl, - ` - interface Node { - _id: String - } - type Person implements Node & Dateable{ - firstName: String - health: String - _id: String - lastName: String - createdAt: String - } - interface Dateable { - createdAt: String - }`, - ); - }); - it('Should merge schemas but maintain original schema node', () => { const baseSchema = ` type DDD{ diff --git a/src/__tests__/TreeOperations/tree.addField.spec.ts b/src/__tests__/TreeOperations/tree.addField.spec.ts index 28aa24c..d5b9524 100644 --- a/src/__tests__/TreeOperations/tree.addField.spec.ts +++ b/src/__tests__/TreeOperations/tree.addField.spec.ts @@ -39,4 +39,16 @@ describe('Tree Operations tests - adding fields', () => { expect(treeMock.nodes[0].args[2].args).toContainEqual(limitNode); }); + test('Add field with args to node that exists in other type', () => { + const treeMock = createMock(); + const oldField = treeMock.nodes[2].args[0].args[0]; + const newField = { + ...oldField, + name: 'secondName', + }; + + mutate(treeMock, treeMock.nodes).addFieldToNode(treeMock.nodes[2].args[0], newField, treeMock.nodes[2].id); + expect(treeMock.nodes[2].args[0].args).toContainEqual(newField); + expect(treeMock.nodes[0].args[3].args).not.toContainEqual(newField); + }); }); diff --git a/src/__tests__/TreeOperations/tree.remove.spec.ts b/src/__tests__/TreeOperations/tree.remove.spec.ts index ee08270..7fc82ad 100644 --- a/src/__tests__/TreeOperations/tree.remove.spec.ts +++ b/src/__tests__/TreeOperations/tree.remove.spec.ts @@ -82,4 +82,12 @@ describe('Tree Operations - node removal tests', () => { expect(treeMock.nodes).not.toContainEqual(oldExtendNode); expect(treeMock.nodes).not.toContainEqual(oldExtendNode2); }); + test('Delete field with args that exists in other type', () => { + const treeMock = createMock(); + const oldField = treeMock.nodes[2].args[0].args[0]; + + mutate(treeMock, treeMock.nodes).removeNode(treeMock.nodes[2].args[0].args[0], treeMock.nodes[2].id); + expect(treeMock.nodes[2].args[0].args).not.toContainEqual(oldField); + expect(treeMock.nodes[0].args[3].args).toContainEqual(oldField); + }); }); diff --git a/src/index.ts b/src/index.ts index f03de8a..841cc4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export * from './Parser'; export * from './Models'; export * from './shared/index'; export * from './TreeOperations/tree'; -export * from './TreeOperations/merge'; +export * from './TreeOperations/merge/merge'; export * from './GqlParser/index'; export * from './GqlParser/GqlParserTreeToGql'; export * from './GqlParser/valueNode'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy