From a7f0556c1b5a49c56b8b5ce899b89538b0e3e97d Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Mon, 5 Jan 2026 11:26:41 +0100 Subject: [PATCH 1/4] mergeSchemaDefinitions doesn't lose implementation + basic tests added --- .../decodeASTSchema.test.ts.snap | 2 +- .../__tests__/mergeSchemaDefinitions.test.ts | 450 ++++++++++++++++++ .../src/utilities/mergeSchemaDefinitions.ts | 28 +- 3 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts diff --git a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap index 7af3e63f2..d69d6c6fd 100644 --- a/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap +++ b/packages/supermassive/src/utilities/__tests__/__snapshots__/decodeASTSchema.test.ts.snap @@ -30,7 +30,7 @@ type AnnotatedObject { type UndefinedType -interface Bar { +interface Bar implements Two { one: Type four(argument: String = "string"): String two(argument: InputType!): Type diff --git a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts new file mode 100644 index 000000000..7919d02c7 --- /dev/null +++ b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts @@ -0,0 +1,450 @@ +import { parse } from "graphql"; +import { + createSchemaDefinitions, + mergeSchemaDefinitions, +} from "../mergeSchemaDefinitions"; +import { encodeASTSchema } from "../encodeASTSchema"; +import { SchemaDefinitions } from "../../schema/definition"; + +function schema(sdl: string): SchemaDefinitions[] { + const doc = parse(sdl); + return encodeASTSchema(doc); +} + +describe("mergeSchemaDefinitions", () => { + it("should return accumulator when no definitions provided", () => { + const accumulator = schema(` + type User { + id: ID + } + `)[0]; + const result = mergeSchemaDefinitions(accumulator, []); + expect(result).toBe(accumulator); + }); + + it("should merge fields from multiple object type definitions with interfaces", () => { + const defs = schema(` + type User implements Node & Named { + id: ID + name: String + } + + type User implements Contactable { + email: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "User": [ + 2, + { + "email": 1, + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + "Contactable", + ], + ], + }, + } + `); + }); + + it("should merge multiple types and preserve interfaces", () => { + const defs = schema(` + type User implements Node { + id: ID + } + + type User { + name: String + } + + type Post { + title: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "Post": [ + 2, + { + "title": 1, + }, + ], + "User": [ + 2, + { + "id": 5, + "name": 1, + }, + [ + "Node", + ], + ], + }, + } + `); + }); + + it("should merge interface and input type definitions", () => { + const defs = schema(` + interface Node { + id: ID + } + + input UserInput { + name: String + } + + interface Node { + typename: String + } + + input UserInput { + email: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "Node": [ + 3, + { + "id": 5, + "typename": 1, + }, + ], + "UserInput": [ + 6, + { + "email": 1, + "name": 1, + }, + ], + }, + } + `); + }); + + it("should handle scalar, union, enum types and directives", () => { + const defs = schema(` + scalar DateTime + + union SearchResult = User | Post + + enum UserRole { + ADMIN + USER + } + + directive @skip on FIELD + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [ + [ + "skip", + [ + 4, + ], + ], + ], + "types": { + "DateTime": [ + 1, + ], + "SearchResult": [ + 4, + [ + "User", + "Post", + ], + ], + "UserRole": [ + 5, + [ + "ADMIN", + "USER", + ], + ], + }, + } + `); + }); + + it("should throw when scalar type definition differs", () => { + const defs = schema(` + scalar DateTime + + enum DateTime { + ADMIN + } + `); + expect(() => { + mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + }).toThrow(); + }); + + it("should merge schema with createSchemaDefinitions", () => { + const defs = schema(` + type User implements Node { + id: ID + } + + type User { + name: String + } + + type Post { + title: String + } + `); + const result = createSchemaDefinitions(defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "Post": [ + 2, + { + "title": 1, + }, + ], + "User": [ + 2, + { + "id": 5, + "name": 1, + }, + [ + "Node", + ], + ], + }, + } + `); + }); + + it("should handle type extensions with multiple interfaces", () => { + const defs = schema(` + type User implements Node { + id: ID + } + + extend type User implements Named { + name: String + } + + extend type User implements Contactable { + email: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "User": [ + 2, + { + "email": 1, + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + "Contactable", + ], + ], + }, + } + `); + }); + + it("should not modify target when source has no interfaces", () => { + const defs = schema(` + type User implements Node { + id: ID + } + + extend type User { + name: String + } + `); + const [base] = defs; + const targetInterfacesBefore = base.types.User[2]; + mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(base.types.User[2]).toEqual(targetInterfacesBefore); + }); + + it("should copy interfaces from source when target has none", () => { + const defs = schema(` + type User { + id: ID + } + + extend type User implements Node & Named { + name: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "User": [ + 2, + { + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + ], + ], + }, + } + `); + }); + + it("should add unique interfaces from source to target", () => { + const defs = schema(` + type User implements Node { + id: ID + } + + extend type User implements Named & Timestamped { + name: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "User": [ + 2, + { + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + "Timestamped", + ], + ], + }, + } + `); + }); + + it("should not duplicate existing interfaces when merging", () => { + const defs = schema(` + type User implements Node & Named { + id: ID + } + + extend type User implements Node & Contactable { + name: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "User": [ + 2, + { + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + "Contactable", + ], + ], + }, + } + `); + }); + + it("should work with interface type definitions", () => { + const defs = schema(` + interface Entity implements Node { + id: ID + } + + extend interface Entity implements Named { + name: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "Entity": [ + 3, + { + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + ], + ], + }, + } + `); + }); + + it("should copy interfaces from source when target interface has none", () => { + const defs = schema(` + interface Entity { + id: ID + } + + extend interface Entity implements Node & Named { + name: String + } + `); + const result = mergeSchemaDefinitions({ types: {}, directives: [] }, defs); + expect(result).toMatchInlineSnapshot(` + { + "directives": [], + "types": { + "Entity": [ + 3, + { + "id": 5, + "name": 1, + }, + [ + "Node", + "Named", + ], + ], + }, + } + `); + }); +}); diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index a626868fd..110262c1e 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -7,9 +7,11 @@ import { getFields, getInputObjectFields, InputValueDefinitionRecord, + InterfaceTypeDefinitionTuple, isInputObjectTypeDefinition, isInterfaceTypeDefinition, isObjectTypeDefinition, + ObjectTypeDefinitionTuple, SchemaDefinitions, setDirectiveDefinitionArgs, setFieldArgs, @@ -40,6 +42,7 @@ export function mergeSchemaDefinitions( mergeDirectives(accumulator.directives, source.directives); } } + return accumulator; } @@ -89,7 +92,7 @@ export function mergeTypes( isInterfaceTypeDefinition(sourceDef)) ) { mergeFields(getFields(targetDef), getFields(sourceDef)); - // Note: not merging implemented interfaces - assuming they are fully defined by the first occurrence + mergeInterfaces(targetDef, sourceDef); continue; } if ( @@ -132,6 +135,29 @@ function mergeFields( } } +function mergeInterfaces( + target: ObjectTypeDefinitionTuple | InterfaceTypeDefinitionTuple, + source: ObjectTypeDefinitionTuple | InterfaceTypeDefinitionTuple, +): void { + const targetInterfaces = target[2]; + const sourceInterfaces = source[2]; + + if (!sourceInterfaces) { + return; + } + + if (!targetInterfaces) { + target[2] = [...sourceInterfaces]; + return; + } + + for (const interfaceName of sourceInterfaces) { + if (!targetInterfaces.includes(interfaceName)) { + targetInterfaces.push(interfaceName); + } + } +} + function mergeInputValues( target: InputValueDefinitionRecord, source: InputValueDefinitionRecord, From eba47aa1b43432a03c77fcde88dc11bcee3e0b32 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:47:46 +0100 Subject: [PATCH 2/4] Change files --- ...-supermassive-ba167129-56f1-4ecc-9446-a58a83d3ea7d.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-supermassive-ba167129-56f1-4ecc-9446-a58a83d3ea7d.json diff --git a/change/@graphitation-supermassive-ba167129-56f1-4ecc-9446-a58a83d3ea7d.json b/change/@graphitation-supermassive-ba167129-56f1-4ecc-9446-a58a83d3ea7d.json new file mode 100644 index 000000000..cd592f8e8 --- /dev/null +++ b/change/@graphitation-supermassive-ba167129-56f1-4ecc-9446-a58a83d3ea7d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "mergeSchemaDefinitions doesn't lose implementation + basic tests added", + "packageName": "@graphitation/supermassive", + "email": "77059398+vejrj@users.noreply.github.com", + "dependentChangeType": "patch" +} From 868d3c74d507ae89e4102b5d7ab05773554aacaf Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:52:33 +0100 Subject: [PATCH 3/4] fix --- .../utilities/__tests__/mergeSchemaDefinitions.test.ts | 8 ++++---- .../supermassive/src/utilities/mergeSchemaDefinitions.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts index 7919d02c7..8f793ba00 100644 --- a/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts +++ b/packages/supermassive/src/utilities/__tests__/mergeSchemaDefinitions.test.ts @@ -29,7 +29,7 @@ describe("mergeSchemaDefinitions", () => { name: String } - type User implements Contactable { + extend type User implements Contactable { email: String } `); @@ -106,11 +106,11 @@ describe("mergeSchemaDefinitions", () => { name: String } - interface Node { + extend interface Node { typename: String } - input UserInput { + extend input UserInput { email: String } `); @@ -204,7 +204,7 @@ describe("mergeSchemaDefinitions", () => { id: ID } - type User { + extend type User { name: String } diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index 110262c1e..c92e49936 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -42,7 +42,6 @@ export function mergeSchemaDefinitions( mergeDirectives(accumulator.directives, source.directives); } } - return accumulator; } @@ -91,6 +90,7 @@ export function mergeTypes( (isInterfaceTypeDefinition(targetDef) && isInterfaceTypeDefinition(sourceDef)) ) { + // Note: not merging implemented interfaces - assuming they are fully defined by the first occurrence mergeFields(getFields(targetDef), getFields(sourceDef)); mergeInterfaces(targetDef, sourceDef); continue; From d5f59b84d8d80c301087639d992474f647ab0c10 Mon Sep 17 00:00:00 2001 From: vejrj <77059398+vejrj@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:52:52 +0100 Subject: [PATCH 4/4] Update packages/supermassive/src/utilities/mergeSchemaDefinitions.ts Co-authored-by: Vladimir Razuvaev --- packages/supermassive/src/utilities/mergeSchemaDefinitions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts index c92e49936..f7ab9040d 100644 --- a/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts +++ b/packages/supermassive/src/utilities/mergeSchemaDefinitions.ts @@ -90,7 +90,6 @@ export function mergeTypes( (isInterfaceTypeDefinition(targetDef) && isInterfaceTypeDefinition(sourceDef)) ) { - // Note: not merging implemented interfaces - assuming they are fully defined by the first occurrence mergeFields(getFields(targetDef), getFields(sourceDef)); mergeInterfaces(targetDef, sourceDef); continue;