From 2895ccfba88531f254951ac22e72a4f198869580 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 19 Jun 2024 17:59:03 +0530 Subject: [PATCH] Add support for id property type (#7775) --- .../src/dml/__tests__/entity-builder.spec.ts | 311 ++++++++++++++++++ .../src/dml/__tests__/id-property.spec.ts | 40 +++ packages/core/utils/src/dml/entity-builder.ts | 9 + .../dml/helpers/create-mikro-orm-entity.ts | 41 ++- packages/core/utils/src/dml/properties/id.ts | 20 ++ packages/core/utils/src/dml/types.ts | 1 + 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 packages/core/utils/src/dml/__tests__/id-property.spec.ts create mode 100644 packages/core/utils/src/dml/properties/id.ts 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 02121a2e4b..206d4cfc47 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -744,6 +744,317 @@ describe("Entity builder", () => { }) }) + describe("Entity builder | id", () => { + test("define an entity with id property", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.id(), + username: model.text(), + email: model.text(), + }) + + const entityBuilder = createMikrORMEntity() + const User = entityBuilder(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: string + username: string + email: string + 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.hooks).toEqual({ + beforeCreate: ["generateId"], + onInit: ["generateId"], + }) + + expect(metaData.filters).toEqual({ + softDeletable: { + name: "softDeletable", + cond: expect.any(Function), + default: true, + args: false, + }, + }) + + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "string", + columnType: "text", + name: "id", + nullable: false, + primary: true, + }, + 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, + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "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", + 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", + nullable: true, + getter: false, + setter: false, + }, + }) + }) + + test("mark id as non-primary", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.id({ primaryKey: false }), + username: model.text(), + email: model.text(), + }) + + const entityBuilder = createMikrORMEntity() + const User = entityBuilder(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: string + username: string + email: string + created_at: Date + updated_at: Date + deleted_at: Date | null + }>() + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + const userInstance = new User() + userInstance["generateId"]() + + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + + expect(metaData.hooks).toEqual({ + beforeCreate: ["generateId"], + onInit: ["generateId"], + }) + + expect(metaData.filters).toEqual({ + softDeletable: { + name: "softDeletable", + cond: expect.any(Function), + default: true, + args: false, + }, + }) + + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "string", + columnType: "text", + 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, + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "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", + 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", + nullable: true, + getter: false, + setter: false, + }, + }) + + expect(userInstance.id).toBeDefined() + }) + + test("define prefix for the id", () => { + const model = new EntityBuilder() + const user = model.define("user", { + id: model.id({ primaryKey: false, prefix: "us" }), + username: model.text(), + email: model.text(), + }) + + const entityBuilder = createMikrORMEntity() + const User = entityBuilder(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: string + username: string + email: string + created_at: Date + updated_at: Date + deleted_at: Date | null + }>() + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + const userInstance = new User() + userInstance["generateId"]() + + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + + expect(metaData.hooks).toEqual({ + beforeCreate: ["generateId"], + onInit: ["generateId"], + }) + + expect(metaData.filters).toEqual({ + softDeletable: { + name: "softDeletable", + cond: expect.any(Function), + default: true, + args: false, + }, + }) + + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "string", + columnType: "text", + 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, + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "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", + 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", + nullable: true, + getter: false, + setter: false, + }, + }) + + expect(userInstance.id.startsWith("us_")).toBeTruthy() + }) + }) + describe("Entity builder | indexes", () => { test("define index on a field", () => { const model = new EntityBuilder() diff --git a/packages/core/utils/src/dml/__tests__/id-property.spec.ts b/packages/core/utils/src/dml/__tests__/id-property.spec.ts new file mode 100644 index 0000000000..c1de03f1e3 --- /dev/null +++ b/packages/core/utils/src/dml/__tests__/id-property.spec.ts @@ -0,0 +1,40 @@ +import { expectTypeOf } from "expect-type" +import { IdProperty } from "../properties/id" + +describe("Id property", () => { + test("create id property type", () => { + const property = new IdProperty() + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("id")).toEqual({ + fieldName: "id", + dataType: { + name: "id", + options: { + primaryKey: true, + }, + }, + nullable: false, + indexes: [], + relationships: [], + }) + }) + + test("create id property type with marking it as a primary key", () => { + const property = new IdProperty({ primaryKey: false }) + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("id")).toEqual({ + fieldName: "id", + dataType: { + name: "id", + options: { + primaryKey: false, + }, + }, + nullable: false, + indexes: [], + relationships: [], + }) + }) +}) diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index 819880f40d..72b67ef029 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -15,6 +15,7 @@ import type { PropertyType, } from "./types" import { NullableModifier } from "./properties/nullable" +import { IdProperty } from "./properties/id" /** * The implicit properties added by EntityBuilder in every schema @@ -63,6 +64,14 @@ export class EntityBuilder { }) } + /** + * Define an id property. Id properties are marked + * primary by default + */ + id(options: ConstructorParameters[0]) { + return new IdProperty(options) + } + /** * Define a text/string based column */ 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 cacfe86d90..5a54ac26ca 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 @@ -7,6 +7,9 @@ import { ManyToMany, ManyToOne, Filter, + PrimaryKey, + BeforeCreate, + OnInit, } from "@mikro-orm/core" import { DmlEntity } from "../entity" import { @@ -14,6 +17,7 @@ import { camelToSnakeCase, createPsqlIndexStatementHelper, toCamelCase, + generateEntityId, } from "../../common" import { upperCaseFirst } from "../../common/upper-case-first" import type { @@ -39,7 +43,7 @@ import { ManyToMany as DmlManyToMany } from "../relations/many-to-many" * mikro orm decorator for that */ const COLUMN_TYPES: { - [K in Exclude]: string + [K in Exclude]: string } = { boolean: "boolean", dateTime: "timestamptz", @@ -56,7 +60,7 @@ const COLUMN_TYPES: { * mikro orm decorator for that */ const PROPERTY_TYPES: { - [K in Exclude]: string + [K in Exclude]: string } = { boolean: "boolean", dateTime: "date", @@ -170,6 +174,39 @@ export function createMikrORMEntity() { return } + /** + * Defining an id property + */ + if (field.dataType.name === "id") { + const IdDecorator = field.dataType.options?.primaryKey + ? PrimaryKey({ + columnType: "text", + type: "string", + nullable: field.nullable, + }) + : Property({ + columnType: "text", + type: "string", + nullable: field.nullable, + }) + + IdDecorator(MikroORMEntity.prototype, field.fieldName) + + /** + * Hook to generate entity within the code + */ + MikroORMEntity.prototype.generateId = function () { + this.id = generateEntityId(this.id, field.dataType.options?.prefix) + } + + /** + * Execute hook via lifecycle decorators + */ + BeforeCreate()(MikroORMEntity.prototype, "generateId") + OnInit()(MikroORMEntity.prototype, "generateId") + return + } + /** * Define rest of properties */ diff --git a/packages/core/utils/src/dml/properties/id.ts b/packages/core/utils/src/dml/properties/id.ts new file mode 100644 index 0000000000..b1e75996d5 --- /dev/null +++ b/packages/core/utils/src/dml/properties/id.ts @@ -0,0 +1,20 @@ +import { BaseProperty } from "./base" + +/** + * The Id property defines a unique identifier for the schema. + * Most of the times it will be the primary as well. + */ +export class IdProperty extends BaseProperty { + protected dataType: { + name: "id" + options: { + primaryKey: boolean + prefix?: string + } + } + + constructor(options?: { primaryKey?: boolean; prefix?: string }) { + super() + this.dataType = { name: "id", options: { primaryKey: true, ...options } } + } +} diff --git a/packages/core/utils/src/dml/types.ts b/packages/core/utils/src/dml/types.ts index 263d639c4c..e9a74e7e08 100644 --- a/packages/core/utils/src/dml/types.ts +++ b/packages/core/utils/src/dml/types.ts @@ -10,6 +10,7 @@ export type KnownDataTypes = | "number" | "dateTime" | "json" + | "id" /** * List of available relationships at DML level