From 5c944ae5d048556c68dba462da8032e369f558cc Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 24 Jun 2024 10:29:18 +0200 Subject: [PATCH] feat(types,utils): DML can create a bigNumber property (#7801) what: - adds bigNumber as a property to DML - creates a bigNumber options field (`raw_{{ field }}`) as a part of the schema RESOLVES CORE-2375 --- packages/core/types/src/dml/index.ts | 1 + .../src/dal/mikro-orm/big-number-field.ts | 20 +++- .../dml/__tests__/big-number-property.spec.ts | 19 ++++ .../src/dml/__tests__/entity-builder.spec.ts | 95 +++++++++++++++++-- packages/core/utils/src/dml/entity-builder.ts | 59 +++++++----- packages/core/utils/src/dml/entity.ts | 8 +- .../dml/helpers/create-mikro-orm-entity.ts | 49 +++++++--- .../create-big-number-properties.ts | 53 +++++++++++ .../create-default-properties.ts | 16 ++++ .../utils/src/dml/properties/big-number.ts | 11 +++ .../core/utils/src/dml/properties/json.ts | 2 +- 11 files changed, 279 insertions(+), 54 deletions(-) create mode 100644 packages/core/utils/src/dml/__tests__/big-number-property.spec.ts create mode 100644 packages/core/utils/src/dml/helpers/entity-builder/create-big-number-properties.ts create mode 100644 packages/core/utils/src/dml/helpers/entity-builder/create-default-properties.ts create mode 100644 packages/core/utils/src/dml/properties/big-number.ts diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 87efd1132c..1c46dc7a30 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -15,6 +15,7 @@ export type KnownDataTypes = | "boolean" | "enum" | "number" + | "bigNumber" | "dateTime" | "json" | "id" diff --git a/packages/core/utils/src/dal/mikro-orm/big-number-field.ts b/packages/core/utils/src/dal/mikro-orm/big-number-field.ts index 5604003403..9aaa769929 100644 --- a/packages/core/utils/src/dal/mikro-orm/big-number-field.ts +++ b/packages/core/utils/src/dal/mikro-orm/big-number-field.ts @@ -1,7 +1,7 @@ -import { Property } from "@mikro-orm/core" -import { isPresent, trimZeros } from "../../common" -import { BigNumber } from "../../totals/big-number" import { BigNumberInput } from "@medusajs/types" +import { Property } from "@mikro-orm/core" +import { isDefined, isPresent, trimZeros } from "../../common" +import { BigNumber } from "../../totals/big-number" export function MikroOrmBigNumberProperty( options: Parameters[0] & { @@ -45,12 +45,22 @@ export function MikroOrmBigNumberProperty( const raw = bigNumber.raw! raw.value = trimZeros(raw.value as string) - this.__helper.__data[columnName] = bigNumber.numeric - this.__helper.__data[rawColumnName] = raw + // Note: this.__helper isn't present when directly working with the entity + // Adding this in optionally for it not to break. + if (isDefined(this.__helper)) { + this.__helper.__data[columnName] = bigNumber.numeric + this.__helper.__data[rawColumnName] = raw + } this[rawColumnName] = raw } + // Note: this.__helper isn't present when directly working with the entity + // Adding this in optionally for it not to break. + if (!isDefined(this.__helper)) { + return + } + // This is custom code to keep track of which fields are bignumber, as well as their data if (!this.__helper.__bignumberdata) { this.__helper.__bignumberdata = {} diff --git a/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts b/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts new file mode 100644 index 0000000000..09ef13aefc --- /dev/null +++ b/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts @@ -0,0 +1,19 @@ +import { expectTypeOf } from "expect-type" +import { BigNumberProperty } from "../properties/big-number" + +describe("Big Number property", () => { + test("create bigNumber property type", () => { + const property = new BigNumberProperty() + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("age")).toEqual({ + fieldName: "age", + dataType: { + name: "bigNumber", + }, + nullable: false, + indexes: [], + relationships: [], + }) + }) +}) 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 6770d0913d..394ca91b8a 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -1,9 +1,12 @@ -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" +import { expectTypeOf } from "expect-type" import { DmlEntity } from "../entity" +import { EntityBuilder } from "../entity-builder" +import { + createMikrORMEntity, + toMikroORMEntity, +} from "../helpers/create-mikro-orm-entity" +import { EntityConstructor } from "../types" describe("Entity builder", () => { beforeEach(() => { @@ -32,14 +35,18 @@ describe("Entity builder", () => { id: model.number(), username: model.text(), email: model.text(), + spend_limit: model.bigNumber(), }) const entityBuilder = createMikrORMEntity() const User = entityBuilder(user) + expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string email: string + spend_limit: number + raw_spend_limit: Record created_at: Date updated_at: Date deleted_at: Date | null @@ -97,6 +104,25 @@ describe("Entity builder", () => { getter: false, setter: false, }, + spend_limit: { + columnType: "numeric", + getter: true, + name: "spend_limit", + nullable: false, + reference: "scalar", + setter: true, + trackChanges: false, + type: "any", + }, + raw_spend_limit: { + columnType: "jsonb", + getter: false, + name: "raw_spend_limit", + nullable: false, + reference: "scalar", + setter: false, + type: "any", + }, updated_at: { reference: "scalar", type: "date", @@ -127,6 +153,7 @@ describe("Entity builder", () => { id: model.number(), username: model.text().default("foo"), email: model.text(), + spend_limit: model.bigNumber().default(500.4), }) const entityBuilder = createMikrORMEntity() @@ -180,6 +207,26 @@ describe("Entity builder", () => { getter: false, setter: false, }, + spend_limit: { + columnType: "numeric", + default: 500.4, + getter: true, + name: "spend_limit", + nullable: false, + reference: "scalar", + setter: true, + trackChanges: false, + type: "any", + }, + raw_spend_limit: { + columnType: "jsonb", + getter: false, + name: "raw_spend_limit", + nullable: false, + reference: "scalar", + setter: false, + type: "any", + }, created_at: { reference: "scalar", type: "date", @@ -221,27 +268,44 @@ describe("Entity builder", () => { id: model.number(), username: model.text().nullable(), email: model.text(), + spend_limit: model.bigNumber().nullable(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string | null email: string + spend_limit: number | null + raw_spend_limit: Record | null + 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") const userInstance = new User() + expect(userInstance.username).toEqual(null) + expect(userInstance.spend_limit).toEqual(undefined) + expect(userInstance.raw_spend_limit).toEqual(null) + userInstance.username = "john" expect(userInstance.username).toEqual("john") + userInstance.spend_limit = 150.5 + expect(userInstance.spend_limit).toEqual(150.5) + expect(userInstance.raw_spend_limit).toEqual({ + precision: 20, + value: "150.5", + }) + expect(metaData.filters).toEqual({ softDeletable: { name: "softDeletable", @@ -279,6 +343,25 @@ describe("Entity builder", () => { getter: false, setter: false, }, + raw_spend_limit: { + columnType: "jsonb", + getter: false, + name: "raw_spend_limit", + nullable: true, + reference: "scalar", + setter: false, + type: "any", + }, + spend_limit: { + columnType: "numeric", + getter: true, + name: "spend_limit", + nullable: true, + reference: "scalar", + setter: true, + trackChanges: false, + type: "any", + }, created_at: { reference: "scalar", type: "date", diff --git a/packages/core/utils/src/dml/entity-builder.ts b/packages/core/utils/src/dml/entity-builder.ts index a84b9b455c..f2afac4237 100644 --- a/packages/core/utils/src/dml/entity-builder.ts +++ b/packages/core/utils/src/dml/entity-builder.ts @@ -1,27 +1,34 @@ -import { DmlEntity } from "./entity" -import { TextProperty } from "./properties/text" -import { EnumProperty } from "./properties/enum" -import { JSONProperty } from "./properties/json" -import { HasOne } from "./relations/has-one" -import { HasMany } from "./relations/has-many" -import { NumberProperty } from "./properties/number" -import { BooleanProperty } from "./properties/boolean" -import { BelongsTo } from "./relations/belongs-to" -import { DateTimeProperty } from "./properties/date-time" -import { ManyToMany } from "./relations/many-to-many" import type { PropertyType, RelationshipOptions, RelationshipType, } from "@medusajs/types" -import { NullableModifier } from "./properties/nullable" +import { DmlEntity } from "./entity" +import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties" +import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties" +import { BigNumberProperty } from "./properties/big-number" +import { BooleanProperty } from "./properties/boolean" +import { DateTimeProperty } from "./properties/date-time" +import { EnumProperty } from "./properties/enum" import { IdProperty } from "./properties/id" +import { JSONProperty } from "./properties/json" +import { NumberProperty } from "./properties/number" +import { TextProperty } from "./properties/text" +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" /** * The implicit properties added by EntityBuilder in every schema */ const IMPLICIT_PROPERTIES = ["created_at", "updated_at", "deleted_at"] +export type DMLSchema = Record< + string, + PropertyType | RelationshipType +> + /** * Entity builder exposes the API to create an entity and define its * schema using the shorthand methods. @@ -45,22 +52,13 @@ export class EntityBuilder { * Define an entity or a model. The name should be unique across * all the entities. */ - define< - Schema extends Record | RelationshipType> - >(name: string, schema: Schema) { + define(name: string, schema: Schema) { this.#disallowImplicitProperties(schema) - return new DmlEntity< - Schema & { - created_at: DateTimeProperty - updated_at: DateTimeProperty - deleted_at: NullableModifier - } - >(name, { + return new DmlEntity(name, { ...schema, - created_at: new DateTimeProperty(), - updated_at: new DateTimeProperty(), - deleted_at: new DateTimeProperty().nullable(), + ...createBigNumberProperties(schema), + ...createDefaultProperties(), }) } @@ -87,12 +85,21 @@ export class EntityBuilder { } /** - * Define a numeric/integer column + * Define an integer column */ number() { return new NumberProperty() } + /** + * Define a numeric column. This property produces an additional + * column - raw_{{ property_name }}, which stores the configuration + * of bignumber (https://github.com/MikeMcl/bignumber.js) + */ + bigNumber() { + return new BigNumberProperty() + } + /** * Define a timestampz column */ diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index 1850fb8dc5..4590bde46a 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -1,4 +1,3 @@ -import { BelongsTo } from "./relations/belongs-to" import { EntityCascades, ExtractEntityRelations, @@ -7,15 +6,14 @@ import { PropertyType, RelationshipType, } from "@medusajs/types" +import { DMLSchema } from "./entity-builder" +import { BelongsTo } from "./relations/belongs-to" /** * Dml entity is a representation of a DML model with a unique * name, its schema and relationships. */ -export class DmlEntity< - Schema extends Record | RelationshipType> -> implements IDmlEntity -{ +export class DmlEntity implements IDmlEntity { [IsDmlEntity]: true = true #cascades: EntityCascades = {} 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 69582e2870..55cea2f929 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,3 +1,13 @@ +import type { + EntityCascades, + EntityConstructor, + Infer, + KnownDataTypes, + PropertyMetadata, + PropertyType, + RelationshipMetadata, + RelationshipType, +} from "@medusajs/types" import { BeforeCreate, Entity, @@ -20,21 +30,14 @@ import { toCamelCase, } from "../../common" import { upperCaseFirst } from "../../common/upper-case-first" +import { + MikroOrmBigNumberProperty, + mikroOrmSoftDeletableFilterOptions, +} from "../../dal" import { DmlEntity } from "../entity" import { HasMany } from "../relations/has-many" import { HasOne } from "../relations/has-one" import { ManyToMany as DmlManyToMany } from "../relations/many-to-many" -import type { - EntityCascades, - EntityConstructor, - Infer, - KnownDataTypes, - PropertyMetadata, - PropertyType, - RelationshipMetadata, - RelationshipType, -} from "@medusajs/types" -import { mikroOrmSoftDeletableFilterOptions } from "../../dal" /** * DML entity data types to PostgreSQL data types via @@ -49,6 +52,7 @@ const COLUMN_TYPES: { boolean: "boolean", dateTime: "timestamptz", number: "integer", + bigNumber: "numeric", text: "text", json: "jsonb", } @@ -66,6 +70,7 @@ const PROPERTY_TYPES: { boolean: "boolean", dateTime: "date", number: "number", + bigNumber: "number", text: "string", json: "any", } @@ -175,6 +180,27 @@ export function createMikrORMEntity() { return } + /** + * Defining an big number property + * A big number property always comes with a raw_{{ fieldName }} column + * where the config of the bigNumber is set. + * The `raw_` field is generated during DML schema generation as a json + * dataType. + */ + if (field.dataType.name === "bigNumber") { + MikroOrmBigNumberProperty({ + nullable: field.nullable, + /** + * MikroORM does not ignore undefined values for default when generating + * the database schema SQL. Conditionally add it here to prevent undefined + * from being set as default value in SQL. + */ + ...(isDefined(field.defaultValue) && { default: field.defaultValue }), + })(MikroORMEntity.prototype, field.fieldName) + + return + } + /** * Defining an enum property */ @@ -615,6 +641,7 @@ export function createMikrORMEntity() { */ Object.entries(schema).forEach(([name, property]) => { const field = property.parse(name) + if ("fieldName" in field) { defineProperty(MikroORMEntity, field) applyIndexes(MikroORMEntity, tableName, field) diff --git a/packages/core/utils/src/dml/helpers/entity-builder/create-big-number-properties.ts b/packages/core/utils/src/dml/helpers/entity-builder/create-big-number-properties.ts new file mode 100644 index 0000000000..02701473f9 --- /dev/null +++ b/packages/core/utils/src/dml/helpers/entity-builder/create-big-number-properties.ts @@ -0,0 +1,53 @@ +import { DMLSchema } from "../../entity-builder" +import { BigNumberProperty } from "../../properties/big-number" +import { JSONProperty } from "../../properties/json" +import { NullableModifier } from "../../properties/nullable" + +/** + * The goal of this type is to look at the schema and see if there are any versions + * of a bigNumber property (nullable or otherwise) and if any are found, we append new + * fields of the same property to the type. These fields will be prepended by raw_ + * These fields will be typed to a json property (nullable or otherwise) + * + * eg: const test = model.define('test', { amount: model.bigNumber() }) + * test.amount // valid | type = number + * test.raw_amount // valid | type = Record + */ +type DMLSchemaWithBigNumber = { + [K in keyof T]: T[K] +} & { + [K in keyof T as T[K] extends + | BigNumberProperty + | NullableModifier + ? `raw_${string & K}` + : never]: T[K] extends NullableModifier + ? NullableModifier, JSONProperty> + : JSONProperty +} + +export function createBigNumberProperties( + schema: Schema +): DMLSchemaWithBigNumber { + const schemaWithBigNumber: DMLSchema = {} + + for (const [key, property] of Object.entries(schema)) { + if ( + property instanceof BigNumberProperty || + property instanceof NullableModifier + ) { + const parsed = property.parse(key) + + if (parsed.dataType?.name !== "bigNumber") { + continue + } + + const jsonProperty = parsed.nullable + ? new JSONProperty().nullable() + : new JSONProperty() + + schemaWithBigNumber[`raw_${key}`] = jsonProperty + } + } + + return schemaWithBigNumber as DMLSchemaWithBigNumber +} diff --git a/packages/core/utils/src/dml/helpers/entity-builder/create-default-properties.ts b/packages/core/utils/src/dml/helpers/entity-builder/create-default-properties.ts new file mode 100644 index 0000000000..3bd9340825 --- /dev/null +++ b/packages/core/utils/src/dml/helpers/entity-builder/create-default-properties.ts @@ -0,0 +1,16 @@ +import { DateTimeProperty } from "../../properties/date-time" +import { NullableModifier } from "../../properties/nullable" + +export type DMLSchemaDefaults = { + created_at: DateTimeProperty + updated_at: DateTimeProperty + deleted_at: NullableModifier +} + +export function createDefaultProperties(): DMLSchemaDefaults { + return { + created_at: new DateTimeProperty(), + updated_at: new DateTimeProperty(), + deleted_at: new DateTimeProperty().nullable(), + } +} diff --git a/packages/core/utils/src/dml/properties/big-number.ts b/packages/core/utils/src/dml/properties/big-number.ts new file mode 100644 index 0000000000..2ab2950df3 --- /dev/null +++ b/packages/core/utils/src/dml/properties/big-number.ts @@ -0,0 +1,11 @@ +import { BaseProperty } from "./base" + +/** + * The NumberProperty is used to define a numeric/integer + * property + */ +export class BigNumberProperty extends BaseProperty { + protected dataType = { + name: "bigNumber", + } as const +} diff --git a/packages/core/utils/src/dml/properties/json.ts b/packages/core/utils/src/dml/properties/json.ts index 0dc748caeb..21d89d5de4 100644 --- a/packages/core/utils/src/dml/properties/json.ts +++ b/packages/core/utils/src/dml/properties/json.ts @@ -4,7 +4,7 @@ import { BaseProperty } from "./base" * The JSONProperty is used to define a property that stores * data as a JSON string */ -export class JSONProperty extends BaseProperty { +export class JSONProperty extends BaseProperty> { protected dataType = { name: "json", } as const