From d64d19d264d10688c5ff44e20ad444a325915dd8 Mon Sep 17 00:00:00 2001 From: Taras Dubyk Date: Mon, 24 Mar 2025 18:50:42 +0200 Subject: [PATCH] add RE of interfaces --- .../mappers/implementsInterfaces.js | 23 + .../mappers/typeDefinitions/interface.js | 59 +++ .../mappers/typeDefinitions/objectType.js | 3 +- .../typeDefinitions/typeDefinitions.js | 30 +- shared/types/re.d.ts | 21 +- .../mappers/typeDefinitions/interface.spec.js | 416 ++++++++++++++++++ .../typeDefinitions/objectType.spec.js | 95 ++++ 7 files changed, 641 insertions(+), 6 deletions(-) create mode 100644 reverse_engineering/mappers/implementsInterfaces.js create mode 100644 reverse_engineering/mappers/typeDefinitions/interface.js create mode 100644 test/reverse_engineering/mappers/typeDefinitions/interface.spec.js diff --git a/reverse_engineering/mappers/implementsInterfaces.js b/reverse_engineering/mappers/implementsInterfaces.js new file mode 100644 index 0000000..bebf747 --- /dev/null +++ b/reverse_engineering/mappers/implementsInterfaces.js @@ -0,0 +1,23 @@ +/** + * @import {NamedTypeNode} from "graphql" + * @import {REImplementsInterface} from "../../shared/types/types" + */ + +/** + * Maps the implements interfaces + * + * @param {object} params + * @param {NamedTypeNode[]} [params.implementsInterfaces] - The implements interfaces + * @returns {REImplementsInterface[]} The mapped implements interfaces + */ +function mapImplementsInterfaces({ implementsInterfaces = [] }) { + return implementsInterfaces.map(interfaceData => { + return { + interface: interfaceData.name.value, + }; + }); +} + +module.exports = { + mapImplementsInterfaces, +}; diff --git a/reverse_engineering/mappers/typeDefinitions/interface.js b/reverse_engineering/mappers/typeDefinitions/interface.js new file mode 100644 index 0000000..3077830 --- /dev/null +++ b/reverse_engineering/mappers/typeDefinitions/interface.js @@ -0,0 +1,59 @@ +/** + * @import {InterfaceTypeDefinitionNode} from "graphql" + * @import {DefinitionNameToTypeNameMap, FieldsOrder, REInterfaceDefinition, REPropertiesSchema} from "../../../shared/types/types" + */ + +const { sortByName } = require('../../helpers/sortByName'); +const { mapDirectivesUsage } = require('../directiveUsage'); +const { mapField } = require('../field'); +const { mapImplementsInterfaces } = require('../implementsInterfaces'); + +/** + * Maps interface type definitions + * + * @param {object} params + * @param {InterfaceTypeDefinitionNode[]} params.interfaces - The interface types + * @param {DefinitionNameToTypeNameMap} params.definitionCategoryByNameMap - The definition category by name map + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @returns {REInterfaceDefinition[]} The mapped interface definitions + */ +function getInterfaceDefinitions({ interfaces = [], definitionCategoryByNameMap, fieldsOrder }) { + return interfaces.map(interfaceType => mapInterface({ interfaceType, definitionCategoryByNameMap, fieldsOrder })); +} + +/** + * Maps a single interface type definition + * + * @param {object} params + * @param {InterfaceTypeDefinitionNode} params.interfaceType - The interface to map + * @param {DefinitionNameToTypeNameMap} params.definitionCategoryByNameMap - The definition category by name map + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @returns {REInterfaceDefinition} The mapped interface type definition + */ +function mapInterface({ interfaceType, definitionCategoryByNameMap, fieldsOrder }) { + const properties = interfaceType.fields + ? interfaceType.fields.map(field => mapField({ field, definitionCategoryByNameMap })) + : []; + const required = properties.filter(property => property.required).map(property => property.name); + const convertedProperties = sortByName({ items: properties, fieldsOrder }).reduce( + (acc, property) => { + acc[property.name] = property; + return acc; + }, + /** @type {REPropertiesSchema} */ {}, + ); + + return { + type: 'interface', + name: interfaceType.name.value, + properties: convertedProperties, + required, + description: interfaceType.description?.value || '', + typeDirectives: mapDirectivesUsage({ directives: [...(interfaceType.directives || [])] }), + implementsInterfaces: mapImplementsInterfaces({ implementsInterfaces: [...(interfaceType.interfaces || [])] }), + }; +} + +module.exports = { + getInterfaceDefinitions, +}; diff --git a/reverse_engineering/mappers/typeDefinitions/objectType.js b/reverse_engineering/mappers/typeDefinitions/objectType.js index 6b07b5d..4dc2d09 100644 --- a/reverse_engineering/mappers/typeDefinitions/objectType.js +++ b/reverse_engineering/mappers/typeDefinitions/objectType.js @@ -6,6 +6,7 @@ const { sortByName } = require('../../helpers/sortByName'); const { mapDirectivesUsage } = require('../directiveUsage'); const { mapField } = require('../field'); +const { mapImplementsInterfaces } = require('../implementsInterfaces'); /** * Maps object type definitions @@ -49,7 +50,7 @@ function mapObjectType({ objectType, definitionCategoryByNameMap, fieldsOrder }) required, description: objectType.description?.value || '', typeDirectives: mapDirectivesUsage({ directives: [...(objectType.directives || [])] }), - // TODO: add interfaces + implementsInterfaces: mapImplementsInterfaces({ implementsInterfaces: [...(objectType.interfaces || [])] }), }; } diff --git a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js index ed33aa9..ba436e3 100644 --- a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js +++ b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js @@ -12,7 +12,9 @@ * EnumStructureType, * ObjectStructureType, * ScalarStructureType, - * REObjectTypeDefinition} from "../../../shared/types/types" + * REObjectTypeDefinition, + * InterfaceStructureType, + * REInterfaceDefinition} from "../../../shared/types/types" */ const { astNodeKind } = require('../../constants/graphqlAST'); @@ -23,6 +25,7 @@ const { getCustomScalarTypeDefinitions } = require('./customScalar'); const { getDirectiveTypeDefinitions } = require('./directive'); const { getObjectTypeDefinitions } = require('./objectType'); const { getEnumTypeDefinitions } = require('./enum'); +const { getInterfaceDefinitions } = require('./interface'); /** * Gets the type definitions structure @@ -56,7 +59,20 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder, rootTypeNames }) { fieldsOrder, }); - const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums, objectTypes }); + const interfaces = getInterfaceDefinitions({ + interfaces: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.INTERFACE_TYPE_DEFINITION }), + definitionCategoryByNameMap, + fieldsOrder, + }); + + const definitions = getTypeDefinitionsStructure({ + fieldsOrder, + directives, + customScalars, + enums, + objectTypes, + interfaces, + }); return definitions; } @@ -70,9 +86,10 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder, rootTypeNames }) { * @param {RECustomScalarDefinition[]} params.customScalars - The custom scalar definitions * @param {REEnumDefinition[]} params.enums - The enum definitions * @param {REObjectTypeDefinition[]} params.objectTypes - The object type definitions + * @param {REInterfaceDefinition[]} params.interfaces - The interface definitions * @returns {REModelDefinitionsSchema} The type definitions structure */ -function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums, objectTypes }) { +function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums, objectTypes, interfaces }) { const definitions = { ['Directives']: /** @type {DirectiveStructureType} */ ( getDefinitionCategoryStructure({ @@ -102,6 +119,13 @@ function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, e properties: enums, }) ), + ['Interfaces']: /** @type {InterfaceStructureType} */ ( + getDefinitionCategoryStructure({ + fieldsOrder, + subtype: 'interface', + properties: interfaces, + }) + ), }; return { diff --git a/shared/types/re.d.ts b/shared/types/re.d.ts index cf67764..480fec4 100644 --- a/shared/types/re.d.ts +++ b/shared/types/re.d.ts @@ -124,8 +124,14 @@ export type REDirectiveDefinitionsSchema = Record export type RECustomScalarDefinitionsSchema = Record; export type REObjectDefinitionsSchema = Record; export type REEnumDefinitionsSchema = Record; +export type REInterfaceDefinitionsSchema = Record; -export type REDefinition = RECustomScalarDefinition | REDirectiveDefinition | REEnumDefinition | REObjectTypeDefinition; +export type REDefinition = + | RECustomScalarDefinition + | REDirectiveDefinition + | REEnumDefinition + | REObjectTypeDefinition + | REInterfaceDefinition; export type REModelDefinitionsSchema = { definitions: { @@ -133,6 +139,7 @@ export type REModelDefinitionsSchema = { Scalars: ScalarStructureType; Objects: ObjectStructureType; Enums: EnumStructureType; + Interfaces: InterfaceStructureType; }; }; @@ -140,7 +147,8 @@ export type DefinitionREStructure = | DirectiveStructureType | ScalarStructureType | EnumStructureType - | ObjectStructureType; + | ObjectStructureType + | InterfaceStructureType; type StructureType = { type: 'type'; @@ -160,6 +168,10 @@ export type EnumStructureType = StructureType & { subtype: 'enum'; }; +export type InterfaceStructureType = StructureType & { + subtype: 'interface'; +}; + export type RECustomScalarDefinition = CustomScalarDefinition & { type: 'scalar'; name: string; @@ -188,6 +200,11 @@ export type REObjectTypeDefinition = REObjectLikeDefinition & { name: string; }; +export type REInterfaceDefinition = REObjectLikeDefinition & { + type: 'interface'; + name: string; +}; + export type PreProcessedFieldData = FieldData & FieldTypeProperties & { name: string; diff --git a/test/reverse_engineering/mappers/typeDefinitions/interface.spec.js b/test/reverse_engineering/mappers/typeDefinitions/interface.spec.js new file mode 100644 index 0000000..7ed8617 --- /dev/null +++ b/test/reverse_engineering/mappers/typeDefinitions/interface.spec.js @@ -0,0 +1,416 @@ +const { describe, it, mock, afterEach } = require('node:test'); +const assert = require('assert'); + +// Mock dependencies +const sortByNameMock = mock.fn(({ items }) => items); +mock.module('../../../../reverse_engineering/helpers/sortByName', { + namedExports: { + sortByName: sortByNameMock, + }, +}); + +const mapDirectivesUsageMock = mock.fn(() => []); +mock.module('../../../../reverse_engineering/mappers/directiveUsage', { + namedExports: { + mapDirectivesUsage: mapDirectivesUsageMock, + }, +}); + +const mapFieldMock = mock.fn(({ field }) => ({ + name: field.name.value, + required: field.type.kind === 'NON_NULL_TYPE', +})); +mock.module('../../../../reverse_engineering/mappers/field', { + namedExports: { + mapField: mapFieldMock, + }, +}); + +const mapImplementsInterfacesMock = mock.fn(() => []); +mock.module('../../../../reverse_engineering/mappers/implementsInterfaces', { + namedExports: { + mapImplementsInterfaces: mapImplementsInterfacesMock, + }, +}); + +const { getInterfaceDefinitions } = require('../../../../reverse_engineering/mappers/typeDefinitions/interface'); + +describe('getInterfaceDefinitions', () => { + afterEach(() => { + sortByNameMock.mock.resetCalls(); + mapDirectivesUsageMock.mock.resetCalls(); + mapFieldMock.mock.resetCalls(); + mapImplementsInterfacesMock.mock.resetCalls(); + }); + + it('should return an empty array when no interface types are provided', () => { + const result = getInterfaceDefinitions({ + interfaces: [], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + assert.deepStrictEqual(result, []); + }); + + it('should correctly map a simple interface with no fields', () => { + const mockInterface = { + name: { value: 'EmptyInterface' }, + fields: [], + directives: [], + interfaces: [], + }; + + const expected = [ + { + type: 'interface', + name: 'EmptyInterface', + properties: {}, + required: [], + description: '', + typeDirectives: [], + implementsInterfaces: [], + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.strictEqual(sortByNameMock.mock.calls.length, 1); + assert.strictEqual(mapFieldMock.mock.calls.length, 0); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + }); + + it('should correctly map an interface with fields', () => { + const mockInterface = { + name: { value: 'Node' }, + description: { value: 'An interface for objects with an ID' }, + fields: [ + { + name: { value: 'id' }, + type: { kind: 'NON_NULL_TYPE' }, + directives: [], + }, + { + name: { value: 'name' }, + type: { kind: 'NAMED_TYPE' }, + directives: [], + }, + ], + directives: [], + interfaces: [], + }; + + // Expected result with the properties based on the mocked mapField responses + const expected = [ + { + type: 'interface', + name: 'Node', + properties: { + id: { name: 'id', required: true }, + name: { name: 'name', required: false }, + }, + required: ['id'], + description: 'An interface for objects with an ID', + typeDirectives: [], + implementsInterfaces: [], + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.strictEqual(sortByNameMock.mock.calls.length, 1); + assert.strictEqual(mapFieldMock.mock.calls.length, 2); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + }); + + it('should correctly map an interface with directives', () => { + const mockDirectiveResult = [{ directiveName: '@deprecated', rawArgumentValues: 'reason: "Use Node instead"' }]; + mapDirectivesUsageMock.mock.mockImplementationOnce(() => mockDirectiveResult); + + const mockInterface = { + name: { value: 'OldInterface' }, + fields: [], + directives: [ + { + name: { value: 'deprecated' }, + arguments: [{ name: { value: 'reason' }, value: { value: 'Use Node instead' } }], + }, + ], + interfaces: [], + }; + + const expected = [ + { + type: 'interface', + name: 'OldInterface', + properties: {}, + required: [], + description: '', + typeDirectives: mockDirectiveResult, + implementsInterfaces: [], + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0], { + directives: mockInterface.directives, + }); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + }); + + it('should correctly handle inherited interfaces', () => { + const mockInterface = { + name: { value: 'ExtendedNode' }, + fields: [], + directives: [], + interfaces: [{ name: { value: 'Node' } }, { name: { value: 'Entity' } }], + }; + + const mockImplementsResult = [{ interface: 'Node' }, { interface: 'Entity' }]; + mapImplementsInterfacesMock.mock.mockImplementationOnce(() => mockImplementsResult); + + const expected = [ + { + type: 'interface', + name: 'ExtendedNode', + properties: {}, + required: [], + description: '', + typeDirectives: [], + implementsInterfaces: mockImplementsResult, + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + assert.deepStrictEqual(mapImplementsInterfacesMock.mock.calls[0].arguments[0], { + implementsInterfaces: mockInterface.interfaces, + }); + }); + + it('should correctly handle fields order', () => { + const mockInterface = { + name: { value: 'OrderedInterface' }, + fields: [ + { name: { value: 'fieldB' }, type: { kind: 'NAMED_TYPE' }, directives: [] }, + { name: { value: 'fieldA' }, type: { kind: 'NAMED_TYPE' }, directives: [] }, + { name: { value: 'fieldC' }, type: { kind: 'NAMED_TYPE' }, directives: [] }, + ], + directives: [], + interfaces: [], + }; + + mapFieldMock.mock.mockImplementation(({ field }) => ({ + name: field.name.value, + required: false, + })); + + // Test both fieldsOrder options + const testCases = [ + { + fieldsOrder: 'alphabetical', + expectedOrder: ['fieldA', 'fieldB', 'fieldC'], // Alphabetical order + }, + { + fieldsOrder: 'field', + expectedOrder: ['fieldB', 'fieldA', 'fieldC'], // Original order + }, + ]; + + for (const testCase of testCases) { + // Reset mocks before each test case + sortByNameMock.mock.resetCalls(); + mapFieldMock.mock.resetCalls(); + mapImplementsInterfacesMock.mock.resetCalls(); + + // Mock sortByName to simulate behavior based on fieldsOrder value + if (testCase.fieldsOrder === 'alphabetical') { + // For 'alphabetical', sort alphabetically + sortByNameMock.mock.mockImplementationOnce(({ items }) => { + return [...items].sort((a, b) => a.name.localeCompare(b.name)); + }); + } else { + // For 'field', maintain original order + sortByNameMock.mock.mockImplementationOnce(({ items }) => items); + } + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: testCase.fieldsOrder, + }); + + // Check that sortByName was called correctly + assert.strictEqual(sortByNameMock.mock.calls.length, 1); + assert.strictEqual(sortByNameMock.mock.calls[0].arguments[0].fieldsOrder, testCase.fieldsOrder); + + // Verify the result structure + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'OrderedInterface'); + + const propertyNames = Object.keys(result[0].properties); + + // Verify the properties are in the expected order for this test case + assert.deepStrictEqual( + propertyNames, + testCase.expectedOrder, + `Field order should be ${testCase.expectedOrder.join(', ')} when fieldsOrder is "${testCase.fieldsOrder}"`, + ); + } + }); + + it('should correctly map multiple interface types', () => { + const mockInterfaces = [ + { + name: { value: 'Interface1' }, + fields: [], + directives: [], + interfaces: [], + }, + { + name: { value: 'Interface2' }, + fields: [], + directives: [], + interfaces: [], + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: mockInterfaces, + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].name, 'Interface1'); + assert.strictEqual(result[1].name, 'Interface2'); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 2); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 2); + }); + + it('should handle undefined fields', () => { + const mockInterface = { + name: { value: 'InterfaceWithoutFields' }, + // fields is undefined + directives: [], + interfaces: [], + }; + + const expected = [ + { + type: 'interface', + name: 'InterfaceWithoutFields', + properties: {}, + required: [], + description: '', + typeDirectives: [], + implementsInterfaces: [], + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + + // Verify no fields were processed + assert.strictEqual(mapFieldMock.mock.calls.length, 0); + + // Verify sortByName was still called (but with empty array) + assert.strictEqual(sortByNameMock.mock.calls.length, 1); + assert.deepStrictEqual(sortByNameMock.mock.calls[0].arguments[0].items, []); + }); + + it('should handle undefined directives', () => { + const mockInterface = { + name: { value: 'InterfaceWithoutDirectives' }, + fields: [], + // directives is undefined + interfaces: [], + }; + + const expected = [ + { + type: 'interface', + name: 'InterfaceWithoutDirectives', + properties: {}, + required: [], + description: '', + typeDirectives: [], // We expect an empty array since our mock returns [] + implementsInterfaces: [], + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + + // Verify the mapDirectivesUsage was called with empty directives array + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0].directives, []); + }); + + it('should handle undefined interfaces', () => { + const mockInterface = { + name: { value: 'InterfaceWithoutImplements' }, + fields: [], + directives: [], + // interfaces is undefined + }; + + const expected = [ + { + type: 'interface', + name: 'InterfaceWithoutImplements', + properties: {}, + required: [], + description: '', + typeDirectives: [], + implementsInterfaces: [], // We expect an empty array since our mock returns [] + }, + ]; + + const result = getInterfaceDefinitions({ + interfaces: [mockInterface], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + + // Verify mapImplementsInterfaces was called with empty array + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + assert.deepStrictEqual(mapImplementsInterfacesMock.mock.calls[0].arguments[0].implementsInterfaces, []); + }); +}); diff --git a/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js b/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js index 495bdbe..9cae004 100644 --- a/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js +++ b/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js @@ -27,6 +27,13 @@ mock.module('../../../../reverse_engineering/mappers/field', { }, }); +const mapImplementsInterfacesMock = mock.fn(() => []); +mock.module('../../../../reverse_engineering/mappers/implementsInterfaces', { + namedExports: { + mapImplementsInterfaces: mapImplementsInterfacesMock, + }, +}); + const { getObjectTypeDefinitions } = require('../../../../reverse_engineering/mappers/typeDefinitions/objectType'); describe('getObjectTypeDefinitions', () => { @@ -34,6 +41,7 @@ describe('getObjectTypeDefinitions', () => { sortByNameMock.mock.resetCalls(); mapDirectivesUsageMock.mock.resetCalls(); mapFieldMock.mock.resetCalls(); + mapImplementsInterfacesMock.mock.resetCalls(); }); it('should return an empty array when no object types are provided', () => { @@ -50,6 +58,7 @@ describe('getObjectTypeDefinitions', () => { name: { value: 'EmptyType' }, fields: [], directives: [], + interfaces: [], }; const expected = [ @@ -60,6 +69,7 @@ describe('getObjectTypeDefinitions', () => { required: [], description: '', typeDirectives: [], + implementsInterfaces: [], }, ]; @@ -73,6 +83,10 @@ describe('getObjectTypeDefinitions', () => { assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); assert.strictEqual(sortByNameMock.mock.calls.length, 1); assert.strictEqual(mapFieldMock.mock.calls.length, 0); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + assert.deepStrictEqual(mapImplementsInterfacesMock.mock.calls[0].arguments[0], { + implementsInterfaces: mockObjectType.interfaces, + }); }); it('should correctly map an object type with fields', () => { @@ -112,6 +126,7 @@ describe('getObjectTypeDefinitions', () => { required: ['id'], description: 'A user object', typeDirectives: [], + implementsInterfaces: [], }, ]; @@ -140,6 +155,7 @@ describe('getObjectTypeDefinitions', () => { arguments: [{ name: { value: 'requires' }, value: { value: 'ADMIN' } }], }, ], + interfaces: [], }; const expected = [ @@ -150,6 +166,7 @@ describe('getObjectTypeDefinitions', () => { required: [], description: '', typeDirectives: mockDirectiveResult, + implementsInterfaces: [], }, ]; @@ -166,6 +183,43 @@ describe('getObjectTypeDefinitions', () => { }); }); + // Add new test for implements interfaces + it('should correctly map an object type that implements interfaces', () => { + const mockObjectType = { + name: { value: 'User' }, + fields: [], + directives: [], + interfaces: [{ name: { value: 'Node' } }, { name: { value: 'Entity' } }], + }; + + const mockInterfacesResult = [{ interface: 'Node' }, { interface: 'Entity' }]; + mapImplementsInterfacesMock.mock.mockImplementationOnce(() => mockInterfacesResult); + + const expected = [ + { + type: 'object', + name: 'User', + properties: {}, + required: [], + description: '', + typeDirectives: [], + implementsInterfaces: mockInterfacesResult, + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + assert.deepStrictEqual(mapImplementsInterfacesMock.mock.calls[0].arguments[0], { + implementsInterfaces: mockObjectType.interfaces, + }); + }); + it('should correctly handle fields order', () => { const mockObjectType = { name: { value: 'OrderedType' }, @@ -175,6 +229,7 @@ describe('getObjectTypeDefinitions', () => { { name: { value: 'fieldC' }, type: { kind: 'NAMED_TYPE' }, directives: [] }, ], directives: [], + interfaces: [], }; mapFieldMock.mock.mockImplementation(({ field }) => ({ @@ -241,11 +296,13 @@ describe('getObjectTypeDefinitions', () => { name: { value: 'Type1' }, fields: [], directives: [], + interfaces: [], }, { name: { value: 'Type2' }, fields: [], directives: [], + interfaces: [], }, ]; @@ -266,6 +323,7 @@ describe('getObjectTypeDefinitions', () => { name: { value: 'TypeWithoutFields' }, // fields is undefined directives: [], + interfaces: [], }; const expected = [ @@ -276,6 +334,7 @@ describe('getObjectTypeDefinitions', () => { required: [], description: '', typeDirectives: [], + implementsInterfaces: [], }, ]; @@ -300,6 +359,7 @@ describe('getObjectTypeDefinitions', () => { name: { value: 'TypeWithoutDirectives' }, fields: [], // directives is undefined + interfaces: [], }; const expected = [ @@ -310,6 +370,7 @@ describe('getObjectTypeDefinitions', () => { required: [], description: '', typeDirectives: [], // We expect an empty array since our mock returns [] + implementsInterfaces: [], }, ]; @@ -325,4 +386,38 @@ describe('getObjectTypeDefinitions', () => { assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0].directives, []); }); + + // Add test for undefined interfaces + it('should handle undefined interfaces', () => { + const mockObjectType = { + name: { value: 'TypeWithoutInterfaces' }, + fields: [], + directives: [], + // interfaces is undefined + }; + + const expected = [ + { + type: 'object', + name: 'TypeWithoutInterfaces', + properties: {}, + required: [], + description: '', + typeDirectives: [], + implementsInterfaces: [], // We expect an empty array since our mock returns [] + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + + // Verify mapImplementsInterfaces was called with empty array + assert.strictEqual(mapImplementsInterfacesMock.mock.calls.length, 1); + assert.deepStrictEqual(mapImplementsInterfacesMock.mock.calls[0].arguments[0].implementsInterfaces, []); + }); });