From fbd8eef18b56719227575f3d83e86c4ae6471f81 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 13 Jun 2024 11:19:53 +0530 Subject: [PATCH] Overall revamp of relationships (#7690) --- ...ship.spec.ts => base-relationship.spec.ts} | 16 +- ...ase_schema.spec.ts => base-schema.spec.ts} | 0 ..._schema.spec.ts => boolean-schema.spec.ts} | 0 ...chema.spec.ts => date-time-schema.spec.ts} | 2 +- .../src/dml/__tests__/entity-builder.spec.ts | 1157 +++++++++++++++++ .../src/dml/__tests__/entity_builder.spec.ts | 509 -------- ...num_schema.spec.ts => enum-schema.spec.ts} | 0 ....spec.ts => has-many-relationship.spec.ts} | 3 +- ...p.spec.ts => has-one-relationship.spec.ts} | 28 +- ...son_schema.spec.ts => json-schema.spec.ts} | 0 ...y_to_many.spec.ts => many-to-many.spec.ts} | 3 +- ...r_schema.spec.ts => number-schema.spec.ts} | 0 ...ext_schema.spec.ts => text-schema.spec.ts} | 0 .../{entity_builder.ts => entity-builder.ts} | 33 +- ...m_entity.ts => create-mikro-orm-entity.ts} | 169 ++- .../core/utils/src/dml/modifiers/nullable.ts | 17 +- packages/core/utils/src/dml/relations/base.ts | 23 +- .../utils/src/dml/relations/belongs-to.ts | 14 + .../relations/{has_many.ts => has-many.ts} | 0 .../dml/relations/{has_one.ts => has-one.ts} | 8 + .../src/dml/relations/has_one_through_many.ts | 6 - .../{many_to_many.ts => many-to-many.ts} | 0 packages/core/utils/src/dml/schema/base.ts | 2 +- .../dml/schema/{date_time.ts => date-time.ts} | 0 packages/core/utils/src/dml/types.ts | 23 +- 25 files changed, 1388 insertions(+), 625 deletions(-) rename packages/core/utils/src/dml/__tests__/{has_one_relationship.spec.ts => base-relationship.spec.ts} (51%) rename packages/core/utils/src/dml/__tests__/{base_schema.spec.ts => base-schema.spec.ts} (100%) rename packages/core/utils/src/dml/__tests__/{boolean_schema.spec.ts => boolean-schema.spec.ts} (100%) rename packages/core/utils/src/dml/__tests__/{date_time_schema.spec.ts => date-time-schema.spec.ts} (89%) create mode 100644 packages/core/utils/src/dml/__tests__/entity-builder.spec.ts delete mode 100644 packages/core/utils/src/dml/__tests__/entity_builder.spec.ts rename packages/core/utils/src/dml/__tests__/{enum_schema.spec.ts => enum-schema.spec.ts} (100%) rename packages/core/utils/src/dml/__tests__/{has_many_relationship.spec.ts => has-many-relationship.spec.ts} (89%) rename packages/core/utils/src/dml/__tests__/{base_relationship.spec.ts => has-one-relationship.spec.ts} (53%) rename packages/core/utils/src/dml/__tests__/{json_schema.spec.ts => json-schema.spec.ts} (100%) rename packages/core/utils/src/dml/__tests__/{many_to_many.spec.ts => many-to-many.spec.ts} (88%) rename packages/core/utils/src/dml/__tests__/{number_schema.spec.ts => number-schema.spec.ts} (100%) rename packages/core/utils/src/dml/__tests__/{text_schema.spec.ts => text-schema.spec.ts} (100%) rename packages/core/utils/src/dml/{entity_builder.ts => entity-builder.ts} (77%) rename packages/core/utils/src/dml/helpers/{create_mikro_orm_entity.ts => create-mikro-orm-entity.ts} (51%) create mode 100644 packages/core/utils/src/dml/relations/belongs-to.ts rename packages/core/utils/src/dml/relations/{has_many.ts => has-many.ts} (100%) rename packages/core/utils/src/dml/relations/{has_one.ts => has-one.ts} (73%) delete mode 100644 packages/core/utils/src/dml/relations/has_one_through_many.ts rename packages/core/utils/src/dml/relations/{many_to_many.ts => many-to-many.ts} (100%) rename packages/core/utils/src/dml/schema/{date_time.ts => date-time.ts} (100%) diff --git a/packages/core/utils/src/dml/__tests__/has_one_relationship.spec.ts b/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts similarity index 51% rename from packages/core/utils/src/dml/__tests__/has_one_relationship.spec.ts rename to packages/core/utils/src/dml/__tests__/base-relationship.spec.ts index d21e16fcd5..d695e67732 100644 --- a/packages/core/utils/src/dml/__tests__/has_one_relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/base-relationship.spec.ts @@ -1,23 +1,29 @@ import { expectTypeOf } from "expect-type" -import { HasOne } from "../relations/has_one" import { TextSchema } from "../schema/text" +import { BaseRelationship } from "../relations/base" + +describe("Base relationship", () => { + test("define a custom relationship", () => { + class HasOne extends BaseRelationship { + protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne" + } -describe("HasOne relationship", () => { - test("define hasOne relationship", () => { const user = { username: new TextSchema(), } const entityRef = () => user - const relationship = new HasOne(entityRef, {}) + const relationship = new HasOne(entityRef, { + mappedBy: "user_id", + }) expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>() expect(relationship.parse("user")).toEqual({ name: "user", type: "hasOne", nullable: false, + mappedBy: "user_id", entity: entityRef, - options: {}, }) }) }) diff --git a/packages/core/utils/src/dml/__tests__/base_schema.spec.ts b/packages/core/utils/src/dml/__tests__/base-schema.spec.ts similarity index 100% rename from packages/core/utils/src/dml/__tests__/base_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/base-schema.spec.ts diff --git a/packages/core/utils/src/dml/__tests__/boolean_schema.spec.ts b/packages/core/utils/src/dml/__tests__/boolean-schema.spec.ts similarity index 100% rename from packages/core/utils/src/dml/__tests__/boolean_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/boolean-schema.spec.ts diff --git a/packages/core/utils/src/dml/__tests__/date_time_schema.spec.ts b/packages/core/utils/src/dml/__tests__/date-time-schema.spec.ts similarity index 89% rename from packages/core/utils/src/dml/__tests__/date_time_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/date-time-schema.spec.ts index f6b5e8ba6a..c36a76fb8f 100644 --- a/packages/core/utils/src/dml/__tests__/date_time_schema.spec.ts +++ b/packages/core/utils/src/dml/__tests__/date-time-schema.spec.ts @@ -1,5 +1,5 @@ import { expectTypeOf } from "expect-type" -import { DateTimeSchema } from "../schema/date_time" +import { DateTimeSchema } from "../schema/date-time" describe("DateTime schema", () => { test("create datetime schema type", () => { diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts new file mode 100644 index 0000000000..092a382f50 --- /dev/null +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -0,0 +1,1157 @@ +import { expectTypeOf } from "expect-type" +import { MetadataStorage } from "@mikro-orm/core" +import { EntityConstructor } from "../types" +import { EntityBuilder } from "../entity-builder" +import { createMikrORMEntity } from "../helpers/create-mikro-orm-entity" + +describe("Entity builder", () => { + beforeEach(() => { + MetadataStorage.clear() + }) + + describe("Entity builder | properties", () => { + test("define an entity", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.number(), + username: model.text(), + email: model.text(), + }) + + const User = createMikrORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: 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: "scalar", + type: "string", + columnType: "text", + name: "email", + nullable: false, + getter: false, + setter: false, + }, + }) + }) + + test("define a property with default value", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.number(), + username: model.text().default("foo"), + email: model.text(), + }) + + const User = createMikrORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: 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", + default: "foo", + columnType: "text", + name: "username", + nullable: false, + getter: false, + setter: false, + }, + email: { + reference: "scalar", + type: "string", + columnType: "text", + name: "email", + nullable: false, + getter: false, + setter: false, + }, + }) + }) + + test("mark property nullable", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.number(), + username: model.text().nullable(), + email: model.text(), + }) + + const User = createMikrORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string | null + email: 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: true, + getter: false, + setter: false, + }, + email: { + reference: "scalar", + type: "string", + columnType: "text", + name: "email", + nullable: false, + getter: false, + setter: false, + }, + }) + }) + + test("define an entity with enum property", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.number(), + username: model.text(), + email: model.text(), + role: model.enum(["moderator", "admin", "guest"]), + }) + + const User = createMikrORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: string + role: "moderator" | "admin" | "guest" + }>() + + 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: "scalar", + type: "string", + columnType: "text", + name: "email", + nullable: false, + getter: false, + setter: false, + }, + role: { + reference: "scalar", + enum: true, + items: expect.any(Function), + nullable: false, + name: "role", + }, + }) + expect(metaData.properties["role"].items()).toEqual([ + "moderator", + "admin", + "guest", + ]) + }) + + test("define enum property with default value", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.number(), + username: model.text(), + email: model.text(), + role: model.enum(["moderator", "admin", "guest"]).default("guest"), + }) + + const User = createMikrORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: string + role: "moderator" | "admin" | "guest" + }>() + + 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: "scalar", + type: "string", + columnType: "text", + name: "email", + nullable: false, + getter: false, + setter: false, + }, + role: { + reference: "scalar", + enum: true, + default: "guest", + items: expect.any(Function), + nullable: false, + name: "role", + }, + }) + expect(metaData.properties["role"].items()).toEqual([ + "moderator", + "admin", + "guest", + ]) + }) + + test("mark enum property nullable", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.number(), + username: model.text(), + email: model.text(), + role: model.enum(["moderator", "admin", "guest"]).nullable(), + }) + + const User = createMikrORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: string + role: "moderator" | "admin" | "guest" | 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", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + columnType: "text", + name: "username", + nullable: false, + getter: false, + setter: false, + }, + email: { + reference: "scalar", + type: "string", + columnType: "text", + name: "email", + nullable: false, + getter: false, + setter: false, + }, + role: { + reference: "scalar", + enum: true, + items: expect.any(Function), + nullable: true, + name: "role", + }, + }) + expect(metaData.properties["role"].items()).toEqual([ + "moderator", + "admin", + "guest", + ]) + }) + }) + + describe("Entity builder | hasOne", () => { + test("define hasOne 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), + }) + + 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, + }, + }) + }) + + test("mark hasOne relationship as nullable", () => { + 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.hasOne(() => email).nullable(), + }) + + const User = createMikrORMEntity(user) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + emails: EntityConstructor<{ email: string; isVerified: boolean }> | 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", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + columnType: "text", + name: "username", + nullable: false, + getter: false, + setter: false, + }, + emails: { + reference: "1:1", + name: "emails", + entity: "Email", + nullable: true, + }, + }) + }) + }) + + describe("Entity builder | hasMany", () => { + test("define hasMany 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(), + emails: model.hasMany(() => email), + }) + + 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", + }, + }) + }) + + test("define custom mappedBy property name for hasMany 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(), + emails: model.hasMany(() => email, { + mappedBy: "the_user", + }), + }) + + const User = createMikrORMEntity(user) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + emails: EntityConstructor<{ email: string; isVerified: boolean }> | 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", + 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", + mappedBy: "the_user", + orphanRemoval: true, + }, + }) + }) + }) + + describe("Entity builder | belongsTo", () => { + test("define belongsTo relationship with hasOne", () => { + 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), + }) + + const User = createMikrORMEntity(user) + const Email = createMikrORMEntity(email) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: EntityConstructor<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + id: number + username: string + }> + }> + }>() + + expectTypeOf(new Email()).toMatchTypeOf<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + 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, + }, + }) + + 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: "1:1", + name: "user", + entity: "User", + nullable: false, + owner: true, + mappedBy: "email", + }, + }) + }) + + test("mark belongsTo with hasOne as nullable", () => { + const model = new EntityBuilder() + + const email = model.define("email", { + email: model.text(), + isVerified: model.boolean(), + user: model.belongsTo(() => user).nullable(), + }) + + const user = model.define("user", { + id: model.number(), + username: model.text(), + email: model.hasOne(() => email), + }) + + const User = createMikrORMEntity(user) + const Email = createMikrORMEntity(email) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: EntityConstructor<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + id: number + username: string + }> | null + }> + }>() + + expectTypeOf(new Email()).toMatchTypeOf<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + id: number + username: string + email: EntityConstructor<{ + email: string + isVerified: boolean + }> + }> | 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", + 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, + }, + }) + + 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: "1:1", + name: "user", + entity: "User", + nullable: true, + owner: true, + mappedBy: "email", + }, + }) + }) + + test("define belongsTo relationship with hasMany", () => { + 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), + }) + + const User = createMikrORMEntity(user) + const Email = createMikrORMEntity(email) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + emails: EntityConstructor<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + id: number + username: string + }> + }> + }>() + + expectTypeOf(new Email()).toMatchTypeOf<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + 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", + mappedBy: "user", + orphanRemoval: true, + }, + }) + + 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", + }, + }) + }) + + test("define belongsTo with hasMany as nullable", () => { + const model = new EntityBuilder() + + const email = model.define("email", { + email: model.text(), + isVerified: model.boolean(), + user: model.belongsTo(() => user, { mappedBy: "emails" }).nullable(), + }) + + const user = model.define("user", { + id: model.number(), + username: model.text(), + emails: model.hasMany(() => email), + }) + + const User = createMikrORMEntity(user) + const Email = createMikrORMEntity(email) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + emails: EntityConstructor<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + id: number + username: string + }> | null + }> + }>() + + expectTypeOf(new Email()).toMatchTypeOf<{ + email: string + isVerified: boolean + user: EntityConstructor<{ + id: number + username: string + emails: EntityConstructor<{ + email: string + isVerified: boolean + }> + }> | 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", + 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", + mappedBy: "user", + orphanRemoval: true, + }, + }) + + 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: true, + reference: "m:1", + }, + }) + }) + + test("throw error when other side relationship is missing", () => { + 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(), + }) + + expect(() => createMikrORMEntity(email)).toThrow( + 'Missing property "email" on "user" entity. Make sure to define it as a relationship' + ) + }) + + test("throw error when other side relationship is invalid", () => { + 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.manyToMany(() => email), + }) + + expect(() => createMikrORMEntity(email)).toThrow( + 'Invalid relationship reference for "email" on "user" entity. Make sure to define a hasOne or hasMany relationship' + ) + }) + }) + + describe("Entity builder | manyToMany", () => { + test("define manyToMany relationship", () => { + const model = new EntityBuilder() + const team = model.define("team", { + id: model.number(), + name: model.text(), + users: model.manyToMany(() => user), + }) + + const user = model.define("user", { + id: model.number(), + username: model.text(), + teams: model.manyToMany(() => team), + }) + + const User = createMikrORMEntity(user) + const Team = createMikrORMEntity(team) + + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + teams: EntityConstructor<{ + id: number + name: string + users: EntityConstructor<{ + id: number + username: string + }> + }> + }>() + + expectTypeOf(new Team()).toMatchTypeOf<{ + id: number + name: string + users: EntityConstructor<{ + id: number + username: string + teams: EntityConstructor<{ + id: number + name: 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, + }, + teams: { + reference: "m:n", + name: "teams", + entity: "Team", + }, + }) + + const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team) + expect(teamMetaData.className).toEqual("Team") + expect(teamMetaData.path).toEqual("Team") + expect(teamMetaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + nullable: false, + getter: false, + setter: false, + }, + name: { + reference: "scalar", + type: "string", + columnType: "text", + name: "name", + nullable: false, + getter: false, + setter: false, + }, + users: { + reference: "m:n", + name: "users", + entity: "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 deleted file mode 100644 index 378cdfd7ed..0000000000 --- a/packages/core/utils/src/dml/__tests__/entity_builder.spec.ts +++ /dev/null @@ -1,509 +0,0 @@ -import { expectTypeOf } from "expect-type" -import { MetadataStorage } from "@mikro-orm/core" -import { EntityBuilder } from "../entity_builder" -import { createMikrORMEntity } from "../helpers/create_mikro_orm_entity" -import { EntityConstructor } from "../types" - -describe("Entity builder", () => { - beforeEach(() => { - MetadataStorage.clear() - }) - - test("define an entity", () => { - const model = new EntityBuilder() - const user = model.define("user", { - id: model.number(), - username: model.text(), - email: model.text(), - }) - - const User = createMikrORMEntity(user) - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - username: string - email: 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: "scalar", - type: "string", - columnType: "text", - name: "email", - nullable: false, - getter: false, - setter: false, - }, - }) - }) - - test("define an entity with enum property", () => { - const model = new EntityBuilder() - const user = model.define("user", { - id: model.number(), - username: model.text(), - email: model.text(), - role: model.enum(["moderator", "admin", "guest"]), - }) - - const User = createMikrORMEntity(user) - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - username: string - email: string - role: "moderator" | "admin" | "guest" - }>() - - 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: "scalar", - type: "string", - columnType: "text", - name: "email", - nullable: false, - getter: false, - setter: false, - }, - role: { - reference: "scalar", - enum: true, - items: expect.any(Function), - nullable: false, - name: "role", - }, - }) - expect(metaData.properties["role"].items()).toEqual([ - "moderator", - "admin", - "guest", - ]) - }) - - test("define an entity with default value", () => { - const model = new EntityBuilder() - const user = model.define("user", { - id: model.number(), - username: model.text().default("foo"), - email: model.text(), - }) - - const User = createMikrORMEntity(user) - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - username: string - email: 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", - default: "foo", - columnType: "text", - name: "username", - nullable: false, - getter: false, - setter: false, - }, - email: { - reference: "scalar", - type: "string", - columnType: "text", - name: "email", - nullable: false, - getter: false, - setter: false, - }, - }) - }) - - test("define hasMany 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(), - emails: model.hasMany(() => email), - }) - - 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", - cascade: ["persist"], - mappedBy: expect.any(Function), - orphanRemoval: true, - }, - }) - }) - - test("define hasMany and hasOneThroughMany relationships", () => { - const model = new EntityBuilder() - const order = model.define("order", { - amount: model.number(), - user: model.hasOneThroughMany(() => user), - }) - - const user = model.define("user", { - id: model.number(), - username: model.text(), - orders: model.hasMany(() => order), - }) - - const User = createMikrORMEntity(user) - const Order = createMikrORMEntity(order) - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - username: string - orders: EntityConstructor<{ - amount: number - 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, - }, - orders: { - reference: "1:m", - name: "orders", - cascade: ["persist"], - mappedBy: expect.any(Function), - orphanRemoval: true, - entity: "Order", - }, - }) - - const orderMetaDdata = MetadataStorage.getMetadataFromDecorator(Order) - expect(orderMetaDdata.className).toEqual("Order") - expect(orderMetaDdata.path).toEqual("Order") - expect(orderMetaDdata.properties).toEqual({ - amount: { - reference: "scalar", - type: "number", - columnType: "integer", - name: "amount", - 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, - onDelete: "cascade", - reference: "m:1", - }, - }) - }) - - test("define hasOne relationship on both sides", () => { - const model = new EntityBuilder() - const user = model.define("user", { - id: model.number(), - email: model.text(), - profile: model.hasOne(() => profile), - }) - - const profile = model.define("profile", { - id: model.number(), - user_id: model.number(), - user: model.hasOne(() => user), - }) - - const User = createMikrORMEntity(user) - const Profile = createMikrORMEntity(profile) - - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - email: string - profile: EntityConstructor<{ - id: number - user_id: number - user: EntityConstructor<{ - id: number - email: 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, - }, - email: { - reference: "scalar", - type: "string", - columnType: "text", - name: "email", - nullable: false, - getter: false, - setter: false, - }, - profile: { - name: "profile", - cascade: ["persist"], - entity: "Profile", - nullable: false, - onDelete: "cascade", - owner: true, - reference: "1:1", - }, - }) - - const profileMetadata = MetadataStorage.getMetadataFromDecorator(Profile) - expect(profileMetadata.className).toEqual("Profile") - expect(profileMetadata.path).toEqual("Profile") - expect(profileMetadata.properties).toEqual({ - id: { - reference: "scalar", - type: "number", - columnType: "integer", - name: "id", - nullable: false, - getter: false, - setter: false, - }, - user: { - cascade: ["persist"], - entity: "User", - name: "user", - nullable: false, - onDelete: "cascade", - owner: true, - reference: "1:1", - }, - user_id: { - columnType: "integer", - getter: false, - name: "user_id", - nullable: false, - reference: "scalar", - setter: false, - type: "number", - }, - }) - }) - - test("define manyToMany relationships", () => { - const model = new EntityBuilder() - const user = model.define("user", { - id: model.number(), - email: model.text(), - books: model.manyToMany(() => book), - }) - - const book = model.define("book", { - id: model.number(), - title: model.text(), - authors: model.manyToMany(() => user), - }) - - const User = createMikrORMEntity(user) - const Book = createMikrORMEntity(book) - - expectTypeOf(new User()).toMatchTypeOf<{ - id: number - email: string - books: EntityConstructor<{ - id: number - title: string - authors: EntityConstructor<{ - id: number - email: 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, - }, - email: { - reference: "scalar", - type: "string", - columnType: "text", - name: "email", - nullable: false, - getter: false, - setter: false, - }, - books: { - name: "books", - entity: "Book", - mappedBy: expect.any(Function), - reference: "m:n", - }, - }) - - const bookMetaData = MetadataStorage.getMetadataFromDecorator(Book) - expect(bookMetaData.className).toEqual("Book") - expect(bookMetaData.path).toEqual("Book") - expect(bookMetaData.properties).toEqual({ - id: { - reference: "scalar", - type: "number", - columnType: "integer", - name: "id", - nullable: false, - getter: false, - setter: false, - }, - authors: { - entity: "User", - name: "authors", - reference: "m:n", - mappedBy: expect.any(Function), - }, - title: { - columnType: "text", - getter: false, - name: "title", - nullable: false, - reference: "scalar", - setter: false, - type: "string", - }, - }) - }) -}) diff --git a/packages/core/utils/src/dml/__tests__/enum_schema.spec.ts b/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts similarity index 100% rename from packages/core/utils/src/dml/__tests__/enum_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/enum-schema.spec.ts diff --git a/packages/core/utils/src/dml/__tests__/has_many_relationship.spec.ts b/packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts similarity index 89% rename from packages/core/utils/src/dml/__tests__/has_many_relationship.spec.ts rename to packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts index f57ba247b1..2ff4d700ff 100644 --- a/packages/core/utils/src/dml/__tests__/has_many_relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/has-many-relationship.spec.ts @@ -1,6 +1,6 @@ import { expectTypeOf } from "expect-type" -import { HasMany } from "../relations/has_many" import { TextSchema } from "../schema/text" +import { HasMany } from "../relations/has-many" describe("HasMany relationship", () => { test("define hasMany relationship", () => { @@ -17,7 +17,6 @@ describe("HasMany relationship", () => { type: "hasMany", nullable: false, entity: entityRef, - options: {}, }) }) }) diff --git a/packages/core/utils/src/dml/__tests__/base_relationship.spec.ts b/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts similarity index 53% rename from packages/core/utils/src/dml/__tests__/base_relationship.spec.ts rename to packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts index 911160e9b8..f3ba7b917b 100644 --- a/packages/core/utils/src/dml/__tests__/base_relationship.spec.ts +++ b/packages/core/utils/src/dml/__tests__/has-one-relationship.spec.ts @@ -1,21 +1,15 @@ import { expectTypeOf } from "expect-type" import { TextSchema } from "../schema/text" -import { BaseRelationship } from "../relations/base" - -describe("Base relationship", () => { - test("define a custom relationship", () => { - class HasOne extends BaseRelationship { - protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne" - } +import { HasOne } from "../relations/has-one" +describe("HasOne relationship", () => { + test("define hasOne relationship", () => { const user = { username: new TextSchema(), } const entityRef = () => user - const relationship = new HasOne(entityRef, { - foreignKey: "user_id", - }) + const relationship = new HasOne(entityRef, {}) expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>() expect(relationship.parse("user")).toEqual({ @@ -23,25 +17,16 @@ describe("Base relationship", () => { type: "hasOne", nullable: false, entity: entityRef, - options: { - foreignKey: "user_id", - }, }) }) test("mark relationship as nullable", () => { - class HasOne extends BaseRelationship { - protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne" - } - const user = { username: new TextSchema(), } const entityRef = () => user - const relationship = new HasOne(entityRef, { - foreignKey: "user_id", - }).nullable() + const relationship = new HasOne(entityRef, {}).nullable() expectTypeOf(relationship["$dataType"]).toEqualTypeOf< (() => typeof user) | null @@ -51,9 +36,6 @@ describe("Base relationship", () => { type: "hasOne", nullable: true, entity: entityRef, - options: { - foreignKey: "user_id", - }, }) }) }) diff --git a/packages/core/utils/src/dml/__tests__/json_schema.spec.ts b/packages/core/utils/src/dml/__tests__/json-schema.spec.ts similarity index 100% rename from packages/core/utils/src/dml/__tests__/json_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/json-schema.spec.ts diff --git a/packages/core/utils/src/dml/__tests__/many_to_many.spec.ts b/packages/core/utils/src/dml/__tests__/many-to-many.spec.ts similarity index 88% rename from packages/core/utils/src/dml/__tests__/many_to_many.spec.ts rename to packages/core/utils/src/dml/__tests__/many-to-many.spec.ts index 6451c3233d..006ad970aa 100644 --- a/packages/core/utils/src/dml/__tests__/many_to_many.spec.ts +++ b/packages/core/utils/src/dml/__tests__/many-to-many.spec.ts @@ -1,6 +1,6 @@ import { expectTypeOf } from "expect-type" import { TextSchema } from "../schema/text" -import { ManyToMany } from "../relations/many_to_many" +import { ManyToMany } from "../relations/many-to-many" describe("ManyToMany relationship", () => { test("define manyToMany relationship", () => { @@ -17,7 +17,6 @@ describe("ManyToMany relationship", () => { type: "manyToMany", nullable: false, entity: entityRef, - options: {}, }) }) }) diff --git a/packages/core/utils/src/dml/__tests__/number_schema.spec.ts b/packages/core/utils/src/dml/__tests__/number-schema.spec.ts similarity index 100% rename from packages/core/utils/src/dml/__tests__/number_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/number-schema.spec.ts diff --git a/packages/core/utils/src/dml/__tests__/text_schema.spec.ts b/packages/core/utils/src/dml/__tests__/text-schema.spec.ts similarity index 100% rename from packages/core/utils/src/dml/__tests__/text_schema.spec.ts rename to packages/core/utils/src/dml/__tests__/text-schema.spec.ts diff --git a/packages/core/utils/src/dml/entity_builder.ts b/packages/core/utils/src/dml/entity-builder.ts similarity index 77% rename from packages/core/utils/src/dml/entity_builder.ts rename to packages/core/utils/src/dml/entity-builder.ts index 753988b30d..dc154e5b92 100644 --- a/packages/core/utils/src/dml/entity_builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -2,14 +2,14 @@ import { DmlEntity } from "./entity" import { TextSchema } from "./schema/text" import { EnumSchema } from "./schema/enum" import { JSONSchema } from "./schema/json" -import { HasOne } from "./relations/has_one" -import { HasMany } from "./relations/has_many" +import { HasOne } from "./relations/has-one" +import { HasMany } from "./relations/has-many" import { NumberSchema } from "./schema/number" import { BooleanSchema } from "./schema/boolean" -import { DateTimeSchema } from "./schema/date_time" -import { ManyToMany } from "./relations/many_to_many" -import { RelationshipType, SchemaType } from "./types" -import { HasOneThroughMany } from "./relations/has_one_through_many" +import { BelongsTo } from "./relations/belongs-to" +import { DateTimeSchema } from "./schema/date-time" +import { ManyToMany } from "./relations/many-to-many" +import type { RelationshipOptions, RelationshipType, SchemaType } from "./types" /** * Entity builder exposes the API to create an entity and define its @@ -80,10 +80,17 @@ export class EntityBuilder { * You may use the "belongsTo" relationship to define the inverse * of the "hasOne" relationship */ - hasOne(entityBuilder: T, options?: Record) { + hasOne(entityBuilder: T, options?: RelationshipOptions) { return new HasOne(entityBuilder, options || {}) } + /** + * Define inverse of "hasOne" and "hasMany" relationship. + */ + belongsTo(entityBuilder: T, options?: RelationshipOptions) { + return new BelongsTo(entityBuilder, options || {}) + } + /** * Has many relationship defines a relationship between two entities * where the owner of the relationship has many instance of the @@ -94,18 +101,10 @@ export class EntityBuilder { * - A user "hasMany" books * - A user "hasMany" addresses */ - hasMany(entityBuilder: T, options?: Record) { + hasMany(entityBuilder: T, options?: RelationshipOptions) { return new HasMany(entityBuilder, options || {}) } - /** - * Define a hasOneThroughMany relationship between two entities. - * @todo Remove in favor of "belongTo" - */ - hasOneThroughMany(entityBuilder: T, options?: Record) { - return new HasOneThroughMany(entityBuilder, options || {}) - } - /** * ManyToMany relationship defines a relationship between two entities * where the owner of the relationship has many instance of the @@ -117,7 +116,7 @@ export class EntityBuilder { * relationship requires a pivot table to establish a many to many * relationship between two entities */ - manyToMany(entityBuilder: T, options?: Record) { + manyToMany(entityBuilder: T, options?: RelationshipOptions) { return new ManyToMany(entityBuilder, options || {}) } } 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 similarity index 51% rename from packages/core/utils/src/dml/helpers/create_mikro_orm_entity.ts rename to packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index 34a1e70dbf..339db15664 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 @@ -1,11 +1,10 @@ import { + Enum, Entity, OneToMany, Property, OneToOne, ManyToMany, - Enum, - Cascade, ManyToOne, } from "@mikro-orm/core" import { DmlEntity } from "../entity" @@ -15,11 +14,13 @@ import type { Infer, SchemaType, KnownDataTypes, - RelationshipType, SchemaMetadata, + RelationshipType, EntityConstructor, RelationshipMetadata, } from "../types" +import { HasOne } from "../relations/has-one" +import { HasMany } from "../relations/has-many" /** * DML entity data types to PostgreSQL data types via @@ -88,6 +89,130 @@ function defineProperty( })(MikroORMEntity.prototype, field.fieldName) } +/** + * Defines has one relationship on the Mikro ORM entity. + */ +function defineHasOneRelationship( + MikroORMEntity: EntityConstructor, + relationship: RelationshipMetadata, + relatedEntity: DmlEntity< + Record | RelationshipType> + > +) { + const relatedModelName = upperCaseFirst(relatedEntity.name) + OneToOne({ + entity: relatedModelName, + nullable: relationship.nullable, + mappedBy: relationship.mappedBy as any, + })(MikroORMEntity.prototype, relationship.name) +} + +/** + * Defines has many relationship on the Mikro ORM entity + */ +function defineHasManyRelationship( + MikroORMEntity: EntityConstructor, + relationship: RelationshipMetadata, + relatedEntity: DmlEntity< + Record | RelationshipType> + > +) { + const relatedModelName = upperCaseFirst(relatedEntity.name) + OneToMany({ + entity: relatedModelName, + orphanRemoval: true, + mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name), + })(MikroORMEntity.prototype, relationship.name) +} + +/** + * Defines belongs to relationship on the Mikro ORM entity. The belongsTo + * relationship inspects the related entity for the other side of + * the relationship and then uses one of the following Mikro ORM + * relationship. + * + * - OneToOne: When the other side uses "hasOne" with "owner: true" + * - ManyToOne: When the other side uses "hasMany" + */ +function defineBelongsToRelationship( + MikroORMEntity: EntityConstructor, + relationship: RelationshipMetadata, + relatedEntity: DmlEntity< + Record | RelationshipType> + > +) { + const mappedBy = + relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name) + const otherSideRelation = relatedEntity.schema[mappedBy] + const relatedModelName = upperCaseFirst(relatedEntity.name) + + /** + * Ensure the mapped by is defined as relationship on the other side + */ + if (!otherSideRelation) { + throw new Error( + `Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship` + ) + } + + /** + * Otherside is a has many. Hence we should defined a ManyToOne + */ + if (otherSideRelation instanceof HasMany) { + ManyToOne({ + entity: relatedModelName, + columnType: "text", + mapToPk: true, + fieldName: camelToSnakeCase(`${relationship.name}Id`), + nullable: relationship.nullable, + })(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`)) + + ManyToOne({ + entity: relatedModelName, + persist: false, + })(MikroORMEntity.prototype, relationship.name) + return + } + + /** + * 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, + })(MikroORMEntity.prototype, relationship.name) + return + } + + /** + * Other side is some unsupported data-type + */ + throw new Error( + `Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a hasOne or hasMany relationship` + ) +} + +/** + * Defines a many to many relationship on the Mikro ORM entity + */ +function defineManyToManyRelationship( + MikroORMEntity: EntityConstructor, + relationship: RelationshipMetadata, + relatedEntity: DmlEntity< + Record | RelationshipType> + > +) { + const relatedModelName = upperCaseFirst(relatedEntity.name) + ManyToMany({ + entity: relatedModelName, + mappedBy: relationship.mappedBy as any, + })(MikroORMEntity.prototype, relationship.name) +} + /** * Defines a DML entity schema field as a Mikro ORM relationship */ @@ -134,44 +259,16 @@ function defineRelationship( */ switch (relationship.type) { case "hasOne": - OneToOne({ - entity: relatedModelName, - cascade: [Cascade.PERSIST], // accept via options - owner: true, // accept via options - onDelete: "cascade", // accept via options - nullable: false, // accept via options - })(MikroORMEntity.prototype, relationship.name) + defineHasOneRelationship(MikroORMEntity, relationship, relatedEntity) break case "hasMany": - OneToMany({ - entity: relatedModelName, - cascade: [Cascade.PERSIST], // accept via options - orphanRemoval: true, - mappedBy: (related: any) => - related[camelToSnakeCase(MikroORMEntity.name)], // optionally via options, - })(MikroORMEntity.prototype, relationship.name) + defineHasManyRelationship(MikroORMEntity, relationship, relatedEntity) break - case "hasOneThroughMany": - ManyToOne({ - entity: relatedModelName, - columnType: "text", // infer from primary key data-type - mapToPk: true, - nullable: false, // accept via options - fieldName: camelToSnakeCase(`${relationship.name}Id`), - onDelete: "cascade", // accept via options - })(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`)) - - ManyToOne({ - entity: relatedModelName, - persist: false, - })(MikroORMEntity.prototype, relationship.name) + case "belongsTo": + defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity) break case "manyToMany": - ManyToMany({ - entity: relatedModelName, - mappedBy: (related: any) => - related[camelToSnakeCase(MikroORMEntity.name)], // optionally via options, - })(MikroORMEntity.prototype, relationship.name) + defineManyToManyRelationship(MikroORMEntity, relationship, relatedEntity) break } } diff --git a/packages/core/utils/src/dml/modifiers/nullable.ts b/packages/core/utils/src/dml/modifiers/nullable.ts index 2a1e223a4f..0dee160ba7 100644 --- a/packages/core/utils/src/dml/modifiers/nullable.ts +++ b/packages/core/utils/src/dml/modifiers/nullable.ts @@ -1,9 +1,12 @@ -import { MaybeFieldMetadata } from "../types" +import { RelationshipType, SchemaType } from "../types" /** * Nullable modifier marks a schema node as nullable */ -export class NullableModifier { +export class NullableModifier< + T, + Schema extends SchemaType | RelationshipType +> { /** * A type-only property to infer the JavScript data-type * of the schema property @@ -14,20 +17,18 @@ export class NullableModifier { * The parent schema on which the nullable modifier is * applied */ - #schema: { - parse(fieldName: string): MaybeFieldMetadata - } + #schema: Schema - constructor(schema: { parse(fieldName: string): MaybeFieldMetadata }) { + constructor(schema: Schema) { this.#schema = schema } /** * Returns the serialized metadata */ - parse(fieldName: string) { + parse(fieldName: string): ReturnType { const schema = this.#schema.parse(fieldName) schema.nullable = true - return schema + return schema as ReturnType } } diff --git a/packages/core/utils/src/dml/relations/base.ts b/packages/core/utils/src/dml/relations/base.ts index 77fbdb02dc..cc519f6208 100644 --- a/packages/core/utils/src/dml/relations/base.ts +++ b/packages/core/utils/src/dml/relations/base.ts @@ -1,5 +1,8 @@ -import { NullableModifier } from "../modifiers/nullable" -import { RelationshipMetadata, RelationshipType } from "../types" +import { + RelationshipMetadata, + RelationshipOptions, + RelationshipType, +} from "../types" /** * The BaseRelationship encapsulates the repetitive parts of defining @@ -7,7 +10,8 @@ import { RelationshipMetadata, RelationshipType } from "../types" */ export abstract class BaseRelationship implements RelationshipType { #referencedEntity: T - #options: Record + + protected options: RelationshipOptions /** * The relationship type. @@ -20,16 +24,9 @@ export abstract class BaseRelationship implements RelationshipType { */ declare $dataType: T - constructor(referencedEntity: T, options: Record) { + constructor(referencedEntity: T, options: RelationshipOptions) { this.#referencedEntity = referencedEntity - this.#options = options - } - - /** - * Apply nullable modifier on the schema - */ - nullable() { - return new NullableModifier(this) + this.options = options } /** @@ -39,8 +36,8 @@ export abstract class BaseRelationship implements RelationshipType { return { name: relationshipName, nullable: false, + mappedBy: this.options.mappedBy, entity: this.#referencedEntity, - options: this.#options, type: this.relationshipType, } } diff --git a/packages/core/utils/src/dml/relations/belongs-to.ts b/packages/core/utils/src/dml/relations/belongs-to.ts new file mode 100644 index 0000000000..6658fb7b58 --- /dev/null +++ b/packages/core/utils/src/dml/relations/belongs-to.ts @@ -0,0 +1,14 @@ +import { BaseRelationship } from "./base" +import { RelationshipMetadata } from "../types" +import { NullableModifier } from "../modifiers/nullable" + +export class BelongsTo extends BaseRelationship { + protected relationshipType: RelationshipMetadata["type"] = "belongsTo" + + /** + * Apply nullable modifier on the schema + */ + nullable() { + return new NullableModifier>(this) + } +} diff --git a/packages/core/utils/src/dml/relations/has_many.ts b/packages/core/utils/src/dml/relations/has-many.ts similarity index 100% rename from packages/core/utils/src/dml/relations/has_many.ts rename to packages/core/utils/src/dml/relations/has-many.ts diff --git a/packages/core/utils/src/dml/relations/has_one.ts b/packages/core/utils/src/dml/relations/has-one.ts similarity index 73% rename from packages/core/utils/src/dml/relations/has_one.ts rename to packages/core/utils/src/dml/relations/has-one.ts index 12ecf2057e..53909987b4 100644 --- a/packages/core/utils/src/dml/relations/has_one.ts +++ b/packages/core/utils/src/dml/relations/has-one.ts @@ -1,4 +1,5 @@ import { BaseRelationship } from "./base" +import { NullableModifier } from "../modifiers/nullable" import { RelationshipMetadata } from "../types" /** @@ -13,4 +14,11 @@ import { RelationshipMetadata } from "../types" */ export class HasOne extends BaseRelationship { protected relationshipType: RelationshipMetadata["type"] = "hasOne" + + /** + * Apply nullable modifier on the schema + */ + nullable() { + return new NullableModifier>(this) + } } diff --git a/packages/core/utils/src/dml/relations/has_one_through_many.ts b/packages/core/utils/src/dml/relations/has_one_through_many.ts deleted file mode 100644 index 87be0a882d..0000000000 --- a/packages/core/utils/src/dml/relations/has_one_through_many.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BaseRelationship } from "./base" -import { RelationshipMetadata } from "../types" - -export class HasOneThroughMany extends BaseRelationship { - protected relationshipType: RelationshipMetadata["type"] = "hasOneThroughMany" -} diff --git a/packages/core/utils/src/dml/relations/many_to_many.ts b/packages/core/utils/src/dml/relations/many-to-many.ts similarity index 100% rename from packages/core/utils/src/dml/relations/many_to_many.ts rename to packages/core/utils/src/dml/relations/many-to-many.ts diff --git a/packages/core/utils/src/dml/schema/base.ts b/packages/core/utils/src/dml/schema/base.ts index 14b4b1b25f..2277856000 100644 --- a/packages/core/utils/src/dml/schema/base.ts +++ b/packages/core/utils/src/dml/schema/base.ts @@ -26,7 +26,7 @@ export abstract class BaseSchema implements SchemaType { * Apply nullable modifier on the schema */ nullable() { - return new NullableModifier(this) + return new NullableModifier(this) } /** diff --git a/packages/core/utils/src/dml/schema/date_time.ts b/packages/core/utils/src/dml/schema/date-time.ts similarity index 100% rename from packages/core/utils/src/dml/schema/date_time.ts rename to packages/core/utils/src/dml/schema/date-time.ts diff --git a/packages/core/utils/src/dml/types.ts b/packages/core/utils/src/dml/types.ts index 54f6721791..1241321dab 100644 --- a/packages/core/utils/src/dml/types.ts +++ b/packages/core/utils/src/dml/types.ts @@ -11,6 +11,16 @@ export type KnownDataTypes = | "dateTime" | "json" +/** + * The available on Delete actions + */ +export type OnDeleteActions = + | "cascade" + | "no action" + | "set null" + | "set default" + | (string & {}) + /** * Any field that contains "nullable" and "optional" properties * in their metadata are qualified as maybe fields. @@ -49,15 +59,22 @@ export type SchemaType = { parse(fieldName: string): SchemaMetadata } +/** + * Options accepted by all the relationships + */ +export type RelationshipOptions = { + mappedBy?: string +} + /** * The meta-data returned by the relationship parse * method */ export type RelationshipMetadata = MaybeFieldMetadata & { name: string - type: "hasOne" | "hasMany" | "hasOneThroughMany" | "manyToMany" + type: "hasOne" | "hasMany" | "belongsTo" | "manyToMany" entity: unknown - options: Record + mappedBy?: string } /** @@ -86,6 +103,8 @@ export type Infer = T extends DmlEntity ? EntityConstructor<{ [K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R ? Infer + : Schema[K]["$dataType"] extends (() => infer R) | null + ? Infer | null : Schema[K]["$dataType"] }> : never