Identify the owner when both sides defines a many to many relationship (#7741)
This commit is contained in:
@@ -18,7 +18,8 @@ describe("Entity builder", () => {
|
||||
email: model.text(),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -67,7 +68,8 @@ describe("Entity builder", () => {
|
||||
email: model.text(),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -117,7 +119,8 @@ describe("Entity builder", () => {
|
||||
email: model.text(),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string | null
|
||||
@@ -167,7 +170,8 @@ describe("Entity builder", () => {
|
||||
role: model.enum(["moderator", "admin", "guest"]),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -231,7 +235,8 @@ describe("Entity builder", () => {
|
||||
role: model.enum(["moderator", "admin", "guest"]).default("guest"),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -296,7 +301,8 @@ describe("Entity builder", () => {
|
||||
role: model.enum(["moderator", "admin", "guest"]).nullable(),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -366,7 +372,8 @@ describe("Entity builder", () => {
|
||||
email: model.hasOne(() => email),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -418,7 +425,8 @@ describe("Entity builder", () => {
|
||||
emails: model.hasOne(() => email).nullable(),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -471,7 +479,8 @@ describe("Entity builder", () => {
|
||||
email: model.hasOne(() => email, { mappedBy: "owner" }),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -527,7 +536,8 @@ describe("Entity builder", () => {
|
||||
delete: ["email"],
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -566,7 +576,7 @@ describe("Entity builder", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const Email = createMikrORMEntity(email)
|
||||
const Email = entityBuilder(email)
|
||||
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||
expect(emailMetaData.className).toEqual("Email")
|
||||
expect(emailMetaData.path).toEqual("Email")
|
||||
@@ -610,7 +620,8 @@ describe("Entity builder", () => {
|
||||
delete: ["email"],
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -656,7 +667,7 @@ describe("Entity builder", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const Email = createMikrORMEntity(email)
|
||||
const Email = entityBuilder(email)
|
||||
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||
expect(emailMetaData.className).toEqual("Email")
|
||||
expect(emailMetaData.path).toEqual("Email")
|
||||
@@ -706,7 +717,8 @@ describe("Entity builder", () => {
|
||||
emails: model.hasMany(() => email),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -760,7 +772,8 @@ describe("Entity builder", () => {
|
||||
}),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -817,7 +830,8 @@ describe("Entity builder", () => {
|
||||
delete: ["emails"],
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -875,8 +889,9 @@ describe("Entity builder", () => {
|
||||
delete: ["emails"],
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const Email = createMikrORMEntity(email)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
const Email = entityBuilder(email)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
@@ -973,8 +988,9 @@ describe("Entity builder", () => {
|
||||
email: model.hasOne(() => email),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const Email = createMikrORMEntity(email)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
const Email = entityBuilder(email)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -1081,8 +1097,9 @@ describe("Entity builder", () => {
|
||||
email: model.hasOne(() => email),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const Email = createMikrORMEntity(email)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
const Email = entityBuilder(email)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -1189,8 +1206,9 @@ describe("Entity builder", () => {
|
||||
emails: model.hasMany(() => email),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const Email = createMikrORMEntity(email)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
const Email = entityBuilder(email)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -1304,8 +1322,9 @@ describe("Entity builder", () => {
|
||||
emails: model.hasMany(() => email),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const Email = createMikrORMEntity(email)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
const Email = entityBuilder(email)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -1418,7 +1437,8 @@ describe("Entity builder", () => {
|
||||
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'
|
||||
)
|
||||
})
|
||||
@@ -1438,7 +1458,8 @@ describe("Entity builder", () => {
|
||||
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'
|
||||
)
|
||||
})
|
||||
@@ -1484,8 +1505,9 @@ describe("Entity builder", () => {
|
||||
teams: model.manyToMany(() => team),
|
||||
})
|
||||
|
||||
const User = createMikrORMEntity(user)
|
||||
const Team = createMikrORMEntity(team)
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const User = entityBuilder(user)
|
||||
const Team = entityBuilder(team)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -1539,6 +1561,7 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
pivotTable: "team_users",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1568,6 +1591,517 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
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"
|
||||
import { HasOne } from "../relations/has-one"
|
||||
import { HasMany } from "../relations/has-many"
|
||||
import { ManyToMany as DmlManyToMany } from "../relations/many-to-many"
|
||||
|
||||
/**
|
||||
* 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(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
field: SchemaMetadata
|
||||
) {
|
||||
export function createMikrORMEntity() {
|
||||
/**
|
||||
* 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") {
|
||||
Enum({
|
||||
items: () => field.dataType.options!.choices,
|
||||
const MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean> = {}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
default: field.defaultValue,
|
||||
})(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]
|
||||
const propertyType = PROPERTY_TYPES[field.dataType.name]
|
||||
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)
|
||||
|
||||
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({
|
||||
entity: relatedModelName,
|
||||
nullable: relationship.nullable,
|
||||
mappedBy: mappedBy,
|
||||
owner: true,
|
||||
onDelete: shouldCascade ? "cascade" : undefined,
|
||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||
cascade: shouldRemoveRelated
|
||||
? (["perist", "soft-remove"] as any)
|
||||
: undefined,
|
||||
})(MikroORMEntity.prototype, relationship.name)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Other side is some unsupported data-type
|
||||
* Defines has many relationship on the Mikro ORM entity
|
||||
*/
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a hasOne or hasMany relationship`
|
||||
)
|
||||
}
|
||||
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)
|
||||
|
||||
/**
|
||||
* Defines a many to many relationship on the Mikro ORM entity
|
||||
*/
|
||||
function defineManyToManyRelationship(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
relationship: RelationshipMetadata,
|
||||
relatedEntity: DmlEntity<
|
||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||
>,
|
||||
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`
|
||||
)
|
||||
OneToMany({
|
||||
entity: relatedModelName,
|
||||
orphanRemoval: true,
|
||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||
cascade: shouldRemoveRelated
|
||||
? (["perist", "soft-remove"] as any)
|
||||
: undefined,
|
||||
})(MikroORMEntity.prototype, relationship.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the relationship callback`
|
||||
)
|
||||
}
|
||||
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()
|
||||
|
||||
/**
|
||||
* Defining relationships
|
||||
*/
|
||||
switch (relationship.type) {
|
||||
case "hasOne":
|
||||
defineHasOneRelationship(
|
||||
MikroORMEntity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
cascades
|
||||
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`
|
||||
)
|
||||
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