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:
@@ -15,6 +15,7 @@ export type KnownDataTypes =
|
||||
| "boolean"
|
||||
| "enum"
|
||||
| "number"
|
||||
| "bigNumber"
|
||||
| "dateTime"
|
||||
| "json"
|
||||
| "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 { 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 = {}
|
||||
|
||||
@@ -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 { 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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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[]> = {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
11
packages/core/utils/src/dml/properties/big-number.ts
Normal file
11
packages/core/utils/src/dml/properties/big-number.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user