chore(utils): DML#hasOne allow mappedBy to not be defined (#10442)

RESOLVES FRMW-2826

**What**
We have many places where we define only one to one on one side of the relation. In that case it is a one to one that does not mapped by to the other side relation since it is not defined. This pr allow to create a one to one without mapped by. In order to remove breaking changes, in that case we ask the user to explicitly define the `mappedBy` as `undefined`
This commit is contained in:
Adrien de Peretti
2024-12-05 10:31:38 +01:00
committed by GitHub
parent 223bcff379
commit 90d7f4ff39
4 changed files with 114 additions and 12 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/utils": patch
---
chore(utils): DML#hasOne allow mappedBy to not be defined

View File

@@ -2461,6 +2461,93 @@ describe("Entity builder", () => {
})
})
test("define custom mappedBy key to undefined to not get the auto generated value", () => {
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
})
const user = model.define("user", {
id: model.number(),
username: model.text(),
email: model.hasOne(() => email, { mappedBy: undefined }),
})
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
email: { email: string; isVerified: boolean }
}>()
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",
fieldName: "id",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
fieldName: "username",
nullable: false,
getter: false,
setter: false,
},
email: {
reference: "1:1",
name: "email",
entity: "Email",
nullable: false,
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
fieldName: "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",
fieldName: "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",
fieldName: "deleted_at",
nullable: true,
getter: false,
setter: false,
},
})
})
test("define custom mappedBy key for relationship", () => {
const email = model.define("email", {
email: model.text(),
@@ -2899,6 +2986,7 @@ describe("Entity builder", () => {
username: model.text(),
email: model.hasOne(() => email, {
foreignKey: true,
mappedBy: undefined,
}),
})
@@ -3010,6 +3098,7 @@ describe("Entity builder", () => {
emails: model
.hasOne(() => email, {
foreignKey: true,
mappedBy: undefined,
})
.nullable(),
})
@@ -3265,6 +3354,7 @@ describe("Entity builder", () => {
entity: "Email",
nullable: false,
cascade: ["persist", "soft-remove"],
mappedBy: "user",
},
email_id: {
columnType: "text",
@@ -3440,6 +3530,7 @@ describe("Entity builder", () => {
entity: "Email",
nullable: false,
cascade: ["persist", "soft-remove"],
mappedBy: "user",
},
email_id: {
columnType: "text",

View File

@@ -11,17 +11,18 @@ import {
ManyToOne,
OneToMany,
OneToOne,
OneToOneOptions,
OnInit,
Property,
rel,
} from "@mikro-orm/core"
import { DmlEntity } from "../../entity"
import { HasOne } from "../../relations/has-one"
import { HasMany } from "../../relations/has-many"
import { parseEntityName } from "./parse-entity-name"
import { camelToSnakeCase, pluralize } from "../../../common"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
import { DmlEntity } from "../../entity"
import { HasMany } from "../../relations/has-many"
import { HasOne } from "../../relations/has-one"
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
import { parseEntityName } from "./parse-entity-name"
import { HasOneWithForeignKey } from "../../relations/has-one-fk"
type Context = {
@@ -141,14 +142,19 @@ export function defineHasOneRelationship(
) {
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
let mappedBy: string | undefined = camelToSnakeCase(MikroORMEntity.name)
if ("mappedBy" in relationship) {
mappedBy = relationship.mappedBy
}
OneToOne({
entity: relatedModelName,
nullable: relationship.nullable,
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
...(mappedBy ? { mappedBy } : {}),
cascade: shouldRemoveRelated
? (["persist", "soft-remove"] as any)
: undefined,
})(MikroORMEntity.prototype, relationship.name)
} as OneToOneOptions<any, any>)(MikroORMEntity.prototype, relationship.name)
}
/**
@@ -163,12 +169,10 @@ export function defineHasOneWithFKRelationship(
) {
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
let mappedBy: string | undefined
let mappedBy: string | undefined = camelToSnakeCase(MikroORMEntity.name)
if ("mappedBy" in relationship) {
mappedBy = relationship.mappedBy
} else {
mappedBy = camelToSnakeCase(MikroORMEntity.name)
}
OneToOne({
@@ -178,7 +182,7 @@ export function defineHasOneWithFKRelationship(
cascade: shouldRemoveRelated
? (["persist", "soft-remove"] as any)
: undefined,
} as any)(MikroORMEntity.prototype, relationship.name)
} as OneToOneOptions<any, any>)(MikroORMEntity.prototype, relationship.name)
Property({
type: "string",

View File

@@ -69,7 +69,9 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
return {
name: relationshipName,
nullable: false,
mappedBy: this.options.mappedBy,
...("mappedBy" in this.options
? { mappedBy: this.options.mappedBy }
: {}),
options: this.options,
searchable: this.#searchable,
entity: this.#referencedEntity,