Skip to content

Commit

Permalink
feat(entity-generator): detect more ManyToMany relations
Browse files Browse the repository at this point in the history
Auto increment columns in pivot entities are now detected.

In addition, pivot tables are allowed to contain additional props,
including relations, but only if those props would not hinder the ORM's ability
to freely insert and remove records from the collection.

In other words, those additional props need to be optional,
and have defaults that are either null or non-unique.
  • Loading branch information
boenrobot committed Nov 28, 2023
1 parent 5c4b3a2 commit a8741d1
Show file tree
Hide file tree
Showing 6 changed files with 630 additions and 78 deletions.
111 changes: 84 additions & 27 deletions packages/entity-generator/src/EntityGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ensureDir, writeFile } from 'fs-extra';
import {
type EntityMetadata,
type EntityProperty,
Expand All @@ -17,8 +16,9 @@ import {
type EntityManager,
type SchemaHelper,
} from '@mikro-orm/knex';
import { SourceFile } from './SourceFile';
import { ensureDir, writeFile } from 'fs-extra';
import { EntitySchemaSourceFile } from './EntitySchemaSourceFile';
import { SourceFile } from './SourceFile';

export class EntityGenerator {

Expand Down Expand Up @@ -122,39 +122,96 @@ export class EntityGenerator {
this.referencedEntities.add(meta);
}


// Entities with non-composite PKs are never pivot tables. Skip.
if (!meta.compositePK) {
continue;
}

// Entities where there are not exactly 2 PK relations that are both ManyToOne are never pivot tables. Skip.
const pkRelations = meta.relations.filter(rel => rel.primary);
if (
meta.compositePK && // needs to have composite PK
meta.relations.length === 2 && // there are exactly two relation properties
!meta.relations.some(rel => !rel.primary || rel.kind !== ReferenceKind.MANY_TO_ONE) && // all relations are m:1 and PKs
(
// all properties are relations...
meta.relations.length === meta.props.length
// ... or at least all fields involved are only the fields of the relations
|| (new Set<string>(meta.props.flatMap(prop => prop.fieldNames)).size === (new Set<string>(meta.relations.flatMap(rel => rel.fieldNames)).size))
)
pkRelations.length !== 2 ||
pkRelations.some(rel => rel.kind !== ReferenceKind.MANY_TO_ONE)
) {
meta.pivotTable = true;
const owner = metadata.find(m => m.className === meta.relations[0].type);
continue;
}

if (!owner) {
continue;
const pkRelationFields = new Set<string>(pkRelations.flatMap(rel => rel.fieldNames));
const nonPkFields = Array.from(new Set<string>(meta.props.flatMap(prop => prop.fieldNames))).filter(fieldName => !pkRelationFields.has(fieldName));

let fixedOrderColumn: string | undefined;

// If there are any fields other than the ones in the two PK relations, table may or may not be a pivot one.
// Check further and skip on disqualification.
if (nonPkFields.length > 0) {
const pkRelationNames = pkRelations.map(rel => rel.name);
let otherProps = meta.props
.filter(prop => !pkRelationNames.includes(prop.name) &&
prop.persist !== false && // Skip checking non-persist props
prop.fieldNames.some(fieldName => nonPkFields.includes(fieldName)),
);

// Deal with the auto increment column first. That is the column used for fixed ordering, if present.
const autoIncrementProp = otherProps.find(prop => prop.autoincrement && prop.fieldNames.length === 1);
if (autoIncrementProp) {
otherProps = otherProps.filter(prop => prop !== autoIncrementProp);
fixedOrderColumn = autoIncrementProp.fieldNames[0];
}

const name = this.namingStrategy.columnNameToProperty(meta.tableName.replace(new RegExp('^' + owner.tableName + '_'), ''));
const ownerProp = {
name,
kind: ReferenceKind.MANY_TO_MANY,
pivotTable: meta.tableName,
type: meta.relations[1].type,
joinColumns: meta.relations[0].fieldNames,
inverseJoinColumns: meta.relations[1].fieldNames,
} as EntityProperty;
if (otherProps.some(prop => {
// If the prop is non-nullable and unique, it will trivially end up causing issues.
// Disqualify entity as a pivot table.
if (!prop.nullable && prop.unique) {
return true;
}

if (this.referencedEntities.has(meta)) {
ownerProp.pivotEntity = meta.className;
// Scalar props need to also have a default or be generated ones.
if (prop.kind === ReferenceKind.SCALAR) {
return !(typeof prop.defaultRaw !== 'undefined' || prop.generated);
}

// Non-scalar props need to also be optional.
// Even if they have a default, we've already checked that not explicitly setting the property
// means the default is either NULL, or a non-unique non-null value, making it safe to use in a pivot entity.
return !prop.optional;
})) {
continue;
}

// If this now proven pivot entity has persistent props other than the fixed order column,
// output it, by considering it as a referenced one.
if (otherProps.length > 0) {
this.referencedEntities.add(meta);
}
owner.addProperty(ownerProp);
}

meta.pivotTable = true;
const owner = metadata.find(m => m.className === meta.relations[0].type);

if (!owner) {
continue;
}

const name = this.namingStrategy.columnNameToProperty(meta.tableName.replace(new RegExp('^' + owner.tableName + '_'), ''));
const ownerProp = {
name,
kind: ReferenceKind.MANY_TO_MANY,
pivotTable: meta.tableName,
type: meta.relations[1].type,
joinColumns: meta.relations[0].fieldNames,
inverseJoinColumns: meta.relations[1].fieldNames,
} as EntityProperty;

if (this.referencedEntities.has(meta)) {
ownerProp.pivotEntity = meta.className;
}
if (fixedOrderColumn) {
ownerProp.fixedOrder = true;
ownerProp.fixedOrderColumn = fixedOrderColumn;
}

owner.addProperty(ownerProp);
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/entity-generator/src/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,13 @@ export class SourceFile {
} else {
options.inverseJoinColumns = `[${prop.inverseJoinColumns.map(this.quote).join(', ')}]`;
}

if (prop.fixedOrder) {
options.fixedOrder = true;
if (prop.fixedOrderColumn !== this.namingStrategy.referenceColumnName()) {
options.fixedOrderColumn = prop.fixedOrderColumn;
}
}
}

protected getOneToManyDecoratorOptions(options: Dictionary, prop: EntityProperty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ export class FooBar2 {
blob2?: Buffer;
array?: string;
objectProperty?: any;
fooParam2 = new Collection<FooBaz2>(this);
fooBarInverse?: Ref<Test2>;
barInverse = new Collection<FooParam2>(this);
barsInverse = new Collection<Test2>(this);
Expand Down Expand Up @@ -457,13 +458,22 @@ export const FooBar2Schema = new EntitySchema({
blob2: { type: 'Buffer', length: 65535, nullable: true },
array: { type: 'string', columnType: 'text', nullable: true },
objectProperty: { type: 'any', columnType: 'json', nullable: true },
fooParam2: {
kind: 'm:n',
entity: () => FooBaz2,
pivotTable: 'foo_param2',
pivotEntity: () => FooParam2,
joinColumn: 'bar_id',
inverseJoinColumn: 'baz_id',
},
fooBarInverse: { kind: '1:1', entity: () => Test2, ref: true, mappedBy: 'fooBar' },
barInverse: { kind: '1:m', entity: () => FooParam2, mappedBy: 'bar' },
barsInverse: { kind: 'm:n', entity: () => Test2, mappedBy: 'bars' },
},
});
",
"import { Collection, EntitySchema, OptionalProps } from '@mikro-orm/core';
import { FooBar2 } from './FooBar2';
import { FooParam2 } from './FooParam2';

export class FooBaz2 {
Expand All @@ -472,6 +482,7 @@ export class FooBaz2 {
name!: string;
version!: Date;
bazInverse = new Collection<FooParam2>(this);
fooParam2Inverse = new Collection<FooBar2>(this);
}

export const FooBaz2Schema = new EntitySchema({
Expand All @@ -481,6 +492,7 @@ export const FooBaz2Schema = new EntitySchema({
name: { type: 'string', length: 255 },
version: { type: 'Date', length: 3, defaultRaw: \`current_timestamp(3)\` },
bazInverse: { kind: '1:m', entity: () => FooParam2, mappedBy: 'baz' },
fooParam2Inverse: { kind: 'm:n', entity: () => FooBar2, mappedBy: 'fooParam2' },
},
});
",
Expand Down Expand Up @@ -1032,8 +1044,9 @@ export class Dummy2 {

}
",
"import { Entity, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { FooBaz2 } from './FooBaz2';
import { FooParam2 } from './FooParam2';

@Entity()
export class FooBar2 {
Expand Down Expand Up @@ -1070,6 +1083,9 @@ export class FooBar2 {
@Property({ columnType: 'json', nullable: true })
objectProperty?: any;

@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);

}
",
"import { Entity, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
Expand Down Expand Up @@ -1632,6 +1648,9 @@ export class FooBar2 {
@Property({ columnType: 'json', nullable: true })
objectProperty?: any;

@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);

@OneToOne({ entity: () => Test2, mappedBy: 'fooBar' })
fooBarInverse?: Test2;

Expand All @@ -1643,7 +1662,8 @@ export class FooBar2 {

}
",
"import { Collection, Entity, OneToMany, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToMany, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { FooBar2 } from './FooBar2';
import { FooParam2 } from './FooParam2';

@Entity()
Expand All @@ -1663,6 +1683,9 @@ export class FooBaz2 {
@OneToMany({ entity: () => FooParam2, mappedBy: 'baz' })
bazInverse = new Collection<FooParam2>(this);

@ManyToMany({ entity: () => FooBar2, mappedBy: 'fooParam2' })
fooParam2Inverse = new Collection<FooBar2>(this);

}
",
"import { Entity, ManyToOne, OptionalProps, PrimaryKeyProp, Property } from '@mikro-orm/core';
Expand Down Expand Up @@ -2230,6 +2253,9 @@ export class FooBar2 {
@Property({ columnType: 'json', nullable: true })
objectProperty?: any;

@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);

@OneToOne({ entity: () => Test2, ref: true, mappedBy: 'fooBar' })
fooBarInverse?: Ref<Test2>;

Expand All @@ -2241,7 +2267,8 @@ export class FooBar2 {

}
",
"import { Collection, Entity, OneToMany, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToMany, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { FooBar2 } from './FooBar2';
import { FooParam2 } from './FooParam2';

@Entity()
Expand All @@ -2261,6 +2288,9 @@ export class FooBaz2 {
@OneToMany({ entity: () => FooParam2, mappedBy: 'baz' })
bazInverse = new Collection<FooParam2>(this);

@ManyToMany({ entity: () => FooBar2, mappedBy: 'fooParam2' })
fooParam2Inverse = new Collection<FooBar2>(this);

}
",
"import { Entity, ManyToOne, OptionalProps, PrimaryKeyProp, Property, Ref } from '@mikro-orm/core';
Expand Down Expand Up @@ -2740,8 +2770,9 @@ export class Dummy2 {

}
",
"import { Entity, OneToOne, OptionalProps, PrimaryKey, Property, Ref } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToOne, OptionalProps, PrimaryKey, Property, Ref } from '@mikro-orm/core';
import { FooBaz2 } from './FooBaz2.js';
import { FooParam2 } from './FooParam2.js';

@Entity()
export class FooBar2 {
Expand Down Expand Up @@ -2778,6 +2809,9 @@ export class FooBar2 {
@Property({ columnType: 'json', nullable: true })
objectProperty?: any;

@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);

}
",
"import { Entity, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
Expand Down Expand Up @@ -3303,8 +3337,9 @@ export class Dummy2 {

}
",
"import { Entity, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { FooBaz2 } from './FooBaz2';
import { FooParam2 } from './FooParam2';

@Entity()
export class FooBar2 {
Expand Down Expand Up @@ -3341,6 +3376,9 @@ export class FooBar2 {
@Property({ columnType: 'json', nullable: true })
objectProperty?: any;

@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);

}
",
"import { Entity, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,9 @@ export class Configuration2 {
}
",
"import { Entity, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { FooBaz2 } from './FooBaz2';
import { FooParam2 } from './FooParam2';
@Entity()
export class FooBar2 {
Expand Down Expand Up @@ -257,6 +258,9 @@ export class FooBar2 {
@Property({ columnType: 'jsonb', nullable: true })
objectProperty?: any;
@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);
}
",
"import { Entity, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
Expand Down Expand Up @@ -620,8 +624,9 @@ export class Configuration2 {
}
",
"import { Entity, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
"import { Collection, Entity, ManyToMany, OneToOne, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
import { FooBaz2 } from './FooBaz2';
import { FooParam2 } from './FooParam2';
@Entity()
export class FooBar2 {
Expand Down Expand Up @@ -658,6 +663,9 @@ export class FooBar2 {
@Property({ columnType: 'jsonb', nullable: true })
objectProperty?: any;
@ManyToMany({ entity: () => FooBaz2, pivotTable: 'foo_param2', pivotEntity: () => FooParam2, joinColumn: 'bar_id', inverseJoinColumn: 'baz_id' })
fooParam2 = new Collection<FooBaz2>(this);
}
",
"import { Entity, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
Expand Down

0 comments on commit a8741d1

Please sign in to comment.