Add support for cascades to DML (#7721)
This commit is contained in:
@@ -5,7 +5,7 @@ import { BaseRelationship } from "../relations/base"
|
||||
describe("Base relationship", () => {
|
||||
test("define a custom relationship", () => {
|
||||
class HasOne<T> extends BaseRelationship<T> {
|
||||
protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne"
|
||||
type: "hasOne" = "hasOne"
|
||||
}
|
||||
|
||||
const user = {
|
||||
|
||||
@@ -400,6 +400,7 @@ describe("Entity builder", () => {
|
||||
name: "email",
|
||||
entity: "Email",
|
||||
nullable: false,
|
||||
mappedBy: "user",
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -452,6 +453,240 @@ describe("Entity builder", () => {
|
||||
name: "emails",
|
||||
entity: "Email",
|
||||
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", () => {
|
||||
@@ -638,6 +1029,7 @@ describe("Entity builder", () => {
|
||||
name: "email",
|
||||
entity: "Email",
|
||||
nullable: false,
|
||||
mappedBy: "user",
|
||||
},
|
||||
})
|
||||
|
||||
@@ -745,6 +1137,7 @@ describe("Entity builder", () => {
|
||||
name: "email",
|
||||
entity: "Email",
|
||||
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'
|
||||
)
|
||||
})
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -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
|
||||
@@ -7,5 +13,50 @@ import { RelationshipType, SchemaType } from "./types"
|
||||
export class DmlEntity<
|
||||
Schema extends Record<string, SchemaType<any> | RelationshipType<any>>
|
||||
> {
|
||||
#cascades: EntityCascades<string[]> = {}
|
||||
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 {
|
||||
Infer,
|
||||
SchemaType,
|
||||
EntityCascades,
|
||||
KnownDataTypes,
|
||||
SchemaMetadata,
|
||||
RelationshipType,
|
||||
@@ -97,13 +98,19 @@ function defineHasOneRelationship(
|
||||
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 as any,
|
||||
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
|
||||
cascade: shouldRemoveRelated
|
||||
? (["perist", "soft-remove"] as any)
|
||||
: undefined,
|
||||
})(MikroORMEntity.prototype, relationship.name)
|
||||
}
|
||||
|
||||
@@ -115,13 +122,19 @@ function defineHasManyRelationship(
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -143,9 +156,20 @@ function defineBelongsToRelationship(
|
||||
) {
|
||||
const mappedBy =
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -165,6 +189,7 @@ function defineBelongsToRelationship(
|
||||
mapToPk: true,
|
||||
fieldName: camelToSnakeCase(`${relationship.name}Id`),
|
||||
nullable: relationship.nullable,
|
||||
onDelete: shouldCascade ? "cascade" : undefined,
|
||||
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
|
||||
|
||||
ManyToOne({
|
||||
@@ -178,12 +203,12 @@ function defineBelongsToRelationship(
|
||||
* Otherside is a has one. Hence we should defined a OneToOne
|
||||
*/
|
||||
if (otherSideRelation instanceof HasOne) {
|
||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||
OneToOne({
|
||||
entity: relatedModelName,
|
||||
nullable: relationship.nullable,
|
||||
mappedBy: mappedBy,
|
||||
owner: true,
|
||||
onDelete: shouldCascade ? "cascade" : undefined,
|
||||
})(MikroORMEntity.prototype, relationship.name)
|
||||
return
|
||||
}
|
||||
@@ -204,7 +229,8 @@ function defineManyToManyRelationship(
|
||||
relationship: RelationshipMetadata,
|
||||
relatedEntity: DmlEntity<
|
||||
Record<string, SchemaType<any> | RelationshipType<any>>
|
||||
>
|
||||
>,
|
||||
cascades: EntityCascades<string[]>
|
||||
) {
|
||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||
ManyToMany({
|
||||
@@ -218,7 +244,8 @@ function defineManyToManyRelationship(
|
||||
*/
|
||||
function defineRelationship(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
relationship: RelationshipMetadata
|
||||
relationship: RelationshipMetadata,
|
||||
cascades: EntityCascades<string[]>
|
||||
) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
switch (relationship.type) {
|
||||
case "hasOne":
|
||||
defineHasOneRelationship(MikroORMEntity, relationship, relatedEntity)
|
||||
defineHasOneRelationship(
|
||||
MikroORMEntity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
cascades
|
||||
)
|
||||
break
|
||||
case "hasMany":
|
||||
defineHasManyRelationship(MikroORMEntity, relationship, relatedEntity)
|
||||
defineHasManyRelationship(
|
||||
MikroORMEntity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
cascades
|
||||
)
|
||||
break
|
||||
case "belongsTo":
|
||||
defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
|
||||
break
|
||||
case "manyToMany":
|
||||
defineManyToManyRelationship(MikroORMEntity, relationship, relatedEntity)
|
||||
defineManyToManyRelationship(
|
||||
MikroORMEntity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
cascades
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -279,12 +315,13 @@ function defineRelationship(
|
||||
* @todo: Handle soft deleted indexes and filters
|
||||
* @todo: Finalize if custom pivot entities are needed
|
||||
*/
|
||||
export function createMikrORMEntity<
|
||||
T extends DmlEntity<Record<string, SchemaType<any> | RelationshipType<any>>>
|
||||
>(entity: T): Infer<T> {
|
||||
export function createMikrORMEntity<T extends DmlEntity<any>>(
|
||||
entity: T
|
||||
): Infer<T> {
|
||||
class MikroORMEntity {}
|
||||
const { name, schema, cascades } = entity.parse()
|
||||
|
||||
const className = upperCaseFirst(entity.name)
|
||||
const className = upperCaseFirst(name)
|
||||
const tableName = pluralize(camelToSnakeCase(className))
|
||||
|
||||
/**
|
||||
@@ -300,12 +337,12 @@ export function createMikrORMEntity<
|
||||
/**
|
||||
* Processing schema fields
|
||||
*/
|
||||
Object.keys(entity.schema).forEach((property) => {
|
||||
const field = entity.schema[property].parse(property)
|
||||
Object.entries(schema).forEach(([name, property]) => {
|
||||
const field = property.parse(name)
|
||||
if ("fieldName" in field) {
|
||||
defineProperty(MikroORMEntity, field)
|
||||
} else {
|
||||
defineRelationship(MikroORMEntity, field)
|
||||
defineRelationship(MikroORMEntity, field, cascades)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
RelationshipMetadata,
|
||||
RelationshipOptions,
|
||||
RelationshipType,
|
||||
RelationshipTypes,
|
||||
} from "../types"
|
||||
|
||||
/**
|
||||
@@ -11,12 +12,15 @@ import {
|
||||
export abstract class BaseRelationship<T> implements RelationshipType<T> {
|
||||
#referencedEntity: T
|
||||
|
||||
/**
|
||||
* Configuration options for the relationship
|
||||
*/
|
||||
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
|
||||
@@ -38,7 +42,7 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
|
||||
nullable: false,
|
||||
mappedBy: this.options.mappedBy,
|
||||
entity: this.#referencedEntity,
|
||||
type: this.relationshipType,
|
||||
type: this.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { BaseRelationship } from "./base"
|
||||
import { RelationshipMetadata } from "../types"
|
||||
import { NullableModifier } from "../modifiers/nullable"
|
||||
import { RelationshipTypes } from "../types"
|
||||
import { NullableModifier } from "./nullable"
|
||||
|
||||
export class BelongsTo<T> extends BaseRelationship<T> {
|
||||
protected relationshipType: RelationshipMetadata["type"] = "belongsTo"
|
||||
type = "belongsTo" as const
|
||||
|
||||
/**
|
||||
* Apply nullable modifier on the schema
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseRelationship } from "./base"
|
||||
import { RelationshipMetadata } from "../types"
|
||||
import { RelationshipTypes } from "../types"
|
||||
|
||||
/**
|
||||
* HasMany relationship defines a relationship between two entities
|
||||
@@ -12,5 +12,5 @@ import { RelationshipMetadata } from "../types"
|
||||
* - A user HasMany addresses
|
||||
*/
|
||||
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 { NullableModifier } from "../modifiers/nullable"
|
||||
import { RelationshipMetadata } from "../types"
|
||||
import { NullableModifier } from "./nullable"
|
||||
import { RelationshipTypes } from "../types"
|
||||
|
||||
/**
|
||||
* HasOne relationship defines a relationship between two entities
|
||||
@@ -13,7 +13,7 @@ import { RelationshipMetadata } from "../types"
|
||||
* of the "HasOne" relationship
|
||||
*/
|
||||
export class HasOne<T> extends BaseRelationship<T> {
|
||||
protected relationshipType: RelationshipMetadata["type"] = "hasOne"
|
||||
type = "hasOne" as const
|
||||
|
||||
/**
|
||||
* Apply nullable modifier on the schema
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseRelationship } from "./base"
|
||||
import { RelationshipMetadata } from "../types"
|
||||
import { RelationshipTypes } from "../types"
|
||||
|
||||
/**
|
||||
* ManyToMany relationship defines a relationship between two entities
|
||||
@@ -13,5 +13,5 @@ import { RelationshipMetadata } from "../types"
|
||||
* relationship between two entities
|
||||
*/
|
||||
export class ManyToMany<T> extends BaseRelationship<T> {
|
||||
protected relationshipType: RelationshipMetadata["type"] = "manyToMany"
|
||||
type = "manyToMany" as const
|
||||
}
|
||||
|
||||
36
packages/core/utils/src/dml/relations/nullable.ts
Normal file
36
packages/core/utils/src/dml/relations/nullable.ts
Normal file
@@ -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 { NullableModifier } from "../modifiers/nullable"
|
||||
import { NullableModifier } from "./nullable"
|
||||
|
||||
/**
|
||||
* The base schema class offers shared affordances to define
|
||||
|
||||
@@ -12,14 +12,13 @@ export type KnownDataTypes =
|
||||
| "json"
|
||||
|
||||
/**
|
||||
* The available on Delete actions
|
||||
* List of available relationships at DML level
|
||||
*/
|
||||
export type OnDeleteActions =
|
||||
| "cascade"
|
||||
| "no action"
|
||||
| "set null"
|
||||
| "set default"
|
||||
| (string & {})
|
||||
export type RelationshipTypes =
|
||||
| "hasOne"
|
||||
| "hasMany"
|
||||
| "belongsTo"
|
||||
| "manyToMany"
|
||||
|
||||
/**
|
||||
* Any field that contains "nullable" and "optional" properties
|
||||
@@ -72,7 +71,7 @@ export type RelationshipOptions = {
|
||||
*/
|
||||
export type RelationshipMetadata = MaybeFieldMetadata & {
|
||||
name: string
|
||||
type: "hasOne" | "hasMany" | "belongsTo" | "manyToMany"
|
||||
type: RelationshipTypes
|
||||
entity: unknown
|
||||
mappedBy?: string
|
||||
}
|
||||
@@ -84,6 +83,7 @@ export type RelationshipMetadata = MaybeFieldMetadata & {
|
||||
*/
|
||||
export type RelationshipType<T> = {
|
||||
$dataType: T
|
||||
type: RelationshipTypes
|
||||
parse(relationshipName: string): RelationshipMetadata
|
||||
}
|
||||
|
||||
@@ -108,3 +108,25 @@ export type Infer<T> = T extends DmlEntity<infer Schema>
|
||||
: Schema[K]["$dataType"]
|
||||
}>
|
||||
: 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