feat: add support for defining hasOne with FK (#10441)
This commit is contained in:
@@ -51,6 +51,7 @@ export type KnownDataTypes =
|
|||||||
*/
|
*/
|
||||||
export type RelationshipTypes =
|
export type RelationshipTypes =
|
||||||
| "hasOne"
|
| "hasOne"
|
||||||
|
| "hasOneWithFK"
|
||||||
| "hasMany"
|
| "hasMany"
|
||||||
| "belongsTo"
|
| "belongsTo"
|
||||||
| "manyToMany"
|
| "manyToMany"
|
||||||
@@ -111,7 +112,7 @@ export type RelationshipMetadata = {
|
|||||||
name: string
|
name: string
|
||||||
type: RelationshipTypes
|
type: RelationshipTypes
|
||||||
entity: unknown
|
entity: unknown
|
||||||
nullable: boolean
|
nullable?: boolean
|
||||||
mappedBy?: string
|
mappedBy?: string
|
||||||
searchable: boolean
|
searchable: boolean
|
||||||
options: Record<string, any>
|
options: Record<string, any>
|
||||||
@@ -142,9 +143,9 @@ export interface EntityConstructor<Props> extends Function {
|
|||||||
* "belongsTo" relation meaning "hasOne" and "ManyToOne"
|
* "belongsTo" relation meaning "hasOne" and "ManyToOne"
|
||||||
*/
|
*/
|
||||||
export type InferForeignKeys<Schema extends DMLSchema> = {
|
export type InferForeignKeys<Schema extends DMLSchema> = {
|
||||||
[K in keyof Schema as Schema[K] extends { type: "belongsTo" }
|
[K in keyof Schema as Schema[K] extends { $foreignKey: true }
|
||||||
? `${K & string}_id`
|
? `${K & string}_id`
|
||||||
: never]: Schema[K] extends { type: "belongsTo" }
|
: never]: Schema[K] extends { $foreignKey: true }
|
||||||
? null extends Schema[K]["$dataType"]
|
? null extends Schema[K]["$dataType"]
|
||||||
? string | null
|
? string | null
|
||||||
: string
|
: string
|
||||||
@@ -191,7 +192,7 @@ export type InferSchemaFields<Schema extends DMLSchema> = Prettify<
|
|||||||
[K in keyof Schema]: Schema[K] extends RelationshipType<any>
|
[K in keyof Schema]: Schema[K] extends RelationshipType<any>
|
||||||
? Schema[K]["type"] extends "belongsTo"
|
? Schema[K]["type"] extends "belongsTo"
|
||||||
? InferBelongsToFields<Schema[K]["$dataType"]>
|
? InferBelongsToFields<Schema[K]["$dataType"]>
|
||||||
: Schema[K]["type"] extends "hasOne"
|
: Schema[K]["type"] extends "hasOne" | "hasOneWithFK"
|
||||||
? InferHasOneFields<Schema[K]["$dataType"]>
|
? InferHasOneFields<Schema[K]["$dataType"]>
|
||||||
: Schema[K]["type"] extends "hasMany"
|
: Schema[K]["type"] extends "hasMany"
|
||||||
? InferHasManyFields<Schema[K]["$dataType"]>
|
? InferHasManyFields<Schema[K]["$dataType"]>
|
||||||
|
|||||||
@@ -2887,6 +2887,690 @@ describe("Entity builder", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Entity builder | hasOneWithFK", () => {
|
||||||
|
test("define hasOne relationship with FK enabled", () => {
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
email: model.hasOne(() => email, {
|
||||||
|
foreignKey: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = toMikroORMEntity(user)
|
||||||
|
|
||||||
|
expectTypeOf(new User()).toEqualTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email_id: string
|
||||||
|
created_at: Date
|
||||||
|
updated_at: Date
|
||||||
|
deleted_at: Date | null
|
||||||
|
email: {
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
created_at: Date
|
||||||
|
updated_at: Date
|
||||||
|
deleted_at: Date | null
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.properties).toEqual({
|
||||||
|
id: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "number",
|
||||||
|
columnType: "integer",
|
||||||
|
name: "id",
|
||||||
|
fieldName: "id",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
fieldName: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
email_id: {
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
reference: "scalar",
|
||||||
|
name: "email_id",
|
||||||
|
nullable: false,
|
||||||
|
persist: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("mark hasOne with FK enabled relationship as nullable", () => {
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
emails: model
|
||||||
|
.hasOne(() => email, {
|
||||||
|
foreignKey: true,
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = toMikroORMEntity(user)
|
||||||
|
|
||||||
|
expectTypeOf(new User().emails_id).toEqualTypeOf<string | null>()
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
emails_id: string | null
|
||||||
|
emails: {
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
} | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.properties).toEqual({
|
||||||
|
id: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "number",
|
||||||
|
columnType: "integer",
|
||||||
|
name: "id",
|
||||||
|
fieldName: "id",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
fieldName: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
emails: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "emails",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
emails_id: {
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
reference: "scalar",
|
||||||
|
name: "emails_id",
|
||||||
|
nullable: true,
|
||||||
|
persist: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define custom mappedBy key for relationship", () => {
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
email: model.hasOne(() => email, {
|
||||||
|
mappedBy: "owner",
|
||||||
|
foreignKey: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = toMikroORMEntity(user)
|
||||||
|
expectTypeOf(new User().email_id).toEqualTypeOf<string>()
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: { email: string; isVerified: boolean }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.properties).toEqual({
|
||||||
|
id: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "number",
|
||||||
|
columnType: "integer",
|
||||||
|
name: "id",
|
||||||
|
fieldName: "id",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
fieldName: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
mappedBy: "owner",
|
||||||
|
},
|
||||||
|
email_id: {
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
reference: "scalar",
|
||||||
|
name: "email_id",
|
||||||
|
nullable: false,
|
||||||
|
persist: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define delete cascades for the entity", () => {
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model
|
||||||
|
.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
email: model.hasOne(() => email, {
|
||||||
|
foreignKey: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
delete: ["email"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = toMikroORMEntity(user)
|
||||||
|
expectTypeOf(new User().email_id).toEqualTypeOf<string>()
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: { email: string; isVerified: boolean }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.properties).toEqual({
|
||||||
|
id: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "number",
|
||||||
|
columnType: "integer",
|
||||||
|
name: "id",
|
||||||
|
fieldName: "id",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
fieldName: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
cascade: ["persist", "soft-remove"],
|
||||||
|
},
|
||||||
|
email_id: {
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
reference: "scalar",
|
||||||
|
name: "email_id",
|
||||||
|
nullable: false,
|
||||||
|
persist: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Email = toMikroORMEntity(email)
|
||||||
|
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||||
|
expect(emailMetaData.className).toEqual("Email")
|
||||||
|
expect(emailMetaData.path).toEqual("Email")
|
||||||
|
expect(emailMetaData.properties).toEqual({
|
||||||
|
email: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "email",
|
||||||
|
fieldName: "email",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
fieldName: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define delete cascades with belongsTo on the other end", () => {
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
user: model.belongsTo(() => user),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model
|
||||||
|
.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
email: model.hasOne(() => email, {
|
||||||
|
foreignKey: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
delete: ["email"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = toMikroORMEntity(user)
|
||||||
|
expectTypeOf(new User().email_id).toEqualTypeOf<string>()
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: {
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
user: {
|
||||||
|
id: number
|
||||||
|
username: 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",
|
||||||
|
fieldName: "id",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
fieldName: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
cascade: ["persist", "soft-remove"],
|
||||||
|
},
|
||||||
|
email_id: {
|
||||||
|
columnType: "text",
|
||||||
|
type: "string",
|
||||||
|
reference: "scalar",
|
||||||
|
name: "email_id",
|
||||||
|
nullable: false,
|
||||||
|
persist: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Email = toMikroORMEntity(email)
|
||||||
|
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||||
|
expect(emailMetaData.className).toEqual("Email")
|
||||||
|
expect(emailMetaData.path).toEqual("Email")
|
||||||
|
expect(emailMetaData.properties).toEqual({
|
||||||
|
email: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "email",
|
||||||
|
fieldName: "email",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
fieldName: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
entity: "User",
|
||||||
|
mappedBy: "email",
|
||||||
|
name: "user",
|
||||||
|
nullable: false,
|
||||||
|
onDelete: "cascade",
|
||||||
|
owner: true,
|
||||||
|
reference: "1:1",
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
columnType: "text",
|
||||||
|
getter: false,
|
||||||
|
name: "user_id",
|
||||||
|
nullable: false,
|
||||||
|
reference: "scalar",
|
||||||
|
setter: false,
|
||||||
|
type: "string",
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
fieldName: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
fieldName: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
fieldName: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Entity builder | indexes", () => {
|
describe("Entity builder | indexes", () => {
|
||||||
test("should define indexes for an entity", () => {
|
test("should define indexes for an entity", () => {
|
||||||
const group = model.define("group", {
|
const group = model.define("group", {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expectTypeOf } from "expect-type"
|
import { expectTypeOf } from "expect-type"
|
||||||
import { TextProperty } from "../properties/text"
|
import { TextProperty } from "../properties/text"
|
||||||
import { HasOne } from "../relations/has-one"
|
import { HasOne } from "../relations/has-one"
|
||||||
|
import { HasOneWithForeignKey } from "../relations/has-one-fk"
|
||||||
|
|
||||||
describe("HasOne relationship", () => {
|
describe("HasOne relationship", () => {
|
||||||
test("define hasOne relationship", () => {
|
test("define hasOne relationship", () => {
|
||||||
@@ -57,4 +58,23 @@ describe("HasOne relationship", () => {
|
|||||||
|
|
||||||
expect(HasOne.isHasOne(relationship)).toEqual(false)
|
expect(HasOne.isHasOne(relationship)).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("enable foreign keys for has one relationship", () => {
|
||||||
|
const user = {
|
||||||
|
username: new TextProperty(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityRef = () => user
|
||||||
|
const relationship = new HasOneWithForeignKey(entityRef, {})
|
||||||
|
|
||||||
|
expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>()
|
||||||
|
expect(relationship.parse("user")).toEqual({
|
||||||
|
name: "user",
|
||||||
|
type: "hasOneWithFK",
|
||||||
|
nullable: false,
|
||||||
|
options: {},
|
||||||
|
searchable: false,
|
||||||
|
entity: entityRef,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { BelongsTo } from "./relations/belongs-to"
|
|||||||
import { HasMany } from "./relations/has-many"
|
import { HasMany } from "./relations/has-many"
|
||||||
import { HasOne } from "./relations/has-one"
|
import { HasOne } from "./relations/has-one"
|
||||||
import { ManyToMany } from "./relations/many-to-many"
|
import { ManyToMany } from "./relations/many-to-many"
|
||||||
|
import { HasOneWithForeignKey } from "./relations/has-one-fk"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The implicit properties added by EntityBuilder in every schema
|
* The implicit properties added by EntityBuilder in every schema
|
||||||
@@ -345,7 +346,27 @@ export class EntityBuilder {
|
|||||||
*
|
*
|
||||||
* @customNamespace Relationship Methods
|
* @customNamespace Relationship Methods
|
||||||
*/
|
*/
|
||||||
hasOne<T>(entityBuilder: T, options?: RelationshipOptions) {
|
hasOne<T>(
|
||||||
|
entityBuilder: T,
|
||||||
|
options: RelationshipOptions & {
|
||||||
|
foreignKey: true
|
||||||
|
}
|
||||||
|
): HasOneWithForeignKey<T>
|
||||||
|
hasOne<T>(
|
||||||
|
entityBuilder: T,
|
||||||
|
options?: RelationshipOptions & {
|
||||||
|
foreignKey?: false
|
||||||
|
}
|
||||||
|
): HasOne<T>
|
||||||
|
hasOne<T>(
|
||||||
|
entityBuilder: T,
|
||||||
|
options?: RelationshipOptions & {
|
||||||
|
foreignKey?: boolean
|
||||||
|
}
|
||||||
|
): HasOneWithForeignKey<T> | HasOne<T> {
|
||||||
|
if (options?.foreignKey) {
|
||||||
|
return new HasOneWithForeignKey<T>(entityBuilder, options || {})
|
||||||
|
}
|
||||||
return new HasOne<T>(entityBuilder, options || {})
|
return new HasOne<T>(entityBuilder, options || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class DmlEntity<
|
|||||||
*/
|
*/
|
||||||
cascades(
|
cascades(
|
||||||
options: EntityCascades<
|
options: EntityCascades<
|
||||||
ExtractEntityRelations<Schema, "hasOne" | "hasMany">
|
ExtractEntityRelations<Schema, "hasOne" | "hasOneWithFK" | "hasMany">
|
||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
const childToParentCascades = options.delete?.filter((relationship) => {
|
const childToParentCascades = options.delete?.filter((relationship) => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { parseEntityName } from "./parse-entity-name"
|
|||||||
import { camelToSnakeCase, pluralize } from "../../../common"
|
import { camelToSnakeCase, pluralize } from "../../../common"
|
||||||
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
|
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
|
||||||
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
|
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
|
||||||
|
import { HasOneWithForeignKey } from "../../relations/has-one-fk"
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
|
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
|
||||||
@@ -150,6 +151,43 @@ export function defineHasOneRelationship(
|
|||||||
})(MikroORMEntity.prototype, relationship.name)
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines has one relationship with Foreign key on the MikroORM
|
||||||
|
* entity
|
||||||
|
*/
|
||||||
|
export function defineHasOneWithFKRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata,
|
||||||
|
{ relatedModelName }: { relatedModelName: string },
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
) {
|
||||||
|
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
let mappedBy: string | undefined
|
||||||
|
|
||||||
|
if ("mappedBy" in relationship) {
|
||||||
|
mappedBy = relationship.mappedBy
|
||||||
|
} else {
|
||||||
|
mappedBy = camelToSnakeCase(MikroORMEntity.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
OneToOne({
|
||||||
|
entity: relatedModelName,
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
...(mappedBy ? { mappedBy } : {}),
|
||||||
|
cascade: shouldRemoveRelated
|
||||||
|
? (["persist", "soft-remove"] as any)
|
||||||
|
: undefined,
|
||||||
|
} as any)(MikroORMEntity.prototype, relationship.name)
|
||||||
|
|
||||||
|
Property({
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
nullable: relationship.nullable,
|
||||||
|
persist: true,
|
||||||
|
})(MikroORMEntity.prototype, foreignKeyName)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines has many relationship on the Mikro ORM entity
|
* Defines has many relationship on the Mikro ORM entity
|
||||||
*/
|
*/
|
||||||
@@ -225,7 +263,10 @@ export function defineBelongsToRelationship(
|
|||||||
* to associate a relation (through the relation or the foreign key) we need to handle it
|
* to associate a relation (through the relation or the foreign key) we need to handle it
|
||||||
* specifically
|
* specifically
|
||||||
*/
|
*/
|
||||||
if (HasOne.isHasOne(otherSideRelation)) {
|
if (
|
||||||
|
HasOne.isHasOne(otherSideRelation) ||
|
||||||
|
HasOneWithForeignKey.isHasOneWithForeignKey(otherSideRelation)
|
||||||
|
) {
|
||||||
const relationMeta = this.__meta.relations.find(
|
const relationMeta = this.__meta.relations.find(
|
||||||
(relation) => relation.name === relationship.name
|
(relation) => relation.name === relationship.name
|
||||||
).targetMeta
|
).targetMeta
|
||||||
@@ -317,7 +358,10 @@ export function defineBelongsToRelationship(
|
|||||||
/**
|
/**
|
||||||
* Otherside is a has one. Hence we should defined a OneToOne
|
* Otherside is a has one. Hence we should defined a OneToOne
|
||||||
*/
|
*/
|
||||||
if (HasOne.isHasOne(otherSideRelation)) {
|
if (
|
||||||
|
HasOne.isHasOne(otherSideRelation) ||
|
||||||
|
HasOneWithForeignKey.isHasOneWithForeignKey(otherSideRelation)
|
||||||
|
) {
|
||||||
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
|
||||||
|
|
||||||
OneToOne({
|
OneToOne({
|
||||||
@@ -600,6 +644,14 @@ export function defineRelationship(
|
|||||||
cascades
|
cascades
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
case "hasOneWithFK":
|
||||||
|
defineHasOneWithFKRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntityInfo,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
|
break
|
||||||
case "hasMany":
|
case "hasMany":
|
||||||
defineHasManyRelationship(
|
defineHasManyRelationship(
|
||||||
MikroORMEntity,
|
MikroORMEntity,
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
|
|||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* export default Product
|
* export default Product
|
||||||
*
|
|
||||||
* @customNamespace Property Configuration Methods
|
|
||||||
*/
|
*/
|
||||||
searchable() {
|
searchable() {
|
||||||
this.#searchable = true
|
this.#searchable = true
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RelationNullableModifier } from "./nullable"
|
|||||||
|
|
||||||
export class BelongsTo<T> extends BaseRelationship<T> {
|
export class BelongsTo<T> extends BaseRelationship<T> {
|
||||||
type = "belongsTo" as const
|
type = "belongsTo" as const
|
||||||
|
declare $foreignKey: true
|
||||||
|
|
||||||
static isBelongsTo<T>(relationship: any): relationship is BelongsTo<T> {
|
static isBelongsTo<T>(relationship: any): relationship is BelongsTo<T> {
|
||||||
return relationship?.type === "belongsTo"
|
return relationship?.type === "belongsTo"
|
||||||
@@ -12,6 +13,6 @@ export class BelongsTo<T> extends BaseRelationship<T> {
|
|||||||
* Apply nullable modifier on the schema
|
* Apply nullable modifier on the schema
|
||||||
*/
|
*/
|
||||||
nullable() {
|
nullable() {
|
||||||
return new RelationNullableModifier<T, BelongsTo<T>>(this)
|
return new RelationNullableModifier<T, BelongsTo<T>, true>(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { BaseRelationship } from "./base"
|
||||||
|
import { RelationNullableModifier } from "./nullable"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HasOne relationship defines a relationship between two entities
|
||||||
|
* where the owner of the relationship has exactly one instance
|
||||||
|
* of the related entity.
|
||||||
|
*
|
||||||
|
* For example: A user HasOne profile
|
||||||
|
*
|
||||||
|
* You may use the "BelongsTo" relationship to define the inverse
|
||||||
|
* of the "HasOne" relationship
|
||||||
|
*/
|
||||||
|
export class HasOneWithForeignKey<T> extends BaseRelationship<T> {
|
||||||
|
type = "hasOneWithFK" as const
|
||||||
|
declare $foreignKey: true
|
||||||
|
|
||||||
|
static isHasOneWithForeignKey<T>(
|
||||||
|
relationship: any
|
||||||
|
): relationship is HasOneWithForeignKey<T> {
|
||||||
|
return relationship?.type === "hasOneWithFK"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply nullable modifier on the schema
|
||||||
|
*/
|
||||||
|
nullable() {
|
||||||
|
return new RelationNullableModifier<T, HasOneWithForeignKey<T>, true>(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,6 @@ export class HasOne<T> extends BaseRelationship<T> {
|
|||||||
* Apply nullable modifier on the schema
|
* Apply nullable modifier on the schema
|
||||||
*/
|
*/
|
||||||
nullable() {
|
nullable() {
|
||||||
return new RelationNullableModifier<T, HasOne<T>>(this)
|
return new RelationNullableModifier<T, HasOne<T>, false>(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,18 @@ const IsNullableModifier = Symbol.for("isNullableModifier")
|
|||||||
/**
|
/**
|
||||||
* Nullable modifier marks a schema node as nullable
|
* Nullable modifier marks a schema node as nullable
|
||||||
*/
|
*/
|
||||||
export class RelationNullableModifier<T, Relation extends RelationshipType<T>>
|
export class RelationNullableModifier<
|
||||||
implements RelationshipType<T | null>
|
T,
|
||||||
|
Relation extends RelationshipType<T>,
|
||||||
|
ForeignKey extends boolean
|
||||||
|
> implements RelationshipType<T | null>
|
||||||
{
|
{
|
||||||
[IsNullableModifier]: true = true;
|
[IsNullableModifier]: true = true;
|
||||||
[IsRelationship]: true = true
|
[IsRelationship]: true = true
|
||||||
|
|
||||||
static isNullableModifier<T>(
|
static isNullableModifier<T>(
|
||||||
modifier: any
|
modifier: any
|
||||||
): modifier is RelationNullableModifier<T, any> {
|
): modifier is RelationNullableModifier<T, any, any> {
|
||||||
return !!modifier?.[IsNullableModifier]
|
return !!modifier?.[IsNullableModifier]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +28,7 @@ export class RelationNullableModifier<T, Relation extends RelationshipType<T>>
|
|||||||
* of the schema property
|
* of the schema property
|
||||||
*/
|
*/
|
||||||
declare $dataType: T | null
|
declare $dataType: T | null
|
||||||
|
declare $foreignKey: ForeignKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The parent schema on which the nullable modifier is
|
* The parent schema on which the nullable modifier is
|
||||||
|
|||||||
Reference in New Issue
Block a user