Add support for cascades to DML (#7721)
This commit is contained in:
1 parent
3d33f06e77
commit
2af3f9e954
13 files changed
+615
-47
No files matched your search
@@ -5,7 +5,7 @@ import { BaseRelationship } from "../relations/base"
|
|||||||
describe("Base relationship", () => {
|
describe("Base relationship", () => {
|
||||||
test("define a custom relationship", () => {
|
test("define a custom relationship", () => {
|
||||||
class HasOne<T> extends BaseRelationship<T> {
|
class HasOne<T> extends BaseRelationship<T> {
|
||||||
protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne"
|
type: "hasOne" = "hasOne"
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
|
|||||||
@@ -400,6 +400,7 @@ describe("Entity builder", () => {
|
|||||||
name: "email",
|
name: "email",
|
||||||
entity: "Email",
|
entity: "Email",
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -452,6 +453,240 @@ describe("Entity builder", () => {
|
|||||||
name: "emails",
|
name: "emails",
|
||||||
entity: "Email",
|
entity: "Email",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
mappedBy: "user",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define custom mappedBy key for relationship", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
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" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = createMikrORMEntity(user)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: EntityConstructor<{ 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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
mappedBy: "owner",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define delete cascades for the entity", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
delete: ["email"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = createMikrORMEntity(user)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: EntityConstructor<{ 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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
|
cascade: ["perist", "soft-remove"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Email = createMikrORMEntity(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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define delete cascades with belongsTo on the other end", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
delete: ["email"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = createMikrORMEntity(user)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email: EntityConstructor<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
user: EntityConstructor<{
|
||||||
|
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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
|
cascade: ["perist", "soft-remove"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const Email = createMikrORMEntity(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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
entity: "User",
|
||||||
|
mappedBy: "email",
|
||||||
|
name: "user",
|
||||||
|
nullable: false,
|
||||||
|
onDelete: "cascade",
|
||||||
|
owner: true,
|
||||||
|
reference: "1:1",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -564,6 +799,162 @@ describe("Entity builder", () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("define delete cascades for the entity", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model
|
||||||
|
.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
emails: model.hasMany(() => email),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
delete: ["emails"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = createMikrORMEntity(user)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
emails: EntityConstructor<{ 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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
emails: {
|
||||||
|
reference: "1:m",
|
||||||
|
name: "emails",
|
||||||
|
entity: "Email",
|
||||||
|
orphanRemoval: true,
|
||||||
|
mappedBy: "user",
|
||||||
|
cascade: ["perist", "soft-remove"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define delete cascades with belongsTo on the other end", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
const email = model.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
user: model.belongsTo(() => user, { mappedBy: "emails" }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model
|
||||||
|
.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
emails: model.hasMany(() => email),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
delete: ["emails"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const User = createMikrORMEntity(user)
|
||||||
|
const Email = createMikrORMEntity(email)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
emails: EntityConstructor<{ 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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "username",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
emails: {
|
||||||
|
reference: "1:m",
|
||||||
|
name: "emails",
|
||||||
|
entity: "Email",
|
||||||
|
orphanRemoval: true,
|
||||||
|
mappedBy: "user",
|
||||||
|
cascade: ["perist", "soft-remove"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
reference: "m:1",
|
||||||
|
name: "user",
|
||||||
|
entity: "User",
|
||||||
|
persist: false,
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
columnType: "text",
|
||||||
|
entity: "User",
|
||||||
|
fieldName: "user_id",
|
||||||
|
mapToPk: true,
|
||||||
|
name: "user_id",
|
||||||
|
nullable: false,
|
||||||
|
reference: "m:1",
|
||||||
|
onDelete: "cascade",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Entity builder | belongsTo", () => {
|
describe("Entity builder | belongsTo", () => {
|
||||||
@@ -638,6 +1029,7 @@ describe("Entity builder", () => {
|
|||||||
name: "email",
|
name: "email",
|
||||||
entity: "Email",
|
entity: "Email",
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -745,6 +1137,7 @@ describe("Entity builder", () => {
|
|||||||
name: "email",
|
name: "email",
|
||||||
entity: "Email",
|
entity: "Email",
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1049,6 +1442,31 @@ describe("Entity builder", () => {
|
|||||||
'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'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("throw error when cascading a parent from a child", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
|
||||||
|
const user = model.define("user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const defineEmail = () =>
|
||||||
|
model
|
||||||
|
.define("email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
user: model.belongsTo(() => user),
|
||||||
|
})
|
||||||
|
.cascades({
|
||||||
|
// @ts-expect-error "User cannot be mentioned in cascades"
|
||||||
|
delete: ["user"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(defineEmail).toThrow(
|
||||||
|
'Cannot cascade delete "user" relationship(s) from "email" entity. Child to parent cascades are not allowed'
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Entity builder | manyToMany", () => {
|
describe("Entity builder | manyToMany", () => {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { RelationshipType, SchemaType } from "./types"
|
import { BelongsTo } from "./relations/belongs-to"
|
||||||
|
import {
|
||||||
|
SchemaType,
|
||||||
|
EntityCascades,
|
||||||
|
RelationshipType,
|
||||||
|
ExtractEntityRelations,
|
||||||
|
} from "./types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dml entity is a representation of a DML model with a unique
|
* Dml entity is a representation of a DML model with a unique
|
||||||
@@ -7,5 +13,50 @@ import { RelationshipType, SchemaType } from "./types"
|
|||||||
export class DmlEntity<
|
export class DmlEntity<
|
||||||
Schema extends Record<string, SchemaType<any> | RelationshipType<any>>
|
Schema extends Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
> {
|
> {
|
||||||
|
#cascades: EntityCascades<string[]> = {}
|
||||||
constructor(public name: string, public schema: Schema) {}
|
constructor(public name: string, public schema: Schema) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse entity to get its underlying information
|
||||||
|
*/
|
||||||
|
parse(): {
|
||||||
|
name: string
|
||||||
|
schema: SchemaType<any> | RelationshipType<any>
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
schema: this.schema as unknown as SchemaType<any> | RelationshipType<any>,
|
||||||
|
cascades: this.#cascades,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete actions to be performed when the entity is deleted. For example:
|
||||||
|
*
|
||||||
|
* You can configure relationship data to be deleted when the current
|
||||||
|
* entity is deleted.
|
||||||
|
*/
|
||||||
|
cascades(
|
||||||
|
options: EntityCascades<
|
||||||
|
ExtractEntityRelations<Schema, "hasOne" | "hasMany">
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const childToParentCascades = options.delete?.filter((relationship) => {
|
||||||
|
return this.schema[relationship] instanceof BelongsTo
|
||||||
|
})
|
||||||
|
|
||||||
|
if (childToParentCascades?.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Cannot cascade delete "${childToParentCascades.join(
|
||||||
|
", "
|
||||||
|
)}" relationship(s) from "${
|
||||||
|
this.name
|
||||||
|
}" entity. Child to parent cascades are not allowed`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#cascades = options
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ import { upperCaseFirst } from "../../common/upper-case-first"
|
|||||||
import type {
|
import type {
|
||||||
Infer,
|
Infer,
|
||||||
SchemaType,
|
SchemaType,
|
||||||
|
EntityCascades,
|
||||||
KnownDataTypes,
|
KnownDataTypes,
|
||||||
SchemaMetadata,
|
SchemaMetadata,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
@@ -97,13 +98,19 @@ function defineHasOneRelationship(
|
|||||||
relationship: RelationshipMetadata,
|
relationship: RelationshipMetadata,
|
||||||
relatedEntity: DmlEntity<
|
relatedEntity: DmlEntity<
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
>
|
>,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
) {
|
) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
OneToOne({
|
OneToOne({
|
||||||
entity: relatedModelName,
|
entity: relatedModelName,
|
||||||
nullable: relationship.nullable,
|
nullable: relationship.nullable,
|
||||||
mappedBy: relationship.mappedBy as any,
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
|
cascade: shouldRemoveRelated
|
||||||
|
? (["perist", "soft-remove"] as any)
|
||||||
|
: undefined,
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +122,19 @@ function defineHasManyRelationship(
|
|||||||
relationship: RelationshipMetadata,
|
relationship: RelationshipMetadata,
|
||||||
relatedEntity: DmlEntity<
|
relatedEntity: DmlEntity<
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
>
|
>,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
) {
|
) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
OneToMany({
|
OneToMany({
|
||||||
entity: relatedModelName,
|
entity: relatedModelName,
|
||||||
orphanRemoval: true,
|
orphanRemoval: true,
|
||||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||||
|
cascade: shouldRemoveRelated
|
||||||
|
? (["perist", "soft-remove"] as any)
|
||||||
|
: undefined,
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +156,20 @@ function defineBelongsToRelationship(
|
|||||||
) {
|
) {
|
||||||
const mappedBy =
|
const mappedBy =
|
||||||
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
||||||
const otherSideRelation = relatedEntity.schema[mappedBy]
|
const { schema: relationSchema, cascades: relationCascades } =
|
||||||
|
relatedEntity.parse()
|
||||||
|
|
||||||
|
const otherSideRelation = relationSchema[mappedBy]
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
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
|
* Ensure the mapped by is defined as relationship on the other side
|
||||||
*/
|
*/
|
||||||
@@ -165,6 +189,7 @@ function defineBelongsToRelationship(
|
|||||||
mapToPk: true,
|
mapToPk: true,
|
||||||
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
||||||
nullable: relationship.nullable,
|
nullable: relationship.nullable,
|
||||||
|
onDelete: shouldCascade ? "cascade" : undefined,
|
||||||
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
||||||
|
|
||||||
ManyToOne({
|
ManyToOne({
|
||||||
@@ -178,12 +203,12 @@ 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 (otherSideRelation instanceof HasOne) {
|
if (otherSideRelation instanceof HasOne) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
OneToOne({
|
OneToOne({
|
||||||
entity: relatedModelName,
|
entity: relatedModelName,
|
||||||
nullable: relationship.nullable,
|
nullable: relationship.nullable,
|
||||||
mappedBy: mappedBy,
|
mappedBy: mappedBy,
|
||||||
owner: true,
|
owner: true,
|
||||||
|
onDelete: shouldCascade ? "cascade" : undefined,
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -204,7 +229,8 @@ function defineManyToManyRelationship(
|
|||||||
relationship: RelationshipMetadata,
|
relationship: RelationshipMetadata,
|
||||||
relatedEntity: DmlEntity<
|
relatedEntity: DmlEntity<
|
||||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
>
|
>,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
) {
|
) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
ManyToMany({
|
ManyToMany({
|
||||||
@@ -218,7 +244,8 @@ function defineManyToManyRelationship(
|
|||||||
*/
|
*/
|
||||||
function defineRelationship(
|
function defineRelationship(
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
relationship: RelationshipMetadata
|
relationship: RelationshipMetadata,
|
||||||
|
cascades: EntityCascades<string[]>
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* We expect the relationship.entity to be a function that
|
* We expect the relationship.entity to be a function that
|
||||||
@@ -248,27 +275,36 @@ function defineRelationship(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Converting the related entity name (which should be in camelCase)
|
|
||||||
* to "PascalCase"
|
|
||||||
*/
|
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defining relationships
|
* Defining relationships
|
||||||
*/
|
*/
|
||||||
switch (relationship.type) {
|
switch (relationship.type) {
|
||||||
case "hasOne":
|
case "hasOne":
|
||||||
defineHasOneRelationship(MikroORMEntity, relationship, relatedEntity)
|
defineHasOneRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case "hasMany":
|
case "hasMany":
|
||||||
defineHasManyRelationship(MikroORMEntity, relationship, relatedEntity)
|
defineHasManyRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case "belongsTo":
|
case "belongsTo":
|
||||||
defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
|
defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
|
||||||
break
|
break
|
||||||
case "manyToMany":
|
case "manyToMany":
|
||||||
defineManyToManyRelationship(MikroORMEntity, relationship, relatedEntity)
|
defineManyToManyRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
cascades
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,12 +315,13 @@ function defineRelationship(
|
|||||||
* @todo: Handle soft deleted indexes and filters
|
* @todo: Handle soft deleted indexes and filters
|
||||||
* @todo: Finalize if custom pivot entities are needed
|
* @todo: Finalize if custom pivot entities are needed
|
||||||
*/
|
*/
|
||||||
export function createMikrORMEntity<
|
export function createMikrORMEntity<T extends DmlEntity<any>>(
|
||||||
T extends DmlEntity<Record<string, SchemaType<any> | RelationshipType<any>>>
|
entity: T
|
||||||
>(entity: T): Infer<T> {
|
): Infer<T> {
|
||||||
class MikroORMEntity {}
|
class MikroORMEntity {}
|
||||||
|
const { name, schema, cascades } = entity.parse()
|
||||||
|
|
||||||
const className = upperCaseFirst(entity.name)
|
const className = upperCaseFirst(name)
|
||||||
const tableName = pluralize(camelToSnakeCase(className))
|
const tableName = pluralize(camelToSnakeCase(className))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,12 +337,12 @@ export function createMikrORMEntity<
|
|||||||
/**
|
/**
|
||||||
* Processing schema fields
|
* Processing schema fields
|
||||||
*/
|
*/
|
||||||
Object.keys(entity.schema).forEach((property) => {
|
Object.entries(schema).forEach(([name, property]) => {
|
||||||
const field = entity.schema[property].parse(property)
|
const field = property.parse(name)
|
||||||
if ("fieldName" in field) {
|
if ("fieldName" in field) {
|
||||||
defineProperty(MikroORMEntity, field)
|
defineProperty(MikroORMEntity, field)
|
||||||
} else {
|
} else {
|
||||||
defineRelationship(MikroORMEntity, field)
|
defineRelationship(MikroORMEntity, field, cascades)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
RelationshipMetadata,
|
RelationshipMetadata,
|
||||||
RelationshipOptions,
|
RelationshipOptions,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
|
RelationshipTypes,
|
||||||
} from "../types"
|
} from "../types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,12 +12,15 @@ import {
|
|||||||
export abstract class BaseRelationship<T> implements RelationshipType<T> {
|
export abstract class BaseRelationship<T> implements RelationshipType<T> {
|
||||||
#referencedEntity: T
|
#referencedEntity: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the relationship
|
||||||
|
*/
|
||||||
protected options: RelationshipOptions
|
protected options: RelationshipOptions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relationship type.
|
* Relationship type
|
||||||
*/
|
*/
|
||||||
protected abstract relationshipType: RelationshipMetadata["type"]
|
abstract type: RelationshipTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type-only property to infer the JavScript data-type
|
* A type-only property to infer the JavScript data-type
|
||||||
@@ -38,7 +42,7 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
|
|||||||
nullable: false,
|
nullable: false,
|
||||||
mappedBy: this.options.mappedBy,
|
mappedBy: this.options.mappedBy,
|
||||||
entity: this.#referencedEntity,
|
entity: this.#referencedEntity,
|
||||||
type: this.relationshipType,
|
type: this.type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { BaseRelationship } from "./base"
|
import { BaseRelationship } from "./base"
|
||||||
import { RelationshipMetadata } from "../types"
|
import { RelationshipTypes } from "../types"
|
||||||
import { NullableModifier } from "../modifiers/nullable"
|
import { NullableModifier } from "./nullable"
|
||||||
|
|
||||||
export class BelongsTo<T> extends BaseRelationship<T> {
|
export class BelongsTo<T> extends BaseRelationship<T> {
|
||||||
protected relationshipType: RelationshipMetadata["type"] = "belongsTo"
|
type = "belongsTo" as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply nullable modifier on the schema
|
* Apply nullable modifier on the schema
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseRelationship } from "./base"
|
import { BaseRelationship } from "./base"
|
||||||
import { RelationshipMetadata } from "../types"
|
import { RelationshipTypes } from "../types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HasMany relationship defines a relationship between two entities
|
* HasMany relationship defines a relationship between two entities
|
||||||
@@ -12,5 +12,5 @@ import { RelationshipMetadata } from "../types"
|
|||||||
* - A user HasMany addresses
|
* - A user HasMany addresses
|
||||||
*/
|
*/
|
||||||
export class HasMany<T> extends BaseRelationship<T> {
|
export class HasMany<T> extends BaseRelationship<T> {
|
||||||
protected relationshipType: RelationshipMetadata["type"] = "hasMany"
|
type = "hasMany" as const
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BaseRelationship } from "./base"
|
import { BaseRelationship } from "./base"
|
||||||
import { NullableModifier } from "../modifiers/nullable"
|
import { NullableModifier } from "./nullable"
|
||||||
import { RelationshipMetadata } from "../types"
|
import { RelationshipTypes } from "../types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HasOne relationship defines a relationship between two entities
|
* HasOne relationship defines a relationship between two entities
|
||||||
@@ -13,7 +13,7 @@ import { RelationshipMetadata } from "../types"
|
|||||||
* of the "HasOne" relationship
|
* of the "HasOne" relationship
|
||||||
*/
|
*/
|
||||||
export class HasOne<T> extends BaseRelationship<T> {
|
export class HasOne<T> extends BaseRelationship<T> {
|
||||||
protected relationshipType: RelationshipMetadata["type"] = "hasOne"
|
type = "hasOne" as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply nullable modifier on the schema
|
* Apply nullable modifier on the schema
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseRelationship } from "./base"
|
import { BaseRelationship } from "./base"
|
||||||
import { RelationshipMetadata } from "../types"
|
import { RelationshipTypes } from "../types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ManyToMany relationship defines a relationship between two entities
|
* ManyToMany relationship defines a relationship between two entities
|
||||||
@@ -13,5 +13,5 @@ import { RelationshipMetadata } from "../types"
|
|||||||
* relationship between two entities
|
* relationship between two entities
|
||||||
*/
|
*/
|
||||||
export class ManyToMany<T> extends BaseRelationship<T> {
|
export class ManyToMany<T> extends BaseRelationship<T> {
|
||||||
protected relationshipType: RelationshipMetadata["type"] = "manyToMany"
|
type = "manyToMany" as const
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { RelationshipType, SchemaType } from "../types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nullable modifier marks a schema node as nullable
|
||||||
|
*/
|
||||||
|
export class NullableModifier<T, Relation extends RelationshipType<T>>
|
||||||
|
implements RelationshipType<T | null>
|
||||||
|
{
|
||||||
|
declare type: RelationshipType<T>["type"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type-only property to infer the JavScript data-type
|
||||||
|
* of the schema property
|
||||||
|
*/
|
||||||
|
declare $dataType: T | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parent schema on which the nullable modifier is
|
||||||
|
* applied
|
||||||
|
*/
|
||||||
|
#relation: Relation
|
||||||
|
|
||||||
|
constructor(relation: Relation) {
|
||||||
|
this.#relation = relation
|
||||||
|
this.type = relation.type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the serialized metadata
|
||||||
|
*/
|
||||||
|
parse(fieldName: string) {
|
||||||
|
const relation = this.#relation.parse(fieldName)
|
||||||
|
relation.nullable = true
|
||||||
|
return relation
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SchemaMetadata, SchemaType } from "../types"
|
import { SchemaMetadata, SchemaType } from "../types"
|
||||||
import { NullableModifier } from "../modifiers/nullable"
|
import { NullableModifier } from "./nullable"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base schema class offers shared affordances to define
|
* The base schema class offers shared affordances to define
|
||||||
|
|||||||
File renamed without changes.
@@ -12,14 +12,13 @@ export type KnownDataTypes =
|
|||||||
| "json"
|
| "json"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The available on Delete actions
|
* List of available relationships at DML level
|
||||||
*/
|
*/
|
||||||
export type OnDeleteActions =
|
export type RelationshipTypes =
|
||||||
| "cascade"
|
| "hasOne"
|
||||||
| "no action"
|
| "hasMany"
|
||||||
| "set null"
|
| "belongsTo"
|
||||||
| "set default"
|
| "manyToMany"
|
||||||
| (string & {})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any field that contains "nullable" and "optional" properties
|
* Any field that contains "nullable" and "optional" properties
|
||||||
@@ -72,7 +71,7 @@ export type RelationshipOptions = {
|
|||||||
*/
|
*/
|
||||||
export type RelationshipMetadata = MaybeFieldMetadata & {
|
export type RelationshipMetadata = MaybeFieldMetadata & {
|
||||||
name: string
|
name: string
|
||||||
type: "hasOne" | "hasMany" | "belongsTo" | "manyToMany"
|
type: RelationshipTypes
|
||||||
entity: unknown
|
entity: unknown
|
||||||
mappedBy?: string
|
mappedBy?: string
|
||||||
}
|
}
|
||||||
@@ -84,6 +83,7 @@ export type RelationshipMetadata = MaybeFieldMetadata & {
|
|||||||
*/
|
*/
|
||||||
export type RelationshipType<T> = {
|
export type RelationshipType<T> = {
|
||||||
$dataType: T
|
$dataType: T
|
||||||
|
type: RelationshipTypes
|
||||||
parse(relationshipName: string): RelationshipMetadata
|
parse(relationshipName: string): RelationshipMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,3 +108,25 @@ export type Infer<T> = T extends DmlEntity<infer Schema>
|
|||||||
: Schema[K]["$dataType"]
|
: Schema[K]["$dataType"]
|
||||||
}>
|
}>
|
||||||
: never
|
: never
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts names of relationships from a schema
|
||||||
|
*/
|
||||||
|
export type ExtractEntityRelations<
|
||||||
|
Schema extends Record<string, any>,
|
||||||
|
OfType extends RelationshipTypes
|
||||||
|
> = {
|
||||||
|
[K in keyof Schema & string]: Schema[K] extends RelationshipType<any>
|
||||||
|
? Schema[K] extends { type: OfType }
|
||||||
|
? K
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
}[keyof Schema & string][]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actions to cascade from a given entity to its
|
||||||
|
* relationship.
|
||||||
|
*/
|
||||||
|
export type EntityCascades<Relationships> = {
|
||||||
|
delete?: Relationships
|
||||||
|
}
|
||||||
Reference in new issue
Block a user