diff --git a/forward_engineering/mappers/fieldDefaultValue.js b/forward_engineering/mappers/fieldDefaultValue.js index 6b1169c..97b985d 100644 --- a/forward_engineering/mappers/fieldDefaultValue.js +++ b/forward_engineering/mappers/fieldDefaultValue.js @@ -1,12 +1,12 @@ /** - * @import {FieldData} from "../../shared/types/types" + * @import {DirectivePropertyData, FieldData} from "../../shared/types/types" */ /** * Generates the default value statement for a field. * * @param {object} param0 - * @param {FieldData} param0.field - The field object. + * @param {FieldData} param0.field - The field object. * @returns {string} - The default value statement. */ function getFieldDefaultValueStatement({ field }) { diff --git a/forward_engineering/mappers/fields.js b/forward_engineering/mappers/fields.js index 39a4e4e..ab2d49a 100644 --- a/forward_engineering/mappers/fields.js +++ b/forward_engineering/mappers/fields.js @@ -1,5 +1,5 @@ /** - * @import {FEStatement, BaseGetFieldParams, GetFieldsParams, ArrayItems, FieldData, ArrayItem, IdToNameMap} from "../../shared/types/types" + * @import {FEStatement, BaseGetFieldParams, GetFieldsParams, ArrayItems, FieldData, ArrayItem, IdToNameMap, DirectivePropertyData} from "../../shared/types/types" */ const { joinInlineStatements } = require('../helpers/feStatementJoinHelper'); @@ -30,7 +30,7 @@ function getFields({ fields = {}, requiredFields = [], definitionsIdToNameMap, a /** * @param {object} params - * @param {FieldData} params.fieldData + * @param {FieldData} params.fieldData * @returns {string} */ function getFieldDescription({ fieldData }) { @@ -48,7 +48,7 @@ function getFieldDescription({ fieldData }) { * * @param {object} param0 * @param {string} param0.name - The name of the field. - * @param {FieldData} param0.fieldData - The field data object. + * @param {FieldData} param0.fieldData - The field data object. * @param {boolean} param0.required - Indicates if the field is required. * @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map. * @param {boolean} param0.addArguments - Indicates if arguments should be added. @@ -79,7 +79,7 @@ function mapField({ name, fieldData, required, definitionsIdToNameMap, addArgume * Gets the field type. * * @param {object} param0 - * @param {FieldData} param0.field - The field data object. + * @param {FieldData} param0.field - The field data object. * @param {boolean} [param0.required] - Indicates if the field is required. * @returns {string} - The field type. */ @@ -116,8 +116,8 @@ function getFieldType({ field, required }) { * Gets the field from array items. * * @param {object} param0 - * @param {ArrayItems} [param0.items] - The array items. - * @returns {ArrayItem | undefined} - The field. + * @param {ArrayItems} [param0.items] - The array items. + * @returns {ArrayItem | undefined} - The field. */ function getFieldFromArrayItems({ items }) { if (Array.isArray(items)) { diff --git a/reverse_engineering/constants/types.js b/reverse_engineering/constants/types.js new file mode 100644 index 0000000..3adae48 --- /dev/null +++ b/reverse_engineering/constants/types.js @@ -0,0 +1,14 @@ +// graphql types +const BUILT_IN_SCALAR = { + String: 'String', + Int: 'Int', + Float: 'Float', + Boolean: 'Boolean', + ID: 'ID', +}; + +const BUILT_IN_SCALAR_LIST = Object.values(BUILT_IN_SCALAR); + +module.exports = { + BUILT_IN_SCALAR_LIST, +}; diff --git a/reverse_engineering/helpers/getDefinitionCategoryByNameMap.js b/reverse_engineering/helpers/getDefinitionCategoryByNameMap.js new file mode 100644 index 0000000..021fbae --- /dev/null +++ b/reverse_engineering/helpers/getDefinitionCategoryByNameMap.js @@ -0,0 +1,38 @@ +/** + * @import {DefinitionNode} from "graphql" + * @import {DefinitionNameToTypeNameMap} from "../../shared/types/re" + */ + +const { astNodeKind } = require('../constants/graphqlAST'); + +const kindToDefinitionTypeName = { + [astNodeKind.SCALAR_TYPE_DEFINITION]: 'Scalars', + [astNodeKind.ENUM_TYPE_DEFINITION]: 'Enums', + [astNodeKind.OBJECT_TYPE_DEFINITION]: 'Objects', + [astNodeKind.INTERFACE_TYPE_DEFINITION]: 'Interfaces', + [astNodeKind.UNION_TYPE_DEFINITION]: 'Unions', + [astNodeKind.INPUT_OBJECT_TYPE_DEFINITION]: 'Input objects', + [astNodeKind.DIRECTIVE_DEFINITION]: 'Directives', +}; + +/** + * Find nodes by kind with proper typing + * + * @param {object} options + * @param {DefinitionNode[]} options.nodes - The nodes to search + * @returns {DefinitionNameToTypeNameMap} The found nodes with proper type + */ +function getDefinitionCategoryByNameMap({ nodes }) { + return nodes + .filter(node => kindToDefinitionTypeName[node.kind]) + .reduce((acc, node) => { + if ('name' in node && node.name !== undefined) { + acc[node.name.value] = kindToDefinitionTypeName[node.kind]; + } + return acc; + }, {}); +} + +module.exports = { + getDefinitionCategoryByNameMap, +}; diff --git a/reverse_engineering/helpers/sortByName.js b/reverse_engineering/helpers/sortByName.js index 0402b1e..4832d28 100644 --- a/reverse_engineering/helpers/sortByName.js +++ b/reverse_engineering/helpers/sortByName.js @@ -1,14 +1,14 @@ /** - * @import {FieldsOrder, REDefinition} from "../../shared/types/types" + * @import {FieldsOrder, PreProcessedFieldData, REDefinition} from "../../shared/types/types" */ /** * Sorts an array of objects by the name according to the fields order option * * @param {object} params - * @param {REDefinition[]} params.items - The items to sort + * @param {REDefinition[] | PreProcessedFieldData[]} params.items - The items to sort * @param {FieldsOrder} params.fieldsOrder - The fields order - * @returns {REDefinition[]} The sorted items + * @returns {REDefinition[] | PreProcessedFieldData[]} The sorted items */ function sortByName({ items, fieldsOrder }) { if (!Array.isArray(items)) { diff --git a/reverse_engineering/mappers/field.js b/reverse_engineering/mappers/field.js new file mode 100644 index 0000000..337735f --- /dev/null +++ b/reverse_engineering/mappers/field.js @@ -0,0 +1,108 @@ +/** + * @import {FieldDefinitionNode, TypeNode} from "graphql" + * @import {DefinitionNameToTypeNameMap, FieldTypeProperties, PreProcessedFieldData} from "./../../shared/types/types" + */ + +const { mapDirectivesUsage } = require('./directiveUsage'); +const { astNodeKind } = require('../constants/graphqlAST'); +const { BUILT_IN_SCALAR_LIST } = require('../constants/types'); + +/** + * Maps a field + * + * @param {object} params + * @param {FieldDefinitionNode} params.field - The field to map + * @param {DefinitionNameToTypeNameMap} params.definitionCategoryByNameMap - The definition category by name map + * @returns {PreProcessedFieldData} The mapped field + */ +function mapField({ field, definitionCategoryByNameMap }) { + const fieldTypeProperties = getTypeProperties({ type: field.type, definitionCategoryByNameMap }); + const sharedProperties = { + name: field.name.value, + fieldDirectives: mapDirectivesUsage({ directives: [...(field.directives || [])] }), + ...fieldTypeProperties, + }; + const description = field.description?.value; + + if ('$ref' in fieldTypeProperties) { + return { + ...sharedProperties, + refDescription: description, + // TODO: add arguments + }; + } + return { + ...sharedProperties, + description, + // TODO: add arguments + }; +} + +/** + * Recursively maps the type properties unwrapping non-null and list types and resolving named types to references + * + * @param {object} params + * @param {TypeNode} params.type - The GraphQL type node to process + * @param {DefinitionNameToTypeNameMap} params.definitionCategoryByNameMap - The definition category by name map + * @returns {FieldTypeProperties} JSON schema representation of the type + */ +function getTypeProperties({ type, definitionCategoryByNameMap }) { + // unwrap required type + if (type.kind === astNodeKind.NON_NULL_TYPE) { + const innerType = getTypeProperties({ type: type.type, definitionCategoryByNameMap }); + return { + ...innerType, + required: true, + }; + } + + if (type.kind === astNodeKind.LIST_TYPE) { + const innerType = getTypeProperties({ type: type.type, definitionCategoryByNameMap }); + return { + type: 'List', + items: [innerType], + required: false, + }; + } + + if (type.kind === astNodeKind.NAMED_TYPE) { + const typeName = type.name.value; + const isScalar = isBuiltInScalar({ typeName }); + + if (isScalar) { + return { + type: typeName, + required: false, + }; + } + + const definitionCategoryName = definitionCategoryByNameMap[typeName]; + if (definitionCategoryName) { + return { + '$ref': `#model/definitions/${definitionCategoryName}/${typeName}`, + required: false, + }; + } + } + + // fallback + return { + type: 'string', + required: false, + }; +} + +/** + * Checks if a type name is a built-in scalar + * + * @param {object} params + * @param {string} params.typeName - The type name to check + * @returns {boolean} True if the type is a built-in scalar, false otherwise + */ +function isBuiltInScalar({ typeName }) { + return BUILT_IN_SCALAR_LIST.includes(typeName); +} + +module.exports = { + mapField, +}; diff --git a/reverse_engineering/mappers/schema.js b/reverse_engineering/mappers/schema.js index 1d9989d..7d0956c 100644 --- a/reverse_engineering/mappers/schema.js +++ b/reverse_engineering/mappers/schema.js @@ -27,8 +27,13 @@ function getMappedSchema({ schemaItems, graphName, logger, fieldsOrder }) { rootSchemaNode: findNodesByKind({ nodes: schemaItems, kind: Kind.SCHEMA_DEFINITION })[0], graphName, }); + const rootTypeNames = [ + container.schemaRootTypes?.rootQuery || 'Query', + container.schemaRootTypes?.rootMutation || 'Mutation', + container.schemaRootTypes?.rootSubscription || 'Subscription', + ]; - const typeDefinitions = getTypeDefinitions({ typeDefinitions: schemaItems, fieldsOrder }); + const typeDefinitions = getTypeDefinitions({ typeDefinitions: schemaItems, fieldsOrder, rootTypeNames }); return [ // TODO: remove test collection diff --git a/reverse_engineering/mappers/typeDefinitions/objectType.js b/reverse_engineering/mappers/typeDefinitions/objectType.js new file mode 100644 index 0000000..6b07b5d --- /dev/null +++ b/reverse_engineering/mappers/typeDefinitions/objectType.js @@ -0,0 +1,58 @@ +/** + * @import {ObjectTypeDefinitionNode} from "graphql" + * @import {DefinitionNameToTypeNameMap, FieldsOrder, REObjectTypeDefinition, REPropertiesSchema} from "../../../shared/types/types" + */ + +const { sortByName } = require('../../helpers/sortByName'); +const { mapDirectivesUsage } = require('../directiveUsage'); +const { mapField } = require('../field'); + +/** + * Maps object type definitions + * + * @param {object} params + * @param {ObjectTypeDefinitionNode[]} params.objectTypes - The object types + * @param {DefinitionNameToTypeNameMap} params.definitionCategoryByNameMap - The definition category by name map + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @returns {REObjectTypeDefinition[]} The mapped object type definitions + */ +function getObjectTypeDefinitions({ objectTypes = [], definitionCategoryByNameMap, fieldsOrder }) { + return objectTypes.map(objectType => mapObjectType({ objectType, definitionCategoryByNameMap, fieldsOrder })); +} + +/** + * Maps a single object type definition + * + * @param {object} params + * @param {ObjectTypeDefinitionNode} params.objectType - The object type to map + * @param {DefinitionNameToTypeNameMap} params.definitionCategoryByNameMap - The definition category by name map + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @returns {REObjectTypeDefinition} The mapped object type definition + */ +function mapObjectType({ objectType, definitionCategoryByNameMap, fieldsOrder }) { + const properties = objectType.fields + ? objectType.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: 'object', + name: objectType.name.value, + properties: convertedProperties, + required, + description: objectType.description?.value || '', + typeDirectives: mapDirectivesUsage({ directives: [...(objectType.directives || [])] }), + // TODO: add interfaces + }; +} + +module.exports = { + getObjectTypeDefinitions, +}; diff --git a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js index cb82d3f..ed33aa9 100644 --- a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js +++ b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js @@ -10,14 +10,18 @@ * DirectiveStructureType, * REEnumDefinition, * EnumStructureType, - * ScalarStructureType} from "../../../shared/types/types" + * ObjectStructureType, + * ScalarStructureType, + * REObjectTypeDefinition} from "../../../shared/types/types" */ const { astNodeKind } = require('../../constants/graphqlAST'); const { findNodesByKind } = require('../../helpers/findNodesByKind'); +const { getDefinitionCategoryByNameMap } = require('../../helpers/getDefinitionCategoryByNameMap'); const { sortByName } = require('../../helpers/sortByName'); const { getCustomScalarTypeDefinitions } = require('./customScalar'); const { getDirectiveTypeDefinitions } = require('./directive'); +const { getObjectTypeDefinitions } = require('./objectType'); const { getEnumTypeDefinitions } = require('./enum'); /** @@ -26,20 +30,33 @@ const { getEnumTypeDefinitions } = require('./enum'); * @param {object} params * @param {DefinitionNode[]} params.typeDefinitions - The type definitions nodes * @param {FieldsOrder} params.fieldsOrder - The fields order + * @param {string[]} params.rootTypeNames - The root type names * @returns {REModelDefinitionsSchema} The mapped type definitions */ -function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { +function getTypeDefinitions({ typeDefinitions, fieldsOrder, rootTypeNames }) { + const definitionCategoryByNameMap = getDefinitionCategoryByNameMap({ nodes: typeDefinitions }); + const directives = getDirectiveTypeDefinitions({ directives: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.DIRECTIVE_DEFINITION }), }); 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 objectDefinitionNodes = findNodesByKind({ + nodes: typeDefinitions, + kind: astNodeKind.OBJECT_TYPE_DEFINITION, + }).filter(node => !rootTypeNames.includes(node.name.value)); + const objectTypes = getObjectTypeDefinitions({ + objectTypes: objectDefinitionNodes, + definitionCategoryByNameMap, + fieldsOrder, + }); - const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums }); + const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums, objectTypes }); return definitions; } @@ -52,9 +69,10 @@ function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { * @param {REDirectiveDefinition[]} params.directives - The directive definitions * @param {RECustomScalarDefinition[]} params.customScalars - The custom scalar definitions * @param {REEnumDefinition[]} params.enums - The enum definitions + * @param {REObjectTypeDefinition[]} params.objectTypes - The object type definitions * @returns {REModelDefinitionsSchema} The type definitions structure */ -function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums }) { +function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, enums, objectTypes }) { const definitions = { ['Directives']: /** @type {DirectiveStructureType} */ ( getDefinitionCategoryStructure({ @@ -70,6 +88,13 @@ function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, e properties: customScalars, }) ), + ['Objects']: /** @type {ObjectStructureType} */ ( + getDefinitionCategoryStructure({ + fieldsOrder, + subtype: 'object', + properties: objectTypes, + }) + ), ['Enums']: /** @type {EnumStructureType} */ ( getDefinitionCategoryStructure({ fieldsOrder, @@ -96,17 +121,18 @@ function getTypeDefinitionsStructure({ fieldsOrder, directives, customScalars, e function getDefinitionCategoryStructure({ fieldsOrder, subtype, properties }) { const sortedFields = sortByName({ items: properties, fieldsOrder }); + const convertedProperties = sortedFields.reduce((acc, prop) => { + const processedProp = { ...prop }; + + acc[processedProp.name] = processedProp; + return acc; + }, {}); + return { type: 'type', subtype, structureType: true, - properties: sortedFields.reduce( - (acc, prop) => { - acc[prop.name] = prop; - return acc; - }, - /** @type {REDefinitionsSchema} */ {}, - ), + properties: convertedProperties, }; } diff --git a/shared/types/fe.d.ts b/shared/types/fe.d.ts index 21d290d..c2d354a 100644 --- a/shared/types/fe.d.ts +++ b/shared/types/fe.d.ts @@ -1,9 +1,12 @@ import { + Argument, ContainerSchemaRootTypes, CustomScalarDefinition, DirectiveDefinition, DirectiveLocations, DirectivePropertyData, + FieldSchema, + ObjectLikeDefinition, EnumDefinition, EnumValue, } from './shared'; @@ -44,61 +47,7 @@ export type FEEnumDefinitionsSchema = Record; // Object type definition export type FEObjectLikeDefinitionsSchema = Record; -export type FEObjectLikeDefinition = { - description?: string; // Description of the object type - isActivated?: boolean; // If the object type is activated - implementsInterfaces?: ImplementsInterface[]; // Interfaces that the object type implements - typeDirectives?: DirectivePropertyData[]; // Directives for the type - properties: Record; // Properties of the object type - required?: boolean; -}; - -// Field data type -export type FieldData = RegularFieldData | ReferenceFieldData; - -export type FieldSchema = Record; - -export type ArrayItems = ArrayItem | ArrayItem[]; - -type RegularFieldData = { - type: string; // Type of the field - isActivated?: boolean; // If the field is activated - description?: string; // Description of the field - fieldDirectives?: DirectivePropertyData[]; // Directives for the field - items?: ArrayItems; // Items of the List type - arguments?: Argument[]; // Arguments of the field - default?: string; // Default value of the field -}; - -type ReferenceFieldData = { - $ref: string; // Reference path to the type definition - isActivated?: boolean; // If the field is activated - refDescription?: string; // Description of the reference - fieldDirectives?: DirectivePropertyData[]; // Directives for the field - arguments?: Argument[]; // Arguments of the field - default?: string; // Default value of the reference -}; - -export type ArrayItem = FieldData & { - required?: boolean; // If the array item is required -}; - -// Field arguments -type ArgumentListItem = { - type?: string; - required?: boolean; -}; - -export type Argument = { - id: string; - type: string; - name: string; - default?: string; - description?: string; - directives?: DirectivePropertyData[]; - required?: boolean; - listItems?: ArgumentListItem[]; -}; +export type FEObjectLikeDefinition = ObjectLikeDefinition; export type ArgumentsResultStatement = { argumentsStatement: string; // The formatted arguments string. @@ -232,7 +181,7 @@ export type ValidateScriptCallback = ( ) => void; export type BaseGetFieldParams = { - fields?: FieldSchema; // The fields to get + fields?: FieldSchema; // The fields to get requiredFields?: string[]; // The required fields list definitionsIdToNameMap: IdToNameMap; // The definitions id to name map }; diff --git a/shared/types/re.d.ts b/shared/types/re.d.ts index 9295b8c..cf67764 100644 --- a/shared/types/re.d.ts +++ b/shared/types/re.d.ts @@ -2,6 +2,8 @@ import { ContainerSchemaRootTypes, CustomScalarDefinition, DirectiveDefinition, + FieldData, + ObjectLikeDefinition, EnumDefinition, EnumValue, StructuredDirective, @@ -120,18 +122,25 @@ export type REDefinitionsSchema = REDirectiveDefinitionsSchema | RECustomScalarD export type REDirectiveDefinitionsSchema = Record; export type RECustomScalarDefinitionsSchema = Record; +export type REObjectDefinitionsSchema = Record; export type REEnumDefinitionsSchema = Record; -export type REDefinition = RECustomScalarDefinition | REDirectiveDefinition | REEnumDefinition; +export type REDefinition = RECustomScalarDefinition | REDirectiveDefinition | REEnumDefinition | REObjectTypeDefinition; export type REModelDefinitionsSchema = { definitions: { Directives: DirectiveStructureType; Scalars: ScalarStructureType; + Objects: ObjectStructureType; + Enums: EnumStructureType; }; }; -export type DefinitionREStructure = DirectiveStructureType | ScalarStructureType | EnumStructureType; +export type DefinitionREStructure = + | DirectiveStructureType + | ScalarStructureType + | EnumStructureType + | ObjectStructureType; type StructureType = { type: 'type'; @@ -163,5 +172,54 @@ export type REEnumDefinition = EnumDefinition & { export type REEnumValue = EnumValue; +export type ObjectStructureType = StructureType & { + subtype: 'object'; +}; + +export type REImplementsInterface = { + interface: string; // Name of the interface +}; + +export type REPropertiesSchema = Record>; +type REObjectLikeDefinition = ObjectLikeDefinition; + +export type REObjectTypeDefinition = REObjectLikeDefinition & { + type: 'object'; + name: string; +}; + +export type PreProcessedFieldData = FieldData & + FieldTypeProperties & { + name: string; + }; + +export type FieldTypeProperties = RegularFieldTypeProperties | ArrayFieldTypeProperties | ReferenceFieldTypeProperties; + +export type RegularFieldTypeProperties = { + type: string; + required: boolean; +}; + +export type ArrayFieldTypeProperties = { + type: 'List'; + items?: [FieldTypeProperties]; + required: boolean; +}; + +export type ReferenceFieldTypeProperties = { + $ref: string; + required: boolean; +}; + +export type DefinitionTypeName = + | 'Scalars' + | 'Enums' + | 'Objects' + | 'Interfaces' + | 'Unions' + | 'Input objects' + | 'Directives'; +export type DefinitionNameToTypeNameMap = Record; + 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 836b259..a3a3beb 100644 --- a/shared/types/shared.d.ts +++ b/shared/types/shared.d.ts @@ -47,7 +47,7 @@ type RawDirective = { rawDirective: string; // Raw directive string }; -type StructuredDirective = { +export type StructuredDirective = { directiveFormat: 'Structured'; // Format of the directive directiveName: string; // Name of a built-in directive or GUID of a custom directive argumentValueFormat: 'Raw'; // Format of the argument values @@ -68,6 +68,63 @@ export type DirectiveDefinition = { directiveLocations: D; }; +export type ObjectLikeDefinition = { + description?: string; // Description of the object type + isActivated?: boolean; // If the object type is activated + implementsInterfaces?: Interface[]; // Interfaces that the object type implements + typeDirectives?: DirectiveUsage[]; // Directives for the type + properties: Record>; // Properties of the object type + required?: string[]; +}; + +// Field data type +export type FieldData = RegularFieldData | ReferenceFieldData; + +export type FieldSchema = Record>; + +export type ArrayItems = ArrayItem | ArrayItem[]; + +type RegularFieldData = { + type: string; // Type of the field + isActivated?: boolean; // If the field is activated + description?: string; // Description of the field + fieldDirectives?: DirectiveUsage[]; // Directives for the field + items?: ArrayItems; // Items of the List type + arguments?: Argument[]; // Arguments of the field + default?: string; // Default value of the field +}; + +type ReferenceFieldData = { + $ref: string; // Reference path to the type definition + isActivated?: boolean; // If the field is activated + refDescription?: string; // Description of the reference + fieldDirectives?: DirectiveUsage[]; // Directives for the field + arguments?: Argument[]; // Arguments of the field + default?: string; // Default value of the reference +}; + +export type ArrayItem = FieldData & { + required?: boolean; // If the array item is required + $ref?: string; // Reference path to the type definition +}; + +// Field arguments +type ArgumentListItem = { + type?: string; + required?: boolean; +}; + +export type Argument = { + id: string; + type: string; + name: string; + default?: string; + description?: string; + directives?: DirectivePropertyData[]; + required?: boolean; + listItems?: ArgumentListItem[]; +}; + // Enum export type EnumValue = { value: string; // The name of the enum value diff --git a/test/reverse_engineering/mappers/field.spec.js b/test/reverse_engineering/mappers/field.spec.js new file mode 100644 index 0000000..4d60cef --- /dev/null +++ b/test/reverse_engineering/mappers/field.spec.js @@ -0,0 +1,271 @@ +const { describe, it, mock, afterEach } = require('node:test'); +const assert = require('assert'); + +// Mock dependencies +const mapDirectivesUsageMock = mock.fn(() => []); + +mock.module('../../../reverse_engineering/mappers/directiveUsage', { + namedExports: { + mapDirectivesUsage: mapDirectivesUsageMock, + }, +}); + +const astNodeKindMock = { + NAMED_TYPE: 'NamedType', + NON_NULL_TYPE: 'NonNullType', + LIST_TYPE: 'ListType', +}; + +const { mapField } = require('../../../reverse_engineering/mappers/field'); + +describe('field', () => { + afterEach(() => { + mapDirectivesUsageMock.mock.resetCalls(); + }); + + describe('mapField', () => { + it('should map basic field with scalar type', () => { + const field = { + name: { value: 'name' }, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'String' }, + }, + directives: [], + }; + const definitionCategoryByNameMap = {}; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'name', + type: 'String', + required: false, + fieldDirectives: [], + description: undefined, + }); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + }); + + it('should map field with description', () => { + const field = { + name: { value: 'name' }, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'String' }, + }, + description: { value: 'User name' }, + directives: [], + }; + const definitionCategoryByNameMap = {}; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'name', + type: 'String', + required: false, + fieldDirectives: [], + description: 'User name', + }); + }); + + it('should map field with directives', () => { + const mockDirectives = [{ name: { value: 'deprecated' }, arguments: [] }]; + const mockMappedDirectives = [{ directiveName: '@deprecated', rawArgumentValues: '' }]; + mapDirectivesUsageMock.mock.mockImplementationOnce(() => mockMappedDirectives); + + const field = { + name: { value: 'oldField' }, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'String' }, + }, + directives: mockDirectives, + }; + const definitionCategoryByNameMap = {}; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'oldField', + type: 'String', + required: false, + fieldDirectives: mockMappedDirectives, + description: undefined, + }); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0], { + directives: mockDirectives, + }); + }); + + it('should map field with reference type', () => { + const field = { + name: { value: 'user' }, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'User' }, + }, + description: { value: 'The user' }, + directives: [], + }; + const definitionCategoryByNameMap = { + 'User': 'Objects', + }; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'user', + $ref: '#model/definitions/Objects/User', + required: false, + fieldDirectives: [], + refDescription: 'The user', + }); + }); + + it('should map field with required type', () => { + const field = { + name: { value: 'id' }, + type: { + kind: astNodeKindMock.NON_NULL_TYPE, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'ID' }, + }, + }, + directives: [], + }; + const definitionCategoryByNameMap = {}; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'id', + type: 'ID', + required: true, + fieldDirectives: [], + description: undefined, + }); + }); + + it('should map field with list type', () => { + const field = { + name: { value: 'friends' }, + type: { + kind: astNodeKindMock.LIST_TYPE, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'User' }, + }, + }, + directives: [], + }; + const definitionCategoryByNameMap = { + 'User': 'Objects', + }; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'friends', + type: 'List', + items: [ + { + $ref: '#model/definitions/Objects/User', + required: false, + }, + ], + required: false, + fieldDirectives: [], + description: undefined, + }); + }); + + it('should map field with required list of required items', () => { + const field = { + name: { value: 'requiredFriends' }, + type: { + kind: astNodeKindMock.NON_NULL_TYPE, + type: { + kind: astNodeKindMock.LIST_TYPE, + type: { + kind: astNodeKindMock.NON_NULL_TYPE, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'User' }, + }, + }, + }, + }, + directives: [], + }; + const definitionCategoryByNameMap = { + 'User': 'Objects', + }; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'requiredFriends', + type: 'List', + items: [ + { + $ref: '#model/definitions/Objects/User', + required: true, + }, + ], + required: true, + fieldDirectives: [], + description: undefined, + }); + }); + + it('should handle undefined directives', () => { + const field = { + name: { value: 'name' }, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'String' }, + }, + // directives property is omitted + }; + const definitionCategoryByNameMap = {}; + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'name', + type: 'String', + required: false, + fieldDirectives: [], + description: undefined, + }); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0].directives, []); + }); + + it('should fallback to string type for unknown named types', () => { + const field = { + name: { value: 'customType' }, + type: { + kind: astNodeKindMock.NAMED_TYPE, + name: { value: 'UnknownType' }, + }, + directives: [], + }; + const definitionCategoryByNameMap = {}; // UnknownType not in map + + const result = mapField({ field, definitionCategoryByNameMap }); + + assert.deepStrictEqual(result, { + name: 'customType', + type: 'string', // Fallback to string + required: false, + fieldDirectives: [], + description: undefined, + }); + }); + }); +}); diff --git a/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js b/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js new file mode 100644 index 0000000..495bdbe --- /dev/null +++ b/test/reverse_engineering/mappers/typeDefinitions/objectType.spec.js @@ -0,0 +1,328 @@ +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', + // Add other properties that mapField would return +})); +mock.module('../../../../reverse_engineering/mappers/field', { + namedExports: { + mapField: mapFieldMock, + }, +}); + +const { getObjectTypeDefinitions } = require('../../../../reverse_engineering/mappers/typeDefinitions/objectType'); + +describe('getObjectTypeDefinitions', () => { + afterEach(() => { + sortByNameMock.mock.resetCalls(); + mapDirectivesUsageMock.mock.resetCalls(); + mapFieldMock.mock.resetCalls(); + }); + + it('should return an empty array when no object types are provided', () => { + const result = getObjectTypeDefinitions({ + objectTypes: [], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + assert.deepStrictEqual(result, []); + }); + + it('should correctly map a simple object type with no fields', () => { + const mockObjectType = { + name: { value: 'EmptyType' }, + fields: [], + directives: [], + }; + + const expected = [ + { + type: 'object', + name: 'EmptyType', + properties: {}, + required: [], + description: '', + typeDirectives: [], + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + 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); + }); + + it('should correctly map an object type with fields', () => { + const mockObjectType = { + name: { value: 'User' }, + description: { value: 'A user object' }, + fields: [ + { + name: { value: 'id' }, + type: { kind: 'NON_NULL_TYPE' }, + directives: [], + }, + { + name: { value: 'name' }, + type: { kind: 'NAMED_TYPE' }, + directives: [], + }, + ], + directives: [], + }; + + // Setup the mapField mock to return expected values + mapFieldMock.mock.mockImplementation(({ field }) => ({ + name: field.name.value, + required: field.type.kind === 'NON_NULL_TYPE', + })); + + // Expected result with the properties based on the mocked mapField responses + const expected = [ + { + type: 'object', + name: 'User', + properties: { + id: { name: 'id', required: true }, + name: { name: 'name', required: false }, + }, + required: ['id'], + description: 'A user object', + typeDirectives: [], + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + 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); + }); + + it('should correctly map an object type with directives', () => { + const mockDirectiveResult = [{ directiveName: '@auth', rawArgumentValues: 'requires: "ADMIN"' }]; + mapDirectivesUsageMock.mock.mockImplementationOnce(() => mockDirectiveResult); + + const mockObjectType = { + name: { value: 'AdminType' }, + fields: [], + directives: [ + { + name: { value: 'auth' }, + arguments: [{ name: { value: 'requires' }, value: { value: 'ADMIN' } }], + }, + ], + }; + + const expected = [ + { + type: 'object', + name: 'AdminType', + properties: {}, + required: [], + description: '', + typeDirectives: mockDirectiveResult, + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.deepStrictEqual(result, expected); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 1); + assert.deepStrictEqual(mapDirectivesUsageMock.mock.calls[0].arguments[0], { + directives: mockObjectType.directives, + }); + }); + + it('should correctly handle fields order', () => { + const mockObjectType = { + name: { value: 'OrderedType' }, + 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: [], + }; + + 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(); + + // 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 = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + 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, 'OrderedType'); + + 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 object types', () => { + const mockObjectTypes = [ + { + name: { value: 'Type1' }, + fields: [], + directives: [], + }, + { + name: { value: 'Type2' }, + fields: [], + directives: [], + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: mockObjectTypes, + definitionCategoryByNameMap: {}, + fieldsOrder: {}, + }); + + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].name, 'Type1'); + assert.strictEqual(result[1].name, 'Type2'); + assert.strictEqual(mapDirectivesUsageMock.mock.calls.length, 2); + }); + + it('should handle undefined fields', () => { + const mockObjectType = { + name: { value: 'TypeWithoutFields' }, + // fields is undefined + directives: [], + }; + + const expected = [ + { + type: 'object', + name: 'TypeWithoutFields', + properties: {}, + required: [], + description: '', + typeDirectives: [], + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + 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 mockObjectType = { + name: { value: 'TypeWithoutDirectives' }, + fields: [], + // directives is undefined + }; + + const expected = [ + { + type: 'object', + name: 'TypeWithoutDirectives', + properties: {}, + required: [], + description: '', + typeDirectives: [], // We expect an empty array since our mock returns [] + }, + ]; + + const result = getObjectTypeDefinitions({ + objectTypes: [mockObjectType], + 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, []); + }); +});