Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions reverse_engineering/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,15 @@ 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
const mappedEntities = getMappedSchema({
schemaItems: parsedSchema.definitions,
graphName: fileName,
logger,
fieldsOrder,
});

callback(null, mappedEntities, {}, [], 'multipleSchema');
Expand Down
5 changes: 5 additions & 0 deletions reverse_engineering/constants/graphqlAST.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { Kind } = require('graphql');

module.exports = {
astNodeKind: Kind,
};
12 changes: 12 additions & 0 deletions reverse_engineering/constants/properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const DIRECTIVE_FORMAT = {
structured: 'Structured',
};

const ARGUMENT_VALUE_FORMAT = {
raw: 'Raw',
};

module.exports = {
DIRECTIVE_FORMAT,
ARGUMENT_VALUE_FORMAT,
};
25 changes: 25 additions & 0 deletions reverse_engineering/helpers/sortByName.js
Original file line number Diff line number Diff line change
@@ -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,
};
73 changes: 73 additions & 0 deletions reverse_engineering/mappers/directiveUsage.js
Original file line number Diff line number Diff line change
@@ -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,
};
3 changes: 2 additions & 1 deletion reverse_engineering/mappers/rootSchemaTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const { OperationTypeNode } = require('graphql');

const { mapStringValueNode } = require('./stringValue');
const { mapDirectivesUsage } = require('./directiveUsage');

/**
* Maps the root schema types to a container
Expand All @@ -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 }),
};
}

Expand Down
9 changes: 7 additions & 2 deletions reverse_engineering/mappers/schema.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
/**
* @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
* @param {Object} params
* @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');
Expand All @@ -25,6 +27,8 @@ function getMappedSchema({ schemaItems, graphName, logger }) {
graphName,
});

const typeDefinitions = getTypeDefinitions({ typeDefinitions: schemaItems, fieldsOrder });

return [
// TODO: remove test collection
{
Expand All @@ -36,6 +40,7 @@ function getMappedSchema({ schemaItems, graphName, logger }) {
bucketInfo: container,
collectionName: 'Test Collection',
dbName: container.name,
modelDefinitions: JSON.stringify(typeDefinitions),
},
},
];
Expand Down
60 changes: 60 additions & 0 deletions reverse_engineering/mappers/typeDefinitions/directive.js
Original file line number Diff line number Diff line change
@@ -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,
};
72 changes: 72 additions & 0 deletions reverse_engineering/mappers/typeDefinitions/typeDefinitions.js
Original file line number Diff line number Diff line change
@@ -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,
};
41 changes: 41 additions & 0 deletions reverse_engineering/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Loading