From 478a7bb7f9ea80062caaef666b8308086842a44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ad=C3=A1mek?= Date: Thu, 10 Sep 2020 23:30:14 +0200 Subject: [PATCH] feat(entity-generator): do not use ts-morph Again, not much need for ts-morph (a bit more than with migrations tho), and it brought the full TS as a dependency. With this change, the CLI package will be no longer having runtime dependency on ts-morph and therefore TS, so it will be fine to install it direct dependency (not a dev one). --- CHANGELOG.md | 2 - packages/entity-generator/package.json | 3 +- .../entity-generator/src/EntityGenerator.ts | 199 +---------------- packages/entity-generator/src/SourceFile.ts | 210 ++++++++++++++++++ 4 files changed, 219 insertions(+), 195 deletions(-) create mode 100644 packages/entity-generator/src/SourceFile.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f77ee7e80080..e8a81950e689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - - # [4.0.0](https://github.com/mikro-orm/mikro-orm/compare/v3.6.15...v4.0.0) (2020-09-08) ### Bug Fixes diff --git a/packages/entity-generator/package.json b/packages/entity-generator/package.json index 2c054761c00c..9b6dcda2f0fe 100644 --- a/packages/entity-generator/package.json +++ b/packages/entity-generator/package.json @@ -48,8 +48,7 @@ }, "dependencies": { "@mikro-orm/knex": "^4.0.1", - "fs-extra": "^9.0.1", - "ts-morph": "^8.0.0" + "fs-extra": "^9.0.1" }, "devDependencies": { "@mikro-orm/core": "^4.0.1" diff --git a/packages/entity-generator/src/EntityGenerator.ts b/packages/entity-generator/src/EntityGenerator.ts index 08e405d28604..293d73a3aa01 100644 --- a/packages/entity-generator/src/EntityGenerator.ts +++ b/packages/entity-generator/src/EntityGenerator.ts @@ -1,7 +1,7 @@ -import { IndentationText, Project, QuoteKind, SourceFile } from 'ts-morph'; import { ensureDir, writeFile } from 'fs-extra'; -import { Dictionary, EntityProperty, ReferenceType, Utils } from '@mikro-orm/core'; +import { Utils } from '@mikro-orm/core'; import { DatabaseSchema, DatabaseTable, EntityManager } from '@mikro-orm/knex'; +import { SourceFile } from './SourceFile'; export class EntityGenerator { @@ -11,209 +11,26 @@ export class EntityGenerator { private readonly helper = this.platform.getSchemaHelper()!; private readonly connection = this.driver.getConnection(); private readonly namingStrategy = this.config.getNamingStrategy(); - private readonly project = new Project(); private readonly sources: SourceFile[] = []; - constructor(private readonly em: EntityManager) { - this.project.manipulationSettings.set({ quoteKind: QuoteKind.Single, indentationText: IndentationText.TwoSpaces }); - } + constructor(private readonly em: EntityManager) { } async generate(options: { baseDir?: string; save?: boolean } = {}): Promise { const baseDir = Utils.normalizePath(options.baseDir || this.config.get('baseDir') + '/generated-entities'); const schema = await DatabaseSchema.create(this.connection, this.helper, this.config); - - for (const table of schema.getTables()) { - await this.createEntity(table); - } - - this.sources.forEach(entity => { - entity.fixMissingImports(); - entity.fixUnusedIdentifiers(); - entity.organizeImports(); - }); + schema.getTables().forEach(table => this.createEntity(table)); if (options.save) { await ensureDir(baseDir); - await Promise.all(this.sources.map(e => writeFile(baseDir + '/' + e.getBaseName(), e.getFullText()))); + await Promise.all(this.sources.map(file => writeFile(baseDir + '/' + file.getBaseName(), file.generate()))); } - return this.sources.map(e => e.getFullText()); + return this.sources.map(file => file.generate()); } - async createEntity(table: DatabaseTable): Promise { + createEntity(table: DatabaseTable): void { const meta = table.getEntityDeclaration(this.namingStrategy, this.helper); - const entity = this.project.createSourceFile(meta.className + '.ts', writer => { - writer.writeLine(`import { Entity, PrimaryKey, Property, ManyToOne, OneToMany, OneToOne, ManyToMany, Cascade, Index, Unique } from '@mikro-orm/core';`); - writer.blankLine(); - writer.writeLine('@Entity()'); - - meta.indexes.forEach(index => { - const properties = Utils.asArray(index.properties).map(prop => `'${prop}'`); - writer.writeLine(`@Index({ name: '${index.name}', properties: [${properties.join(', ')}] })`); - }); - - meta.uniques.forEach(index => { - const properties = Utils.asArray(index.properties).map(prop => `'${prop}'`); - writer.writeLine(`@Unique({ name: '${index.name}', properties: [${properties.join(', ')}] })`); - }); - - writer.write(`export class ${meta.className}`); - writer.block(() => Object.values(meta.properties).forEach(prop => { - const decorator = this.getPropertyDecorator(prop); - const definition = this.getPropertyDefinition(prop); - writer.blankLineIfLastNot(); - writer.writeLine(decorator); - writer.writeLine(definition); - writer.blankLine(); - })); - writer.write(''); - }); - - this.sources.push(entity); - } - - private getPropertyDefinition(prop: EntityProperty): string { - // string defaults are usually things like SQL functions - const useDefault = prop.default && typeof prop.default !== 'string'; - const optional = prop.nullable ? '?' : (useDefault ? '' : '!'); - const ret = `${prop.name}${optional}: ${prop.type}`; - - if (!useDefault) { - return ret + ';'; - } - - return `${ret} = ${prop.default};`; - } - - private getPropertyDecorator(prop: EntityProperty): string { - const options = {} as Dictionary; - const columnType = this.helper.getTypeFromDefinition(prop.columnTypes[0], '__false') === '__false' ? prop.columnTypes[0] : undefined; - let decorator = this.getDecoratorType(prop); - - if (prop.reference !== ReferenceType.SCALAR) { - this.getForeignKeyDecoratorOptions(options, prop); - } else { - this.getScalarPropertyDecoratorOptions(options, prop, columnType); - } - - this.getCommonDecoratorOptions(options, prop, columnType); - const indexes = this.getPropertyIndexes(prop, options); - decorator = [...indexes.sort(), decorator].join('\n'); - - if (Object.keys(options).length === 0) { - return `${decorator}()`; - } - - return `${decorator}({ ${Object.entries(options).map(([opt, val]) => `${opt}: ${val}`).join(', ')} })`; - } - - private getPropertyIndexes(prop: EntityProperty, options: Dictionary): string[] { - if (prop.reference === ReferenceType.SCALAR) { - const ret: string[] = []; - - if (prop.index) { - ret.push(`@Index({ name: '${prop.index}' })`); - } - - if (prop.unique) { - ret.push(`@Unique({ name: '${prop.unique}' })`); - } - - return ret; - } - - if (prop.index) { - options.index = `'${prop.index}'`; - } - - if (prop.unique) { - options.unique = `'${prop.unique}'`; - } - - return []; - } - - private getCommonDecoratorOptions(options: Dictionary, prop: EntityProperty, columnType: string | undefined) { - if (columnType) { - options.columnType = `'${columnType}'`; - } - - if (prop.nullable) { - options.nullable = true; - } - - if (prop.default && typeof prop.default === 'string') { - if ([`''`, ''].includes(prop.default)) { - options.default = `''`; - } else if (prop.default.match(/^'.*'$/)) { - options.default = prop.default; - } else { - options.defaultRaw = `\`${prop.default}\``; - } - } - } - - private getScalarPropertyDecoratorOptions(options: Dictionary, prop: EntityProperty, columnType: string | undefined): void { - const defaultColumnType = this.helper.getTypeDefinition(prop).replace(/\(\d+\)/, ''); - - if (!columnType && prop.columnTypes[0] !== defaultColumnType && prop.type !== columnType) { - options.columnType = `'${prop.columnTypes[0]}'`; - } - - if (prop.fieldNames[0] !== this.namingStrategy.propertyToColumnName(prop.name)) { - options.fieldName = `'${prop.fieldNames[0]}'`; - } - - if (prop.length && prop.columnTypes[0] !== 'enum') { - options.length = prop.length; - } - } - - private getForeignKeyDecoratorOptions(options: Dictionary, prop: EntityProperty) { - options.entity = `() => ${this.namingStrategy.getClassName(prop.referencedTableName, '_')}`; - - if (prop.fieldNames[0] !== this.namingStrategy.joinKeyColumnName(prop.name, prop.referencedColumnNames[0])) { - options.fieldName = `'${prop.fieldNames[0]}'`; - } - - const cascade = ['Cascade.MERGE']; - - if (prop.onUpdateIntegrity === 'cascade') { - cascade.push('Cascade.PERSIST'); - } - - if (prop.onDelete === 'cascade') { - cascade.push('Cascade.REMOVE'); - } - - if (cascade.length === 3) { - cascade.length = 0; - cascade.push('Cascade.ALL'); - } - - if (!(cascade.length === 2 && cascade.includes('Cascade.PERSIST') && cascade.includes('Cascade.MERGE'))) { - options.cascade = `[${cascade.sort().join(', ')}]`; - } - - if (prop.primary) { - options.primary = true; - } - } - - private getDecoratorType(prop: EntityProperty): string { - if (prop.reference === ReferenceType.ONE_TO_ONE) { - return '@OneToOne'; - } - - if (prop.reference === ReferenceType.MANY_TO_ONE) { - return '@ManyToOne'; - } - - if (prop.primary) { - return '@PrimaryKey'; - } - - return '@Property'; + this.sources.push(new SourceFile(meta, this.namingStrategy, this.helper)); } } diff --git a/packages/entity-generator/src/SourceFile.ts b/packages/entity-generator/src/SourceFile.ts new file mode 100644 index 000000000000..6bc4564bb66a --- /dev/null +++ b/packages/entity-generator/src/SourceFile.ts @@ -0,0 +1,210 @@ +import { Dictionary, EntityMetadata, EntityProperty, NamingStrategy, ReferenceType, Utils } from '@mikro-orm/core'; +import { SchemaHelper } from '@mikro-orm/knex'; + +export class SourceFile { + + private readonly coreImports = new Set(); + private readonly entityImports = new Set(); + + constructor(private readonly meta: EntityMetadata, + private readonly namingStrategy: NamingStrategy, + private readonly helper: SchemaHelper) { } + + generate(): string { + this.coreImports.add('Entity'); + let ret = `@Entity()\n`; + + this.meta.indexes.forEach(index => { + this.coreImports.add('Index'); + const properties = Utils.asArray(index.properties).map(prop => `'${prop}'`); + ret += `@Index({ name: '${index.name}', properties: [${properties.join(', ')}] })\n`; + }); + + this.meta.uniques.forEach(index => { + this.coreImports.add('Unique'); + const properties = Utils.asArray(index.properties).map(prop => `'${prop}'`); + ret += `@Unique({ name: '${index.name}', properties: [${properties.join(', ')}] })\n`; + }); + + ret += `export class ${this.meta.className} {\n`; + Object.values(this.meta.properties).forEach(prop => { + const decorator = this.getPropertyDecorator(prop, 2); + const definition = this.getPropertyDefinition(prop, 2); + + if (!ret.endsWith('\n\n')) { + ret += '\n'; + } + + ret += decorator; + ret += definition; + ret += '\n'; + }); + ret += '}\n'; + + const imports = [`import { ${([...this.coreImports].sort().join(', '))} } from '@mikro-orm/core';`]; + const entityImports = [...this.entityImports].filter(e => e !== this.meta.className); + entityImports.sort().forEach(entity => { + imports.push(`import { ${entity} } from './${entity}';`); + }); + + return `${imports.join('\n')}\n\n${ret}`; + } + + getBaseName() { + return this.meta.className + '.ts'; + } + + private getPropertyDefinition(prop: EntityProperty, padLeft: number): string { + // string defaults are usually things like SQL functions + const useDefault = prop.default && typeof prop.default !== 'string'; + const optional = prop.nullable ? '?' : (useDefault ? '' : '!'); + const ret = `${prop.name}${optional}: ${prop.type}`; + const padding = ' '.repeat(padLeft); + + if (!useDefault) { + return `${padding + ret};\n`; + } + + return `${padding}${ret} = ${prop.default};\n`; + } + + private getPropertyDecorator(prop: EntityProperty, padLeft: number): string { + const padding = ' '.repeat(padLeft); + const options = {} as Dictionary; + const columnType = this.helper.getTypeFromDefinition(prop.columnTypes[0], '__false') === '__false' ? prop.columnTypes[0] : undefined; + let decorator = this.getDecoratorType(prop); + this.coreImports.add(decorator.substr(1)); + + if (prop.reference !== ReferenceType.SCALAR) { + this.getForeignKeyDecoratorOptions(options, prop); + } else { + this.getScalarPropertyDecoratorOptions(options, prop, columnType); + } + + this.getCommonDecoratorOptions(options, prop, columnType); + const indexes = this.getPropertyIndexes(prop, options); + decorator = [...indexes.sort(), decorator].map(d => padding + d).join('\n'); + + if (Object.keys(options).length === 0) { + return `${decorator}()\n`; + } + + return `${decorator}({ ${Object.entries(options).map(([opt, val]) => `${opt}: ${val}`).join(', ')} })\n`; + } + + private getPropertyIndexes(prop: EntityProperty, options: Dictionary): string[] { + if (prop.reference === ReferenceType.SCALAR) { + const ret: string[] = []; + + if (prop.index) { + this.coreImports.add('Index'); + ret.push(`@Index({ name: '${prop.index}' })`); + } + + if (prop.unique) { + this.coreImports.add('Unique'); + ret.push(`@Unique({ name: '${prop.unique}' })`); + } + + return ret; + } + + if (prop.index) { + options.index = `'${prop.index}'`; + } + + if (prop.unique) { + options.unique = `'${prop.unique}'`; + } + + return []; + } + + private getCommonDecoratorOptions(options: Dictionary, prop: EntityProperty, columnType: string | undefined) { + if (columnType) { + options.columnType = `'${columnType}'`; + } + + if (prop.nullable) { + options.nullable = true; + } + + if (prop.default && typeof prop.default === 'string') { + if ([`''`, ''].includes(prop.default)) { + options.default = `''`; + } else if (prop.default.match(/^'.*'$/)) { + options.default = prop.default; + } else { + options.defaultRaw = `\`${prop.default}\``; + } + } + } + + private getScalarPropertyDecoratorOptions(options: Dictionary, prop: EntityProperty, columnType: string | undefined): void { + const defaultColumnType = this.helper.getTypeDefinition(prop).replace(/\(\d+\)/, ''); + + if (!columnType && prop.columnTypes[0] !== defaultColumnType && prop.type !== columnType) { + options.columnType = `'${prop.columnTypes[0]}'`; + } + + if (prop.fieldNames[0] !== this.namingStrategy.propertyToColumnName(prop.name)) { + options.fieldName = `'${prop.fieldNames[0]}'`; + } + + if (prop.length && prop.columnTypes[0] !== 'enum') { + options.length = prop.length; + } + } + + private getForeignKeyDecoratorOptions(options: Dictionary, prop: EntityProperty) { + const className = this.namingStrategy.getClassName(prop.referencedTableName, '_'); + this.entityImports.add(className); + options.entity = `() => ${className}`; + + if (prop.fieldNames[0] !== this.namingStrategy.joinKeyColumnName(prop.name, prop.referencedColumnNames[0])) { + options.fieldName = `'${prop.fieldNames[0]}'`; + } + + const cascade = ['Cascade.MERGE']; + + if (prop.onUpdateIntegrity === 'cascade') { + cascade.push('Cascade.PERSIST'); + } + + if (prop.onDelete === 'cascade') { + cascade.push('Cascade.REMOVE'); + } + + if (cascade.length === 3) { + cascade.length = 0; + cascade.push('Cascade.ALL'); + } + + // do not set cascade when it matches the defaults (persist + merge) + if (!(cascade.length === 2 && cascade.includes('Cascade.PERSIST') && cascade.includes('Cascade.MERGE'))) { + this.coreImports.add('Cascade'); + options.cascade = `[${cascade.sort().join(', ')}]`; + } + + if (prop.primary) { + options.primary = true; + } + } + + private getDecoratorType(prop: EntityProperty): string { + if (prop.reference === ReferenceType.ONE_TO_ONE) { + return '@OneToOne'; + } + + if (prop.reference === ReferenceType.MANY_TO_ONE) { + return '@ManyToOne'; + } + + if (prop.primary) { + return '@PrimaryKey'; + } + + return '@Property'; + } + +}