From 661ea7865ca0b04c33243e51f86946b4b6116777 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 19 Nov 2024 16:28:33 +0530 Subject: [PATCH] feat: add optional fields (#10150) --- .changeset/beige-parents-roll.md | 6 + packages/core/types/src/dml/index.ts | 41 ++++-- .../src/dml/__tests__/array-property.spec.ts | 1 + .../src/dml/__tests__/base-property.spec.ts | 97 +++++++++++++- .../dml/__tests__/big-number-property.spec.ts | 1 + .../dml/__tests__/boolean-property.spec.ts | 1 + .../dml/__tests__/date-time-property.spec.ts | 1 + .../src/dml/__tests__/entity-builder.spec.ts | 125 ++++++++++++++++++ .../src/dml/__tests__/enum-schema.spec.ts | 4 + .../src/dml/__tests__/id-property.spec.ts | 2 + .../src/dml/__tests__/json-property.spec.ts | 2 + .../src/dml/__tests__/number-property.spec.ts | 1 + .../src/dml/__tests__/text-property.spec.ts | 2 + .../core/utils/src/dml/properties/base.ts | 25 +++- .../core/utils/src/dml/properties/index.ts | 1 + .../core/utils/src/dml/properties/nullable.ts | 23 +++- .../core/utils/src/dml/properties/optional.ts | 62 +++++++++ 17 files changed, 378 insertions(+), 17 deletions(-) create mode 100644 .changeset/beige-parents-roll.md create mode 100644 packages/core/utils/src/dml/properties/optional.ts diff --git a/.changeset/beige-parents-roll.md b/.changeset/beige-parents-roll.md new file mode 100644 index 0000000000000..17fb56a39a190 --- /dev/null +++ b/.changeset/beige-parents-roll.md @@ -0,0 +1,6 @@ +--- +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat: add optional fields diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index e53b9196e7f16..0f3849a51dbc5 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -62,6 +62,7 @@ export type PropertyMetadata = { fieldName: string defaultValue?: any nullable: boolean + optional: boolean dataType: { name: KnownDataTypes options?: Record @@ -177,23 +178,37 @@ export type InferHasManyFields = Relation extends () => IDmlEntity< */ export type InferManyToManyFields = InferHasManyFields +/** + * Only processed property that can be undefined and mark them as optional + */ +export type InferOptionalFields = Prettify<{ + [K in keyof Schema as undefined extends Schema[K]["$dataType"] + ? K + : never]?: Schema[K]["$dataType"] +}> + /** * Inferring the types of the schema fields from the DML * entity */ -export type InferSchemaFields = Prettify<{ - [K in keyof Schema]: Schema[K] extends RelationshipType - ? Schema[K]["type"] extends "belongsTo" - ? InferBelongsToFields - : Schema[K]["type"] extends "hasOne" - ? InferHasOneFields - : Schema[K]["type"] extends "hasMany" - ? InferHasManyFields - : Schema[K]["type"] extends "manyToMany" - ? InferManyToManyFields - : never - : Schema[K]["$dataType"] -}> +export type InferSchemaFields = Prettify< + { + // Omit optional properties to manage them separately and mark them as optional + [K in keyof Schema as undefined extends Schema[K]["$dataType"] + ? never + : K]: Schema[K] extends RelationshipType + ? Schema[K]["type"] extends "belongsTo" + ? InferBelongsToFields + : Schema[K]["type"] extends "hasOne" + ? InferHasOneFields + : Schema[K]["type"] extends "hasMany" + ? InferHasManyFields + : Schema[K]["type"] extends "manyToMany" + ? InferManyToManyFields + : never + : Schema[K]["$dataType"] + } & InferOptionalFields +> /** * Helper to infer the schema type of a DmlEntity diff --git a/packages/core/utils/src/dml/__tests__/array-property.spec.ts b/packages/core/utils/src/dml/__tests__/array-property.spec.ts index b768b21156f39..78d5d5b7afbb6 100644 --- a/packages/core/utils/src/dml/__tests__/array-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/array-property.spec.ts @@ -12,6 +12,7 @@ describe("Array property", () => { name: "array", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/base-property.spec.ts b/packages/core/utils/src/dml/__tests__/base-property.spec.ts index dedb719f1622a..cfa6acef3cbd9 100644 --- a/packages/core/utils/src/dml/__tests__/base-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/base-property.spec.ts @@ -1,6 +1,6 @@ +import { PropertyMetadata } from "@medusajs/types" import { expectTypeOf } from "expect-type" import { BaseProperty } from "../properties/base" -import { PropertyMetadata } from "@medusajs/types" import { TextProperty } from "../properties/text" describe("Base property", () => { @@ -20,6 +20,7 @@ describe("Base property", () => { name: "text", }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -38,6 +39,7 @@ describe("Base property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -59,6 +61,7 @@ describe("Base property", () => { name: "text", }, nullable: true, + optional: false, indexes: [], relationships: [], }) @@ -81,6 +84,98 @@ describe("Base property", () => { }, defaultValue: "foo", nullable: false, + optional: false, + indexes: [], + relationships: [], + }) + }) + + test("apply optional modifier", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().optional() + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: false, + optional: true, + indexes: [], + relationships: [], + }) + }) + + test("apply optional and nullable modifier", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().optional().nullable() + expectTypeOf(property["$dataType"]).toEqualTypeOf< + string | undefined | null + >() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: true, + optional: true, + indexes: [], + relationships: [], + }) + }) + + test("apply nullable and optional modifier", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().nullable().optional() + expectTypeOf(property["$dataType"]).toEqualTypeOf< + string | null | undefined + >() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: true, + optional: true, + indexes: [], + relationships: [], + }) + }) + + test("define default value as a callback", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().default(() => "22") + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + defaultValue: expect.any(Function), + nullable: false, + optional: false, indexes: [], relationships: [], }) 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 index 09ef13aefc5cc..4861fa2cb67e8 100644 --- a/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/big-number-property.spec.ts @@ -12,6 +12,7 @@ describe("Big Number property", () => { name: "bigNumber", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts b/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts index dc3714d08107c..545f3718fdbd2 100644 --- a/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/boolean-property.spec.ts @@ -12,6 +12,7 @@ describe("Boolean property", () => { name: "boolean", }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts b/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts index 497d059a434e3..10ebc9fa06303 100644 --- a/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/date-time-property.spec.ts @@ -12,6 +12,7 @@ describe("DateTime property", () => { name: "dateTime", }, nullable: false, + optional: 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 db304de5a9d9b..73a11a122d460 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -8,6 +8,7 @@ import { toMikroOrmEntities, toMikroORMEntity, } from "../helpers/create-mikro-orm-entity" +import { InferTypeOf } from "@medusajs/types" describe("Entity builder", () => { beforeEach(() => { @@ -1447,6 +1448,130 @@ describe("Entity builder", () => { }, }) }) + + test("define a property with default runtime value", () => { + const user = model.define("user", { + id: model.number(), + username: model.text().default((schema) => { + const { email } = schema as InferTypeOf + return email.replace(/\@.*/, "") + }), + email: model.text(), + spend_limit: model.bigNumber().default(500.4), + }) + + const User = toMikroORMEntity(user) + expectTypeOf(new User()).toMatchTypeOf<{ + id: number + username: string + email: string + deleted_at: Date | null + }>() + + const metaData = MetadataStorage.getMetadataFromDecorator(User) + expect(metaData.className).toEqual("User") + expect(metaData.path).toEqual("User") + + expect(metaData.filters).toEqual({ + softDeletable: { + name: "softDeletable", + cond: expect.any(Function), + default: true, + args: false, + }, + }) + + expect(metaData.properties).toEqual({ + id: { + reference: "scalar", + type: "number", + columnType: "integer", + name: "id", + fieldName: "id", + nullable: false, + getter: false, + setter: false, + }, + username: { + reference: "scalar", + type: "string", + default: expect.any(Function), + columnType: "text", + name: "username", + fieldName: "username", + nullable: false, + getter: false, + setter: false, + }, + email: { + reference: "scalar", + type: "string", + columnType: "text", + name: "email", + fieldName: "email", + nullable: false, + getter: false, + setter: false, + }, + spend_limit: { + columnType: "numeric", + default: 500.4, + getter: true, + name: "spend_limit", + fieldName: "spend_limit", + nullable: false, + reference: "scalar", + setter: true, + trackChanges: false, + type: "any", + }, + raw_spend_limit: { + columnType: "jsonb", + getter: false, + name: "raw_spend_limit", + fieldName: "raw_spend_limit", + nullable: false, + reference: "scalar", + setter: false, + type: "any", + }, + created_at: { + reference: "scalar", + type: "date", + columnType: "timestamptz", + name: "created_at", + fieldName: "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", + fieldName: "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", + fieldName: "deleted_at", + nullable: true, + getter: false, + setter: false, + }, + }) + }) }) describe("Entity builder | id", () => { diff --git a/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts b/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts index 98cf2ad4deb57..fabcec7cf1057 100644 --- a/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts +++ b/packages/core/utils/src/dml/__tests__/enum-schema.spec.ts @@ -17,6 +17,7 @@ describe("Enum property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -42,6 +43,7 @@ describe("Enum property", () => { }, }, nullable: true, + optional: false, indexes: [], relationships: [], }) @@ -66,6 +68,7 @@ describe("Enum property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -90,6 +93,7 @@ describe("Enum property", () => { }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/id-property.spec.ts b/packages/core/utils/src/dml/__tests__/id-property.spec.ts index ce63f59da3b4f..dc6aedbad1a0c 100644 --- a/packages/core/utils/src/dml/__tests__/id-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/id-property.spec.ts @@ -13,6 +13,7 @@ describe("Id property", () => { options: {}, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -29,6 +30,7 @@ describe("Id property", () => { options: {}, }, nullable: false, + optional: false, indexes: [], relationships: [], primaryKey: true, diff --git a/packages/core/utils/src/dml/__tests__/json-property.spec.ts b/packages/core/utils/src/dml/__tests__/json-property.spec.ts index 887c615276afa..d965ddf34a298 100644 --- a/packages/core/utils/src/dml/__tests__/json-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/json-property.spec.ts @@ -12,6 +12,7 @@ describe("JSON property", () => { name: "json", }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -30,6 +31,7 @@ describe("JSON property", () => { a: 1, }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/number-property.spec.ts b/packages/core/utils/src/dml/__tests__/number-property.spec.ts index 6d4231f3125b3..6f5e8b7bdd0aa 100644 --- a/packages/core/utils/src/dml/__tests__/number-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/number-property.spec.ts @@ -13,6 +13,7 @@ describe("Number property", () => { options: {}, }, nullable: false, + optional: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/text-property.spec.ts b/packages/core/utils/src/dml/__tests__/text-property.spec.ts index b14c2eca7e0b8..f30bf9f60296e 100644 --- a/packages/core/utils/src/dml/__tests__/text-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/text-property.spec.ts @@ -13,6 +13,7 @@ describe("Text property", () => { options: { searchable: false }, }, nullable: false, + optional: false, indexes: [], relationships: [], }) @@ -29,6 +30,7 @@ describe("Text property", () => { options: { searchable: false }, }, nullable: false, + optional: false, indexes: [], relationships: [], primaryKey: true, diff --git a/packages/core/utils/src/dml/properties/base.ts b/packages/core/utils/src/dml/properties/base.ts index 2ab719c4a1c25..e18bcba22ecd6 100644 --- a/packages/core/utils/src/dml/properties/base.ts +++ b/packages/core/utils/src/dml/properties/base.ts @@ -1,5 +1,6 @@ import { PropertyMetadata, PropertyType } from "@medusajs/types" import { NullableModifier } from "./nullable" +import { OptionalModifier } from "./optional" /** * The BaseProperty class offers shared affordances to define @@ -15,7 +16,7 @@ export abstract class BaseProperty implements PropertyType { /** * Default value for the property */ - #defaultValue?: T + #defaultValue?: T | ((schema: any) => T) /** * The runtime dataType for the schema. It is not the same as @@ -48,6 +49,25 @@ export abstract class BaseProperty implements PropertyType { return new NullableModifier(this) } + /** + * This method indicates that a property's value can be `optional`. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * price: model.bigNumber().optional(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + optional() { + return new OptionalModifier(this) + } + /** * This method defines an index on a property. * @@ -119,7 +139,7 @@ export abstract class BaseProperty implements PropertyType { * * @customNamespace Property Configuration Methods */ - default(value: T) { + default(value: T | ((schema: any) => T)) { this.#defaultValue = value return this } @@ -132,6 +152,7 @@ export abstract class BaseProperty implements PropertyType { fieldName, dataType: this.dataType, nullable: false, + optional: false, defaultValue: this.#defaultValue, indexes: this.#indexes, relationships: this.#relationships, diff --git a/packages/core/utils/src/dml/properties/index.ts b/packages/core/utils/src/dml/properties/index.ts index 9c55e703660a0..19c597fa23d65 100644 --- a/packages/core/utils/src/dml/properties/index.ts +++ b/packages/core/utils/src/dml/properties/index.ts @@ -7,6 +7,7 @@ export * from "./enum" export * from "./id" export * from "./json" export * from "./nullable" +export * from "./optional" export * from "./number" export * from "./primary-key" export * from "./text" diff --git a/packages/core/utils/src/dml/properties/nullable.ts b/packages/core/utils/src/dml/properties/nullable.ts index 685addae025a6..238bde9c59b90 100644 --- a/packages/core/utils/src/dml/properties/nullable.ts +++ b/packages/core/utils/src/dml/properties/nullable.ts @@ -1,10 +1,11 @@ import { PropertyType } from "@medusajs/types" +import { OptionalModifier } from "./optional" const IsNullableModifier = Symbol.for("isNullableModifier") /** * Nullable modifier marks a schema node as nullable */ -export class NullableModifier> +export class NullableModifier>> implements PropertyType { [IsNullableModifier]: true = true @@ -12,6 +13,7 @@ export class NullableModifier> static isNullableModifier(obj: any): obj is NullableModifier { return !!obj?.[IsNullableModifier] } + /** * A type-only property to infer the JavScript data-type * of the schema property @@ -28,6 +30,25 @@ export class NullableModifier> this.#schema = schema } + /** + * This method indicates that a property's value can be `optional`. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * price: model.bigNumber().optional(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + optional() { + return new OptionalModifier(this) + } + /** * Returns the serialized metadata */ diff --git a/packages/core/utils/src/dml/properties/optional.ts b/packages/core/utils/src/dml/properties/optional.ts new file mode 100644 index 0000000000000..8c514c1900922 --- /dev/null +++ b/packages/core/utils/src/dml/properties/optional.ts @@ -0,0 +1,62 @@ +import { PropertyType } from "@medusajs/types" +import { NullableModifier } from "./nullable" + +const IsOptionalModifier = Symbol.for("IsOptionalModifier") + +/** + * Nullable modifier marks a schema node as optional and + * allows for default values + */ +export class OptionalModifier> + implements PropertyType +{ + [IsOptionalModifier]: true = true + + static isOptionalModifier(obj: any): obj is OptionalModifier { + return !!obj?.[IsOptionalModifier] + } + + /** + * A type-only property to infer the JavScript data-type + * of the schema property + */ + declare $dataType: T | undefined + + /** + * The parent schema on which the nullable modifier is + * applied + */ + #schema: Schema + + constructor(schema: Schema) { + this.#schema = schema + } + + /** + * This method indicates that a property's value can be `null`. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * price: model.bigNumber().optional().nullable(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + nullable() { + return new NullableModifier(this) + } + + /** + * Returns the serialized metadata + */ + parse(fieldName: string) { + const schema = this.#schema.parse(fieldName) + schema.optional = true + return schema + } +}