From 2af3f9e9541a5393c7cf340779131b0601dc0cdf Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 14 Jun 2024 13:45:11 +0530 Subject: [PATCH] Add support for cascades to DML (#7721) --- .../dml/__tests__/base-relationship.spec.ts | 2 +- .../src/dml/__tests__/entity-builder.spec.ts | 418 ++++++++++++++++++ packages/core/utils/src/dml/entity.ts | 53 ++- .../dml/helpers/create-mikro-orm-entity.ts | 83 +++- packages/core/utils/src/dml/relations/base.ts | 10 +- .../utils/src/dml/relations/belongs-to.ts | 6 +- .../core/utils/src/dml/relations/has-many.ts | 4 +- .../core/utils/src/dml/relations/has-one.ts | 6 +- .../utils/src/dml/relations/many-to-many.ts | 4 +- .../core/utils/src/dml/relations/nullable.ts | 36 ++ packages/core/utils/src/dml/schema/base.ts | 2 +- .../src/dml/{modifiers => schema}/nullable.ts | 0 packages/core/utils/src/dml/types.ts | 38 +- 13 files changed, 615 insertions(+), 47 deletions(-) create mode 100644 packages/core/utils/src/dml/relations/nullable.ts rename packages/core/utils/src/dml/{modifiers => schema}/nullable.ts (100%) diff --git a/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts b/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts index d695e67732..3f909fc46a 100644 --- a/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts @@ -5,7 +5,7 @@ import { BaseRelationship } from "../relations/base" describe("Base relationship", () => { test("define a custom relationship", () => { class HasOne extends BaseRelationship { - protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne" + type: "hasOne" = "hasOne" } const user = { diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index 092a382f50..66bae76ef1 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -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", () => { diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index 591618891c..2b6753a154 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -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 | RelationshipType> > { + #cascades: EntityCascades = {} constructor(public name: string, public schema: Schema) {} + + /** + * Parse entity to get its underlying information + */ + parse(): { + name: string + schema: SchemaType | RelationshipType + cascades: EntityCascades + } { + return { + name: this.name, + schema: this.schema as unknown as SchemaType | RelationshipType, + 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 + > + ) { + 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 + } } diff --git a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index 339db15664..379cd9f420 100644 --- a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts +++ b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts @@ -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 | RelationshipType> - > + >, + cascades: EntityCascades ) { 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 | RelationshipType> - > + >, + cascades: EntityCascades ) { 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 | RelationshipType> - > + >, + cascades: EntityCascades ) { const relatedModelName = upperCaseFirst(relatedEntity.name) ManyToMany({ @@ -218,7 +244,8 @@ function defineManyToManyRelationship( */ function defineRelationship( MikroORMEntity: EntityConstructor, - relationship: RelationshipMetadata + relationship: RelationshipMetadata, + cascades: EntityCascades ) { /** * 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 | RelationshipType>> ->(entity: T): Infer { +export function createMikrORMEntity>( + entity: T +): Infer { 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) } }) diff --git a/packages/core/utils/src/dml/relations/base.ts b/packages/core/utils/src/dml/relations/base.ts index cc519f6208..62ec0ffc6b 100644 --- a/packages/core/utils/src/dml/relations/base.ts +++ b/packages/core/utils/src/dml/relations/base.ts @@ -2,6 +2,7 @@ import { RelationshipMetadata, RelationshipOptions, RelationshipType, + RelationshipTypes, } from "../types" /** @@ -11,12 +12,15 @@ import { export abstract class BaseRelationship implements RelationshipType { #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 implements RelationshipType { nullable: false, mappedBy: this.options.mappedBy, entity: this.#referencedEntity, - type: this.relationshipType, + type: this.type, } } } diff --git a/packages/core/utils/src/dml/relations/belongs-to.ts b/packages/core/utils/src/dml/relations/belongs-to.ts index 6658fb7b58..d513faf70a 100644 --- a/packages/core/utils/src/dml/relations/belongs-to.ts +++ b/packages/core/utils/src/dml/relations/belongs-to.ts @@ -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 extends BaseRelationship { - protected relationshipType: RelationshipMetadata["type"] = "belongsTo" + type = "belongsTo" as const /** * Apply nullable modifier on the schema diff --git a/packages/core/utils/src/dml/relations/has-many.ts b/packages/core/utils/src/dml/relations/has-many.ts index 18adba4b2c..7f15e656ae 100644 --- a/packages/core/utils/src/dml/relations/has-many.ts +++ b/packages/core/utils/src/dml/relations/has-many.ts @@ -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 extends BaseRelationship { - protected relationshipType: RelationshipMetadata["type"] = "hasMany" + type = "hasMany" as const } diff --git a/packages/core/utils/src/dml/relations/has-one.ts b/packages/core/utils/src/dml/relations/has-one.ts index 53909987b4..80c77a6433 100644 --- a/packages/core/utils/src/dml/relations/has-one.ts +++ b/packages/core/utils/src/dml/relations/has-one.ts @@ -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 extends BaseRelationship { - protected relationshipType: RelationshipMetadata["type"] = "hasOne" + type = "hasOne" as const /** * Apply nullable modifier on the schema diff --git a/packages/core/utils/src/dml/relations/many-to-many.ts b/packages/core/utils/src/dml/relations/many-to-many.ts index dd8da98860..5d58c8ebf7 100644 --- a/packages/core/utils/src/dml/relations/many-to-many.ts +++ b/packages/core/utils/src/dml/relations/many-to-many.ts @@ -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 extends BaseRelationship { - protected relationshipType: RelationshipMetadata["type"] = "manyToMany" + type = "manyToMany" as const } diff --git a/packages/core/utils/src/dml/relations/nullable.ts b/packages/core/utils/src/dml/relations/nullable.ts new file mode 100644 index 0000000000..4b467d415f --- /dev/null +++ b/packages/core/utils/src/dml/relations/nullable.ts @@ -0,0 +1,36 @@ +import { RelationshipType, SchemaType } from "../types" + +/** + * Nullable modifier marks a schema node as nullable + */ +export class NullableModifier> + implements RelationshipType +{ + declare type: RelationshipType["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 + } +} diff --git a/packages/core/utils/src/dml/schema/base.ts b/packages/core/utils/src/dml/schema/base.ts index 2277856000..3558e481c0 100644 --- a/packages/core/utils/src/dml/schema/base.ts +++ b/packages/core/utils/src/dml/schema/base.ts @@ -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 diff --git a/packages/core/utils/src/dml/modifiers/nullable.ts b/packages/core/utils/src/dml/schema/nullable.ts similarity index 100% rename from packages/core/utils/src/dml/modifiers/nullable.ts rename to packages/core/utils/src/dml/schema/nullable.ts diff --git a/packages/core/utils/src/dml/types.ts b/packages/core/utils/src/dml/types.ts index 1241321dab..51cbe0f9ab 100644 --- a/packages/core/utils/src/dml/types.ts +++ b/packages/core/utils/src/dml/types.ts @@ -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 = { $dataType: T + type: RelationshipTypes parse(relationshipName: string): RelationshipMetadata } @@ -108,3 +108,25 @@ export type Infer = T extends DmlEntity : Schema[K]["$dataType"] }> : never + +/** + * Extracts names of relationships from a schema + */ +export type ExtractEntityRelations< + Schema extends Record, + OfType extends RelationshipTypes +> = { + [K in keyof Schema & string]: Schema[K] extends RelationshipType + ? 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 = { + delete?: Relationships +}