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 authored and GitHub committed 2024-06-24 08:29:18 +00:00
1 parent ae6dbc06be
commit 5c944ae5d0
11 files changed
+277 -52

No files matched your search

+1
View File
@@ -15,6 +15,7 @@ export type KnownDataTypes =
| "boolean" | "boolean"
| "enum" | "enum"
| "number" | "number"
| "bigNumber"
| "dateTime" | "dateTime"
| "json" | "json"
| "id" | "id"
@@ -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 { 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( export function MikroOrmBigNumberProperty(
options: Parameters<typeof Property>[0] & { options: Parameters<typeof Property>[0] & {
@@ -45,12 +45,22 @@ export function MikroOrmBigNumberProperty(
const raw = bigNumber.raw! const raw = bigNumber.raw!
raw.value = trimZeros(raw.value as string) raw.value = trimZeros(raw.value as string)
// 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[columnName] = bigNumber.numeric
this.__helper.__data[rawColumnName] = raw this.__helper.__data[rawColumnName] = raw
}
this[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 // This is custom code to keep track of which fields are bignumber, as well as their data
if (!this.__helper.__bignumberdata) { if (!this.__helper.__bignumberdata) {
this.__helper.__bignumberdata = {} this.__helper.__bignumberdata = {}
@@ -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: [],
})
})
})
@@ -1,9 +1,12 @@
import { expectTypeOf } from "expect-type"
import { MetadataStorage } from "@mikro-orm/core" import { MetadataStorage } from "@mikro-orm/core"
import { EntityConstructor } from "../types" import { expectTypeOf } from "expect-type"
import { EntityBuilder } from "../entity-builder"
import { createMikrORMEntity } from "../helpers/create-mikro-orm-entity"
import { DmlEntity } from "../entity" 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", () => { describe("Entity builder", () => {
beforeEach(() => { beforeEach(() => {
@@ -32,14 +35,18 @@ describe("Entity builder", () => {
id: model.number(), id: model.number(),
username: model.text(), username: model.text(),
email: model.text(), email: model.text(),
spend_limit: model.bigNumber(),
}) })
const entityBuilder = createMikrORMEntity() const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user) const User = entityBuilder(user)
expectTypeOf(new User()).toMatchTypeOf<{ expectTypeOf(new User()).toMatchTypeOf<{
id: number id: number
username: string username: string
email: string email: string
spend_limit: number
raw_spend_limit: Record<string, unknown>
created_at: Date created_at: Date
updated_at: Date updated_at: Date
deleted_at: Date | null deleted_at: Date | null
@@ -97,6 +104,25 @@ describe("Entity builder", () => {
getter: false, getter: false,
setter: 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: { updated_at: {
reference: "scalar", reference: "scalar",
type: "date", type: "date",
@@ -127,6 +153,7 @@ describe("Entity builder", () => {
id: model.number(), id: model.number(),
username: model.text().default("foo"), username: model.text().default("foo"),
email: model.text(), email: model.text(),
spend_limit: model.bigNumber().default(500.4),
}) })
const entityBuilder = createMikrORMEntity() const entityBuilder = createMikrORMEntity()
@@ -180,6 +207,26 @@ describe("Entity builder", () => {
getter: false, getter: false,
setter: 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: { created_at: {
reference: "scalar", reference: "scalar",
type: "date", type: "date",
@@ -221,27 +268,44 @@ describe("Entity builder", () => {
id: model.number(), id: model.number(),
username: model.text().nullable(), username: model.text().nullable(),
email: model.text(), email: model.text(),
spend_limit: model.bigNumber().nullable(),
}) })
const entityBuilder = createMikrORMEntity() const User = toMikroORMEntity(user)
const User = entityBuilder(user)
expectTypeOf(new User()).toMatchTypeOf<{ expectTypeOf(new User()).toMatchTypeOf<{
id: number id: number
username: string | null username: string | null
email: string email: string
spend_limit: number | null
raw_spend_limit: Record<string, unknown> | null
created_at: Date
updated_at: Date
deleted_at: Date | null deleted_at: Date | null
}>() }>()
const metaData = MetadataStorage.getMetadataFromDecorator(User) const metaData = MetadataStorage.getMetadataFromDecorator(User)
expect(metaData.className).toEqual("User") expect(metaData.className).toEqual("User")
expect(metaData.path).toEqual("User") expect(metaData.path).toEqual("User")
const userInstance = new User() const userInstance = new User()
expect(userInstance.username).toEqual(null) expect(userInstance.username).toEqual(null)
expect(userInstance.spend_limit).toEqual(undefined)
expect(userInstance.raw_spend_limit).toEqual(null)
userInstance.username = "john" userInstance.username = "john"
expect(userInstance.username).toEqual("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({ expect(metaData.filters).toEqual({
softDeletable: { softDeletable: {
name: "softDeletable", name: "softDeletable",
@@ -279,6 +343,25 @@ describe("Entity builder", () => {
getter: false, getter: false,
setter: 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: { created_at: {
reference: "scalar", reference: "scalar",
type: "date", type: "date",
+33 -26
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 { import type {
PropertyType, PropertyType,
RelationshipOptions, RelationshipOptions,
RelationshipType, RelationshipType,
} from "@medusajs/types" } 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 { 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 * The implicit properties added by EntityBuilder in every schema
*/ */
const IMPLICIT_PROPERTIES = ["created_at", "updated_at", "deleted_at"] 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 * Entity builder exposes the API to create an entity and define its
* schema using the shorthand methods. * schema using the shorthand methods.
@@ -45,22 +52,13 @@ export class EntityBuilder {
* Define an entity or a model. The name should be unique across * Define an entity or a model. The name should be unique across
* all the entities. * all the entities.
*/ */
define< define<Schema extends DMLSchema>(name: string, schema: Schema) {
Schema extends Record<string, PropertyType<any> | RelationshipType<any>>
>(name: string, schema: Schema) {
this.#disallowImplicitProperties(schema) this.#disallowImplicitProperties(schema)
return new DmlEntity< return new DmlEntity(name, {
Schema & {
created_at: DateTimeProperty
updated_at: DateTimeProperty
deleted_at: NullableModifier<Date, DateTimeProperty>
}
>(name, {
...schema, ...schema,
created_at: new DateTimeProperty(), ...createBigNumberProperties(schema),
updated_at: new DateTimeProperty(), ...createDefaultProperties(),
deleted_at: new DateTimeProperty().nullable(),
}) })
} }
@@ -87,12 +85,21 @@ export class EntityBuilder {
} }
/** /**
* Define a numeric/integer column * Define an integer column
*/ */
number() { number() {
return new NumberProperty() 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 * Define a timestampz column
*/ */
+3 -5
View File
@@ -1,4 +1,3 @@
import { BelongsTo } from "./relations/belongs-to"
import { import {
EntityCascades, EntityCascades,
ExtractEntityRelations, ExtractEntityRelations,
@@ -7,15 +6,14 @@ import {
PropertyType, PropertyType,
RelationshipType, RelationshipType,
} from "@medusajs/types" } 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 * Dml entity is a representation of a DML model with a unique
* name, its schema and relationships. * name, its schema and relationships.
*/ */
export class DmlEntity< export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
Schema extends Record<string, PropertyType<any> | RelationshipType<any>>
> implements IDmlEntity<Schema>
{
[IsDmlEntity]: true = true [IsDmlEntity]: true = true
#cascades: EntityCascades<string[]> = {} #cascades: EntityCascades<string[]> = {}
@@ -1,3 +1,13 @@
import type {
EntityCascades,
EntityConstructor,
Infer,
KnownDataTypes,
PropertyMetadata,
PropertyType,
RelationshipMetadata,
RelationshipType,
} from "@medusajs/types"
import { import {
BeforeCreate, BeforeCreate,
Entity, Entity,
@@ -20,21 +30,14 @@ import {
toCamelCase, toCamelCase,
} from "../../common" } from "../../common"
import { upperCaseFirst } from "../../common/upper-case-first" import { upperCaseFirst } from "../../common/upper-case-first"
import {
MikroOrmBigNumberProperty,
mikroOrmSoftDeletableFilterOptions,
} from "../../dal"
import { DmlEntity } from "../entity" import { DmlEntity } from "../entity"
import { HasMany } from "../relations/has-many" import { HasMany } from "../relations/has-many"
import { HasOne } from "../relations/has-one" import { HasOne } from "../relations/has-one"
import { ManyToMany as DmlManyToMany } from "../relations/many-to-many" 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 * DML entity data types to PostgreSQL data types via
@@ -49,6 +52,7 @@ const COLUMN_TYPES: {
boolean: "boolean", boolean: "boolean",
dateTime: "timestamptz", dateTime: "timestamptz",
number: "integer", number: "integer",
bigNumber: "numeric",
text: "text", text: "text",
json: "jsonb", json: "jsonb",
} }
@@ -66,6 +70,7 @@ const PROPERTY_TYPES: {
boolean: "boolean", boolean: "boolean",
dateTime: "date", dateTime: "date",
number: "number", number: "number",
bigNumber: "number",
text: "string", text: "string",
json: "any", json: "any",
} }
@@ -175,6 +180,27 @@ export function createMikrORMEntity() {
return 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 * Defining an enum property
*/ */
@@ -615,6 +641,7 @@ export function createMikrORMEntity() {
*/ */
Object.entries(schema).forEach(([name, property]) => { Object.entries(schema).forEach(([name, property]) => {
const field = property.parse(name) const field = property.parse(name)
if ("fieldName" in field) { if ("fieldName" in field) {
defineProperty(MikroORMEntity, field) defineProperty(MikroORMEntity, field)
applyIndexes(MikroORMEntity, tableName, field) applyIndexes(MikroORMEntity, tableName, field)
@@ -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>
}
@@ -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(),
}
}
@@ -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
}
@@ -4,7 +4,7 @@ import { BaseProperty } from "./base"
* The JSONProperty is used to define a property that stores * The JSONProperty is used to define a property that stores
* data as a JSON string * data as a JSON string
*/ */
export class JSONProperty extends BaseProperty<string> { export class JSONProperty extends BaseProperty<Record<string, unknown>> {
protected dataType = { protected dataType = {
name: "json", name: "json",
} as const } as const