Skip to content

Commit

Permalink
feat(entity-generator): do not use ts-morph
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
B4nan committed Sep 10, 2020
1 parent 9800dc1 commit 478a7bb
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 195 deletions.
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions packages/entity-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
199 changes: 8 additions & 191 deletions packages/entity-generator/src/EntityGenerator.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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<string[]> {
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<void> {
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));
}

}
Loading

0 comments on commit 478a7bb

Please sign in to comment.