diff --git a/reverse_engineering/mappers/directiveName.js b/reverse_engineering/mappers/directiveName.js new file mode 100644 index 0000000..8b53b1e --- /dev/null +++ b/reverse_engineering/mappers/directiveName.js @@ -0,0 +1,12 @@ +/** + * @param {Object} params + * @param {String} params.name + * @returns {string} The directive name with "@" prefix + */ +function getDirectiveName({ name }) { + return `@${name}`; +} + +module.exports = { + getDirectiveName, +}; diff --git a/reverse_engineering/mappers/directiveUsage.js b/reverse_engineering/mappers/directiveUsage.js index d581d31..021e33c 100644 --- a/reverse_engineering/mappers/directiveUsage.js +++ b/reverse_engineering/mappers/directiveUsage.js @@ -1,5 +1,6 @@ const { astNodeKind } = require('../constants/graphqlAST'); const { DIRECTIVE_FORMAT, ARGUMENT_VALUE_FORMAT } = require('../constants/properties'); +const { getDirectiveName } = require('./directiveName'); /** * @import { DirectiveNode, ArgumentNode, ValueNode } from "graphql" @@ -16,7 +17,7 @@ function mapDirectivesUsage({ directives = [] }) { return directives.map(directive => { return { directiveFormat: DIRECTIVE_FORMAT.structured, - directiveName: directive.name.value, + directiveName: getDirectiveName({ name: directive.name.value }), argumentValueFormat: ARGUMENT_VALUE_FORMAT.raw, rawArgumentValues: getRawArguments({ argumentNodes: directive.arguments }), }; @@ -39,7 +40,7 @@ function getRawArguments({ argumentNodes = [] }) { * @returns {string} The string representation of the value */ function getArgumentValue(value) { - switch (value.astNodeKind) { + switch (value.kind) { case astNodeKind.INT: case astNodeKind.FLOAT: return value.value; diff --git a/reverse_engineering/mappers/typeDefinitions/customScalar.js b/reverse_engineering/mappers/typeDefinitions/customScalar.js new file mode 100644 index 0000000..09553df --- /dev/null +++ b/reverse_engineering/mappers/typeDefinitions/customScalar.js @@ -0,0 +1,35 @@ +/** + * @import { ScalarTypeDefinitionNode } from "graphql" + * @import { CustomScalarDefinition } from "../../types/types" + */ + +const { mapDirectivesUsage } = require('../directiveUsage'); + +/** + * Maps the custom scalar type definitions + * @param {Object} params + * @param {ScalarTypeDefinitionNode[]} params.customScalars - The custom scalars + * @returns {CustomScalarDefinition[]} The mapped custom scalar type definitions + */ +function getCustomScalarTypeDefinitions({ customScalars = [] }) { + return customScalars.map(scalar => mapCustomScalar({ scalar })); +} + +/** + * Maps a single custom scalar definition + * @param {Object} params + * @param {ScalarTypeDefinitionNode} params.scalar - The scalar to map + * @returns {CustomScalarDefinition} The mapped custom scalar definition + */ +function mapCustomScalar({ scalar }) { + return { + type: 'scalar', + name: scalar.name.value, + description: scalar.description?.value || '', + typeDirectives: mapDirectivesUsage({ directives: scalar.directives }), + }; +} + +module.exports = { + getCustomScalarTypeDefinitions, +}; diff --git a/reverse_engineering/mappers/typeDefinitions/directive.js b/reverse_engineering/mappers/typeDefinitions/directive.js index 5aab9f8..675f139 100644 --- a/reverse_engineering/mappers/typeDefinitions/directive.js +++ b/reverse_engineering/mappers/typeDefinitions/directive.js @@ -3,6 +3,8 @@ * @import { DirectiveDefinition } from "../../types/types" */ +const { getDirectiveName } = require('../directiveName'); + const locationMap = { 'SCHEMA': 'schema', 'QUERY': 'query', @@ -48,7 +50,7 @@ function mapDirective({ directive }) { return { type: 'directive', - name: directive.name.value, + name: getDirectiveName({ name: directive.name.value }), description: directive.description?.value || '', arguments: [], // TODO: implement argument mapping directiveLocations: locations, diff --git a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js index 5c3db8e..0d50ab7 100644 --- a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js +++ b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js @@ -1,10 +1,11 @@ const { astNodeKind } = require('../../constants/graphqlAST'); const { findNodesByKind } = require('../../helpers/findNodesByKind'); const { sortByName } = require('../../helpers/sortByName'); +const { getCustomScalarTypeDefinitions } = require('./customScalar'); const { getDirectiveTypeDefinitions } = require('./directive'); /** - * @import { DirectiveDefinition, FieldsOrder } from "../../types/types" + * @import { FieldsOrder, DirectiveDefinition, CustomScalarDefinition } from "../../types/types" */ /** @@ -18,8 +19,11 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { const directives = getDirectiveTypeDefinitions({ directives: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.DIRECTIVE_DEFINITION }), }); + const customScalars = getCustomScalarTypeDefinitions({ + customScalars: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.SCALAR_TYPE_DEFINITION }), + }); - const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives }); + const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars }); return definitions; } @@ -29,15 +33,21 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { * @param {Object} params * @param {FieldsOrder} params.fieldsOrder - The fields order * @param {DirectiveDefinition[]} params.directives - The directive definitions + * @param {CustomScalarDefinition[]} params.customScalars - The custom scalar definitions * @returns {Object} The type definitions structure */ -function getTypeDefinitionsStructure({ fieldsOrder, directives }) { +function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars }) { const definitions = { ['Directives']: getDefinitionCategoryStructure({ fieldsOrder, subtype: 'directive', properties: directives, }), + ['Scalars']: getDefinitionCategoryStructure({ + fieldsOrder, + subtype: 'scalar', + properties: customScalars, + }), }; return { diff --git a/reverse_engineering/types/types.d.ts b/reverse_engineering/types/types.d.ts index 9ffbe3d..442887f 100644 --- a/reverse_engineering/types/types.d.ts +++ b/reverse_engineering/types/types.d.ts @@ -138,5 +138,12 @@ export type DirectiveDefinition = { directiveLocations: DirectiveLocations; } +export type CustomScalarDefinition = { + type: 'scalar'; + name: string; + description?: string; + typeDirectives?: DirectiveUsage[]; +} + export type TestConnectionCallback = (err: Error | null) => void; export type DisconnectCallback = TestConnectionCallback; diff --git a/test/reverse_engineering/mappers/directiveUsage.spec.js b/test/reverse_engineering/mappers/directiveUsage.spec.js index 041f726..1695fb2 100644 --- a/test/reverse_engineering/mappers/directiveUsage.spec.js +++ b/test/reverse_engineering/mappers/directiveUsage.spec.js @@ -18,7 +18,7 @@ describe('mapDirectivesUsage', () => { const expected = [ { directiveFormat: 'Structured', - directiveName: 'deprecated', + directiveName: '@deprecated', argumentValueFormat: 'Raw', rawArgumentValues: '', }, @@ -34,15 +34,15 @@ describe('mapDirectivesUsage', () => { arguments: [ { name: { value: 'intArg' }, - value: { astNodeKind: astNodeKind.INT, value: '42' }, + value: { kind: astNodeKind.INT, value: '42' }, }, { name: { value: 'stringArg' }, - value: { astNodeKind: astNodeKind.STRING, value: 'hello' }, + value: { kind: astNodeKind.STRING, value: 'hello' }, }, { name: { value: 'boolArg' }, - value: { astNodeKind: astNodeKind.BOOLEAN, value: true }, + value: { kind: astNodeKind.BOOLEAN, value: true }, }, ], }; @@ -50,7 +50,7 @@ describe('mapDirectivesUsage', () => { const expected = [ { directiveFormat: 'Structured', - directiveName: 'test', + directiveName: '@test', argumentValueFormat: 'Raw', rawArgumentValues: 'intArg: 42, stringArg: "hello", boolArg: true', }, @@ -67,21 +67,21 @@ describe('mapDirectivesUsage', () => { { name: { value: 'listArg' }, value: { - astNodeKind: astNodeKind.LIST, + kind: astNodeKind.LIST, values: [ - { astNodeKind: astNodeKind.INT, value: '1' }, - { astNodeKind: astNodeKind.INT, value: '2' }, + { kind: astNodeKind.INT, value: '1' }, + { kind: astNodeKind.INT, value: '2' }, ], }, }, { name: { value: 'objectArg' }, value: { - astNodeKind: astNodeKind.OBJECT, + kind: astNodeKind.OBJECT, fields: [ { name: { value: 'field1' }, - value: { astNodeKind: astNodeKind.STRING, value: 'value1' }, + value: { kind: astNodeKind.STRING, value: 'value1' }, }, ], }, @@ -92,7 +92,7 @@ describe('mapDirectivesUsage', () => { const expected = [ { directiveFormat: 'Structured', - directiveName: 'complex', + directiveName: '@complex', argumentValueFormat: 'Raw', rawArgumentValues: 'listArg: [1, 2], objectArg: {field1: "value1"}', }, @@ -108,11 +108,11 @@ describe('mapDirectivesUsage', () => { arguments: [ { name: { value: 'varArg' }, - value: { astNodeKind: astNodeKind.VARIABLE, name: { value: 'var' } }, + value: { kind: astNodeKind.VARIABLE, name: { value: 'var' } }, }, { name: { value: 'enumArg' }, - value: { astNodeKind: astNodeKind.ENUM, value: 'ENUM_VALUE' }, + value: { kind: astNodeKind.ENUM, value: 'ENUM_VALUE' }, }, ], }; @@ -120,7 +120,7 @@ describe('mapDirectivesUsage', () => { const expected = [ { directiveFormat: 'Structured', - directiveName: 'test', + directiveName: '@test', argumentValueFormat: 'Raw', rawArgumentValues: 'varArg: $var, enumArg: ENUM_VALUE', }, diff --git a/test/reverse_engineering/mappers/typeDefinitions/customScalars.spec.js b/test/reverse_engineering/mappers/typeDefinitions/customScalars.spec.js new file mode 100644 index 0000000..6bc67b0 --- /dev/null +++ b/test/reverse_engineering/mappers/typeDefinitions/customScalars.spec.js @@ -0,0 +1,154 @@ +const { describe, it, mock, afterEach } = require('node:test'); +const assert = require('assert'); + +const mapDirectivesUsageMock = mock.fn(() => []); + +mock.module('../../../../reverse_engineering/mappers/directiveUsage', { + namedExports: { + mapDirectivesUsage: mapDirectivesUsageMock, + }, +}); + +const { + getCustomScalarTypeDefinitions, +} = require('../../../../reverse_engineering/mappers/typeDefinitions/customScalar'); + +describe('getCustomScalarTypeDefinitions', () => { + afterEach(() => { + mapDirectivesUsageMock.mock.resetCalls(); + }); + + it('should return an empty array when no custom scalars are provided', () => { + const result = getCustomScalarTypeDefinitions({ customScalars: [] }); + assert.deepStrictEqual(result, []); + }); + + it('should correctly map a scalar with just a name', () => { + const mockScalar = { + name: { value: 'DateTime' }, + directives: [], + }; + + const expected = [ + { + type: 'scalar', + name: 'DateTime', + description: '', + typeDirectives: [], + }, + ]; + + const result = getCustomScalarTypeDefinitions({ customScalars: [mockScalar] }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0], { directives: [] }); + }); + + it('should correctly map a scalar with description', () => { + const mockScalar = { + name: { value: 'DateTime' }, + description: { value: 'ISO-8601 encoded UTC date string' }, + directives: [], + }; + + const expected = [ + { + type: 'scalar', + name: 'DateTime', + description: 'ISO-8601 encoded UTC date string', + typeDirectives: [], + }, + ]; + + const result = getCustomScalarTypeDefinitions({ customScalars: [mockScalar] }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + }); + + it('should correctly map a scalar with directives', () => { + const mockDirectiveResult = [ + { directiveName: '@specifiedBy', rawArgumentValues: 'url: "https://example.com/datetime"' }, + ]; + mapDirectivesUsageMock.mock.mockImplementationOnce(() => mockDirectiveResult); + + const mockScalar = { + name: { value: 'DateTime' }, + directives: [ + { + name: { value: 'specifiedBy' }, + arguments: [{ name: { value: 'url' }, value: { value: 'https://example.com/datetime' } }], + }, + ], + }; + + const expected = [ + { + type: 'scalar', + name: 'DateTime', + description: '', + typeDirectives: mockDirectiveResult, + }, + ]; + + const result = getCustomScalarTypeDefinitions({ customScalars: [mockScalar] }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0], { + directives: mockScalar.directives, + }); + }); + + it('should correctly map multiple scalars', () => { + const mockScalars = [ + { + name: { value: 'DateTime' }, + directives: [], + }, + { + name: { value: 'URL' }, + description: { value: 'URL scalar type' }, + directives: [], + }, + ]; + + const expected = [ + { + type: 'scalar', + name: 'DateTime', + description: '', + typeDirectives: [], + }, + { + type: 'scalar', + name: 'URL', + description: 'URL scalar type', + typeDirectives: [], + }, + ]; + + const result = getCustomScalarTypeDefinitions({ customScalars: mockScalars }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 2); + }); + + it('should handle undefined directives', () => { + const mockScalar = { + name: { value: 'DateTime' }, + // directives are undefined + }; + + const expected = [ + { + type: 'scalar', + name: 'DateTime', + description: '', + typeDirectives: [], + }, + ]; + + const result = getCustomScalarTypeDefinitions({ customScalars: [mockScalar] }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.strictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0].directives, undefined); + }); +}); diff --git a/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js b/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js index 4196d7d..7cc3cbf 100644 --- a/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js +++ b/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js @@ -17,7 +17,7 @@ describe('getDirectiveTypeDefinitions', () => { const expected = [ { type: 'directive', - name: 'testDirective', + name: '@testDirective', arguments: [], description: '', directiveLocations: { @@ -40,7 +40,7 @@ describe('getDirectiveTypeDefinitions', () => { const expected = [ { type: 'directive', - name: 'testDirective', + name: '@testDirective', description: 'Test description', arguments: [], directiveLocations: { @@ -62,7 +62,7 @@ describe('getDirectiveTypeDefinitions', () => { const expected = [ { type: 'directive', - name: 'testDirective', + name: '@testDirective', arguments: [], description: '', directiveLocations: { @@ -92,7 +92,7 @@ describe('getDirectiveTypeDefinitions', () => { const expected = [ { type: 'directive', - name: 'directive1', + name: '@directive1', arguments: [], description: '', directiveLocations: { @@ -101,7 +101,7 @@ describe('getDirectiveTypeDefinitions', () => { }, { type: 'directive', - name: 'directive2', + name: '@directive2', arguments: [], description: '', directiveLocations: { @@ -123,7 +123,7 @@ describe('getDirectiveTypeDefinitions', () => { const expected = [ { type: 'directive', - name: 'testDirective', + name: '@testDirective', arguments: [], description: '', directiveLocations: {},