Skip to content

Commit

Permalink
feat(entity-generator): add support for generating M:N properties
Browse files Browse the repository at this point in the history
  • Loading branch information
B4nan committed Feb 1, 2022
1 parent da7ee09 commit c0628c5
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 179 deletions.
1 change: 0 additions & 1 deletion docs/docs/entity-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ $ ts-node generate-entities

## Current limitations

- many to many relations are not supported, pivot table will be represented as separate entity
- in mysql, tinyint columns will be defined as boolean properties
44 changes: 35 additions & 9 deletions packages/entity-generator/src/EntityGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ensureDir, writeFile } from 'fs-extra';
import { Utils } from '@mikro-orm/core';
import type { DatabaseTable, EntityManager } from '@mikro-orm/knex';
import type { EntityProperty } from '@mikro-orm/core';
import { ReferenceType, Utils } from '@mikro-orm/core';
import type { EntityManager } from '@mikro-orm/knex';
import { DatabaseSchema } from '@mikro-orm/knex';
import { SourceFile } from './SourceFile';

Expand All @@ -19,9 +20,39 @@ export class EntityGenerator {
async generate(options: { baseDir?: string; save?: boolean; schema?: string } = {}): Promise<string[]> {
const baseDir = Utils.normalizePath(options.baseDir || this.config.get('baseDir') + '/generated-entities');
const schema = await DatabaseSchema.create(this.connection, this.platform, this.config);
schema.getTables()

const metadata = schema.getTables()
.filter(table => !options.schema || table.schema === options.schema)
.forEach(table => this.createEntity(table));
.map(table => table.getEntityDeclaration(this.namingStrategy, this.helper));

// detect M:N relations
for (const meta of metadata) {
if (
meta.compositePK && // needs to have composite PK
meta.primaryKeys.length === meta.relations.length && // all relations are PKs
meta.relations.length === 2 && // there are exactly two relation properties
meta.relations.length === meta.props.length && // all properties are relations
meta.relations.every(prop => prop.reference === ReferenceType.MANY_TO_ONE) // all relations are m:1
) {
meta.pivotTable = true;
const owner = metadata.find(m => m.className === meta.relations[0].type)!;
const name = this.namingStrategy.columnNameToProperty(meta.tableName.replace(new RegExp('^' + owner.tableName + '_'), ''));
owner.addProperty({
name,
reference: ReferenceType.MANY_TO_MANY,
pivotTable: meta.tableName,
type: meta.relations[1].type,
joinColumns: meta.relations[0].fieldNames,
inverseJoinColumns: meta.relations[1].fieldNames,
} as EntityProperty);
}
}

for (const meta of metadata) {
if (!meta.pivotTable) {
this.sources.push(new SourceFile(meta, this.namingStrategy, this.platform));
}
}

if (options.save) {
await ensureDir(baseDir);
Expand All @@ -31,9 +62,4 @@ export class EntityGenerator {
return this.sources.map(file => file.generate());
}

createEntity(table: DatabaseTable): void {
const meta = table.getEntityDeclaration(this.namingStrategy, this.helper);
this.sources.push(new SourceFile(meta, this.namingStrategy, this.platform));
}

}
40 changes: 36 additions & 4 deletions packages/entity-generator/src/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,18 @@ export class SourceFile {
}

private getPropertyDefinition(prop: EntityProperty, padLeft: number): string {
// string defaults are usually things like SQL functions
// string defaults can also be enums, for that useDefault should be true.
const padding = ' '.repeat(padLeft);

if (prop.reference === ReferenceType.MANY_TO_MANY) {
this.coreImports.add('Collection');
return `${padding}${prop.name} = new Collection<${prop.type}>(this);\n`;
}

// string defaults are usually things like SQL functions, but can be also enums, for that `useDefault` should be true
const isEnumOrNonStringDefault = prop.enum || typeof prop.default !== 'string';
const useDefault = prop.default != null && isEnumOrNonStringDefault;
const optional = prop.nullable ? '?' : (useDefault ? '' : '!');
const ret = `${prop.name}${optional}: ${prop.type}`;
const padding = ' '.repeat(padLeft);

if (!useDefault) {
return `${padding + ret};\n`;
Expand Down Expand Up @@ -122,7 +127,9 @@ export class SourceFile {
let decorator = this.getDecoratorType(prop);
this.coreImports.add(decorator.substring(1));

if (prop.reference !== ReferenceType.SCALAR) {
if (prop.reference === ReferenceType.MANY_TO_MANY) {
this.getManyToManyDecoratorOptions(options, prop);
} else if (prop.reference !== ReferenceType.SCALAR) {
this.getForeignKeyDecoratorOptions(options, prop);
} else {
this.getScalarPropertyDecoratorOptions(options, prop);
Expand Down Expand Up @@ -237,6 +244,27 @@ export class SourceFile {
}
}

private getManyToManyDecoratorOptions(options: Dictionary, prop: EntityProperty) {
this.entityImports.add(prop.type);
options.entity = `() => ${prop.type}`;

if (prop.pivotTable !== this.namingStrategy.joinTableName(this.meta.collection, prop.type, prop.name)) {
options.pivotTable = this.quote(prop.pivotTable);
}

if (prop.joinColumns.length === 1) {
options.joinColumn = this.quote(prop.joinColumns[0]);
} else {
options.joinColumns = `[${prop.joinColumns.map(this.quote).join(', ')}]`;
}

if (prop.inverseJoinColumns.length === 1) {
options.inverseJoinColumn = this.quote(prop.inverseJoinColumns[0]);
} else {
options.inverseJoinColumns = `[${prop.inverseJoinColumns.map(this.quote).join(', ')}]`;
}
}

private getForeignKeyDecoratorOptions(options: Dictionary, prop: EntityProperty) {
const parts = prop.referencedTableName.split('.', 2);
const className = this.namingStrategy.getClassName(parts.length > 1 ? parts[1] : parts[0], '_');
Expand Down Expand Up @@ -274,6 +302,10 @@ export class SourceFile {
return '@ManyToOne';
}

if (prop.reference === ReferenceType.MANY_TO_MANY) {
return '@ManyToMany';
}

if (prop.primary) {
return '@PrimaryKey';
}
Expand Down
10 changes: 6 additions & 4 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,11 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
async syncCollection<T extends AnyEntity<T>, O extends AnyEntity<O>>(coll: Collection<T, O>, options?: DriverMethodOptions): Promise<void> {
const wrapped = coll.owner.__helper!;
const meta = wrapped.__meta;
const pks = wrapped.getPrimaryKeys(true);
const pks = wrapped.getPrimaryKeys(true)!;
const snap = coll.getSnapshot();
const includes = <T>(arr: T[], item: T) => !!arr.find(i => Utils.equals(i, item));
const snapshot = snap ? snap.map(item => item.__helper!.getPrimaryKeys(true)) : [];
const current = coll.getItems(false).map(item => item.__helper!.getPrimaryKeys(true));
const snapshot = snap ? snap.map(item => item.__helper!.getPrimaryKeys(true)!) : [];
const current = coll.getItems(false).map(item => item.__helper!.getPrimaryKeys(true)!);
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
const insertDiff = current.filter(item => !includes(snapshot, item));
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
Expand All @@ -438,15 +438,17 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
return this.rethrow(this.execute<any>(qb));
}

/* istanbul ignore next */
const ownerSchema = wrapped.getSchema() === '*' ? this.config.get('schema') : wrapped.getSchema();
const pivotMeta = this.metadata.find(coll.property.pivotTable)!;

if (pivotMeta.schema === '*') {
/* istanbul ignore next */
options ??= {};
options.schema = ownerSchema;
}

return this.rethrow(this.updateCollectionDiff<T, O>(meta, coll.property, pks as any, deleteDiff as any, insertDiff as any, options));
return this.rethrow(this.updateCollectionDiff<T, O>(meta, coll.property, pks, deleteDiff, insertDiff, options));
}

async loadFromPivotTable<T, O>(prop: EntityProperty, owners: Primary<O>[][], where: FilterQuery<T> = {} as FilterQuery<T>, orderBy?: QueryOrderMap<T>[], ctx?: Transaction, options?: FindOptions<T>): Promise<Dictionary<T[]>> {
Expand Down
Loading

0 comments on commit c0628c5

Please sign in to comment.