Identify the owner when both sides defines a many to many relationship (#7741)
This commit is contained in:
1 parent
3ecbec9685
commit
0b9a6d5a52
2 files changed
+905
-292
No files matched your search
@@ -18,7 +18,8 @@ describe("Entity builder", () => {
|
|||||||
email: model.text(),
|
email: model.text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -67,7 +68,8 @@ describe("Entity builder", () => {
|
|||||||
email: model.text(),
|
email: model.text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -117,7 +119,8 @@ describe("Entity builder", () => {
|
|||||||
email: model.text(),
|
email: model.text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string | null
|
username: string | null
|
||||||
@@ -167,7 +170,8 @@ describe("Entity builder", () => {
|
|||||||
role: model.enum(["moderator", "admin", "guest"]),
|
role: model.enum(["moderator", "admin", "guest"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -231,7 +235,8 @@ describe("Entity builder", () => {
|
|||||||
role: model.enum(["moderator", "admin", "guest"]).default("guest"),
|
role: model.enum(["moderator", "admin", "guest"]).default("guest"),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -296,7 +301,8 @@ describe("Entity builder", () => {
|
|||||||
role: model.enum(["moderator", "admin", "guest"]).nullable(),
|
role: model.enum(["moderator", "admin", "guest"]).nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -366,7 +372,8 @@ describe("Entity builder", () => {
|
|||||||
email: model.hasOne(() => email),
|
email: model.hasOne(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -418,7 +425,8 @@ describe("Entity builder", () => {
|
|||||||
emails: model.hasOne(() => email).nullable(),
|
emails: model.hasOne(() => email).nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -471,7 +479,8 @@ describe("Entity builder", () => {
|
|||||||
email: model.hasOne(() => email, { mappedBy: "owner" }),
|
email: model.hasOne(() => email, { mappedBy: "owner" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -527,7 +536,8 @@ describe("Entity builder", () => {
|
|||||||
delete: ["email"],
|
delete: ["email"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -566,7 +576,7 @@ describe("Entity builder", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Email = createMikrORMEntity(email)
|
const Email = entityBuilder(email)
|
||||||
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||||
expect(emailMetaData.className).toEqual("Email")
|
expect(emailMetaData.className).toEqual("Email")
|
||||||
expect(emailMetaData.path).toEqual("Email")
|
expect(emailMetaData.path).toEqual("Email")
|
||||||
@@ -610,7 +620,8 @@ describe("Entity builder", () => {
|
|||||||
delete: ["email"],
|
delete: ["email"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -656,7 +667,7 @@ describe("Entity builder", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const Email = createMikrORMEntity(email)
|
const Email = entityBuilder(email)
|
||||||
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||||
expect(emailMetaData.className).toEqual("Email")
|
expect(emailMetaData.className).toEqual("Email")
|
||||||
expect(emailMetaData.path).toEqual("Email")
|
expect(emailMetaData.path).toEqual("Email")
|
||||||
@@ -706,7 +717,8 @@ describe("Entity builder", () => {
|
|||||||
emails: model.hasMany(() => email),
|
emails: model.hasMany(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -760,7 +772,8 @@ describe("Entity builder", () => {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -817,7 +830,8 @@ describe("Entity builder", () => {
|
|||||||
delete: ["emails"],
|
delete: ["emails"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -875,8 +889,9 @@ describe("Entity builder", () => {
|
|||||||
delete: ["emails"],
|
delete: ["emails"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
const Email = createMikrORMEntity(email)
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
@@ -973,8 +988,9 @@ describe("Entity builder", () => {
|
|||||||
email: model.hasOne(() => email),
|
email: model.hasOne(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
const Email = createMikrORMEntity(email)
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -1081,8 +1097,9 @@ describe("Entity builder", () => {
|
|||||||
email: model.hasOne(() => email),
|
email: model.hasOne(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
const Email = createMikrORMEntity(email)
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -1189,8 +1206,9 @@ describe("Entity builder", () => {
|
|||||||
emails: model.hasMany(() => email),
|
emails: model.hasMany(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
const Email = createMikrORMEntity(email)
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -1304,8 +1322,9 @@ describe("Entity builder", () => {
|
|||||||
emails: model.hasMany(() => email),
|
emails: model.hasMany(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
const Email = createMikrORMEntity(email)
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -1418,7 +1437,8 @@ describe("Entity builder", () => {
|
|||||||
username: model.text(),
|
username: model.text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => createMikrORMEntity(email)).toThrow(
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
expect(() => entityBuilder(email)).toThrow(
|
||||||
'Missing property "email" on "user" entity. Make sure to define it as a relationship'
|
'Missing property "email" on "user" entity. Make sure to define it as a relationship'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1438,7 +1458,8 @@ describe("Entity builder", () => {
|
|||||||
email: model.manyToMany(() => email),
|
email: model.manyToMany(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(() => createMikrORMEntity(email)).toThrow(
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
expect(() => entityBuilder(email)).toThrow(
|
||||||
'Invalid relationship reference for "email" on "user" entity. Make sure to define a hasOne or hasMany relationship'
|
'Invalid relationship reference for "email" on "user" entity. Make sure to define a hasOne or hasMany relationship'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -1484,8 +1505,9 @@ describe("Entity builder", () => {
|
|||||||
teams: model.manyToMany(() => team),
|
teams: model.manyToMany(() => team),
|
||||||
})
|
})
|
||||||
|
|
||||||
const User = createMikrORMEntity(user)
|
const entityBuilder = createMikrORMEntity()
|
||||||
const Team = createMikrORMEntity(team)
|
const User = entityBuilder(user)
|
||||||
|
const Team = entityBuilder(team)
|
||||||
|
|
||||||
expectTypeOf(new User()).toMatchTypeOf<{
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
@@ -1539,6 +1561,7 @@ describe("Entity builder", () => {
|
|||||||
reference: "m:n",
|
reference: "m:n",
|
||||||
name: "teams",
|
name: "teams",
|
||||||
entity: "Team",
|
entity: "Team",
|
||||||
|
pivotTable: "team_users",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1568,6 +1591,517 @@ describe("Entity builder", () => {
|
|||||||
reference: "m:n",
|
reference: "m:n",
|
||||||
name: "users",
|
name: "users",
|
||||||
entity: "User",
|
entity: "User",
|
||||||
|
pivotTable: "team_users",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define mappedBy on one side", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const team = model.define("team", {
|
||||||
|
id: model.number(),
|
||||||
|
name: model.text(),
|
||||||
|
users: model.manyToMany(() => user),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
mappedBy: "users",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throw error when unable to locate relationship via mappedBy", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const team = model.define("team", {
|
||||||
|
id: model.number(),
|
||||||
|
name: model.text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
expect(() => entityBuilder(user)).toThrow(
|
||||||
|
'Missing property "users" on "team" entity. Make sure to define it as a relationship'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throw error when mappedBy relationship is not a manyToMany", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const team = model.define("team", {
|
||||||
|
id: model.number(),
|
||||||
|
name: model.text(),
|
||||||
|
users: model.belongsTo(() => team, { mappedBy: "teams" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
expect(() => entityBuilder(user)).toThrow(
|
||||||
|
'Invalid relationship reference for "users" on "team" entity. Make sure to define a manyToMany relationship'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define mappedBy on both sides", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const team = model.define("team", {
|
||||||
|
id: model.number(),
|
||||||
|
name: model.text(),
|
||||||
|
users: model.manyToMany(() => user, { mappedBy: "teams" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
mappedBy: "users",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
/**
|
||||||
|
* The other side should be inversed in order for Mikro ORM
|
||||||
|
* to work. Both sides cannot have mappedBy.
|
||||||
|
*/
|
||||||
|
inversedBy: "teams",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define mappedBy on both sides and reverse order of registering entities", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const team = model.define("team", {
|
||||||
|
id: model.number(),
|
||||||
|
name: model.text(),
|
||||||
|
users: model.manyToMany(() => user, { mappedBy: "teams" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const Team = entityBuilder(team)
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
/**
|
||||||
|
* The other side should be inversed in order for Mikro ORM
|
||||||
|
* to work. Both sides cannot have mappedBy.
|
||||||
|
*/
|
||||||
|
inversedBy: "users",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
mappedBy: "teams",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define multiple many to many relationships to the same entity", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const team = model.define("team", {
|
||||||
|
id: model.number(),
|
||||||
|
name: model.text(),
|
||||||
|
activeTeamsUsers: model.manyToMany(() => user, {
|
||||||
|
mappedBy: "activeTeams",
|
||||||
|
}),
|
||||||
|
users: model.manyToMany(() => user, { mappedBy: "teams" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
activeTeams: model.manyToMany(() => team, {
|
||||||
|
mappedBy: "activeTeamsUsers",
|
||||||
|
}),
|
||||||
|
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const Team = entityBuilder(team)
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
teams: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
users: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
activeTeams: 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
|
||||||
|
}>
|
||||||
|
activeTeams: 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: "team_users",
|
||||||
|
/**
|
||||||
|
* The other side should be inversed in order for Mikro ORM
|
||||||
|
* to work. Both sides cannot have mappedBy.
|
||||||
|
*/
|
||||||
|
inversedBy: "users",
|
||||||
|
},
|
||||||
|
activeTeams: {
|
||||||
|
reference: "m:n",
|
||||||
|
name: "activeTeams",
|
||||||
|
entity: "Team",
|
||||||
|
pivotTable: "team_users",
|
||||||
|
inversedBy: "activeTeamsUsers",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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: "team_users",
|
||||||
|
mappedBy: "teams",
|
||||||
|
},
|
||||||
|
activeTeamsUsers: {
|
||||||
|
reference: "m:n",
|
||||||
|
name: "activeTeamsUsers",
|
||||||
|
entity: "User",
|
||||||
|
pivotTable: "team_users",
|
||||||
|
mappedBy: "activeTeams",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
} from "../types"
|
} from "../types"
|
||||||
import { HasOne } from "../relations/has-one"
|
import { HasOne } from "../relations/has-one"
|
||||||
import { HasMany } from "../relations/has-many"
|
import { HasMany } from "../relations/has-many"
|
||||||
|
import { ManyToMany as DmlManyToMany } from "../relations/many-to-many"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DML entity data types to PostgreSQL data types via
|
* DML entity data types to PostgreSQL data types via
|
||||||
@@ -58,296 +59,374 @@ const PROPERTY_TYPES: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a DML entity schema field as a Mikro ORM property
|
* Factory function to create the mikro orm entity builder. The return
|
||||||
|
* value is a function that can be used to convert DML entities
|
||||||
|
* to Mikro ORM entities.
|
||||||
*/
|
*/
|
||||||
function defineProperty(
|
export function createMikrORMEntity() {
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
field: SchemaMetadata
|
|
||||||
) {
|
|
||||||
/**
|
/**
|
||||||
* Defining an enum property
|
* The following property is used to track many to many relationship
|
||||||
|
* between two entities. It is needed because we have to mark one
|
||||||
|
* of them as the owner of the relationship without exposing
|
||||||
|
* any user land APIs to explicitly define an owner.
|
||||||
|
*
|
||||||
|
* The object contains values as follows.
|
||||||
|
* - [entityname.relationship]: true // true means, it is already marked as owner
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* - [user.teams]: true // the teams relationship on user is an owner
|
||||||
|
* - [team.users] // cannot be an owner
|
||||||
*/
|
*/
|
||||||
if (field.dataType.name === "enum") {
|
const MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean> = {}
|
||||||
Enum({
|
|
||||||
items: () => field.dataType.options!.choices,
|
/**
|
||||||
|
* Defines a DML entity schema field as a Mikro ORM property
|
||||||
|
*/
|
||||||
|
function defineProperty(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
field: SchemaMetadata
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Defining an enum property
|
||||||
|
*/
|
||||||
|
if (field.dataType.name === "enum") {
|
||||||
|
Enum({
|
||||||
|
items: () => field.dataType.options!.choices,
|
||||||
|
nullable: field.nullable,
|
||||||
|
default: field.defaultValue,
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define rest of properties
|
||||||
|
*/
|
||||||
|
const columnType = COLUMN_TYPES[field.dataType.name]
|
||||||
|
const propertyType = PROPERTY_TYPES[field.dataType.name]
|
||||||
|
|
||||||
|
Property({
|
||||||
|
columnType,
|
||||||
|
type: propertyType,
|
||||||
nullable: field.nullable,
|
nullable: field.nullable,
|
||||||
default: field.defaultValue,
|
default: field.defaultValue,
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define rest of properties
|
* Defines has one relationship on the Mikro ORM entity.
|
||||||
*/
|
*/
|
||||||
const columnType = COLUMN_TYPES[field.dataType.name]
|
function defineHasOneRelationship(
|
||||||
const propertyType = PROPERTY_TYPES[field.dataType.name]
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
relatedEntity: DmlEntity<
|
||||||
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
|
>,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
Property({
|
|
||||||
columnType,
|
|
||||||
type: propertyType,
|
|
||||||
nullable: field.nullable,
|
|
||||||
default: field.defaultValue,
|
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines has one relationship on the Mikro ORM entity.
|
|
||||||
*/
|
|
||||||
function defineHasOneRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
relatedEntity: DmlEntity<
|
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
|
||||||
>,
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
|
||||||
|
|
||||||
OneToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
|
||||||
cascade: shouldRemoveRelated
|
|
||||||
? (["perist", "soft-remove"] as any)
|
|
||||||
: undefined,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines has many relationship on the Mikro ORM entity
|
|
||||||
*/
|
|
||||||
function defineHasManyRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
relatedEntity: DmlEntity<
|
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
|
||||||
>,
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
|
||||||
|
|
||||||
OneToMany({
|
|
||||||
entity: relatedModelName,
|
|
||||||
orphanRemoval: true,
|
|
||||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
|
||||||
cascade: shouldRemoveRelated
|
|
||||||
? (["perist", "soft-remove"] as any)
|
|
||||||
: undefined,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines belongs to relationship on the Mikro ORM entity. The belongsTo
|
|
||||||
* relationship inspects the related entity for the other side of
|
|
||||||
* the relationship and then uses one of the following Mikro ORM
|
|
||||||
* relationship.
|
|
||||||
*
|
|
||||||
* - OneToOne: When the other side uses "hasOne" with "owner: true"
|
|
||||||
* - ManyToOne: When the other side uses "hasMany"
|
|
||||||
*/
|
|
||||||
function defineBelongsToRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
relatedEntity: DmlEntity<
|
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
|
||||||
>
|
|
||||||
) {
|
|
||||||
const mappedBy =
|
|
||||||
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
|
||||||
const { schema: relationSchema, cascades: relationCascades } =
|
|
||||||
relatedEntity.parse()
|
|
||||||
|
|
||||||
const otherSideRelation = relationSchema[mappedBy]
|
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In DML the relationships are cascaded from parent to child. A belongsTo
|
|
||||||
* relationship is always a child, therefore we look at the parent and
|
|
||||||
* define a onDelete: cascade when we are included in the delete
|
|
||||||
* list of parent cascade.
|
|
||||||
*/
|
|
||||||
const shouldCascade = relationCascades.delete?.includes(mappedBy)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the mapped by is defined as relationship on the other side
|
|
||||||
*/
|
|
||||||
if (!otherSideRelation) {
|
|
||||||
throw new Error(
|
|
||||||
`Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Otherside is a has many. Hence we should defined a ManyToOne
|
|
||||||
*/
|
|
||||||
if (otherSideRelation instanceof HasMany) {
|
|
||||||
ManyToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
columnType: "text",
|
|
||||||
mapToPk: true,
|
|
||||||
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
|
||||||
nullable: relationship.nullable,
|
|
||||||
onDelete: shouldCascade ? "cascade" : undefined,
|
|
||||||
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
|
||||||
|
|
||||||
ManyToOne({
|
|
||||||
entity: relatedModelName,
|
|
||||||
persist: false,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Otherside is a has one. Hence we should defined a OneToOne
|
|
||||||
*/
|
|
||||||
if (otherSideRelation instanceof HasOne) {
|
|
||||||
OneToOne({
|
OneToOne({
|
||||||
entity: relatedModelName,
|
entity: relatedModelName,
|
||||||
nullable: relationship.nullable,
|
nullable: relationship.nullable,
|
||||||
mappedBy: mappedBy,
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
owner: true,
|
cascade: shouldRemoveRelated
|
||||||
onDelete: shouldCascade ? "cascade" : undefined,
|
? (["perist", "soft-remove"] as any)
|
||||||
|
: undefined,
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Other side is some unsupported data-type
|
* Defines has many relationship on the Mikro ORM entity
|
||||||
*/
|
*/
|
||||||
throw new Error(
|
function defineHasManyRelationship(
|
||||||
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a hasOne or hasMany relationship`
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
)
|
relationship: RelationshipMetadata,
|
||||||
}
|
relatedEntity: DmlEntity<
|
||||||
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
|
>,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
/**
|
OneToMany({
|
||||||
* Defines a many to many relationship on the Mikro ORM entity
|
entity: relatedModelName,
|
||||||
*/
|
orphanRemoval: true,
|
||||||
function defineManyToManyRelationship(
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
cascade: shouldRemoveRelated
|
||||||
relationship: RelationshipMetadata,
|
? (["perist", "soft-remove"] as any)
|
||||||
relatedEntity: DmlEntity<
|
: undefined,
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
>,
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
ManyToMany({
|
|
||||||
entity: relatedModelName,
|
|
||||||
mappedBy: relationship.mappedBy as any,
|
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines a DML entity schema field as a Mikro ORM relationship
|
|
||||||
*/
|
|
||||||
function defineRelationship(
|
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
|
||||||
relationship: RelationshipMetadata,
|
|
||||||
cascades: EntityCascades<string[]>
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* We expect the relationship.entity to be a function that
|
|
||||||
* lazily returns the related entity
|
|
||||||
*/
|
|
||||||
const relatedEntity =
|
|
||||||
typeof relationship.entity === "function"
|
|
||||||
? (relationship.entity() as unknown)
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since we don't type-check relationships, we should validate
|
|
||||||
* them at runtime
|
|
||||||
*/
|
|
||||||
if (!relatedEntity) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the relationship using a factory function`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the return value is a DML entity instance
|
* Defines belongs to relationship on the Mikro ORM entity. The belongsTo
|
||||||
|
* relationship inspects the related entity for the other side of
|
||||||
|
* the relationship and then uses one of the following Mikro ORM
|
||||||
|
* relationship.
|
||||||
|
*
|
||||||
|
* - OneToOne: When the other side uses "hasOne" with "owner: true"
|
||||||
|
* - ManyToOne: When the other side uses "hasMany"
|
||||||
*/
|
*/
|
||||||
if (!(relatedEntity instanceof DmlEntity)) {
|
function defineBelongsToRelationship(
|
||||||
throw new Error(
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the relationship callback`
|
relationship: RelationshipMetadata,
|
||||||
)
|
relatedEntity: DmlEntity<
|
||||||
}
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const mappedBy =
|
||||||
|
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
||||||
|
const { schema: relationSchema, cascades: relationCascades } =
|
||||||
|
relatedEntity.parse()
|
||||||
|
|
||||||
/**
|
const otherSideRelation = relationSchema[mappedBy]
|
||||||
* Defining relationships
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
*/
|
|
||||||
switch (relationship.type) {
|
/**
|
||||||
case "hasOne":
|
* In DML the relationships are cascaded from parent to child. A belongsTo
|
||||||
defineHasOneRelationship(
|
* relationship is always a child, therefore we look at the parent and
|
||||||
MikroORMEntity,
|
* define a onDelete: cascade when we are included in the delete
|
||||||
relationship,
|
* list of parent cascade.
|
||||||
relatedEntity,
|
*/
|
||||||
cascades
|
const shouldCascade = relationCascades.delete?.includes(mappedBy)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the mapped by is defined as relationship on the other side
|
||||||
|
*/
|
||||||
|
if (!otherSideRelation) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship`
|
||||||
)
|
)
|
||||||
break
|
|
||||||
case "hasMany":
|
|
||||||
defineHasManyRelationship(
|
|
||||||
MikroORMEntity,
|
|
||||||
relationship,
|
|
||||||
relatedEntity,
|
|
||||||
cascades
|
|
||||||
)
|
|
||||||
break
|
|
||||||
case "belongsTo":
|
|
||||||
defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
|
|
||||||
break
|
|
||||||
case "manyToMany":
|
|
||||||
defineManyToManyRelationship(
|
|
||||||
MikroORMEntity,
|
|
||||||
relationship,
|
|
||||||
relatedEntity,
|
|
||||||
cascades
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper function to define a Mikro ORM entity from a
|
|
||||||
* DML entity.
|
|
||||||
* @todo: Handle soft deleted indexes and filters
|
|
||||||
* @todo: Finalize if custom pivot entities are needed
|
|
||||||
*/
|
|
||||||
export function createMikrORMEntity<T extends DmlEntity<any>>(
|
|
||||||
entity: T
|
|
||||||
): Infer<T> {
|
|
||||||
class MikroORMEntity {}
|
|
||||||
const { name, schema, cascades } = entity.parse()
|
|
||||||
|
|
||||||
const className = upperCaseFirst(name)
|
|
||||||
const tableName = pluralize(camelToSnakeCase(className))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigning name to the class constructor, so that it matches
|
|
||||||
* the DML entity name
|
|
||||||
*/
|
|
||||||
Object.defineProperty(MikroORMEntity, "name", {
|
|
||||||
get: function () {
|
|
||||||
return className
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processing schema fields
|
|
||||||
*/
|
|
||||||
Object.entries(schema).forEach(([name, property]) => {
|
|
||||||
const field = property.parse(name)
|
|
||||||
if ("fieldName" in field) {
|
|
||||||
defineProperty(MikroORMEntity, field)
|
|
||||||
} else {
|
|
||||||
defineRelationship(MikroORMEntity, field, cascades)
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
/**
|
||||||
|
* Otherside is a has many. Hence we should defined a ManyToOne
|
||||||
|
*/
|
||||||
|
if (otherSideRelation instanceof HasMany) {
|
||||||
|
ManyToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
columnType: "text",
|
||||||
|
mapToPk: true,
|
||||||
|
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
onDelete: shouldCascade ? "cascade" : undefined,
|
||||||
|
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
||||||
|
|
||||||
|
ManyToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
persist: false,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Otherside is a has one. Hence we should defined a OneToOne
|
||||||
|
*/
|
||||||
|
if (otherSideRelation instanceof HasOne) {
|
||||||
|
OneToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
mappedBy: mappedBy,
|
||||||
|
owner: true,
|
||||||
|
onDelete: shouldCascade ? "cascade" : undefined,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Other side is some unsupported data-type
|
||||||
|
*/
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a hasOne or hasMany relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converting class to a MikroORM entity
|
* Defines a many to many relationship on the Mikro ORM entity
|
||||||
*/
|
*/
|
||||||
return Entity({ tableName })(MikroORMEntity) as Infer<T>
|
function defineManyToManyRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
relatedEntity: DmlEntity<
|
||||||
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
|
>,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
|
let mappedBy = relationship.mappedBy
|
||||||
|
let inversedBy: 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.
|
||||||
|
*/
|
||||||
|
const pivotTableName = [
|
||||||
|
MikroORMEntity.name.toLowerCase(),
|
||||||
|
relatedEntity.name.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) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (otherSideRelation instanceof DmlManyToMany === false) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a manyToMany relationship`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the other side has defined a mapped by and if that
|
||||||
|
* mapping is already tracked as the owner.
|
||||||
|
*
|
||||||
|
* - If yes, we will inverse our mapped by
|
||||||
|
* - Otherwise, we will track ourselves as the owner.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
otherSideRelation.parse(mappedBy).mappedBy &&
|
||||||
|
MANY_TO_MANY_TRACKED_REALTIONS[`${relatedModelName}.${mappedBy}`]
|
||||||
|
) {
|
||||||
|
inversedBy = mappedBy
|
||||||
|
mappedBy = undefined
|
||||||
|
} else {
|
||||||
|
MANY_TO_MANY_TRACKED_REALTIONS[
|
||||||
|
`${MikroORMEntity.name}.${relationship.name}`
|
||||||
|
] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ManyToMany({
|
||||||
|
entity: relatedModelName,
|
||||||
|
pivotTable: pivotTableName,
|
||||||
|
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
|
||||||
|
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a DML entity schema field as a Mikro ORM relationship
|
||||||
|
*/
|
||||||
|
function defineRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* We expect the relationship.entity to be a function that
|
||||||
|
* lazily returns the related entity
|
||||||
|
*/
|
||||||
|
const relatedEntity =
|
||||||
|
typeof relationship.entity === "function"
|
||||||
|
? (relationship.entity() as unknown)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since we don't type-check relationships, we should validate
|
||||||
|
* them at runtime
|
||||||
|
*/
|
||||||
|
if (!relatedEntity) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the relationship using a factory function`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the return value is a DML entity instance
|
||||||
|
*/
|
||||||
|
if (!(relatedEntity instanceof DmlEntity)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the relationship callback`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining relationships
|
||||||
|
*/
|
||||||
|
switch (relationship.type) {
|
||||||
|
case "hasOne":
|
||||||
|
defineHasOneRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "hasMany":
|
||||||
|
defineHasManyRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case "belongsTo":
|
||||||
|
defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
|
||||||
|
break
|
||||||
|
case "manyToMany":
|
||||||
|
defineManyToManyRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function to define a Mikro ORM entity from a
|
||||||
|
* DML entity.
|
||||||
|
*/
|
||||||
|
return function createEntity<T extends DmlEntity<any>>(entity: T): Infer<T> {
|
||||||
|
class MikroORMEntity {}
|
||||||
|
const { name, schema, cascades } = entity.parse()
|
||||||
|
|
||||||
|
const className = upperCaseFirst(name)
|
||||||
|
const tableName = pluralize(camelToSnakeCase(className))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigning name to the class constructor, so that it matches
|
||||||
|
* the DML entity name
|
||||||
|
*/
|
||||||
|
Object.defineProperty(MikroORMEntity, "name", {
|
||||||
|
get: function () {
|
||||||
|
return className
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processing schema fields
|
||||||
|
*/
|
||||||
|
Object.entries(schema).forEach(([name, property]) => {
|
||||||
|
const field = property.parse(name)
|
||||||
|
if ("fieldName" in field) {
|
||||||
|
defineProperty(MikroORMEntity, field)
|
||||||
|
} else {
|
||||||
|
defineRelationship(MikroORMEntity, field, cascades)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converting class to a MikroORM entity
|
||||||
|
*/
|
||||||
|
return Entity({ tableName })(MikroORMEntity) as Infer<T>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user