diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index a49d9d33f3158..f45752fa8fd27 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -51,6 +51,7 @@ export type KnownDataTypes = */ export type RelationshipTypes = | "hasOne" + | "hasOneWithFK" | "hasMany" | "belongsTo" | "manyToMany" @@ -111,7 +112,7 @@ export type RelationshipMetadata = { name: string type: RelationshipTypes entity: unknown - nullable: boolean + nullable?: boolean mappedBy?: string searchable: boolean options: Record @@ -142,9 +143,9 @@ export interface EntityConstructor extends Function { * "belongsTo" relation meaning "hasOne" and "ManyToOne" */ export type InferForeignKeys = { - [K in keyof Schema as Schema[K] extends { type: "belongsTo" } + [K in keyof Schema as Schema[K] extends { $foreignKey: true } ? `${K & string}_id` - : never]: Schema[K] extends { type: "belongsTo" } + : never]: Schema[K] extends { $foreignKey: true } ? null extends Schema[K]["$dataType"] ? string | null : string @@ -191,7 +192,7 @@ export type InferSchemaFields = Prettify< [K in keyof Schema]: Schema[K] extends RelationshipType ? Schema[K]["type"] extends "belongsTo" ? InferBelongsToFields - : Schema[K]["type"] extends "hasOne" + : Schema[K]["type"] extends "hasOne" | "hasOneWithFK" ? InferHasOneFields : Schema[K]["type"] extends "hasMany" ? InferHasManyFields 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 b14fed3d32faf..4dba3a02b3a1a 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -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() + 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() + 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() + 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() + 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", () => { test("should define indexes for an entity", () => { const group = model.define("group", { diff --git a/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts b/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts index 49643647068d7..d12a93793dfa9 100644 --- a/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts @@ -1,6 +1,7 @@ import { expectTypeOf } from "expect-type" import { TextProperty } from "../properties/text" import { HasOne } from "../relations/has-one" +import { HasOneWithForeignKey } from "../relations/has-one-fk" describe("HasOne relationship", () => { test("define hasOne relationship", () => { @@ -57,4 +58,23 @@ describe("HasOne relationship", () => { 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, + }) + }) }) diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index ac4396d7ebf15..f93d2a2044bb0 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -25,6 +25,7 @@ import { BelongsTo } from "./relations/belongs-to" import { HasMany } from "./relations/has-many" import { HasOne } from "./relations/has-one" import { ManyToMany } from "./relations/many-to-many" +import { HasOneWithForeignKey } from "./relations/has-one-fk" /** * The implicit properties added by EntityBuilder in every schema @@ -345,7 +346,27 @@ export class EntityBuilder { * * @customNamespace Relationship Methods */ - hasOne(entityBuilder: T, options?: RelationshipOptions) { + hasOne( + entityBuilder: T, + options: RelationshipOptions & { + foreignKey: true + } + ): HasOneWithForeignKey + hasOne( + entityBuilder: T, + options?: RelationshipOptions & { + foreignKey?: false + } + ): HasOne + hasOne( + entityBuilder: T, + options?: RelationshipOptions & { + foreignKey?: boolean + } + ): HasOneWithForeignKey | HasOne { + if (options?.foreignKey) { + return new HasOneWithForeignKey(entityBuilder, options || {}) + } return new HasOne(entityBuilder, options || {}) } diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index 0748da3d6f225..b4d00755b6169 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -139,7 +139,7 @@ export class DmlEntity< */ cascades( options: EntityCascades< - ExtractEntityRelations + ExtractEntityRelations > ) { const childToParentCascades = options.delete?.filter((relationship) => { diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts index 10f8a79823fc6..b41a704560ff5 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts @@ -22,6 +22,7 @@ import { parseEntityName } from "./parse-entity-name" import { camelToSnakeCase, pluralize } from "../../../common" import { applyEntityIndexes } from "../mikro-orm/apply-indexes" import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many" +import { HasOneWithForeignKey } from "../../relations/has-one-fk" type Context = { MANY_TO_MANY_TRACKED_RELATIONS: Record @@ -150,6 +151,43 @@ export function defineHasOneRelationship( })(MikroORMEntity.prototype, relationship.name) } +/** + * Defines has one relationship with Foreign key on the MikroORM + * entity + */ +export function defineHasOneWithFKRelationship( + MikroORMEntity: EntityConstructor, + relationship: RelationshipMetadata, + { relatedModelName }: { relatedModelName: string }, + cascades: EntityCascades +) { + 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 */ @@ -225,7 +263,10 @@ export function defineBelongsToRelationship( * to associate a relation (through the relation or the foreign key) we need to handle it * specifically */ - if (HasOne.isHasOne(otherSideRelation)) { + if ( + HasOne.isHasOne(otherSideRelation) || + HasOneWithForeignKey.isHasOneWithForeignKey(otherSideRelation) + ) { const relationMeta = this.__meta.relations.find( (relation) => relation.name === relationship.name ).targetMeta @@ -317,7 +358,10 @@ export function defineBelongsToRelationship( /** * 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`) OneToOne({ @@ -600,6 +644,14 @@ export function defineRelationship( cascades ) break + case "hasOneWithFK": + defineHasOneWithFKRelationship( + MikroORMEntity, + relationship, + relatedEntityInfo, + cascades + ) + break case "hasMany": defineHasManyRelationship( MikroORMEntity, diff --git a/packages/core/utils/src/dml/relations/base.ts b/packages/core/utils/src/dml/relations/base.ts index e29e74f2058c6..ea343af09b2e2 100644 --- a/packages/core/utils/src/dml/relations/base.ts +++ b/packages/core/utils/src/dml/relations/base.ts @@ -56,8 +56,6 @@ export abstract class BaseRelationship implements RelationshipType { * }) * * export default Product - * - * @customNamespace Property Configuration Methods */ searchable() { this.#searchable = true diff --git a/packages/core/utils/src/dml/relations/belongs-to.ts b/packages/core/utils/src/dml/relations/belongs-to.ts index 70ac8321d96a7..cbd40fd0713d8 100644 --- a/packages/core/utils/src/dml/relations/belongs-to.ts +++ b/packages/core/utils/src/dml/relations/belongs-to.ts @@ -3,6 +3,7 @@ import { RelationNullableModifier } from "./nullable" export class BelongsTo extends BaseRelationship { type = "belongsTo" as const + declare $foreignKey: true static isBelongsTo(relationship: any): relationship is BelongsTo { return relationship?.type === "belongsTo" @@ -12,6 +13,6 @@ export class BelongsTo extends BaseRelationship { * Apply nullable modifier on the schema */ nullable() { - return new RelationNullableModifier>(this) + return new RelationNullableModifier, true>(this) } } diff --git a/packages/core/utils/src/dml/relations/has-one-fk.ts b/packages/core/utils/src/dml/relations/has-one-fk.ts new file mode 100644 index 0000000000000..a6123f59f3ad6 --- /dev/null +++ b/packages/core/utils/src/dml/relations/has-one-fk.ts @@ -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 extends BaseRelationship { + type = "hasOneWithFK" as const + declare $foreignKey: true + + static isHasOneWithForeignKey( + relationship: any + ): relationship is HasOneWithForeignKey { + return relationship?.type === "hasOneWithFK" + } + + /** + * Apply nullable modifier on the schema + */ + nullable() { + return new RelationNullableModifier, true>(this) + } +} diff --git a/packages/core/utils/src/dml/relations/has-one.ts b/packages/core/utils/src/dml/relations/has-one.ts index f6def0ace9170..73f059bf9b1c1 100644 --- a/packages/core/utils/src/dml/relations/has-one.ts +++ b/packages/core/utils/src/dml/relations/has-one.ts @@ -22,6 +22,6 @@ export class HasOne extends BaseRelationship { * Apply nullable modifier on the schema */ nullable() { - return new RelationNullableModifier>(this) + return new RelationNullableModifier, false>(this) } } diff --git a/packages/core/utils/src/dml/relations/nullable.ts b/packages/core/utils/src/dml/relations/nullable.ts index 23f54cb527c49..c65262a7ae3d0 100644 --- a/packages/core/utils/src/dml/relations/nullable.ts +++ b/packages/core/utils/src/dml/relations/nullable.ts @@ -6,15 +6,18 @@ const IsNullableModifier = Symbol.for("isNullableModifier") /** * Nullable modifier marks a schema node as nullable */ -export class RelationNullableModifier> - implements RelationshipType +export class RelationNullableModifier< + T, + Relation extends RelationshipType, + ForeignKey extends boolean +> implements RelationshipType { [IsNullableModifier]: true = true; [IsRelationship]: true = true static isNullableModifier( modifier: any - ): modifier is RelationNullableModifier { + ): modifier is RelationNullableModifier { return !!modifier?.[IsNullableModifier] } @@ -25,6 +28,7 @@ export class RelationNullableModifier> * of the schema property */ declare $dataType: T | null + declare $foreignKey: ForeignKey /** * The parent schema on which the nullable modifier is