diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index 0e001e4..ea918cf 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -92,6 +92,7 @@ module.exports = { */ async reFromFile(data, logger, callback) { try { + const fieldsOrder = data.fieldInference.active; const fileContent = await readFileContent({ filePath: data.filePath }); const fileName = getFileName(data.filePath); const { parsedSchema /*validationErrors*/ } = parseSchema({ schemaContent: fileContent }); // TODO: validation warnings can be returned in modelData @@ -99,6 +100,7 @@ module.exports = { schemaItems: parsedSchema.definitions, graphName: fileName, logger, + fieldsOrder, }); callback(null, mappedEntities, {}, [], 'multipleSchema'); diff --git a/reverse_engineering/constants/graphqlAST.js b/reverse_engineering/constants/graphqlAST.js new file mode 100644 index 0000000..f4dd9b2 --- /dev/null +++ b/reverse_engineering/constants/graphqlAST.js @@ -0,0 +1,5 @@ +const { Kind } = require('graphql'); + +module.exports = { + astNodeKind: Kind, +}; diff --git a/reverse_engineering/constants/properties.js b/reverse_engineering/constants/properties.js new file mode 100644 index 0000000..fe4107a --- /dev/null +++ b/reverse_engineering/constants/properties.js @@ -0,0 +1,12 @@ +const DIRECTIVE_FORMAT = { + structured: 'Structured', +}; + +const ARGUMENT_VALUE_FORMAT = { + raw: 'Raw', +}; + +module.exports = { + DIRECTIVE_FORMAT, + ARGUMENT_VALUE_FORMAT, +}; diff --git a/reverse_engineering/helpers/sortByName.js b/reverse_engineering/helpers/sortByName.js new file mode 100644 index 0000000..ef3b81e --- /dev/null +++ b/reverse_engineering/helpers/sortByName.js @@ -0,0 +1,25 @@ +/** + * @import { FieldsOrder } from "../types/types" + */ + +/** + * Sorts an array of objects by the name according to the fields order option + * @param {Object} params + * @param {Object[]} params.items - The items to sort + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @returns {Object[]} The sorted items + */ +function sortByName({ items, fieldsOrder }) { + if (!Array.isArray(items)) { + return items; + } + if (fieldsOrder === 'alphabetical') { + return items.toSorted((a, b) => a.name.localeCompare(b.name)); + } + + return items; +} + +module.exports = { + sortByName, +}; diff --git a/reverse_engineering/mappers/directiveUsage.js b/reverse_engineering/mappers/directiveUsage.js new file mode 100644 index 0000000..d581d31 --- /dev/null +++ b/reverse_engineering/mappers/directiveUsage.js @@ -0,0 +1,73 @@ +const { astNodeKind } = require('../constants/graphqlAST'); +const { DIRECTIVE_FORMAT, ARGUMENT_VALUE_FORMAT } = require('../constants/properties'); + +/** + * @import { DirectiveNode, ArgumentNode, ValueNode } from "graphql" + * @import { DirectiveUsage } from "../types/types" + */ + +/** + * Maps the directives usage + * @param {Object} params + * @param {DirectiveNode[]} params.directives - The directives + * @returns {DirectiveUsage[]} The mapped directives usage + */ +function mapDirectivesUsage({ directives = [] }) { + return directives.map(directive => { + return { + directiveFormat: DIRECTIVE_FORMAT.structured, + directiveName: directive.name.value, + argumentValueFormat: ARGUMENT_VALUE_FORMAT.raw, + rawArgumentValues: getRawArguments({ argumentNodes: directive.arguments }), + }; + }); +} + +/** + * Gets the raw arguments + * @param {Object} params + * @param {ArgumentNode[]} params.argumentNodes - The arguments + * @returns {string} The raw arguments + */ +function getRawArguments({ argumentNodes = [] }) { + return argumentNodes.map(arg => `${arg.name.value}: ${getArgumentValue(arg.value)}`).join(', '); +} + +/** + * Gets the string representation of an argument value + * @param {ValueNode} value - The value node + * @returns {string} The string representation of the value + */ +function getArgumentValue(value) { + switch (value.astNodeKind) { + case astNodeKind.INT: + case astNodeKind.FLOAT: + return value.value; + case astNodeKind.STRING: + return `"${value.value}"`; + case astNodeKind.BOOLEAN: + return value.value.toString(); + case astNodeKind.NULL: + return 'null'; + case astNodeKind.ENUM: + return value.value; + case astNodeKind.LIST: + return `[${value.values.map(getArgumentValue).join(', ')}]`; + case astNodeKind.OBJECT: { + const fieldStrings = value.fields.map(field => { + const fieldName = field.name.value; + const fieldValue = getArgumentValue(field.value); + return `${fieldName}: ${fieldValue}`; + }); + return `{${fieldStrings.join(', ')}}`; + } + case astNodeKind.VARIABLE: + return `$${value.name.value}`; + default: + return ''; + } +} + +module.exports = { + mapDirectivesUsage, +}; diff --git a/reverse_engineering/mappers/rootSchemaTypes.js b/reverse_engineering/mappers/rootSchemaTypes.js index 113e05e..899ea69 100644 --- a/reverse_engineering/mappers/rootSchemaTypes.js +++ b/reverse_engineering/mappers/rootSchemaTypes.js @@ -6,6 +6,7 @@ const { OperationTypeNode } = require('graphql'); const { mapStringValueNode } = require('./stringValue'); +const { mapDirectivesUsage } = require('./directiveUsage'); /** * Maps the root schema types to a container @@ -23,7 +24,7 @@ function mapRootSchemaTypesToContainer({ rootSchemaNode, graphName = 'New Graph' name: graphName, description: mapStringValueNode({ node: rootSchemaNode.description }), schemaRootTypes: mapSchemaRootTypes({ schemaRootTypes: rootSchemaNode.operationTypes }), - // TODO: add directives + graphDirectives: mapDirectivesUsage({ directives: rootSchemaNode.directives }), }; } diff --git a/reverse_engineering/mappers/schema.js b/reverse_engineering/mappers/schema.js index a432d41..d0471a3 100644 --- a/reverse_engineering/mappers/schema.js +++ b/reverse_engineering/mappers/schema.js @@ -1,11 +1,12 @@ /** * @import { DocumentNode } from "graphql" - * @import { Logger, FileREEntityResponseData } from "../types/types" + * @import { Logger, FileREEntityResponseData, FieldsOrder } from "../types/types" */ const { Kind } = require('graphql'); const { mapRootSchemaTypesToContainer } = require('./rootSchemaTypes'); const { findNodesByKind } = require('../helpers/findNodesByKind'); +const { getTypeDefinitions } = require('./typeDefinitions/typeDefinitions'); /** * Maps a GraphQL schema to a RE response @@ -13,9 +14,10 @@ const { findNodesByKind } = require('../helpers/findNodesByKind'); * @param {DocumentNode[]} params.schemaItems - The schema items * @param {string} params.graphName - The name of the graph to be mapped as the container name * @param {Logger} params.logger - The logger + * @param {FieldsOrder} params.fieldsOrder - The fields order * @returns {FileREEntityResponseData[]} The mapped entities */ -function getMappedSchema({ schemaItems, graphName, logger }) { +function getMappedSchema({ schemaItems, graphName, logger, fieldsOrder }) { try { if (!schemaItems) { throw new Error('Schema items are empty'); @@ -25,6 +27,8 @@ function getMappedSchema({ schemaItems, graphName, logger }) { graphName, }); + const typeDefinitions = getTypeDefinitions({ typeDefinitions: schemaItems, fieldsOrder }); + return [ // TODO: remove test collection { @@ -36,6 +40,7 @@ function getMappedSchema({ schemaItems, graphName, logger }) { bucketInfo: container, collectionName: 'Test Collection', dbName: container.name, + modelDefinitions: JSON.stringify(typeDefinitions), }, }, ]; diff --git a/reverse_engineering/mappers/typeDefinitions/directive.js b/reverse_engineering/mappers/typeDefinitions/directive.js new file mode 100644 index 0000000..5aab9f8 --- /dev/null +++ b/reverse_engineering/mappers/typeDefinitions/directive.js @@ -0,0 +1,60 @@ +/** + * @import { DirectiveDefinitionNode } from "graphql" + * @import { DirectiveDefinition } from "../../types/types" + */ + +const locationMap = { + 'SCHEMA': 'schema', + 'QUERY': 'query', + 'MUTATION': 'mutation', + 'SUBSCRIPTION': 'subscription', + 'SCALAR': 'scalar', + 'ENUM': 'enum', + 'ENUM_VALUE': 'enumValue', + 'OBJECT': 'object', + 'INTERFACE': 'interface', + 'UNION': 'union', + 'INPUT_OBJECT': 'inputObject', + 'FIELD': 'field', + 'FIELD_DEFINITION': 'fieldDefinition', + 'INPUT_FIELD_DEFINITION': 'inputFieldDefinition', + 'ARGUMENT_DEFINITION': 'argumentDefinition', +}; + +/** + * Maps the directive type definitions + * @param {Object} params + * @param {DirectiveDefinitionNode[]} params.directives - The directives + * @returns {DirectiveDefinition[]} The mapped directive type definitions + */ +function getDirectiveTypeDefinitions({ directives = [] }) { + return directives.map(directive => mapDirective({ directive })); +} + +/** + * Maps a single directive definition + * @param {Object} params + * @param {DirectiveDefinitionNode} params.directive - The directive to map + * @returns {DirectiveDefinition} The mapped directive definition + */ +function mapDirective({ directive }) { + const locations = directive.locations.reduce((acc, location) => { + const locationKey = locationMap[location.value]; + if (locationKey) { + acc[locationKey] = true; + } + return acc; + }, {}); + + return { + type: 'directive', + name: directive.name.value, + description: directive.description?.value || '', + arguments: [], // TODO: implement argument mapping + directiveLocations: locations, + }; +} + +module.exports = { + getDirectiveTypeDefinitions, +}; diff --git a/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js new file mode 100644 index 0000000..5c3db8e --- /dev/null +++ b/reverse_engineering/mappers/typeDefinitions/typeDefinitions.js @@ -0,0 +1,72 @@ +const { astNodeKind } = require('../../constants/graphqlAST'); +const { findNodesByKind } = require('../../helpers/findNodesByKind'); +const { sortByName } = require('../../helpers/sortByName'); +const { getDirectiveTypeDefinitions } = require('./directive'); + +/** + * @import { DirectiveDefinition, FieldsOrder } from "../../types/types" + */ + +/** + * Gets the type definitions structure + * @param {Object} params + * @param {Object[]} params.typeDefinitions - The type definitions nodes + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @returns {Object} The mapped type definitions + */ +function getTypeDefinitions({ typeDefinitions, fieldsOrder }) { + const directives = getDirectiveTypeDefinitions({ + directives: findNodesByKind({ nodes: typeDefinitions, kind: astNodeKind.DIRECTIVE_DEFINITION }), + }); + + const definitions = getTypeDefinitionsStructure({ fieldsOrder, directives }); + + return definitions; +} + +/** + * Creates the model definitions structure + * @param {Object} params + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @param {DirectiveDefinition[]} params.directives - The directive definitions + * @returns {Object} The type definitions structure + */ +function getTypeDefinitionsStructure({ fieldsOrder, directives }) { + const definitions = { + ['Directives']: getDefinitionCategoryStructure({ + fieldsOrder, + subtype: 'directive', + properties: directives, + }), + }; + + return { + definitions, + }; +} + +/** + * Creates a definition category structure + * @param {Object} params + * @param {FieldsOrder} params.fieldsOrder - The fields order + * @param {string} params.subtype - The subtype of the definition + * @param {Object[]} params.properties - The properties to structure + * @returns {Object} The definition category structure + */ +function getDefinitionCategoryStructure({ fieldsOrder, subtype, properties }) { + const sortedFields = sortByName({ items: properties, fieldsOrder }); + + return { + type: 'type', + subtype, + structureType: true, + properties: sortedFields.reduce((acc, prop) => { + acc[prop.name] = prop; + return acc; + }, {}), + }; +} + +module.exports = { + getTypeDefinitions, +}; diff --git a/reverse_engineering/types/types.d.ts b/reverse_engineering/types/types.d.ts index c25ee38..9ffbe3d 100644 --- a/reverse_engineering/types/types.d.ts +++ b/reverse_engineering/types/types.d.ts @@ -4,6 +4,7 @@ export type ContainerInfo = { name: ContainerName; description?: string; // container description schemaRootTypes?: ContainerSchemaRootTypes; // container schema root types + graphDirectives?: DirectiveUsage[]; // container graph directives }; export type ContainerSchemaRootTypes = { @@ -30,8 +31,13 @@ export type FileREModelLevelResponseData = { description?: string; // model description }; +export type FieldsOrder = 'field' | 'alphabetical'; + export type FileREData = { filePath: string; + fieldInference: { + active: FieldsOrder; + }; }; export type REFromFileCallback = (err: Error | null, entitiesData: FileREEntityResponseData[], modelData: FileREModelLevelResponseData) => void; @@ -97,5 +103,40 @@ export type REConnectionInfo = GeneralRESettings & { connectionSettings: ConnectionSettings; }; +export type REFromFileCallback = (err: Error | null, entitiesData: FileREEntityResponseData[], modelData: FileREModelLevelResponseData) => void; + +export type DirectiveUsage = { + directiveFormat: 'Structured'; + directiveName: string; + argumentValueFormat: 'Raw'; + rawArgumentValues: string; +}; + +export type DirectiveLocations = { + schema: boolean; + query: boolean; + mutation: boolean; + subscription: boolean; + scalar: boolean; + enum: boolean; + enumValue: boolean; + object: boolean; + interface: boolean; + union: boolean; + inputObject: boolean; + field: boolean; + fieldDefinition: boolean; + inputFieldDefinition: boolean; + argumentDefinition: boolean; +}; + +export type DirectiveDefinition = { + type: 'directive'; + name: string; + description?: string; + arguments?: Object[]; // TODO: update when arguments are ready + directiveLocations: DirectiveLocations; +} + 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 new file mode 100644 index 0000000..041f726 --- /dev/null +++ b/test/reverse_engineering/mappers/directiveUsage.spec.js @@ -0,0 +1,132 @@ +const { describe, it } = require('node:test'); +const assert = require('assert'); +const { mapDirectivesUsage } = require('../../../reverse_engineering/mappers/directiveUsage'); +const { astNodeKind } = require('../../../reverse_engineering/constants/graphqlAST'); + +describe('mapDirectivesUsage', () => { + it('should return an empty array when no directives are provided', () => { + const result = mapDirectivesUsage({ directives: [] }); + assert.deepStrictEqual(result, []); + }); + + it('should map directive without arguments', () => { + const mockDirective = { + name: { value: 'deprecated' }, + arguments: [], + }; + + const expected = [ + { + directiveFormat: 'Structured', + directiveName: 'deprecated', + argumentValueFormat: 'Raw', + rawArgumentValues: '', + }, + ]; + + const result = mapDirectivesUsage({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); + + it('should map directive with primitive argument values', () => { + const mockDirective = { + name: { value: 'test' }, + arguments: [ + { + name: { value: 'intArg' }, + value: { astNodeKind: astNodeKind.INT, value: '42' }, + }, + { + name: { value: 'stringArg' }, + value: { astNodeKind: astNodeKind.STRING, value: 'hello' }, + }, + { + name: { value: 'boolArg' }, + value: { astNodeKind: astNodeKind.BOOLEAN, value: true }, + }, + ], + }; + + const expected = [ + { + directiveFormat: 'Structured', + directiveName: 'test', + argumentValueFormat: 'Raw', + rawArgumentValues: 'intArg: 42, stringArg: "hello", boolArg: true', + }, + ]; + + const result = mapDirectivesUsage({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); + + it('should map directive with complex argument values', () => { + const mockDirective = { + name: { value: 'complex' }, + arguments: [ + { + name: { value: 'listArg' }, + value: { + astNodeKind: astNodeKind.LIST, + values: [ + { astNodeKind: astNodeKind.INT, value: '1' }, + { astNodeKind: astNodeKind.INT, value: '2' }, + ], + }, + }, + { + name: { value: 'objectArg' }, + value: { + astNodeKind: astNodeKind.OBJECT, + fields: [ + { + name: { value: 'field1' }, + value: { astNodeKind: astNodeKind.STRING, value: 'value1' }, + }, + ], + }, + }, + ], + }; + + const expected = [ + { + directiveFormat: 'Structured', + directiveName: 'complex', + argumentValueFormat: 'Raw', + rawArgumentValues: 'listArg: [1, 2], objectArg: {field1: "value1"}', + }, + ]; + + const result = mapDirectivesUsage({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); + + it('should map directive with variable and enum values', () => { + const mockDirective = { + name: { value: 'test' }, + arguments: [ + { + name: { value: 'varArg' }, + value: { astNodeKind: astNodeKind.VARIABLE, name: { value: 'var' } }, + }, + { + name: { value: 'enumArg' }, + value: { astNodeKind: astNodeKind.ENUM, value: 'ENUM_VALUE' }, + }, + ], + }; + + const expected = [ + { + directiveFormat: 'Structured', + directiveName: 'test', + argumentValueFormat: 'Raw', + rawArgumentValues: 'varArg: $var, enumArg: ENUM_VALUE', + }, + ]; + + const result = mapDirectivesUsage({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); +}); diff --git a/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js b/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js new file mode 100644 index 0000000..4196d7d --- /dev/null +++ b/test/reverse_engineering/mappers/typeDefinitions/directive.spec.js @@ -0,0 +1,136 @@ +const { describe, it } = require('node:test'); +const assert = require('assert'); +const { getDirectiveTypeDefinitions } = require('../../../../reverse_engineering/mappers/typeDefinitions/directive'); + +describe('getDirectiveTypeDefinitions', () => { + it('should return an empty array when no directives are provided', () => { + const result = getDirectiveTypeDefinitions({ directives: [] }); + assert.deepStrictEqual(result, []); + }); + + it('should correctly map a directive with basic properties', () => { + const mockDirective = { + name: { value: 'testDirective' }, + locations: [{ value: 'FIELD' }], + }; + + const expected = [ + { + type: 'directive', + name: 'testDirective', + arguments: [], + description: '', + directiveLocations: { + field: true, + }, + }, + ]; + + const result = getDirectiveTypeDefinitions({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); + + it('should correctly map a directive with description', () => { + const mockDirective = { + name: { value: 'testDirective' }, + description: { value: 'Test description' }, + locations: [{ value: 'FIELD' }], + }; + + const expected = [ + { + type: 'directive', + name: 'testDirective', + description: 'Test description', + arguments: [], + directiveLocations: { + field: true, + }, + }, + ]; + + const result = getDirectiveTypeDefinitions({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); + + it('should correctly map multiple locations', () => { + const mockDirective = { + name: { value: 'testDirective' }, + locations: [{ value: 'FIELD' }, { value: 'OBJECT' }, { value: 'SCHEMA' }], + }; + + const expected = [ + { + type: 'directive', + name: 'testDirective', + arguments: [], + description: '', + directiveLocations: { + field: true, + object: true, + schema: true, + }, + }, + ]; + + const result = getDirectiveTypeDefinitions({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); + + it('should correctly map multiple directives', () => { + const mockDirectives = [ + { + name: { value: 'directive1' }, + locations: [{ value: 'FIELD' }], + }, + { + name: { value: 'directive2' }, + locations: [{ value: 'OBJECT' }], + }, + ]; + + const expected = [ + { + type: 'directive', + name: 'directive1', + arguments: [], + description: '', + directiveLocations: { + field: true, + }, + }, + { + type: 'directive', + name: 'directive2', + arguments: [], + description: '', + directiveLocations: { + object: true, + }, + }, + ]; + + const result = getDirectiveTypeDefinitions({ directives: mockDirectives }); + assert.deepStrictEqual(result, expected); + }); + + it('should skip unknown location values', () => { + const mockDirective = { + name: { value: 'testDirective' }, + locations: [{ value: 'my_location' }], + }; + + const expected = [ + { + type: 'directive', + name: 'testDirective', + arguments: [], + description: '', + directiveLocations: {}, + }, + ]; + + const result = getDirectiveTypeDefinitions({ directives: [mockDirective] }); + assert.deepStrictEqual(result, expected); + }); +});