UNPKG

@reldens/storage

Version:
570 lines (538 loc) 22.9 kB
/** * * Reldens - ModelsGeneration * */ const { FileHandler } = require('@reldens/server-utils'); const { Logger, sc } = require('@reldens/utils'); const { TypeMapper } = require('../type-mapper'); const { BaseGenerator } = require('./base-generator'); class ModelsGeneration extends BaseGenerator { constructor(props) { super(); this.modelsFolder = sc.get(props, 'modelsFolder', ''); this.templates = sc.get(props, 'templates', {}); this.generatedModels = {}; this.allTablesData = {}; this.removeIdFromMultipleRelations = sc.get(props, 'removeIdFromMultipleRelations', true); } async generateModelFile(tableName, tableData, driverKey, entityInfo, relationMetadata = {}) { let modelTemplatePath = this.templates[driverKey]; if(!FileHandler.exists(modelTemplatePath)){ Logger.critical('Model template file "'+modelTemplatePath+'" not found for driver "'+driverKey+'".'); return false; } let modelTemplateContent = FileHandler.fetchFileContents(modelTemplatePath); if(!modelTemplateContent){ Logger.critical('Failed to read model template file: '+modelTemplatePath); return false; } let modelClassName = sc.capitalizedCamelCase(tableName)+'Model'; let modelPropertiesList = Object.keys(tableData.columns).join(', '); let modelPropertiesConstructor = Object.keys(tableData.columns) .map(columnName => 'this.'+columnName+' = '+columnName+';') .join('\n '); let modelRelations = this.generateModelRelations(tableName, tableData, relationMetadata, driverKey); let entityPropertiesDefinition = this.getEntityPropertiesDefinition(tableData.columns, driverKey); let idColumn = this.generateIdColumn(tableData.columns, driverKey); let replacements = { modelClassName, tableName: 'prisma' === driverKey ? tableName.toLowerCase() : tableName, modelPropertiesList, modelPropertiesConstructor, modelRelations, entityPropertiesDefinition, idColumn }; let modelContent = this.applyReplacements(modelTemplateContent, replacements); let fileName = sc.kebabCase(tableName)+'-model.js'; let driverFolder = FileHandler.joinPaths(this.modelsFolder, driverKey); FileHandler.createFolder(driverFolder); let filePath = FileHandler.joinPaths(driverFolder, fileName); if(!FileHandler.writeFile(filePath, modelContent)){ Logger.critical('Failed to write model file: '+fileName); return false; } Logger.info('Generated model file: '+fileName); this.generatedModels[tableName] = { modelClassName, modelFileName: fileName, driverKey }; return true; } generateIdColumn(columns, driverKey) { if('objection-js' !== driverKey){ return ''; } let primaryKeyColumn = this.detectPrimaryKeyColumn(columns); if(!primaryKeyColumn || 'id' === primaryKeyColumn){ return ''; } return '\n static get idColumn()\n {\n return \''+primaryKeyColumn+'\';\n }\n'; } detectPrimaryKeyColumn(columns) { for(let columnName of Object.keys(columns)){ let column = columns[columnName]; if('PRI' === column.key){ return columnName; } } return null; } setAllTablesData(allTablesData) { this.allTablesData = allTablesData; } generateModelRelations(tableName, tableData, relationMetadata, driverKey) { if('prisma' === driverKey){ return this.generatePrismaRelations(tableName, tableData, relationMetadata); } if('objection-js' === driverKey){ return this.generateObjectionJsRelations(tableName, tableData); } return ''; } generatePrismaRelations(tableName, tableData, relationMetadata) { if(!relationMetadata || 0 === Object.keys(relationMetadata).length){ return ''; } let relationTypesArray = []; let relationMappingsArray = []; let forwardMappings = this.buildPrismaForwardMappings(tableData, relationMetadata); let reverseMappings = this.buildPrismaReverseMappings(tableName, relationMetadata); let allMappings = Object.assign({}, forwardMappings, reverseMappings); for(let prismaRelationName of Object.keys(relationMetadata)){ let relation = relationMetadata[prismaRelationName]; relationTypesArray.push(' '+prismaRelationName+': \''+relation.type+'\''); } for(let reldensKey of Object.keys(allMappings)){ let prismaName = allMappings[reldensKey]; relationMappingsArray.push(' \''+reldensKey+'\': \''+prismaName+'\''); } let result = ''; if(0 < relationTypesArray.length){ result += '\n\n static get relationTypes()\n {\n return {\n'; result += relationTypesArray.join(',\n'); result += '\n };\n }'; } if(0 < relationMappingsArray.length){ result += '\n\n static get relationMappings()\n {\n return {\n'; result += relationMappingsArray.join(',\n'); result += '\n };\n }'; } return result; } buildPrismaForwardMappings(tableData, relationMetadata) { let mappings = {}; let referenceCounts = this.countReferencesPerTable(tableData); for(let columnName of Object.keys(tableData.columns)){ let column = tableData.columns[columnName]; if(!sc.hasOwn(column, 'referencedTable')){ continue; } let reldensKey = this.generateForwardRelationKey( column.referencedTable, columnName, referenceCounts ); let prismaName = this.findPrismaRelationForFk(columnName, column.referencedTable, relationMetadata); if(!prismaName){ continue; } mappings[reldensKey] = prismaName; } return mappings; } findPrismaRelationForFk(columnName, referencedTable, relationMetadata) { let lowerColumn = columnName.toLowerCase(); let lowerTable = referencedTable.toLowerCase(); for(let prismaName of Object.keys(relationMetadata)){ let relation = relationMetadata[prismaName]; if(relation.model.toLowerCase() !== lowerTable){ continue; } if(relation.isList){ continue; } let lowerPrisma = prismaName.toLowerCase(); if(lowerPrisma.includes(lowerColumn.replace('_id', ''))){ return prismaName; } if(lowerPrisma.includes(lowerColumn)){ return prismaName; } } for(let prismaName of Object.keys(relationMetadata)){ let relation = relationMetadata[prismaName]; if(relation.model.toLowerCase() !== lowerTable){ continue; } if(relation.isList){ continue; } return prismaName; } return null; } buildPrismaReverseMappings(tableName, relationMetadata) { let mappings = {}; let reverseRelations = {}; for(let prismaName of Object.keys(relationMetadata)){ let relation = relationMetadata[prismaName]; if(this.isForwardRelation(relation)){ continue; } let relatedTable = relation.model.toLowerCase(); if(!reverseRelations[relatedTable]){ reverseRelations[relatedTable] = []; } reverseRelations[relatedTable].push({prismaName, relation}); } for(let relatedTable of Object.keys(reverseRelations)){ let relations = reverseRelations[relatedTable]; let referenceCount = relations.length; for(let relInfo of relations){ let reldensKey = this.generatePrismaReverseRelationKey( relatedTable, relInfo.prismaName, tableName, referenceCount ); mappings[reldensKey] = relInfo.prismaName; } } return mappings; } isForwardRelation(relation) { return sc.hasOwn(relation, 'foreignKeys') && sc.isArray(relation.foreignKeys) && 0 < relation.foreignKeys.length; } generatePrismaReverseRelationKey(relatedTable, prismaName, tableName, referenceCount) { if(1 === referenceCount){ return 'related_'+relatedTable; } let columnHint = this.extractColumnHintFromPrismaName(prismaName, relatedTable, tableName); if(columnHint){ return 'related_'+relatedTable+'_'+columnHint; } return 'related_'+relatedTable; } extractColumnHintFromPrismaName(prismaName, relatedTable, tableName) { let lowerName = prismaName.toLowerCase(); let lowerTable = tableName.toLowerCase(); let lowerRelated = relatedTable.toLowerCase(); let toSuffix = 'to'+lowerTable; if(!lowerName.endsWith(toSuffix)){ toSuffix = 'to'+lowerRelated; if(!lowerName.endsWith(toSuffix)){ return null; } } let withoutSuffix = prismaName.slice(0, -toSuffix.length); let lowerWithout = withoutSuffix.toLowerCase(); let prefixes = [ lowerRelated+'_'+lowerRelated+'_', lowerRelated+'_', lowerTable+'_' ]; for(let prefix of prefixes){ if(lowerWithout.startsWith(prefix)){ withoutSuffix = withoutSuffix.slice(prefix.length); lowerWithout = withoutSuffix.toLowerCase(); } } if(withoutSuffix.endsWith('_id')){ withoutSuffix = withoutSuffix.slice(0, -3); } return withoutSuffix.toLowerCase(); } generateObjectionJsRelations(tableName, tableData) { let relations = this.detectObjectionJsRelations(tableName, tableData); let reverseRelations = this.detectReverseObjectionJsRelations(tableName); let allRelations = Object.assign({}, relations, reverseRelations); if(0 === Object.keys(allRelations).length){ return ''; } let relationMappings = []; let requiredModels = []; for(let relationKey of Object.keys(allRelations)){ let relation = allRelations[relationKey]; let tableReference = relation.referencedTable || relation.referencingTable; let relatedModelClass = sc.capitalizedCamelCase(tableReference)+'Model'; if(!requiredModels.find(m => m.modelClass === relatedModelClass)){ requiredModels.push({ modelClass: relatedModelClass, fileName: sc.kebabCase(tableReference)+'-model' }); } let relationMapping = ' '+relationKey+': {\n'; relationMapping += ' relation: this.'+relation.relationType+',\n'; relationMapping += ' modelClass: '+relatedModelClass+',\n'; relationMapping += ' join: {\n'; relationMapping += ' from: this.tableName+\'.'+relation.fromColumn+'\',\n'; relationMapping += ' to: '+relatedModelClass+'.tableName+\'.'+relation.toColumn+'\'\n'; relationMapping += ' }\n'; relationMapping += ' }'; relationMappings.push(relationMapping); } if(0 === relationMappings.length){ return ''; } let requireStatements = []; for(let modelInfo of requiredModels){ requireStatements.push(' const { '+modelInfo.modelClass+' } = require(\'./'+modelInfo.fileName+'\');'); } let relationMappingsMethod = '\n static get relationMappings()\n {\n'; relationMappingsMethod += requireStatements.join('\n')+'\n'; relationMappingsMethod += ' return {\n'; relationMappingsMethod += relationMappings.join(',\n'); relationMappingsMethod += '\n };\n'; relationMappingsMethod += ' }'; return relationMappingsMethod; } detectObjectionJsRelations(tableName, tableData) { let relations = {}; let referenceCounts = this.countReferencesPerTable(tableData); for(let columnName of Object.keys(tableData.columns)){ let column = tableData.columns[columnName]; if(!sc.hasOwn(column, 'referencedTable')){ continue; } let relationKey = this.generateForwardRelationKey(column.referencedTable, columnName, referenceCounts); let relationType = this.determineForwardRelationType(tableName, columnName, column); relations[relationKey] = { fromColumn: columnName, toColumn: column.referencedColumn, referencedTable: column.referencedTable, relationType: relationType }; } return relations; } detectReverseObjectionJsRelations(tableName) { let reverseRelations = {}; for(let otherTableName of Object.keys(this.allTablesData)){ if(otherTableName === tableName){ continue; } let otherTableData = this.allTablesData[otherTableName]; let referenceCounts = this.countReferencesToTable(tableName, otherTableData); for(let columnName of Object.keys(otherTableData.columns)){ let column = otherTableData.columns[columnName]; if(!sc.hasOwn(column, 'referencedTable')){ continue; } if(column.referencedTable !== tableName){ continue; } let relationKey = this.generateReverseRelationKey(otherTableName, columnName, referenceCounts); let relationType = this.determineReverseRelationType(otherTableName, columnName, column); reverseRelations[relationKey] = { fromColumn: column.referencedColumn, toColumn: columnName, referencingTable: otherTableName, relationType: relationType }; } } return reverseRelations; } countReferencesPerTable(tableData) { let counts = {}; for(let columnName of Object.keys(tableData.columns)){ let column = tableData.columns[columnName]; if(!sc.hasOwn(column, 'referencedTable')){ continue; } if(!counts[column.referencedTable]){ counts[column.referencedTable] = 0; } counts[column.referencedTable]++; } return counts; } countReferencesToTable(targetTable, sourceTableData) { let count = 0; for(let columnName of Object.keys(sourceTableData.columns)){ let column = sourceTableData.columns[columnName]; if(!sc.hasOwn(column, 'referencedTable')){ continue; } if(column.referencedTable === targetTable){ count++; } } return count; } generateForwardRelationKey(referencedTable, columnName, referenceCounts) { if(1 === referenceCounts[referencedTable]){ return 'related_'+referencedTable; } let columnSuffix = columnName; if(this.removeIdFromMultipleRelations){ columnSuffix = columnName.replace(/_id$/i, ''); } return 'related_'+referencedTable+'_'+columnSuffix; } generateReverseRelationKey(referencingTable, columnName, referenceCount) { if(1 === referenceCount){ return 'related_'+referencingTable; } let columnSuffix = columnName; if(this.removeIdFromMultipleRelations){ columnSuffix = columnName.replace(/_id$/i, ''); } return 'related_'+referencingTable+'_'+columnSuffix; } determineForwardRelationType(tableName, columnName, column) { return 'BelongsToOneRelation'; } determineReverseRelationType(referencingTableName, referencingColumnName, referencingColumn) { if(!this.allTablesData[referencingTableName]){ return 'HasManyRelation'; } let referencingTableData = this.allTablesData[referencingTableName]; let referencingColumnData = referencingTableData.columns[referencingColumnName]; if(!referencingColumnData){ return 'HasManyRelation'; } if('UNI' === referencingColumnData.key || 'PRI' === referencingColumnData.key){ return 'HasOneRelation'; } return 'HasManyRelation'; } determineRelationType(column, isForwardRelation) { if(isForwardRelation){ return 'BelongsToOneRelation'; } if('PRI' === column.key || 'UNI' === column.key){ return 'HasOneRelation'; } return 'HasManyRelation'; } getEntityPropertiesDefinition(columns, driverKey) { if('mikro-orm' === driverKey){ let entityProps = []; for(let columnName of Object.keys(columns)){ let column = columns[columnName]; let isPrimary = 'PRI' === column.key; let type = TypeMapper.mapDbTypeToJsType(column.type); let propDef = columnName+': { type: \''+type+'\''; if(isPrimary){ propDef += ', primary: true'; } if(column.nullable){ propDef += ', nullable: true'; } propDef += ' }'; entityProps.push(propDef); } return entityProps.join(',\n '); } if('prisma' === driverKey){ let prismaProps = []; for(let columnName of Object.keys(columns)){ let column = columns[columnName]; let prismaType = TypeMapper.mapDbTypeToPrismaType(column.type); let isPrimary = 'PRI' === column.key; let propDef = columnName+': {\n type: \''+prismaType+'\''; if(isPrimary){ propDef += ',\n id: true'; } if(column.nullable){ propDef += ',\n optional: true'; } propDef += '\n }'; prismaProps.push(propDef); } return prismaProps.join(',\n '); } return ''; } generateRegisteredModelsFile(entitiesInfo, existingModels, driverKey, templatePath) { if(!FileHandler.exists(templatePath)){ Logger.critical('Registered models template file not found: '+templatePath); return false; } let registeredTemplateContent = FileHandler.fetchFileContents(templatePath); if(!registeredTemplateContent){ Logger.critical('Failed to read registered models template file: '+templatePath); return false; } let allEntities = Object.assign({}, entitiesInfo); let replacements = { registeredModels: this.getRegisteredModels(allEntities, existingModels, driverKey), registeredEntitiesObject: this.getRegisteredEntitiesObject(allEntities, existingModels, driverKey) }; let registeredContent = this.applyReplacements(registeredTemplateContent, replacements); let driverFolder = FileHandler.joinPaths(this.modelsFolder, driverKey); let filePath = FileHandler.joinPaths(driverFolder, 'registered-models-'+driverKey+'.js'); if(!FileHandler.writeFile(filePath, registeredContent)){ Logger.critical('Failed to write registered models file: '+filePath); return false; } Logger.info('Generated registered models file: '+filePath); return true; } getRegisteredModels(allEntities, existingModels, driverKey) { let registeredModels = []; for(let tableName of Object.keys(allEntities)){ let entity = allEntities[tableName]; if(entity.driverKey && driverKey !== entity.driverKey){ if(!existingModels[tableName] || !existingModels[tableName][driverKey]){ continue; } } let modelClassName = entity.modelClassName || sc.capitalizedCamelCase(tableName)+'Model'; let modelFileName = entity.modelFileName || sc.kebabCase(tableName)+'-model.js'; registeredModels.push( 'const { '+modelClassName+' } = require(\'./'+modelFileName.replace('.js', '')+'\');' ); } return registeredModels.join('\n'); } getRegisteredEntitiesObject(allEntities, existingModels, driverKey) { let registeredEntitiesObject = []; for(let tableName of Object.keys(allEntities)){ let entity = allEntities[tableName]; if(entity.driverKey && driverKey !== entity.driverKey){ if(!existingModels[tableName] || !existingModels[tableName][driverKey]){ continue; } } let modelClassName = entity.modelClassName || sc.capitalizedCamelCase(tableName)+'Model'; registeredEntitiesObject.push(sc.camelCase(tableName)+': '+modelClassName); } return registeredEntitiesObject.join(',\n '); } } module.exports.ModelsGeneration = ModelsGeneration;