Skip to content

Commit

Permalink
HCK-9710: add root types FE (#49)
Browse files Browse the repository at this point in the history
* add root types FE

* add types

* add tests

* handle container or entity is deactivated

* remove mocked script

* update types

---------

Co-authored-by: Vitalii Bedletskyi <70570504+VitaliiBedletskyi@users.noreply.github.com>
  • Loading branch information
taras-dubyk and VitaliiBedletskyi authored Jan 30, 2025
1 parent 1f0003c commit 9256b45
Show file tree
Hide file tree
Showing 7 changed files with 541 additions and 60 deletions.
64 changes: 15 additions & 49 deletions forward_engineering/api.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,32 @@
const validationHelper = require('./helpers/schemaValidationHelper');
const { getTypeDefinitionStatements } = require('./mappers/typeDefinitions');
const { generateIdToNameMap } = require('./helpers/generateIdToNameMap');

/**
* @typedef {Object} Container
* @property {Object[]} containerData - Container level data properties by tab
* @property {string[]} entities - Entities ids
* @property {string[]} jsonSchema - JSON schema by entity id
*/

/**
* @typedef {Object} Options
* @property {Object[]} additionalOptions
* @property {boolean} isCalledFromFETab
*/

/**
* @typedef {Object} Data
* @property {Object[]} modelLevelData - Model level data properties by tab
* @property {Options} options
* @property {Container[]} containers
* @property {string} externalDefinitions
* @property {string} modelDefinitions
* @property {Object} targetScriptOptions
*/

/**
* @typedef {Object} Logger
* @property {Function} log
*/

/**
* @callback GenerateScriptCallback
* @param {Error|null} error
* @param {string} [result]
*/

/**
* @callback ValidateScriptCallback
* @param {Error|null} error
* @param {Array} [result]
* @import { ModelScriptFEData, Logger, GenerateModelScriptCallback } from "./types/types"
*/

const mockedRootQuery = `# The type Query is hardcoded for now, to remove validation error.
type Query {
getSomething: String
}`;
const validationHelper = require('./helpers/schemaValidationHelper');
const { getTypeDefinitionStatements } = require('./mappers/typeDefinitions');
const { generateIdToNameMap } = require('./helpers/generateIdToNameMap');
const { getSchemaRootTypeStatements } = require('./mappers/rootTypes');

module.exports = {
/**
* Generates the model FE script for the given data.
* @param {Data} data - The data for generating the model script.
* @param {ModelScriptFEData} data - The data for generating the model script.
* @param {Logger} logger - The logger for logging errors.
* @param {GenerateScriptCallback} cb - The callback function.
* @param {GenerateModelScriptCallback} cb - The callback function.
*/
generateModelScript(data, logger, cb) {
try {
const modelDefinitions = JSON.parse(data.modelDefinitions);
const definitionsIdToNameMap = generateIdToNameMap(modelDefinitions.properties);
const typeDefinitions = getTypeDefinitionStatements({ modelDefinitions, definitionsIdToNameMap });

const schemaScript = mockedRootQuery + '\n\n' + typeDefinitions;
const rootTypeStatements = getSchemaRootTypeStatements({
containers: data.containers,
definitionsIdToNameMap,
});
const typeDefinitionStatements = getTypeDefinitionStatements({ modelDefinitions, definitionsIdToNameMap });

const schemaScript = [rootTypeStatements, typeDefinitionStatements].filter(Boolean).join('\n\n');

cb(null, schemaScript);
} catch (err) {
logger.log('error', { error: err }, 'GraphQL FE Error');
Expand Down
7 changes: 7 additions & 0 deletions forward_engineering/constants/feScriptConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ const DIRECTIVE_LOCATIONS = {
union: 'UNION',
};

const QUERY_ROOT_TYPE = 'Query';
const MUTATION_ROOT_TYPE = 'Mutation';
const SUBSCRIPTION_ROOT_TYPE = 'Subscription';

module.exports = {
GRAPHQL_SCHEMA_SCRIPT_INDENT,
DIRECTIVE_LOCATIONS,
QUERY_ROOT_TYPE,
MUTATION_ROOT_TYPE,
SUBSCRIPTION_ROOT_TYPE,
};
18 changes: 7 additions & 11 deletions forward_engineering/helpers/schemaValidationHelper.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
const { buildSchema, validateSchema } = require('graphql');

/**
* @typedef {Object} ValidationResponseEntity
* @property {string} type - The type of the entity (e.g., 'error', 'success').
* @property {string} label - The label for the entity, typically indicating the location.
* @property {string} title - The title of the entity, typically the error message.
* @property {string} [context] - The context of the entity, typically additional information.
* @import { ValidationResponseItem } from "../types/types"
*/

const { buildSchema, validateSchema } = require('graphql');

/**
* Validates the given GraphQL schema.
* @param {Object} params - The parameters for validation.
* @param {string} params.schema - The GraphQL schema to be validated.
* @returns {ValidationResponseEntity[]} An array of validation results.
* @returns {ValidationResponseItem[]} An array of validation results.
*/
function validate({ schema }) {
let builtSchema;
Expand All @@ -35,7 +31,7 @@ function validate({ schema }) {
* @param {Object} error - The GraphQL validation error.
* @param {string} error.message - The error message.
* @param {Object[]} [error.locations] - The locations of the error in the schema.
* @returns {ValidationResponseEntity} The mapped error object.
* @returns {ValidationResponseItem} The mapped error object.
*/
function mapValidationError(error) {
return getResponseEntity({
Expand Down Expand Up @@ -63,7 +59,7 @@ function getErrorPositionMessage(error) {

/**
* Gets the success response for a valid GraphQL schema.
* @returns {ValidationResponseEntity[]} An array containing the success response.
* @returns {ValidationResponseItem[]} An array containing the success response.
*/
function getSucceedResponse() {
return [getResponseEntity({ type: 'success', label: '', title: 'GraphQL schema is valid' })];
Expand All @@ -76,7 +72,7 @@ function getSucceedResponse() {
* @param {string} params.label - The label for the entity.
* @param {string} params.title - The title of the entity.
* @param {string} [params.context] - The context of the entity.
* @returns {ValidationResponseEntity} The response entity.
* @returns {ValidationResponseItem} The response entity.
*/
function getResponseEntity({ type, label, title, context = '' }) {
return {
Expand Down
1 change: 1 addition & 0 deletions forward_engineering/mappers/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ module.exports = {
getObjectTypeFields: params => getFields({ ...params, addArguments: true, addDefaultValue: false }),
getInterfaceTypeFields: params => getFields({ ...params, addArguments: true, addDefaultValue: false }),
getInputTypeFields: params => getFields({ ...params, addArguments: false, addDefaultValue: true }),
getRootTypeFields: params => getFields({ ...params, addArguments: true, addDefaultValue: false }),
// exported only for tests:
mapField,
getFieldType,
Expand Down
193 changes: 193 additions & 0 deletions forward_engineering/mappers/rootTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* @import { FEStatement, IdToNameMap, RootTypeNamesParameter, ContainerData } from "../types/types"
*/

const { QUERY_ROOT_TYPE, MUTATION_ROOT_TYPE, SUBSCRIPTION_ROOT_TYPE } = require('../constants/feScriptConstants');
const { formatFEStatement } = require('../helpers/feStatementFormatHelper');
const { getRootTypeFields } = require('./fields');

/**
* Gets root schema statement the root type statements.
* If all root types have default values, root schema statement is not returned.
*
* @param {Object} param0
* @param {ContainerData[]} param0.containers - The containers.
* @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map.
* @returns {FEStatement[]} - The root type statements.
*/
function getSchemaRootTypeStatements({ containers = [], definitionsIdToNameMap }) {
const rootTypeNames = getRootTypeNames({ containers });
const rootSchemaStatement = getRootSchemaStatement({ rootTypeNames });
const rootTypes = getRootTypes({ containers, rootTypeNames, definitionsIdToNameMap });

return [rootSchemaStatement, ...rootTypes]
.filter(Boolean)
.map(rootType => formatFEStatement({ feStatement: rootType }))
.join('\n\n');
}

/**
* Gets the root schema statement.
* If all root types have default values, return null.
*
* @param {Object} param0
* @param {RootTypeNamesParameter} param0.rootTypeNames - The root type names.
* @returns {FEStatement | null} - The root schema statement or null if all root types have default values.
*/
function getRootSchemaStatement({ rootTypeNames }) {
const { query, mutation, subscription } = rootTypeNames;

const nestedStatements = [];

if (query !== QUERY_ROOT_TYPE) {
nestedStatements.push({ statement: `query: ${query}` });
}

if (mutation !== MUTATION_ROOT_TYPE) {
nestedStatements.push({ statement: `mutation: ${mutation}` });
}

if (subscription !== SUBSCRIPTION_ROOT_TYPE) {
nestedStatements.push({ statement: `subscription: ${subscription}` });
}

if (nestedStatements.length === 0) {
return null;
}

return {
statement: 'schema',
nestedStatements,
};
}

/**
* Gets the root type names.
* Iterate over the containers and get the root type names.
*
* @param {Object} param0
* @param {ContainerData[]} param0.containers - The containers.
* @returns {RootTypeNamesParameter} - The root type names.
*/
function getRootTypeNames({ containers = [] }) {
const rootContainersNames = {
query: QUERY_ROOT_TYPE,
mutation: MUTATION_ROOT_TYPE,
subscription: SUBSCRIPTION_ROOT_TYPE,
};

containers.forEach(container => {
const containerRootTypesPropertyValue = container.containerData?.[0]?.schemaRootTypes;

if (containerRootTypesPropertyValue) {
const { rootQuery, rootMutation, rootSubscription } = containerRootTypesPropertyValue;

const trimmedRootQuery = rootQuery?.trim() || '';
const trimmedRootMutation = rootMutation?.trim() || '';
const trimmedRootSubscription = rootSubscription?.trim() || '';

if (trimmedRootQuery && trimmedRootQuery !== rootContainersNames.query) {
rootContainersNames.query = trimmedRootQuery;
}

if (trimmedRootMutation && trimmedRootMutation !== rootContainersNames.mutation) {
rootContainersNames.mutation = trimmedRootMutation;
}

if (trimmedRootSubscription && trimmedRootSubscription !== rootContainersNames.subscription) {
rootContainersNames.subscription = trimmedRootSubscription;
}
}
});

return rootContainersNames;
}

/**
* Gets the root types.
* Iterate over the containers and get the root types.
* For each root type, get the nested statements composed of the fields of the entities with the operation type equal to the root type.
* If there are no entities with the operation type equal to the root type, return null.
*
* @param {Object} param0
* @param {ContainerData[]} param0.containers - The containers.
* @param {RootTypeNamesParameter} param0.rootTypeNames - The root type names.
* @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map.
* @returns {FEStatement[]} - The root types.
*/
function getRootTypes({ containers, rootTypeNames, definitionsIdToNameMap }) {
const { query, mutation, subscription } = rootTypeNames;

const rootTypes = [
getRootType({ containers, rootTypeName: query, definitionsIdToNameMap, rootType: QUERY_ROOT_TYPE }),
getRootType({ containers, rootTypeName: mutation, definitionsIdToNameMap, rootType: MUTATION_ROOT_TYPE }),
getRootType({
containers,
rootTypeName: subscription,
definitionsIdToNameMap,
rootType: SUBSCRIPTION_ROOT_TYPE,
}),
];

return rootTypes.filter(Boolean);
}

/**
* Gets the root type.
* Iterate over the containers and get the root type.
* For each root type, get the nested statements composed of the fields of the entities with the operation type equal to the root type.
* If there are no entities with the operation type equal to the root type, return null.
*
* @param {Object} param0
* @param {ContainerData[]} param0.containers - The containers.
* @param {string} param0.rootTypeName - The root type name.
* @param {IdToNameMap} param0.definitionsIdToNameMap - The definitions id to name map.
* @param {string} param0.rootType - The root type.
* @returns {FEStatement | null} - The root type or null if there are no entities with the operation type equal to the root type.
*/
function getRootType({ containers, rootTypeName, definitionsIdToNameMap, rootType }) {
const rootTypeNestedStatements = [];
containers.forEach(container => {
Object.entries(container.jsonSchema).forEach(([entityId, entityJson]) => {
const entityOperationType = container.entityData[entityId]?.[0]?.operationType;
if (entityOperationType !== rootType) {
return;
}

const entityData = JSON.parse(entityJson);
const entityFields = getRootTypeFields({
fields: entityData.properties,
requiredFields: entityData.required,
definitionsIdToNameMap,
}).map(field => {
if ([container.containerData?.[0]?.isActivated, entityData.isActivated].includes(false)) {
// If the container or entity is not activated, set the field as not activated
return {
...field,
isActivated: false,
};
}
return field;
});

rootTypeNestedStatements.push(...entityFields);
});
});

if (rootTypeNestedStatements.length === 0) {
return null;
}
return {
statement: `type ${rootTypeName}`,
nestedStatements: rootTypeNestedStatements,
};
}

module.exports = {
getSchemaRootTypeStatements,
// For testing purposes
getRootSchemaStatement,
getRootTypeNames,
getRootTypes,
getRootType,
};
41 changes: 41 additions & 0 deletions forward_engineering/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,44 @@ export type Union = {
}

export type UnionDefinitions = Record<string, Union>

// Root types
export type RootTypeNamesParameter = {
query: string;
mutation: string;
subscription: string;
}

export type ContainerData = {
containerData: object[]; // container properties by tab
jsonSchema: Record<string, string>; // JSON schemas of entities by entity ID
entityData: Record<string, object[]>; // entity properties by entity ID
}

// API parameters
export type ModelScriptFEData = {
modelLevelData: object[]; // model level data
containers: ContainerData[]; // containers data
externalDefinitions: string; // external definitions JSON Schema
modelDefinitions: string; // model definitions JSON Schema
targetScriptOptions: object; // target script options
options: {
additionalOptions: object[]; // additional options
isCalledFromFETab: boolean; // if the script is called from the forward engineering tab
}
};

export type Logger = {
log: (logType: string, logData: object, logMessage: string) => void;
};

export type GenerateModelScriptCallback = (error: Error | null, script?: string) => void;

export type ValidationResponseItem = {
type: string; // The type of the entity (e.g., 'error', 'success').
label: string; // The label for the entity, typically indicating the location.
title: string; // The title of the entity, typically the error message.
context?: string; // The context of the entity, typically additional information.
}

export type ValidateScriptCallback = (error: Error | null, validationErrors?: ValidationResponseItem[]) => void;
Loading

0 comments on commit 9256b45

Please sign in to comment.