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
This commit is contained in:
Riqwan Thamir
2024-06-24 10:29:18 +02:00
committed by GitHub
parent ae6dbc06be
commit 5c944ae5d0
11 changed files with 279 additions and 54 deletions

View File

@@ -15,6 +15,7 @@ export type KnownDataTypes =
| "boolean"
| "enum"
| "number"
| "bigNumber"
| "dateTime"
| "json"
| "id"

View File

@@ -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<typeof Property>[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 = {}

View File

@@ -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<number>()
expect(property.parse("age")).toEqual({
fieldName: "age",
dataType: {
name: "bigNumber",
},
nullable: false,
indexes: [],
relationships: [],
})
})
})

View File

@@ -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<string, unknown>
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<string, unknown> | 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",

View File

@@ -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<any> | RelationshipType<any>
>
/**
* 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<string, PropertyType<any> | RelationshipType<any>>
>(name: string, schema: Schema) {
define<Schema extends DMLSchema>(name: string, schema: Schema) {
this.#disallowImplicitProperties(schema)
return new DmlEntity<
Schema & {
created_at: DateTimeProperty
updated_at: DateTimeProperty
deleted_at: NullableModifier<Date, DateTimeProperty>
}
>(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
*/

View File

@@ -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<string, PropertyType<any> | RelationshipType<any>>
> implements IDmlEntity<Schema>
{
export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
[IsDmlEntity]: true = true
#cascades: EntityCascades<string[]> = {}

View File

@@ -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)

View File

@@ -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<string, any>
*/
type DMLSchemaWithBigNumber<T extends DMLSchema> = {
[K in keyof T]: T[K]
} & {
[K in keyof T as T[K] extends
| BigNumberProperty
| NullableModifier<number, BigNumberProperty>
? `raw_${string & K}`
: never]: T[K] extends NullableModifier<number, BigNumberProperty>
? NullableModifier<Record<string, unknown>, JSONProperty>
: JSONProperty
}
export function createBigNumberProperties<Schema extends DMLSchema>(
schema: Schema
): DMLSchemaWithBigNumber<Schema> {
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<Schema>
}

View File

@@ -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<Date, DateTimeProperty>
}
export function createDefaultProperties(): DMLSchemaDefaults {
return {
created_at: new DateTimeProperty(),
updated_at: new DateTimeProperty(),
deleted_at: new DateTimeProperty().nullable(),
}
}

View File

@@ -0,0 +1,11 @@
import { BaseProperty } from "./base"
/**
* The NumberProperty is used to define a numeric/integer
* property
*/
export class BigNumberProperty extends BaseProperty<number> {
protected dataType = {
name: "bigNumber",
} as const
}

View File

@@ -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<string> {
export class JSONProperty extends BaseProperty<Record<string, unknown>> {
protected dataType = {
name: "json",
} as const