diff --git a/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts b/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts index d4df8398c5..015c9126c9 100644 --- a/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts @@ -22,6 +22,7 @@ describe("Base relationship", () => { name: "user", type: "hasOne", nullable: false, + options: { mappedBy: "user_id" }, mappedBy: "user_id", entity: entityRef, }) diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index 206d4cfc47..094a91218a 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -3023,7 +3023,7 @@ describe("Entity builder", () => { const user = model.define("user", { id: model.number(), username: model.text(), - email: model.manyToMany(() => email), + email: model.belongsTo(() => email), }) const entityBuilder = createMikrORMEntity() @@ -4526,5 +4526,435 @@ describe("Entity builder", () => { }, }) }) + + test("define custom pivot table name", () => { + const model = new EntityBuilder() + const team = model.define("team", { + id: model.number(), + name: model.text(), + users: model.manyToMany(() => user, { + pivotTable: "users_teams", + }), + }) + + const user = model.define("user", { + id: model.number(), + username: model.text(), + teams: model.manyToMany(() => team, { pivotTable: "users_teams" }), + }) + + const entityBuilder = createMikrORMEntity() + const User = entityBuilder(user) + const Team = entityBuilder(team) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + teams: EntityConstructor<{ + id: number + name: string + users: EntityConstructor<{ + id: number + username: string + }> + }> + }>() + + expectTypeOf(new Team()).toMatchTypeOf<{ + id: number + name: string + users: EntityConstructor<{ + id: number + username: string + teams: EntityConstructor<{ + id: number + name: string + }> + }> + }>() + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + columnType: "text", + name: "username", + nullable: false, + getter: false, + setter: false, + }, + teams: { + reference: "m:n", + name: "teams", + entity: "Team", + pivotTable: "users_teams", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect(teamMetaData.className).toEqual("Team") + expect(teamMetaData.path).toEqual("Team") + expect(teamMetaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + nullable: false, + getter: false, + setter: false, + }, + name: { + reference: "scalar", + type: "string", + columnType: "text", + name: "name", + nullable: false, + getter: false, + setter: false, + }, + users: { + reference: "m:n", + name: "users", + entity: "User", + pivotTable: "users_teams", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + }) + + test("define custom pivot entity", () => { + const model = new EntityBuilder() + const team = model.define("team", { + id: model.number(), + name: model.text(), + users: model.manyToMany(() => user, { + pivotEntity: () => squad, + }), + }) + + const squad = model.define("teamUsers", { + id: model.number(), + user: model.belongsTo(() => user, { mappedBy: "teams" }), + team: model.belongsTo(() => team, { mappedBy: "users" }), + }) + + const user = model.define("user", { + id: model.number(), + username: model.text(), + teams: model.manyToMany(() => team, { pivotEntity: () => squad }), + }) + + const entityBuilder = createMikrORMEntity() + const User = entityBuilder(user) + const Team = entityBuilder(team) + const Squad = entityBuilder(squad) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + teams: EntityConstructor<{ + id: number + name: string + users: EntityConstructor<{ + id: number + username: string + }> + }> + }>() + + expectTypeOf(new Team()).toMatchTypeOf<{ + id: number + name: string + users: EntityConstructor<{ + id: number + username: string + teams: EntityConstructor<{ + id: number + name: string + }> + }> + }>() + + const squadMetaData = MetadataStorage.getMetadataFromDecorator(Squad) + expect(squadMetaData.className).toEqual("TeamUsers") + expect(squadMetaData.path).toEqual("TeamUsers") + expect(squadMetaData.tableName).toEqual("team_users") + + expect(squadMetaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + nullable: false, + getter: false, + setter: false, + }, + team: { + entity: "Team", + name: "team", + persist: false, + reference: "m:1", + }, + team_id: { + columnType: "text", + entity: "Team", + fieldName: "team_id", + mapToPk: true, + name: "team_id", + nullable: false, + onDelete: undefined, + reference: "m:1", + }, + user: { + entity: "User", + name: "user", + persist: false, + reference: "m:1", + }, + user_id: { + columnType: "text", + entity: "User", + fieldName: "user_id", + mapToPk: true, + name: "user_id", + nullable: false, + onDelete: undefined, + reference: "m:1", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + columnType: "text", + name: "username", + nullable: false, + getter: false, + setter: false, + }, + teams: { + reference: "m:n", + name: "teams", + entity: "Team", + pivotEntity: "TeamUsers", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect(teamMetaData.className).toEqual("Team") + expect(teamMetaData.path).toEqual("Team") + expect(teamMetaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + nullable: false, + getter: false, + setter: false, + }, + name: { + reference: "scalar", + type: "string", + columnType: "text", + name: "name", + nullable: false, + getter: false, + setter: false, + }, + users: { + reference: "m:n", + name: "users", + entity: "User", + pivotEntity: "TeamUsers", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + updated_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "updated_at", + defaultRaw: "now()", + onCreate: expect.any(Function), + onUpdate: expect.any(Function), + nullable: false, + getter: false, + setter: false, + }, + deleted_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + }) }) }) diff --git a/packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts b/packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts index 431980a80f..f1c0a7eb3c 100644 --- a/packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts @@ -16,6 +16,7 @@ describe("HasMany relationship", () => { name: "user", type: "hasMany", nullable: false, + options: {}, entity: entityRef, }) }) diff --git a/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts b/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts index 60eb7347b9..57fedb3908 100644 --- a/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts @@ -16,6 +16,7 @@ describe("HasOne relationship", () => { name: "user", type: "hasOne", nullable: false, + options: {}, entity: entityRef, }) }) @@ -35,6 +36,7 @@ describe("HasOne relationship", () => { name: "user", type: "hasOne", nullable: true, + options: {}, entity: entityRef, }) }) diff --git a/packages/core/utils/src/dml/__tests__/many-to-many.spec.ts b/packages/core/utils/src/dml/__tests__/many-to-many.spec.ts index 310155e9b5..90fd733d5d 100644 --- a/packages/core/utils/src/dml/__tests__/many-to-many.spec.ts +++ b/packages/core/utils/src/dml/__tests__/many-to-many.spec.ts @@ -16,6 +16,7 @@ describe("ManyToMany relationship", () => { name: "user", type: "manyToMany", nullable: false, + options: {}, entity: entityRef, }) }) diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index 72b67ef029..0495f422c2 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -68,7 +68,7 @@ export class EntityBuilder { * Define an id property. Id properties are marked * primary by default */ - id(options: ConstructorParameters[0]) { + id(options?: ConstructorParameters[0]) { return new IdProperty(options) } @@ -162,7 +162,20 @@ export class EntityBuilder { * relationship requires a pivot table to establish a many to many * relationship between two entities */ - manyToMany(entityBuilder: T, options?: RelationshipOptions) { + manyToMany( + entityBuilder: T, + options?: RelationshipOptions & + ( + | { + pivotTable?: string + pivotEntity?: never + } + | { + pivotTable?: never + pivotEntity?: () => DmlEntity + } + ) + ) { return new ManyToMany(entityBuilder, options || {}) } } diff --git a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index 5a54ac26ca..0a0f56a330 100644 --- a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts +++ b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts @@ -327,7 +327,10 @@ export function createMikrORMEntity() { /** * Otherside is a has many. Hence we should defined a ManyToOne */ - if (otherSideRelation instanceof HasMany) { + if ( + otherSideRelation instanceof HasMany || + otherSideRelation instanceof DmlManyToMany + ) { ManyToOne({ entity: relatedModelName, columnType: "text", @@ -382,28 +385,12 @@ export function createMikrORMEntity() { ) { let mappedBy = relationship.mappedBy let inversedBy: undefined | string + let pivotEntityName: undefined | string + let pivotTableName: undefined | string /** - * A consistent pivot table name is created by: - * - * - Combining both the entity's names. - * - Sorting them by alphabetical order - * - Converting them from camelCase to snake_case. - * - And finally pluralizing the second entity name. + * Validating other side of relationship when mapped by is defined */ - const pivotTableName = [ - MikroORMEntity.name.toLowerCase(), - relatedModelName.toLowerCase(), - ] - .sort() - .map((token, index) => { - if (index === 1) { - return pluralize(camelToSnakeCase(token)) - } - return camelToSnakeCase(token) - }) - .join("_") - if (mappedBy) { const otherSideRelation = relatedEntity.parse().schema[mappedBy] if (!otherSideRelation) { @@ -438,9 +425,59 @@ export function createMikrORMEntity() { } } + /** + * Validating pivot entity when it is defined and computing + * its name + */ + if (relationship.options.pivotEntity) { + if (typeof relationship.options.pivotEntity !== "function") { + throw new Error( + `Invalid pivotEntity reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the pivotEntity using a factory function` + ) + } + + const pivotEntity = relationship.options.pivotEntity() + if (!(pivotEntity instanceof DmlEntity)) { + throw new Error( + `Invalid pivotEntity reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the pivotEntity callback` + ) + } + + pivotEntityName = parseEntityName(pivotEntity.parse().name).modelName + } + + if (!pivotEntityName) { + /** + * Pivot table name is created as follows (when not explicitly provided) + * + * - Combining both the entity's names. + * - Sorting them by alphabetical order + * - Converting them from camelCase to snake_case. + * - And finally pluralizing the second entity name. + */ + pivotTableName = + relationship.options.pivotTable ?? + [MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()] + .sort() + .map((token, index) => { + if (index === 1) { + return pluralize(camelToSnakeCase(token)) + } + return camelToSnakeCase(token) + }) + .join("_") + } + ManyToMany({ entity: relatedModelName, - pivotTable: pgSchema ? `${pgSchema}.${pivotTableName}` : pivotTableName, + ...(pivotTableName + ? { + pivotTable: pgSchema + ? `${pgSchema}.${pivotTableName}` + : pivotTableName, + } + : {}), + ...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}), ...(mappedBy ? { mappedBy: mappedBy as any } : {}), ...(inversedBy ? { inversedBy: inversedBy as any } : {}), })(MikroORMEntity.prototype, relationship.name) diff --git a/packages/core/utils/src/dml/relations/base.ts b/packages/core/utils/src/dml/relations/base.ts index 62ec0ffc6b..ca905c41b1 100644 --- a/packages/core/utils/src/dml/relations/base.ts +++ b/packages/core/utils/src/dml/relations/base.ts @@ -41,6 +41,7 @@ export abstract class BaseRelationship implements RelationshipType { name: relationshipName, nullable: false, mappedBy: this.options.mappedBy, + options: this.options, entity: this.#referencedEntity, type: this.type, } diff --git a/packages/core/utils/src/dml/types.ts b/packages/core/utils/src/dml/types.ts index e9a74e7e08..44726b7e0a 100644 --- a/packages/core/utils/src/dml/types.ts +++ b/packages/core/utils/src/dml/types.ts @@ -54,7 +54,7 @@ export type PropertyType = { */ export type RelationshipOptions = { mappedBy?: string -} +} & Record /** * The meta-data returned by the relationship parse @@ -66,6 +66,7 @@ export type RelationshipMetadata = { entity: unknown nullable: boolean mappedBy?: string + options: Record } /**