diff --git a/forward_engineering/mappers/enums.js b/forward_engineering/mappers/enums.js index 1b8698e..dc7d110 100644 --- a/forward_engineering/mappers/enums.js +++ b/forward_engineering/mappers/enums.js @@ -1,5 +1,5 @@ /** - * @import {FEStatement, FEEnumDefinition, FEEnumDefinitionsSchema, EnumValue, IdToNameMap} from "../../shared/types/types" + * @import {FEStatement, FEEnumDefinition, FEEnumDefinitionsSchema, FEEnumValue, IdToNameMap} from "../../shared/types/types" */ const { joinInlineStatements } = require('../helpers/feStatementJoinHelper'); @@ -47,7 +47,7 @@ function mapEnum({ name, enumDefinition, definitionsIdToNameMap }) { * Maps the enum values to an array of FEStatement. * * @param {object} param0 - * @param {EnumValue[]} param0.enumValues - The enum values. + * @param {FEEnumValue[]} param0.enumValues - The enum values. * @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map. * @returns {FEStatement[]} */ diff --git a/package.json b/package.json index f8d23a0..32b3ea7 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "graphql": "16.10.0" }, "lint-staged": { - "*.{js,json}": "prettier --write" + "*.{js,ts,json}": "prettier --write" }, "simple-git-hooks": { "pre-commit": "npx lint-staged", diff --git a/reverse_engineering/mappers/typeDefinitions/enum.js b/reverse_engineering/mappers/typeDefinitions/enum.js new file mode 100644 index 0000000..18e5581 --- /dev/null +++ b/reverse_engineering/mappers/typeDefinitions/enum.js @@ -0,0 +1,57 @@ +/** + * @import {EnumTypeDefinitionNode, EnumValueDefinitionNode} from "graphql" + * @import {REEnumDefinition, REEnumValue} from "../../../shared/types/types" + */ + +const { mapDirectivesUsage } = require('../directiveUsage'); + +/** + * Maps the enum type nodes to enum definitions + * + * @param {object} params + * @param {EnumTypeDefinitionNode[]} params.enums - The enums nodes + * @returns {REEnumDefinition[]} The mapped enum type definitions + */ +function getEnumTypeDefinitions({ enums = [] }) { + return enums.map(enumNode => mapEnum({ enumNode })); +} + +/** + * Maps a single enum node to enum definition + * + * @param {object} params + * @param {EnumTypeDefinitionNode} params.enumNode - The enum to map + * @returns {REEnumDefinition} The mapped enum definition + */ +function mapEnum({ enumNode }) { + return { + type: 'enum', + name: enumNode.name.value, + description: enumNode.description?.value || '', + enumValues: mapEnumValues({ values: [...(enumNode.values || [])] }), + typeDirectives: mapDirectivesUsage({ directives: [...(enumNode.directives || [])] }), + }; +} + +/** + * Maps the enum value nodes to enum values definitions + * + * @param {object} params + * @param {EnumValueDefinitionNode[]} params.values + * @returns {REEnumValue[]} + */ +function mapEnumValues({ values }) { + return values.map(value => ({ + value: value.name.value, + description: value.description?.value || '', + valueDirectives: mapDirectivesUsage({ directives: [...(value.directives || [])] }), + })); +} + +module.exports = { + getEnumTypeDefinitions, + + // For testing + mapEnum, + mapEnumValues, +}; diff --git a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js index 712ed26..cb82d3f 100644 --- a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js +++ b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js @@ -1,6 +1,16 @@ /** * @import {DefinitionNode} from "graphql" - * @import {REDirectiveDefinition, REDefinitionsSchema, FieldsOrder, RECustomScalarDefinition, REDefinition, REModelDefinitionsSchema, DefinitionREStructure, DirectiveStructureType, ScalarStructureType} from "../../../shared/types/types" + * @import {REDirectiveDefinition, + * REDefinitionsSchema, + * FieldsOrder, + * RECustomScalarDefinition, + * REDefinition, + * REModelDefinitionsSchema, + * DefinitionREStructure, + * DirectiveStructureType, + * REEnumDefinition, + * EnumStructureType, + * ScalarStructureType} from "../../../shared/types/types" */ const { astNodeKind } = require('../../constants/graphqlAST'); @@ -8,6 +18,7 @@ const { findNodesByKind } = require('../../helpers/findNodesByKind'); const { sortByName } = require('../../helpers/sortByName'); const { getCustomScalarTypeDefinitions } = require('./customScalar'); const { getDirectiveTypeDefinitions } = require('./directive'); +const { getEnumTypeDefinitions } = require('./enum'); /** * Gets the type definitions structure @@ -24,8 +35,11 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { const customScalars = getCustomScalarTypeDefinitions({ customScalars: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.SCALAR_TYPE_DEFINITION }), }); + const enums = getEnumTypeDefinitions({ + enums: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.ENUM_TYPE_DEFINITION }), + }); - const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars }); + const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums }); return definitions; } @@ -37,9 +51,10 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { * @param {FieldsOrder} params.fieldsOrder - The fields order * @param {REDirectiveDefinition[]} params.directives - The directive definitions * @param {RECustomScalarDefinition[]} params.customScalars - The custom scalar definitions + * @param {REEnumDefinition[]} params.enums - The enum definitions * @returns {REModelDefinitionsSchema} The type definitions structure */ -function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars }) { +function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums }) { const definitions = { ['Directives']: /** @type {DirectiveStructureType} */ ( getDefinitionCategoryStructure({ @@ -55,6 +70,13 @@ function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars }) properties: customScalars, }) ), + ['Enums']: /** @type {EnumStructureType} */ ( + getDefinitionCategoryStructure({ + fieldsOrder, + subtype: 'enum', + properties: enums, + }) + ), }; return { diff --git a/shared/types/fe.d.ts b/shared/types/fe.d.ts index 3a070c6..21d290d 100644 --- a/shared/types/fe.d.ts +++ b/shared/types/fe.d.ts @@ -4,6 +4,8 @@ import { DirectiveDefinition, DirectiveLocations, DirectivePropertyData, + EnumDefinition, + EnumValue, } from './shared'; export type FEStatement = { @@ -20,7 +22,8 @@ export type FEStatement = { export type IdToNameMap = Record; -export type FEDefinitionsSchema = FEObjectLikeDefinitionsSchema +export type FEDefinitionsSchema = + | FEObjectLikeDefinitionsSchema | FECustomScalarDefinitionsSchema | FEEnumDefinitionsSchema | FEUnionDefinitionsSchema @@ -32,18 +35,9 @@ export type FECustomScalarDefinitionsSchema = Record; // Enum -export type EnumValue = { - value: string; // The name of the enum value - description?: string; // The description of the enum value - typeDirectives?: DirectivePropertyData[]; // The directives of the enum value -}; +export type FEEnumValue = EnumValue; -export type FEEnumDefinition = { - description?: string; // The description of the enum - isActivated?: boolean; // Indicates if the enum is activated - typeDirectives?: DirectivePropertyData[]; // The directives of the enum - enumValues: EnumValue[]; // The values of the enum -}; +export type FEEnumDefinition = EnumDefinition; export type FEEnumDefinitionsSchema = Record; @@ -62,7 +56,7 @@ export type FEObjectLikeDefinition = { // Field data type export type FieldData = RegularFieldData | ReferenceFieldData; -export type FieldSchema = Record +export type FieldSchema = Record; export type ArrayItems = ArrayItem | ArrayItem[]; @@ -109,7 +103,7 @@ export type Argument = { export type ArgumentsResultStatement = { argumentsStatement: string; // The formatted arguments string. argumentsWarningComment: string; // The warning comment if any argument is missing a type. -} +}; // Directives export type FEDirectiveLocations = DirectiveLocations & { @@ -169,9 +163,9 @@ export type RootTypeNamesParameter = { }; type EntityDetails = { - operationType?: string + operationType?: string; typeDirectives?: DirectivePropertyData[]; -} +}; export type EntityIdToJsonSchemaMap = Record; export type EntityIdToPropertiesMap = Record; @@ -232,15 +226,18 @@ export type ValidationResponseItem = { context?: string; // The context of the entity, typically additional information. }; -export type ValidateScriptCallback = (error: Error | null | unknown, validationErrors?: ValidationResponseItem[]) => void; +export type ValidateScriptCallback = ( + error: Error | null | unknown, + validationErrors?: ValidationResponseItem[], +) => void; export type BaseGetFieldParams = { fields?: FieldSchema; // The fields to get requiredFields?: string[]; // The required fields list definitionsIdToNameMap: IdToNameMap; // The definitions id to name map -} +}; export type GetFieldsParams = BaseGetFieldParams & { addArguments: boolean; // Indicates if arguments should be added. addDefaultValue: boolean; // Indicates if default value should be added. -} +}; diff --git a/shared/types/re.d.ts b/shared/types/re.d.ts index 8639017..9295b8c 100644 --- a/shared/types/re.d.ts +++ b/shared/types/re.d.ts @@ -2,6 +2,8 @@ import { ContainerSchemaRootTypes, CustomScalarDefinition, DirectiveDefinition, + EnumDefinition, + EnumValue, StructuredDirective, } from './shared'; @@ -118,36 +120,48 @@ export type REDefinitionsSchema = REDirectiveDefinitionsSchema | RECustomScalarD export type REDirectiveDefinitionsSchema = Record; export type RECustomScalarDefinitionsSchema = Record; +export type REEnumDefinitionsSchema = Record; -export type REDefinition = RECustomScalarDefinition | REDirectiveDefinition; +export type REDefinition = RECustomScalarDefinition | REDirectiveDefinition | REEnumDefinition; export type REModelDefinitionsSchema = { definitions: { Directives: DirectiveStructureType; Scalars: ScalarStructureType; - } -} + }; +}; -export type DefinitionREStructure = DirectiveStructureType | ScalarStructureType; +export type DefinitionREStructure = DirectiveStructureType | ScalarStructureType | EnumStructureType; type StructureType = { type: 'type'; - structureType: true, - properties: T -} + structureType: true; + properties: T; +}; export type DirectiveStructureType = StructureType & { subtype: 'directive'; -} +}; export type ScalarStructureType = StructureType & { subtype: 'scalar'; -} +}; + +export type EnumStructureType = StructureType & { + subtype: 'enum'; +}; export type RECustomScalarDefinition = CustomScalarDefinition & { type: 'scalar'; name: string; -} +}; + +export type REEnumDefinition = EnumDefinition & { + type: 'enum'; + name: string; +}; + +export type REEnumValue = EnumValue; export type TestConnectionCallback = (err?: Error | unknown) => void; export type DisconnectCallback = TestConnectionCallback; diff --git a/shared/types/shared.d.ts b/shared/types/shared.d.ts index bc3ea35..836b259 100644 --- a/shared/types/shared.d.ts +++ b/shared/types/shared.d.ts @@ -57,8 +57,8 @@ type StructuredDirective = { export type CustomScalarDefinition = { description?: string; isActivated?: boolean; - typeDirectives?: T[] -} + typeDirectives?: T[]; +}; export type DirectiveDefinition = { type: 'directive'; @@ -66,4 +66,18 @@ export type DirectiveDefinition = { comments?: string; arguments?: T[]; directiveLocations: D; -} +}; + +// Enum +export type EnumValue = { + value: string; // The name of the enum value + description?: string; // The description of the enum value + typeDirectives?: T[]; // The directives of the enum value +}; + +export type EnumDefinition = { + description?: string; // The description of the enum + isActivated?: boolean; // Indicates if the enum is activated + typeDirectives?: T[]; // The directives of the enum + enumValues: EnumValue[]; // The values of the enum +}; diff --git a/test/reverse_engineering/mappers/typeDefinitions/enum.spec.js b/test/reverse_engineering/mappers/typeDefinitions/enum.spec.js new file mode 100644 index 0000000..bdf05e1 --- /dev/null +++ b/test/reverse_engineering/mappers/typeDefinitions/enum.spec.js @@ -0,0 +1,270 @@ +/** + * @import {EnumTypeDefinitionNode, EnumValueDefinitionNode} from "graphql" + * @import {REEnumDefinition, REEnumValue, StructuredDirective} from "../../../shared/types/types" + */ + +const { describe, it, mock, afterEach } = require('node:test'); +const assert = require('assert'); +const { astNodeKind } = require('../../../../reverse_engineering/constants/graphqlAST'); + +const mapDirectivesUsageMock = mock.fn(() => []); +mock.module('../../../../reverse_engineering/mappers/directiveUsage', { + namedExports: { + mapDirectivesUsage: mapDirectivesUsageMock, + }, +}); + +// This require should be after the mocks to ensure that the mocks are applied before the module is required +const { + getEnumTypeDefinitions, + mapEnum, + mapEnumValues, +} = require('../../../../reverse_engineering/mappers/typeDefinitions/enum'); + +describe('getEnumTypeDefinitions', () => { + afterEach(() => { + mapDirectivesUsageMock.mock.resetCalls(); + }); + + it('should return an empty array when no enums are provided', () => { + const result = getEnumTypeDefinitions({ enums: [] }); + assert.deepStrictEqual(result, []); + }); + + it('should correctly map multiple enum type definitions', () => { + const mockEnums = /** @type {EnumTypeDefinitionNode[]} */ ([ + { + kind: astNodeKind.ENUM_TYPE_DEFINITION, + name: { + kind: astNodeKind.NAME, + value: 'Status', + }, + values: [ + { + kind: astNodeKind.ENUM_VALUE_DEFINITION, + name: { value: 'ACTIVE', kind: astNodeKind.NAME }, + directives: [], + }, + { + kind: astNodeKind.ENUM_VALUE_DEFINITION, + name: { value: 'INACTIVE', kind: astNodeKind.NAME }, + directives: [], + }, + ], + directives: [], + }, + { + name: { + kind: astNodeKind.NAME, + value: 'Role', + }, + description: { value: 'User roles' }, + values: [ + { + kind: astNodeKind.ENUM_VALUE_DEFINITION, + name: { value: 'ADMIN', kind: astNodeKind.NAME }, + directives: [], + }, + { + kind: astNodeKind.ENUM_VALUE_DEFINITION, + name: { value: 'USER', kind: astNodeKind.NAME }, + directives: [], + }, + ], + directives: [], + }, + ]); + + /** @type {REEnumDefinition[]} */ + const expected = [ + { + type: 'enum', + name: 'Status', + description: '', + enumValues: [ + { value: 'ACTIVE', description: '', valueDirectives: [] }, + { value: 'INACTIVE', description: '', valueDirectives: [] }, + ], + typeDirectives: [], + }, + { + type: 'enum', + name: 'Role', + description: 'User roles', + enumValues: [ + { value: 'ADMIN', description: '', valueDirectives: [] }, + { value: 'USER', description: '', valueDirectives: [] }, + ], + typeDirectives: [], + }, + ]; + + const result = getEnumTypeDefinitions({ enums: mockEnums }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 6); // 2 enums + 4 values + }); +}); + +describe('mapEnum', () => { + afterEach(() => { + mapDirectivesUsageMock.mock.resetCalls(); + }); + + it('should correctly map an enum with just a name', () => { + const mockEnum = /** @type {EnumTypeDefinitionNode} */ ({ + name: { value: 'Status' }, + values: [], + directives: [], + }); + + /** @type {REEnumDefinition} */ + const expected = { + type: 'enum', + name: 'Status', + description: '', + enumValues: [], + typeDirectives: [], + }; + + const result = mapEnum({ enumNode: mockEnum }); + assert.deepStrictEqual(result, expected); + }); + + it('should correctly map an enum with description', () => { + const mockEnum = /** @type {EnumTypeDefinitionNode} */ ({ + name: { value: 'Status' }, + description: { value: 'Status of an entity' }, + values: [], + directives: [], + }); + + /** @type {REEnumDefinition} */ + const expected = { + type: 'enum', + name: 'Status', + description: 'Status of an entity', + enumValues: [], + typeDirectives: [], + }; + + const result = mapEnum({ enumNode: mockEnum }); + assert.deepStrictEqual(result, expected); + }); + + it('should correctly map an enum with directives', () => { + const mockDirectiveResult = /** @type {StructuredDirective[]} */ ([ + { directiveName: '@deprecated', rawArgumentValues: 'reason: "Use new enum"' }, + ]); + + mapDirectivesUsageMock.mock.mockImplementationOnce(() => mockDirectiveResult); + + const mockEnum = /** @type {EnumTypeDefinitionNode} */ ({ + name: { value: 'Status' }, + values: [], + directives: [{ name: { value: 'deprecated' } }], + }); + + /** @type {REEnumDefinition} */ + const expected = { + type: 'enum', + name: 'Status', + description: '', + enumValues: [], + typeDirectives: mockDirectiveResult, + }; + + const result = mapEnum({ enumNode: mockEnum }); + assert.deepStrictEqual(result, expected); + }); + + it('should handle undefined values and directives', () => { + const mockEnum = /** @type {EnumTypeDefinitionNode} */ ({ + name: { value: 'Status' }, + // values and directives are undefined + }); + + /** @type {REEnumDefinition} */ + const expected = { + type: 'enum', + name: 'Status', + description: '', + enumValues: [], + typeDirectives: [], + }; + + const result = mapEnum({ enumNode: mockEnum }); + assert.deepStrictEqual(result, expected); + }); +}); + +describe('mapEnumValues', () => { + afterEach(() => { + mapDirectivesUsageMock.mock.resetCalls(); + }); + + it('should correctly map enum values', () => { + const mockValues = /** @type {EnumValueDefinitionNode[]} */ ([ + { + kind: astNodeKind.ENUM_VALUE_DEFINITION, + name: { value: 'ACTIVE' }, + directives: [], + }, + { + kind: astNodeKind.ENUM_VALUE_DEFINITION, + name: { value: 'INACTIVE' }, + description: { value: 'Inactive status' }, + directives: [], + }, + ]); + + /** @type {REEnumValue[]} */ + const expected = [ + { value: 'ACTIVE', description: '', valueDirectives: [] }, + { value: 'INACTIVE', description: 'Inactive status', valueDirectives: [] }, + ]; + + const result = mapEnumValues({ values: mockValues }); + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 2); + }); + + it('should correctly map enum values with directives', () => { + /** @type {StructuredDirective[]} */ + const mockDirectiveResult = [ + { + directiveFormat: 'Structured', + argumentValueFormat: 'Raw', + directiveName: '@deprecated', + rawArgumentValues: 'reason: "Use new value"', + }, + ]; + mapDirectivesUsageMock.mock.mockImplementationOnce(() => [], 0); + mapDirectivesUsageMock.mock.mockImplementationOnce(() => mockDirectiveResult, 1); + + const mockValues = /** @type {EnumValueDefinitionNode[]} */ ([ + { + name: { value: 'ACTIVE' }, + directives: [], + }, + { + name: { value: 'INACTIVE' }, + directives: [{ name: { value: 'deprecated' } }], + }, + ]); + + /** @type {REEnumValue[]} */ + const expected = [ + { value: 'ACTIVE', description: '', valueDirectives: [] }, + { value: 'INACTIVE', description: '', valueDirectives: mockDirectiveResult }, + ]; + + const result = mapEnumValues({ values: mockValues }); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 2); + assert.deepStrictEqual(result, expected); + }); + + it('should return an empty array when no values are provided', () => { + const result = mapEnumValues({ values: [] }); + assert.deepStrictEqual(result, []); + }); +});