diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 838f3409d7..e53b9196e7 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -90,7 +90,7 @@ export type PropertyType = { export type RelationshipOptions = { /** * The name of the relationship as defined in the other - * data model. This is only required by the `belongsTo` + * data model. This is only required by the `belongsTo` and `manyToMany` * relationship method. */ mappedBy?: string 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 85dc14f078..75cd192d5a 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -2971,6 +2971,7 @@ describe("Entity builder", () => { }) .indexes([ { + // @ts-expect-error on: ["email", "account", "doesnotexist", "anotherdoesnotexist"], }, ]) @@ -4714,7 +4715,9 @@ describe("Entity builder", () => { const user = model.define("user", { id: model.number(), username: model.text(), - teams: model.manyToMany(() => team), + teams: model.manyToMany(() => team, { + mappedBy: "users", + }), }) const User = toMikroORMEntity(user) @@ -4775,6 +4778,7 @@ describe("Entity builder", () => { name: "teams", entity: "Team", pivotTable: "team_users", + mappedBy: "users", }, created_at: { reference: "scalar", @@ -5686,7 +5690,9 @@ describe("Entity builder", () => { const user = model.define("platform.user", { id: model.number(), username: model.text(), - teams: model.manyToMany(() => team), + teams: model.manyToMany(() => team, { + mappedBy: "users", + }), }) const User = toMikroORMEntity(user) @@ -5748,6 +5754,7 @@ describe("Entity builder", () => { name: "teams", entity: "Team", pivotTable: "platform.team_users", + mappedBy: "users", }, created_at: { reference: "scalar", @@ -5867,7 +5874,10 @@ describe("Entity builder", () => { const user = model.define("user", { id: model.number(), username: model.text(), - teams: model.manyToMany(() => team, { pivotTable: "users_teams" }), + teams: model.manyToMany(() => team, { + pivotTable: "users_teams", + mappedBy: "users", + }), }) const User = toMikroORMEntity(user) @@ -5928,6 +5938,7 @@ describe("Entity builder", () => { name: "teams", entity: "Team", pivotTable: "users_teams", + mappedBy: "users", }, created_at: { reference: "scalar", @@ -6052,7 +6063,10 @@ describe("Entity builder", () => { const user = model.define("user", { id: model.number(), username: model.text(), - teams: model.manyToMany(() => team, { pivotEntity: () => squad }), + teams: model.manyToMany(() => team, { + pivotEntity: () => squad, + mappedBy: "users", + }), }) const [User, Team, Squad] = toMikroOrmEntities([user, team, squad]) @@ -6203,6 +6217,7 @@ describe("Entity builder", () => { name: "teams", entity: "Team", pivotEntity: "TeamUsers", + mappedBy: "users", }, created_at: { reference: "scalar", diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts index 36d46708f6..fdf8c392c3 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts @@ -9,9 +9,9 @@ import { BeforeCreate, ManyToMany, ManyToOne, - OnInit, OneToMany, OneToOne, + OnInit, Property, rel, } from "@mikro-orm/core" @@ -27,6 +27,66 @@ type Context = { MANY_TO_MANY_TRACKED_RELATIONS: Record } +/** + * Validates a many to many relationship without mappedBy and checks if the other side of the relationship is defined and possesses mappedBy. + * @param MikroORMEntity + * @param relationship + * @param relatedEntity + * @param relatedModelName + */ +function validateManyToManyRelationshipWithoutMappedBy({ + MikroORMEntity, + relationship, + relatedEntity, + relatedModelName, +}: { + MikroORMEntity: EntityConstructor + relationship: RelationshipMetadata + relatedEntity: DmlEntity< + Record | RelationshipType>, + any + > + relatedModelName: string +}) { + /** + * Since we don't have the information about the other side of the + * relationship, we will try to find all the other side many to many that refers to the current entity. + * If there is any, we will try to find if at least one of them has a mappedBy. + */ + const potentialOtherSides = Object.entries(relatedEntity.schema) + .filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig)) + .filter(([prop, propConfig]) => { + const parsedProp = propConfig.parse(prop) as RelationshipMetadata + const relatedEntity = + typeof parsedProp.entity === "function" + ? parsedProp.entity() + : undefined + + if (!relatedEntity) { + throw new Error( + `Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function` + ) + } + + return parseEntityName(relatedEntity).modelName === MikroORMEntity.name + }) as unknown as [string, RelationshipType][] + + if (potentialOtherSides.length) { + const hasMappedBy = potentialOtherSides.some( + ([, propConfig]) => !!propConfig.parse("").mappedBy + ) + if (!hasMappedBy) { + throw new Error( + `Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.` + ) + } + } else { + throw new Error( + `Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". The other side of the relationship is missing.` + ) + } +} + /** * Defines has one relationship on the Mikro ORM entity. */ @@ -280,6 +340,13 @@ export function defineManyToManyRelationship( `${MikroORMEntity.name}.${relationship.name}` ] = true } + } else { + validateManyToManyRelationshipWithoutMappedBy({ + MikroORMEntity, + relationship, + relatedEntity, + relatedModelName, + }) } /** diff --git a/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts b/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts index 54c5afa017..4d65a4eef4 100644 --- a/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts +++ b/packages/core/utils/src/dml/integration-tests/__tests__/many-to-many.spec.ts @@ -186,4 +186,65 @@ describe("manyToMany - manyToMany", () => { ]), }) }) + + it(`should fail to load the dml's if both side of the relation are missing the mappedBy options`, () => { + const team = model.define("team", { + id: model.id().primaryKey(), + name: model.text(), + users: model.manyToMany(() => user, { + pivotEntity: () => squad, + }), + }) + + const squad = model.define("teamUsers", { + id: model.id().primaryKey(), + user: model.belongsTo(() => user, { mappedBy: "squads" }), + squad: model.belongsTo(() => team, { mappedBy: "users" }), + }) + + const user = model.define("user", { + id: model.id().primaryKey(), + username: model.text(), + squads: model.manyToMany(() => team, { + pivotEntity: () => squad, + }), + }) + + let error!: Error + try { + ;[User, Squad, Team] = toMikroOrmEntities([user, squad, team]) + } catch (e) { + error = e + } + + expect(error).toBeTruthy() + expect(error.message).toEqual( + 'Invalid relationship reference for "User.squads". "mappedBy" should be defined on one side or the other.' + ) + }) + + it(`should fail to load the dml's if the relation is defined only on one side`, () => { + const team = model.define("team", { + id: model.id().primaryKey(), + name: model.text(), + users: model.manyToMany(() => user), + }) + + const user = model.define("user", { + id: model.id().primaryKey(), + username: model.text(), + }) + + let error!: Error + try { + ;[User, Team] = toMikroOrmEntities([user, team]) + } catch (e) { + error = e + } + + expect(error).toBeTruthy() + expect(error.message).toEqual( + 'Invalid relationship reference for "Team.users". The other side of the relationship is missing.' + ) + }) })