fix(dml): Throw when many to many are not configured properly (#9085)
**What** When both sides of a many to many relationship do not define the mapped by option, it leads to misconfigured relations and a malformed SQL query. (ref. https://github.com/medusajs/medusa/issues/9075) - When both mapped by are not defined, infer look up to try to identify the missing configuration if any - Disallow defining many to many only on one side
This commit is contained in:
@@ -90,7 +90,7 @@ export type PropertyType<T> = {
|
|||||||
export type RelationshipOptions = {
|
export type RelationshipOptions = {
|
||||||
/**
|
/**
|
||||||
* The name of the relationship as defined in the other
|
* 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.
|
* relationship method.
|
||||||
*/
|
*/
|
||||||
mappedBy?: string
|
mappedBy?: string
|
||||||
|
|||||||
@@ -2971,6 +2971,7 @@ describe("Entity builder", () => {
|
|||||||
})
|
})
|
||||||
.indexes([
|
.indexes([
|
||||||
{
|
{
|
||||||
|
// @ts-expect-error
|
||||||
on: ["email", "account", "doesnotexist", "anotherdoesnotexist"],
|
on: ["email", "account", "doesnotexist", "anotherdoesnotexist"],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -4714,7 +4715,9 @@ describe("Entity builder", () => {
|
|||||||
const user = model.define("user", {
|
const user = model.define("user", {
|
||||||
id: model.number(),
|
id: model.number(),
|
||||||
username: model.text(),
|
username: model.text(),
|
||||||
teams: model.manyToMany(() => team),
|
teams: model.manyToMany(() => team, {
|
||||||
|
mappedBy: "users",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = toMikroORMEntity(user)
|
const User = toMikroORMEntity(user)
|
||||||
@@ -4775,6 +4778,7 @@ describe("Entity builder", () => {
|
|||||||
name: "teams",
|
name: "teams",
|
||||||
entity: "Team",
|
entity: "Team",
|
||||||
pivotTable: "team_users",
|
pivotTable: "team_users",
|
||||||
|
mappedBy: "users",
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
reference: "scalar",
|
reference: "scalar",
|
||||||
@@ -5686,7 +5690,9 @@ describe("Entity builder", () => {
|
|||||||
const user = model.define("platform.user", {
|
const user = model.define("platform.user", {
|
||||||
id: model.number(),
|
id: model.number(),
|
||||||
username: model.text(),
|
username: model.text(),
|
||||||
teams: model.manyToMany(() => team),
|
teams: model.manyToMany(() => team, {
|
||||||
|
mappedBy: "users",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = toMikroORMEntity(user)
|
const User = toMikroORMEntity(user)
|
||||||
@@ -5748,6 +5754,7 @@ describe("Entity builder", () => {
|
|||||||
name: "teams",
|
name: "teams",
|
||||||
entity: "Team",
|
entity: "Team",
|
||||||
pivotTable: "platform.team_users",
|
pivotTable: "platform.team_users",
|
||||||
|
mappedBy: "users",
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
reference: "scalar",
|
reference: "scalar",
|
||||||
@@ -5867,7 +5874,10 @@ describe("Entity builder", () => {
|
|||||||
const user = model.define("user", {
|
const user = model.define("user", {
|
||||||
id: model.number(),
|
id: model.number(),
|
||||||
username: model.text(),
|
username: model.text(),
|
||||||
teams: model.manyToMany(() => team, { pivotTable: "users_teams" }),
|
teams: model.manyToMany(() => team, {
|
||||||
|
pivotTable: "users_teams",
|
||||||
|
mappedBy: "users",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = toMikroORMEntity(user)
|
const User = toMikroORMEntity(user)
|
||||||
@@ -5928,6 +5938,7 @@ describe("Entity builder", () => {
|
|||||||
name: "teams",
|
name: "teams",
|
||||||
entity: "Team",
|
entity: "Team",
|
||||||
pivotTable: "users_teams",
|
pivotTable: "users_teams",
|
||||||
|
mappedBy: "users",
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
reference: "scalar",
|
reference: "scalar",
|
||||||
@@ -6052,7 +6063,10 @@ describe("Entity builder", () => {
|
|||||||
const user = model.define("user", {
|
const user = model.define("user", {
|
||||||
id: model.number(),
|
id: model.number(),
|
||||||
username: model.text(),
|
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])
|
const [User, Team, Squad] = toMikroOrmEntities([user, team, squad])
|
||||||
@@ -6203,6 +6217,7 @@ describe("Entity builder", () => {
|
|||||||
name: "teams",
|
name: "teams",
|
||||||
entity: "Team",
|
entity: "Team",
|
||||||
pivotEntity: "TeamUsers",
|
pivotEntity: "TeamUsers",
|
||||||
|
mappedBy: "users",
|
||||||
},
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
reference: "scalar",
|
reference: "scalar",
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
BeforeCreate,
|
BeforeCreate,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OnInit,
|
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
|
OnInit,
|
||||||
Property,
|
Property,
|
||||||
rel,
|
rel,
|
||||||
} from "@mikro-orm/core"
|
} from "@mikro-orm/core"
|
||||||
@@ -27,6 +27,66 @@ type Context = {
|
|||||||
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
|
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<any>
|
||||||
|
relationship: RelationshipMetadata
|
||||||
|
relatedEntity: DmlEntity<
|
||||||
|
Record<string, PropertyType<any> | RelationshipType<any>>,
|
||||||
|
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<any>][]
|
||||||
|
|
||||||
|
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.
|
* Defines has one relationship on the Mikro ORM entity.
|
||||||
*/
|
*/
|
||||||
@@ -280,6 +340,13 @@ export function defineManyToManyRelationship(
|
|||||||
`${MikroORMEntity.name}.${relationship.name}`
|
`${MikroORMEntity.name}.${relationship.name}`
|
||||||
] = true
|
] = true
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
validateManyToManyRelationshipWithoutMappedBy({
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
relatedModelName,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user