From 729eb5da7b6daf9781b8bdcbc2fab344e942d444 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Fri, 13 Dec 2024 09:51:26 -0300 Subject: [PATCH] chore(inventory): convert to dml (#10569) Fixes: FRMW-2848 Co-authored-by: Harminder Virk <1706381+thetutlage@users.noreply.github.com> --- .changeset/big-days-train.md | 7 + .../__tests__/product/store/product.spec.ts | 2 +- packages/core/types/src/dml/index.ts | 1 + .../src/dml/__tests__/array-property.spec.ts | 1 + .../__tests__/autoincrement-property.spec.ts | 1 + .../src/dml/__tests__/base-property.spec.ts | 41 +- .../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__/enum-schema.spec.ts | 4 + .../src/dml/__tests__/float-property.spec.ts | 1 + .../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 + .../create-big-number-properties.ts | 8 +- .../helpers/entity-builder/define-property.ts | 4 + .../core/utils/src/dml/properties/base.ts | 23 ++ .../core/utils/src/dml/properties/computed.ts | 39 ++ .../core/utils/src/dml/properties/index.ts | 3 +- .../core/utils/src/dml/properties/nullable.ts | 8 + .../__tests__/pg_utils.spec.ts | 4 +- .../inventory-module-service.spec.ts | 23 ++ .../.snapshot-medusa-inventory.json | 358 +++++++++++------- .../src/migrations/Migration20241213063611.ts | 72 ++++ .../modules/inventory/src/models/index.ts | 6 +- .../inventory/src/models/inventory-item.ts | 193 ++-------- .../inventory/src/models/inventory-level.ts | 169 ++------- .../inventory/src/models/reservation-item.ts | 163 ++------ .../inventory/src/repositories/index.ts | 2 +- .../inventory/src/services/inventory-level.ts | 7 +- .../src/services/inventory-module.ts | 24 +- .../inventory/src/utils/apply-decorators.ts | 45 +++ 33 files changed, 644 insertions(+), 575 deletions(-) create mode 100644 .changeset/big-days-train.md create mode 100644 packages/core/utils/src/dml/properties/computed.ts create mode 100644 packages/modules/inventory/src/migrations/Migration20241213063611.ts create mode 100644 packages/modules/inventory/src/utils/apply-decorators.ts diff --git a/.changeset/big-days-train.md b/.changeset/big-days-train.md new file mode 100644 index 0000000000000..0f594406b98a8 --- /dev/null +++ b/.changeset/big-days-train.md @@ -0,0 +1,7 @@ +--- +"@medusajs/inventory": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +chore: Inventory DML diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index 4b64e5e1b96c4..a30f1214b9831 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -1353,7 +1353,7 @@ medusaIntegrationTestRunner({ { title: "variant two", options: { color: "blue" } }, ], }) - console.log(product) + const [variantOne, variantTwo] = product.variants const [itemOne, itemTwo, itemThree] = diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 040b5bcdbae70..4f126af40414d 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -72,6 +72,7 @@ export type PropertyMetadata = { fieldName: string defaultValue?: any nullable: boolean + computed: boolean dataType: { name: KnownDataTypes options?: Record 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..2d98fdadc4118 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, + computed: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/autoincrement-property.spec.ts b/packages/core/utils/src/dml/__tests__/autoincrement-property.spec.ts index dd2a1e851bee2..39f976c1eb8e9 100644 --- a/packages/core/utils/src/dml/__tests__/autoincrement-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/autoincrement-property.spec.ts @@ -13,6 +13,7 @@ describe("Autoincrement property", () => { options: {}, }, nullable: false, + computed: 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..e29f34721f608 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, + computed: false, indexes: [], relationships: [], }) @@ -38,6 +39,7 @@ describe("Base property", () => { }, }, nullable: false, + computed: false, indexes: [], relationships: [], }) @@ -59,6 +61,42 @@ describe("Base property", () => { name: "text", }, nullable: true, + computed: false, + indexes: [], + relationships: [], + }) + }) + + test("apply computed property", () => { + class StringProperty extends BaseProperty { + protected dataType: PropertyMetadata["dataType"] = { + name: "text", + } + } + + const property = new StringProperty().computed() + const property2 = new StringProperty().nullable().computed() + + expectTypeOf(property["$dataType"]).toEqualTypeOf() + expect(property.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: false, + computed: true, + indexes: [], + relationships: [], + }) + + expectTypeOf(property2["$dataType"]).toEqualTypeOf() + expect(property2.parse("username")).toEqual({ + fieldName: "username", + dataType: { + name: "text", + }, + nullable: true, + computed: true, indexes: [], relationships: [], }) @@ -81,6 +119,7 @@ describe("Base property", () => { }, defaultValue: "foo", nullable: false, + computed: 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..cd2599b1f74e7 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, + computed: 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..888c84e21436f 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, + computed: 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..187c69e2204e1 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, + computed: false, indexes: [], relationships: [], }) 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..0236f8efd0af3 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, + computed: false, indexes: [], relationships: [], }) @@ -42,6 +43,7 @@ describe("Enum property", () => { }, }, nullable: true, + computed: false, indexes: [], relationships: [], }) @@ -66,6 +68,7 @@ describe("Enum property", () => { }, }, nullable: false, + computed: false, indexes: [], relationships: [], }) @@ -90,6 +93,7 @@ describe("Enum property", () => { }, }, nullable: false, + computed: false, indexes: [], relationships: [], }) diff --git a/packages/core/utils/src/dml/__tests__/float-property.spec.ts b/packages/core/utils/src/dml/__tests__/float-property.spec.ts index ed8c94b4c50e3..c702670594820 100644 --- a/packages/core/utils/src/dml/__tests__/float-property.spec.ts +++ b/packages/core/utils/src/dml/__tests__/float-property.spec.ts @@ -12,6 +12,7 @@ describe("Float property", () => { name: "float", }, nullable: false, + computed: 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..5348bc20efd48 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, + computed: false, indexes: [], relationships: [], }) @@ -29,6 +30,7 @@ describe("Id property", () => { options: {}, }, nullable: false, + computed: 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..066505f84ca00 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, + computed: false, indexes: [], relationships: [], }) @@ -30,6 +31,7 @@ describe("JSON property", () => { a: 1, }, nullable: false, + computed: 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..2bf4a1a3d6126 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, + computed: 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..7f0fb52eeab9d 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, + computed: false, indexes: [], relationships: [], }) @@ -29,6 +30,7 @@ describe("Text property", () => { options: { searchable: false }, }, nullable: false, + computed: false, indexes: [], relationships: [], primaryKey: true, 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 index 9716a98bad825..55c9de8466a7c 100644 --- 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 @@ -41,10 +41,16 @@ export function createBigNumberProperties( continue } - const jsonProperty = parsed.nullable + let jsonProperty = parsed.nullable ? new JSONProperty().nullable() : new JSONProperty() + if (parsed.computed) { + jsonProperty = jsonProperty.computed() as unknown as + | JSONProperty + | NullableModifier, JSONProperty> + } + schemaWithBigNumber[`raw_${key}`] = jsonProperty } } diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts index 9db3bcbfdc17a..4504a3d0919bc 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-property.ts @@ -140,6 +140,10 @@ export function defineProperty( BeforeCreate()(MikroORMEntity.prototype, defaultValueSetterHookName) } + if (field.computed) { + return + } + if (SPECIAL_PROPERTIES[field.fieldName]) { SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field, tableName) return diff --git a/packages/core/utils/src/dml/properties/base.ts b/packages/core/utils/src/dml/properties/base.ts index 2ab719c4a1c25..52f708cc84149 100644 --- a/packages/core/utils/src/dml/properties/base.ts +++ b/packages/core/utils/src/dml/properties/base.ts @@ -1,4 +1,5 @@ import { PropertyMetadata, PropertyType } from "@medusajs/types" +import { ComputedProperty } from "./computed" import { NullableModifier } from "./nullable" /** @@ -48,6 +49,27 @@ export abstract class BaseProperty implements PropertyType { return new NullableModifier(this) } + /** + * This method indicated that the property is a computed property. + * Computed properties are not stored in the database but are + * computed on the fly. + * + * @example + * import { model } from "@medusajs/framework/utils" + * + * const MyCustom = model.define("my_custom", { + * calculated_price: model.bigNumber().computed(), + * // ... + * }) + * + * export default MyCustom + * + * @customNamespace Property Configuration Methods + */ + computed() { + return new ComputedProperty(this) + } + /** * This method defines an index on a property. * @@ -132,6 +154,7 @@ export abstract class BaseProperty implements PropertyType { fieldName, dataType: this.dataType, nullable: false, + computed: false, defaultValue: this.#defaultValue, indexes: this.#indexes, relationships: this.#relationships, diff --git a/packages/core/utils/src/dml/properties/computed.ts b/packages/core/utils/src/dml/properties/computed.ts new file mode 100644 index 0000000000000..ed37d63f608cb --- /dev/null +++ b/packages/core/utils/src/dml/properties/computed.ts @@ -0,0 +1,39 @@ +import { PropertyType } from "@medusajs/types" + +const IsComputedProperty = Symbol.for("isComputedProperty") +/** + * Computed property marks a schema node as computed + */ +export class ComputedProperty> + implements PropertyType +{ + [IsComputedProperty]: true = true + + static isComputedProperty(obj: any): obj is ComputedProperty { + return !!obj?.[IsComputedProperty] + } + /** + * A type-only property to infer the JavScript data-type + * of the schema property + */ + declare $dataType: T | null + + /** + * The parent schema on which the computed property is + * applied + */ + #schema: Schema + + constructor(schema: Schema) { + this.#schema = schema + } + + /** + * Returns the serialized metadata + */ + parse(fieldName: string) { + const schema = this.#schema.parse(fieldName) + schema.computed = true + return schema + } +} diff --git a/packages/core/utils/src/dml/properties/index.ts b/packages/core/utils/src/dml/properties/index.ts index 7c08efe19e81e..6a2bba0f1cce7 100644 --- a/packages/core/utils/src/dml/properties/index.ts +++ b/packages/core/utils/src/dml/properties/index.ts @@ -3,12 +3,13 @@ export * from "./autoincrement" export * from "./base" export * from "./big-number" export * from "./boolean" +export * from "./computed" export * from "./date-time" export * from "./enum" +export * from "./float" export * from "./id" export * from "./json" export * from "./nullable" export * from "./number" -export * from "./float" 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..eb5712526cdc8 100644 --- a/packages/core/utils/src/dml/properties/nullable.ts +++ b/packages/core/utils/src/dml/properties/nullable.ts @@ -1,4 +1,5 @@ import { PropertyType } from "@medusajs/types" +import { ComputedProperty } from "./computed" const IsNullableModifier = Symbol.for("isNullableModifier") /** @@ -28,6 +29,13 @@ export class NullableModifier> this.#schema = schema } + /** + * This method indicated that the property is a computed property. + */ + computed() { + return new ComputedProperty(this) + } + /** * Returns the serialized metadata */ diff --git a/packages/core/utils/src/pg/integration-tests/__tests__/pg_utils.spec.ts b/packages/core/utils/src/pg/integration-tests/__tests__/pg_utils.spec.ts index c6daff7648acb..ee664dcc2d178 100644 --- a/packages/core/utils/src/pg/integration-tests/__tests__/pg_utils.spec.ts +++ b/packages/core/utils/src/pg/integration-tests/__tests__/pg_utils.spec.ts @@ -1,9 +1,9 @@ import { dropDatabase } from "pg-god" import { createClient, - parseConnectionString, - dbExists, createDb, + dbExists, + parseConnectionString, } from "../../index" const DB_HOST = process.env.DB_HOST ?? "localhost" diff --git a/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts b/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts index 89eb399d1ef46..5d88297993b45 100644 --- a/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts +++ b/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts @@ -229,6 +229,29 @@ moduleIntegrationTestRunner({ expect(inventoryLevel).toEqual( expect.objectContaining({ id: expect.any(String), ...data }) ) + + const getItems = await service.listInventoryItems( + {}, + { + select: [ + "id", + "sku", + "origin_country", + "reserved_quantity", + "stocked_quantity", + ], + } + ) + + expect(getItems).toEqual([ + { + id: inventoryItem.id, + sku: "test-sku", + origin_country: "test-country", + reserved_quantity: 0, + stocked_quantity: 2, + }, + ]) }) it("should create inventoryLevels from array", async () => { diff --git a/packages/modules/inventory/src/migrations/.snapshot-medusa-inventory.json b/packages/modules/inventory/src/migrations/.snapshot-medusa-inventory.json index f59b0c2ac30ef..8b04031e1b912 100644 --- a/packages/modules/inventory/src/migrations/.snapshot-medusa-inventory.json +++ b/packages/modules/inventory/src/migrations/.snapshot-medusa-inventory.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -13,38 +15,6 @@ "nullable": false, "mappedType": "text" }, - "created_at": { - "name": "created_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 6, - "mappedType": "datetime" - }, "sku": { "name": "sku", "type": "text", @@ -92,7 +62,7 @@ }, "weight": { "name": "weight", - "type": "int", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, @@ -101,7 +71,7 @@ }, "length": { "name": "length", - "type": "int", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, @@ -110,7 +80,7 @@ }, "height": { "name": "height", - "type": "int", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, @@ -119,7 +89,7 @@ }, "width": { "name": "width", - "type": "int", + "type": "integer", "unsigned": false, "autoincrement": false, "primary": false, @@ -171,6 +141,38 @@ "primary": false, "nullable": true, "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "inventory_item", @@ -178,23 +180,33 @@ "indexes": [ { "keyName": "IDX_inventory_item_deleted_at", - "columnNames": ["deleted_at"], + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_item_deleted_at\" ON \"inventory_item\" (deleted_at) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_inventory_item_deleted_at", + "columnNames": [], "composite": false, "primary": false, "unique": false, "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_item_deleted_at\" ON \"inventory_item\" (deleted_at) WHERE deleted_at IS NOT NULL" }, { - "keyName": "IDX_inventory_item_sku_unique", - "columnNames": ["sku"], + "keyName": "IDX_inventory_item_sku", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_inventory_item_sku_unique\" ON \"inventory_item\" (sku)" + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_inventory_item_sku\" ON \"inventory_item\" (sku) WHERE deleted_at IS NULL" }, { "keyName": "inventory_item_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -214,37 +226,53 @@ "nullable": false, "mappedType": "text" }, - "created_at": { - "name": "created_at", - "type": "timestamptz", + "location_id": { + "name": "location_id", + "type": "text", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" + "mappedType": "text" }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz", + "stocked_quantity": { + "name": "stocked_quantity", + "type": "numeric", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" + "default": "0", + "mappedType": "decimal" }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", + "reserved_quantity": { + "name": "reserved_quantity", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "decimal" + }, + "incoming_quantity": { + "name": "incoming_quantity", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "decimal" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "length": 6, - "mappedType": "datetime" + "mappedType": "json" }, "inventory_item_id": { "name": "inventory_item_id", @@ -255,93 +283,106 @@ "nullable": false, "mappedType": "text" }, - "location_id": { - "name": "location_id", - "type": "text", + "raw_stocked_quantity": { + "name": "raw_stocked_quantity", + "type": "jsonb", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "mappedType": "text" + "mappedType": "json" }, - "stocked_quantity": { - "name": "stocked_quantity", - "type": "int", + "raw_reserved_quantity": { + "name": "raw_reserved_quantity", + "type": "jsonb", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "default": "0", - "mappedType": "integer" + "mappedType": "json" }, - "reserved_quantity": { - "name": "reserved_quantity", - "type": "int", + "raw_incoming_quantity": { + "name": "raw_incoming_quantity", + "type": "jsonb", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "default": "0", - "mappedType": "integer" + "mappedType": "json" }, - "incoming_quantity": { - "name": "incoming_quantity", - "type": "int", + "created_at": { + "name": "created_at", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "default": "0", - "mappedType": "integer" + "length": 6, + "default": "now()", + "mappedType": "datetime" }, - "metadata": { - "name": "metadata", - "type": "jsonb", + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "json" + "length": 6, + "mappedType": "datetime" } }, "name": "inventory_level", "schema": "public", "indexes": [ { - "keyName": "IDX_inventory_level_deleted_at", - "columnNames": ["deleted_at"], + "keyName": "IDX_inventory_level_inventory_item_id", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_deleted_at\" ON \"inventory_level\" (deleted_at) WHERE deleted_at IS NOT NULL" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_inventory_item_id\" ON \"inventory_level\" (inventory_item_id) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_inventory_level_inventory_item_id", - "columnNames": ["inventory_item_id"], + "keyName": "IDX_inventory_level_deleted_at", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_inventory_item_id\" ON \"inventory_level\" (inventory_item_id)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_deleted_at\" ON \"inventory_level\" (deleted_at) WHERE deleted_at IS NULL" }, { "keyName": "IDX_inventory_level_location_id", - "columnNames": ["location_id"], + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_location_id\" ON \"inventory_level\" (location_id)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_location_id\" ON \"inventory_level\" (location_id) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_inventory_level_location_id", + "keyName": "IDX_inventory_level_location_id_inventory_item_id", "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_inventory_level_location_id\" ON \"inventory_level\" (location_id)" + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_inventory_level_location_id_inventory_item_id\" ON \"inventory_level\" (inventory_item_id, location_id) WHERE deleted_at IS NULL" }, { "keyName": "inventory_level_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -351,9 +392,13 @@ "foreignKeys": { "inventory_level_inventory_item_id_foreign": { "constraintName": "inventory_level_inventory_item_id_foreign", - "columnNames": ["inventory_item_id"], + "columnNames": [ + "inventory_item_id" + ], "localTableName": "public.inventory_level", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.inventory_item", "deleteRule": "cascade", "updateRule": "cascade" @@ -371,38 +416,6 @@ "nullable": false, "mappedType": "text" }, - "created_at": { - "name": "created_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "default": "now()", - "mappedType": "datetime" - }, - "deleted_at": { - "name": "deleted_at", - "type": "timestamptz", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 6, - "mappedType": "datetime" - }, "line_item_id": { "name": "line_item_id", "type": "text", @@ -412,6 +425,16 @@ "nullable": true, "mappedType": "text" }, + "allow_backorder": { + "name": "allow_backorder", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, "location_id": { "name": "location_id", "type": "text", @@ -423,12 +446,21 @@ }, "quantity": { "name": "quantity", - "type": "integer", + "type": "numeric", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "mappedType": "integer" + "mappedType": "decimal" + }, + "raw_quantity": { + "name": "raw_quantity", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" }, "external_id": { "name": "external_id", @@ -474,46 +506,80 @@ "primary": false, "nullable": false, "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "reservation_item", "schema": "public", "indexes": [ { - "keyName": "IDX_reservation_item_deleted_at", - "columnNames": ["deleted_at"], + "keyName": "IDX_reservation_item_inventory_item_id", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_deleted_at\" ON \"reservation_item\" (deleted_at) WHERE deleted_at IS NOT NULL" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_inventory_item_id\" ON \"reservation_item\" (inventory_item_id) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_reservation_item_line_item_id", - "columnNames": ["line_item_id"], + "keyName": "IDX_reservation_item_deleted_at", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_line_item_id\" ON \"reservation_item\" (line_item_id)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_deleted_at\" ON \"reservation_item\" (deleted_at) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_reservation_item_location_id", - "columnNames": ["location_id"], + "keyName": "IDX_reservation_item_line_item_id", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_location_id\" ON \"reservation_item\" (location_id)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_line_item_id\" ON \"reservation_item\" (line_item_id) WHERE deleted_at IS NULL" }, { - "keyName": "IDX_reservation_item_inventory_item_id", - "columnNames": ["inventory_item_id"], + "keyName": "IDX_reservation_item_location_id", + "columnNames": [], "composite": false, "primary": false, "unique": false, - "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_inventory_item_id\" ON \"reservation_item\" (inventory_item_id)" + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_location_id\" ON \"reservation_item\" (location_id) WHERE deleted_at IS NULL" }, { "keyName": "reservation_item_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -523,9 +589,13 @@ "foreignKeys": { "reservation_item_inventory_item_id_foreign": { "constraintName": "reservation_item_inventory_item_id_foreign", - "columnNames": ["inventory_item_id"], + "columnNames": [ + "inventory_item_id" + ], "localTableName": "public.reservation_item", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.inventory_item", "deleteRule": "cascade", "updateRule": "cascade" diff --git a/packages/modules/inventory/src/migrations/Migration20241213063611.ts b/packages/modules/inventory/src/migrations/Migration20241213063611.ts new file mode 100644 index 0000000000000..041418ca44ba8 --- /dev/null +++ b/packages/modules/inventory/src/migrations/Migration20241213063611.ts @@ -0,0 +1,72 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241213063611 extends Migration { + async up(): Promise { + this.addSql('drop index if exists "IDX_inventory_item_sku_unique";') + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku" ON "inventory_item" (sku) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'alter table if exists "inventory_level" add column if not exists "raw_stocked_quantity" jsonb not null, add column if not exists "raw_reserved_quantity" jsonb not null, add column if not exists "raw_incoming_quantity" jsonb not null;' + ) + this.addSql( + 'alter table if exists "inventory_level" alter column "stocked_quantity" type numeric using ("stocked_quantity"::numeric);' + ) + this.addSql( + 'alter table if exists "inventory_level" alter column "reserved_quantity" type numeric using ("reserved_quantity"::numeric);' + ) + this.addSql( + 'alter table if exists "inventory_level" alter column "incoming_quantity" type numeric using ("incoming_quantity"::numeric);' + ) + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_level_location_id_inventory_item_id" ON "inventory_level" (inventory_item_id, location_id) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'alter table if exists "reservation_item" add column if not exists "allow_backorder" boolean not null default false, add column if not exists "raw_quantity" jsonb not null;' + ) + this.addSql( + 'alter table if exists "reservation_item" alter column "quantity" type numeric using ("quantity"::numeric);' + ) + } + + async down(): Promise { + this.addSql('drop index if exists "IDX_inventory_item_sku";') + this.addSql( + 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_inventory_item_sku_unique" ON "inventory_item" (sku);' + ) + + this.addSql( + 'alter table if exists "inventory_level" alter column "stocked_quantity" type int using ("stocked_quantity"::int);' + ) + this.addSql( + 'alter table if exists "inventory_level" alter column "reserved_quantity" type int using ("reserved_quantity"::int);' + ) + this.addSql( + 'alter table if exists "inventory_level" alter column "incoming_quantity" type int using ("incoming_quantity"::int);' + ) + this.addSql( + 'drop index if exists "IDX_inventory_level_location_id_inventory_item_id";' + ) + this.addSql( + 'alter table if exists "inventory_level" drop column if exists "raw_stocked_quantity";' + ) + this.addSql( + 'alter table if exists "inventory_level" drop column if exists "raw_reserved_quantity";' + ) + this.addSql( + 'alter table if exists "inventory_level" drop column if exists "raw_incoming_quantity";' + ) + + this.addSql( + 'alter table if exists "reservation_item" alter column "quantity" type integer using ("quantity"::integer);' + ) + this.addSql( + 'alter table if exists "reservation_item" drop column if exists "allow_backorder";' + ) + this.addSql( + 'alter table if exists "reservation_item" drop column if exists "raw_quantity";' + ) + } +} diff --git a/packages/modules/inventory/src/models/index.ts b/packages/modules/inventory/src/models/index.ts index 8a77e08089fff..ddf2fcf086b7d 100644 --- a/packages/modules/inventory/src/models/index.ts +++ b/packages/modules/inventory/src/models/index.ts @@ -1,3 +1,3 @@ -export * from "./reservation-item" -export * from "./inventory-item" -export * from "./inventory-level" +export { default as InventoryItem } from "./inventory-item" +export { default as InventoryLevel } from "./inventory-level" +export { default as ReservationItem } from "./reservation-item" diff --git a/packages/modules/inventory/src/models/inventory-item.ts b/packages/modules/inventory/src/models/inventory-item.ts index 5ee899fc27509..79173f4b9f29c 100644 --- a/packages/modules/inventory/src/models/inventory-item.ts +++ b/packages/modules/inventory/src/models/inventory-item.ts @@ -1,156 +1,43 @@ -import { - createPsqlIndexStatementHelper, - DALUtils, - generateEntityId, - Searchable, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Collection, - Entity, - Filter, - Formula, - OneToMany, - OnInit, - OptionalProps, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" - -import { DAL } from "@medusajs/framework/types" -import { InventoryLevel } from "./inventory-level" -import { ReservationItem } from "./reservation-item" - -const InventoryItemDeletedAtIndex = createPsqlIndexStatementHelper({ - tableName: "inventory_item", - columns: "deleted_at", - where: "deleted_at IS NOT NULL", -}) - -const InventoryItemSkuIndex = createPsqlIndexStatementHelper({ - tableName: "inventory_item", - columns: "sku", - unique: true, - where: "deleted_at IS NULL", -}) - -type InventoryItemOptionalProps = DAL.SoftDeletableModelDateColumns - -@Entity() -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export class InventoryItem { - [OptionalProps]: InventoryItemOptionalProps - - @PrimaryKey({ columnType: "text" }) - id: string - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", +import { model } from "@medusajs/framework/utils" +import InventoryLevel from "./inventory-level" +import ReservationItem from "./reservation-item" + +const InventoryItem = model + .define("InventoryItem", { + id: model.id({ prefix: "iitem" }).primaryKey(), + sku: model.text().searchable().nullable(), + origin_country: model.text().nullable(), + hs_code: model.text().searchable().nullable(), + mid_code: model.text().searchable().nullable(), + material: model.text().nullable(), + weight: model.number().nullable(), + length: model.number().nullable(), + height: model.number().nullable(), + width: model.number().nullable(), + requires_shipping: model.boolean().default(true), + description: model.text().searchable().nullable(), + title: model.text().searchable().nullable(), + thumbnail: model.text().nullable(), + metadata: model.json().nullable(), + location_levels: model.hasMany(() => InventoryLevel, { + mappedBy: "inventory_item", + }), + reservation_items: model.hasMany(() => ReservationItem, { + mappedBy: "inventory_item", + }), + reserved_quantity: model.number().computed(), + stocked_quantity: model.number().computed(), }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", + .cascades({ + delete: ["location_levels", "reservation_items"], }) - updated_at: Date - - @InventoryItemDeletedAtIndex.MikroORMIndex() - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null - - @InventoryItemSkuIndex.MikroORMIndex() - @Searchable() - @Property({ columnType: "text", nullable: true }) - sku: string | null = null - - @Property({ columnType: "text", nullable: true }) - origin_country: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - hs_code: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - mid_code: string | null = null - - @Property({ columnType: "text", nullable: true }) - material: string | null = null - - @Property({ type: "int", nullable: true }) - weight: number | null = null - - @Property({ type: "int", nullable: true }) - length: number | null = null - - @Property({ type: "int", nullable: true }) - height: number | null = null - - @Property({ type: "int", nullable: true }) - width: number | null = null - - @Property({ columnType: "boolean" }) - requires_shipping: boolean = true - - @Searchable() - @Property({ columnType: "text", nullable: true }) - description: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - title: string | null = null - - @Property({ columnType: "text", nullable: true }) - thumbnail: string | null = null - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @OneToMany( - () => InventoryLevel, - (inventoryLevel) => inventoryLevel.inventory_item, - { - cascade: ["soft-remove" as any], - } - ) - location_levels = new Collection>(this) - - @OneToMany( - () => ReservationItem, - (reservationItem) => reservationItem.inventory_item, + .indexes([ { - cascade: ["soft-remove" as any], - } - ) - reservation_items = new Collection>(this) - - @Formula( - (item) => - `(SELECT SUM(reserved_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id AND il.deleted_at IS NULL)`, - { lazy: true, serializer: Number, hidden: true } - ) - reserved_quantity: number - - @Formula( - (item) => - `(SELECT SUM(stocked_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id AND il.deleted_at IS NULL)`, - { lazy: true, serializer: Number, hidden: true } - ) - stocked_quantity: number - - @BeforeCreate() - beforeCreate(): void { - this.id = generateEntityId(this.id, "iitem") - } - - @OnInit() - onInit(): void { - this.id = generateEntityId(this.id, "iitem") - } -} + name: "IDX_inventory_item_sku", + on: ["sku"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) + +export default InventoryItem diff --git a/packages/modules/inventory/src/models/inventory-level.ts b/packages/modules/inventory/src/models/inventory-level.ts index 01969b7e9f46c..2f8c020836a10 100644 --- a/packages/modules/inventory/src/models/inventory-level.ts +++ b/packages/modules/inventory/src/models/inventory-level.ts @@ -1,135 +1,36 @@ -import { DALUtils, isDefined, MathBN } from "@medusajs/framework/utils" -import { - BeforeCreate, - Entity, - Filter, - ManyToOne, - OnInit, - OnLoad, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" - -import { BigNumberRawValue } from "@medusajs/framework/types" -import { - BigNumber, - createPsqlIndexStatementHelper, - generateEntityId, - MikroOrmBigNumberProperty, -} from "@medusajs/framework/utils" -import { InventoryItem } from "./inventory-item" - -const InventoryLevelDeletedAtIndex = createPsqlIndexStatementHelper({ - tableName: "inventory_level", - columns: "deleted_at", - where: "deleted_at IS NOT NULL", -}) - -const InventoryLevelInventoryItemIdIndex = createPsqlIndexStatementHelper({ - tableName: "inventory_level", - columns: "inventory_item_id", - where: "deleted_at IS NULL", -}) - -const InventoryLevelLocationIdIndex = createPsqlIndexStatementHelper({ - tableName: "inventory_level", - columns: "location_id", - where: "deleted_at IS NULL", -}) - -const InventoryLevelLocationIdInventoryItemIdIndex = - createPsqlIndexStatementHelper({ - tableName: "inventory_level", - columns: ["inventory_item_id", "location_id"], - unique: true, - where: "deleted_at IS NULL", - }) - -@Entity() -@InventoryLevelLocationIdInventoryItemIdIndex.MikroORMIndex() -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export class InventoryLevel { - @PrimaryKey({ columnType: "text" }) - id: string - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", +import { model } from "@medusajs/framework/utils" +import InventoryItem from "./inventory-item" + +const InventoryLevel = model + .define("InventoryLevel", { + id: model.id({ prefix: "ilev" }).primaryKey(), + location_id: model.text(), + stocked_quantity: model.bigNumber().default(0), + reserved_quantity: model.bigNumber().default(0), + incoming_quantity: model.bigNumber().default(0), + metadata: model.json().nullable(), + inventory_item: model.belongsTo(() => InventoryItem, { + mappedBy: "location_levels", + }), + available_quantity: model.bigNumber().computed(), }) - updated_at: Date - - @InventoryLevelDeletedAtIndex.MikroORMIndex() - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null - - @ManyToOne(() => InventoryItem, { - fieldName: "inventory_item_id", - type: "text", - mapToPk: true, - onDelete: "cascade", - }) - @InventoryLevelInventoryItemIdIndex.MikroORMIndex() - inventory_item_id: string - - @InventoryLevelLocationIdIndex.MikroORMIndex() - @Property({ type: "text" }) - location_id: string - - @MikroOrmBigNumberProperty() - stocked_quantity: BigNumber | number = 0 - - @Property({ columnType: "jsonb" }) - raw_stocked_quantity: BigNumberRawValue - - @MikroOrmBigNumberProperty() - reserved_quantity: BigNumber | number = 0 - - @Property({ columnType: "jsonb" }) - raw_reserved_quantity: BigNumberRawValue - - @MikroOrmBigNumberProperty() - incoming_quantity: BigNumber | number = 0 - - @Property({ columnType: "jsonb" }) - raw_incoming_quantity: BigNumberRawValue - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null - - @ManyToOne(() => InventoryItem, { - persist: false, - }) - inventory_item: Rel - - available_quantity: BigNumber | number | null = null - - @BeforeCreate() - beforeCreate(): void { - this.id = generateEntityId(this.id, "ilev") - this.inventory_item_id ??= this.inventory_item?.id - } - - @OnInit() - onInit(): void { - this.id = generateEntityId(this.id, "ilev") - } - - @OnLoad() - onLoad(): void { - if (isDefined(this.stocked_quantity) && isDefined(this.reserved_quantity)) { - this.available_quantity = new BigNumber( - MathBN.sub(this.raw_stocked_quantity, this.raw_reserved_quantity) - ) - } - } -} + .indexes([ + { + name: "IDX_inventory_level_inventory_item_id", + on: ["inventory_item_id"], + where: "deleted_at IS NULL", + }, + { + name: "IDX_inventory_level_location_id", + on: ["location_id"], + where: "deleted_at IS NULL", + }, + { + name: "IDX_inventory_level_location_id_inventory_item_id", + on: ["inventory_item_id", "location_id"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) + +export default InventoryLevel diff --git a/packages/modules/inventory/src/models/reservation-item.ts b/packages/modules/inventory/src/models/reservation-item.ts index a4646e52bbda0..c7e9ce606ccad 100644 --- a/packages/modules/inventory/src/models/reservation-item.ts +++ b/packages/modules/inventory/src/models/reservation-item.ts @@ -1,125 +1,40 @@ -import { - BeforeCreate, - Entity, - Filter, - ManyToOne, - OnInit, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" - -import { BigNumberRawValue } from "@medusajs/framework/types" -import { - BigNumber, - DALUtils, - MikroOrmBigNumberProperty, - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" -import { InventoryItem } from "./inventory-item" - -const ReservationItemDeletedAtIndex = createPsqlIndexStatementHelper({ - tableName: "reservation_item", - columns: "deleted_at", - where: "deleted_at IS NOT NULL", -}) -const ReservationItemLineItemIdIndex = createPsqlIndexStatementHelper({ - tableName: "reservation_item", - columns: "line_item_id", - where: "deleted_at IS NULL", -}) - -const ReservationItemInventoryItemIdIndex = createPsqlIndexStatementHelper({ - tableName: "reservation_item", - columns: "inventory_item_id", - where: "deleted_at IS NULL", -}) - -const ReservationItemLocationIdIndex = createPsqlIndexStatementHelper({ - tableName: "reservation_item", - columns: "location_id", - where: "deleted_at IS NULL", -}) - -@Entity() -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export class ReservationItem { - @PrimaryKey({ columnType: "text" }) - id: string - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @ReservationItemDeletedAtIndex.MikroORMIndex() - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null - - @ReservationItemLineItemIdIndex.MikroORMIndex() - @Property({ type: "text", nullable: true }) - line_item_id: string | null = null - - @Property({ type: "boolean" }) - allow_backorder: boolean = false - - @ReservationItemLocationIdIndex.MikroORMIndex() - @Property({ type: "text" }) - location_id: string - - @MikroOrmBigNumberProperty() - quantity: BigNumber | number - - @Property({ columnType: "jsonb" }) - raw_quantity: BigNumberRawValue - - @Property({ type: "text", nullable: true }) - external_id: string | null = null - - @Searchable() - @Property({ type: "text", nullable: true }) - description: string | null = null - - @Property({ type: "text", nullable: true }) - created_by: string | null = null - - @Property({ type: "jsonb", nullable: true }) - metadata: Record | null = null - - @ReservationItemInventoryItemIdIndex.MikroORMIndex() - @ManyToOne(() => InventoryItem, { - fieldName: "inventory_item_id", - type: "text", - mapToPk: true, - onDelete: "cascade", - }) - inventory_item_id: string - - @Searchable() - @ManyToOne(() => InventoryItem, { - persist: false, +import { model } from "@medusajs/framework/utils" +import InventoryItem from "./inventory-item" + +const ReservationItem = model + .define("ReservationItem", { + id: model.id({ prefix: "resitem" }).primaryKey(), + line_item_id: model.text().nullable(), + allow_backorder: model.boolean().default(false), + location_id: model.text(), + quantity: model.bigNumber(), + raw_quantity: model.json(), + external_id: model.text().nullable(), + description: model.text().searchable().nullable(), + created_by: model.text().nullable(), + metadata: model.json().nullable(), + inventory_item: model + .belongsTo(() => InventoryItem, { + mappedBy: "reservation_items", + }) + .searchable(), }) - inventory_item: Rel - - @BeforeCreate() - beforeCreate(): void { - this.id = generateEntityId(this.id, "resitem") - } - - @OnInit() - onInit(): void { - this.id = generateEntityId(this.id, "resitem") - } -} + .indexes([ + { + name: "IDX_reservation_item_line_item_id", + on: ["line_item_id"], + where: "deleted_at IS NULL", + }, + { + name: "IDX_reservation_item_location_id", + on: ["location_id"], + where: "deleted_at IS NULL", + }, + { + name: "IDX_reservation_item_inventory_item_id", + on: ["inventory_item_id"], + where: "deleted_at IS NULL", + }, + ]) + +export default ReservationItem diff --git a/packages/modules/inventory/src/repositories/index.ts b/packages/modules/inventory/src/repositories/index.ts index 87f816987d403..965a2cde67262 100644 --- a/packages/modules/inventory/src/repositories/index.ts +++ b/packages/modules/inventory/src/repositories/index.ts @@ -1,2 +1,2 @@ -export * from "./inventory-level" export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/framework/utils" +export * from "./inventory-level" diff --git a/packages/modules/inventory/src/services/inventory-level.ts b/packages/modules/inventory/src/services/inventory-level.ts index 5771f72f68c1a..023c2408c5d1a 100644 --- a/packages/modules/inventory/src/services/inventory-level.ts +++ b/packages/modules/inventory/src/services/inventory-level.ts @@ -1,8 +1,9 @@ import { Context } from "@medusajs/framework/types" import { BigNumber, ModulesSdkUtils } from "@medusajs/framework/utils" +import { applyEntityHooks } from "../utils/apply-decorators" +import { InventoryLevel } from "@models" import { InventoryLevelRepository } from "@repositories" -import { InventoryLevel } from "../models/inventory-level" type InjectedDependencies = { inventoryLevelRepository: InventoryLevelRepository @@ -10,7 +11,7 @@ type InjectedDependencies = { export default class InventoryLevelService extends ModulesSdkUtils.MedusaInternalService< InjectedDependencies, - InventoryLevel + typeof InventoryLevel >(InventoryLevel) { protected readonly inventoryLevelRepository: InventoryLevelRepository @@ -67,3 +68,5 @@ export default class InventoryLevelService extends ModulesSdkUtils.MedusaInterna ) } } + +applyEntityHooks() diff --git a/packages/modules/inventory/src/services/inventory-module.ts b/packages/modules/inventory/src/services/inventory-module.ts index e3ac99cf439f6..8804b352c1d71 100644 --- a/packages/modules/inventory/src/services/inventory-module.ts +++ b/packages/modules/inventory/src/services/inventory-module.ts @@ -3,6 +3,7 @@ import { Context, DAL, IInventoryService, + InferEntityType, InternalModuleDeclaration, InventoryTypes, ModuleJoinerConfig, @@ -29,6 +30,7 @@ import { } from "@medusajs/framework/utils" import { InventoryItem, InventoryLevel, ReservationItem } from "@models" import { joinerConfig } from "../joiner-config" +import { applyEntityHooks } from "../utils/apply-decorators" import InventoryLevelService from "./inventory-level" type InjectedDependencies = { @@ -46,6 +48,8 @@ type InventoryItemCheckLevel = { allow_backorder?: boolean } +applyEntityHooks() + export default class InventoryModuleService extends MedusaService<{ InventoryItem: { @@ -66,8 +70,12 @@ export default class InventoryModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly inventoryItemService_: ModulesSdkTypes.IMedusaInternalService - protected readonly reservationItemService_: ModulesSdkTypes.IMedusaInternalService + protected readonly inventoryItemService_: ModulesSdkTypes.IMedusaInternalService< + typeof InventoryItem + > + protected readonly reservationItemService_: ModulesSdkTypes.IMedusaInternalService< + typeof ReservationItem + > protected readonly inventoryLevelService_: InventoryLevelService constructor( @@ -263,7 +271,7 @@ export default class InventoryModuleService async createReservationItems_( input: InventoryTypes.CreateReservationItemInput[], @MedusaContext() context: Context = {} - ): Promise { + ): Promise[]> { const inventoryLevels = await this.ensureInventoryLevels( input.map( ({ location_id, inventory_item_id, quantity, allow_backorder }) => ({ @@ -417,7 +425,7 @@ export default class InventoryModuleService async createInventoryLevels_( input: InventoryTypes.CreateInventoryLevelInput[], @MedusaContext() context: Context = {} - ): Promise { + ): Promise[]> { return await this.inventoryLevelService_.create(input, context) } @@ -473,7 +481,7 @@ export default class InventoryModuleService id: string })[], @MedusaContext() context: Context = {} - ): Promise { + ): Promise[]> { return await this.inventoryItemService_.update(input, context) } @@ -670,7 +678,7 @@ export default class InventoryModuleService async updateReservationItems_( input: (InventoryTypes.UpdateReservationItemInput & { id: string })[], @MedusaContext() context: Context = {} - ): Promise { + ): Promise[]> { const ids = input.map((u) => u.id) const reservationItems = await this.listReservationItems( { id: ids }, @@ -989,7 +997,7 @@ export default class InventoryModuleService ] } - const results: InventoryLevel[] = [] + const results: InferEntityType[] = [] for (const data of all) { const result = await this.adjustInventory_( @@ -1024,7 +1032,7 @@ export default class InventoryModuleService locationId: string, adjustment: BigNumberInput, @MedusaContext() context: Context = {} - ): Promise { + ): Promise> { const inventoryLevel = await this.retrieveInventoryLevelByItemAndLocation( inventoryItemId, locationId, diff --git a/packages/modules/inventory/src/utils/apply-decorators.ts b/packages/modules/inventory/src/utils/apply-decorators.ts new file mode 100644 index 0000000000000..a5a2c82f01f3f --- /dev/null +++ b/packages/modules/inventory/src/utils/apply-decorators.ts @@ -0,0 +1,45 @@ +import { + BigNumber, + isDefined, + MathBN, + toMikroORMEntity, +} from "@medusajs/framework/utils" +import { Formula, OnInit } from "@mikro-orm/core" + +import InventoryItem from "../models/inventory-item" +import InventoryLevel from "../models/inventory-level" + +function applyHook() { + const MikroORMEntity = toMikroORMEntity(InventoryLevel) + + MikroORMEntity.prototype["onInit"] = function () { + if (isDefined(this.stocked_quantity) && isDefined(this.reserved_quantity)) { + this.available_quantity = new BigNumber( + MathBN.sub(this.raw_stocked_quantity, this.raw_reserved_quantity) + ) + } + } + + OnInit()(MikroORMEntity.prototype, "onInit") +} + +function applyFormulas() { + const MikroORMEntity = toMikroORMEntity(InventoryItem) + + Formula( + (item) => + `(SELECT SUM(reserved_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id AND il.deleted_at IS NULL)`, + { lazy: true, serializer: Number, hidden: true, type: "number" } + )(MikroORMEntity.prototype, "reserved_quantity") + + Formula( + (item) => + `(SELECT SUM(stocked_quantity) FROM inventory_level il WHERE il.inventory_item_id = ${item}.id AND il.deleted_at IS NULL)`, + { lazy: true, serializer: Number, hidden: true, type: "number" } + )(MikroORMEntity.prototype, "stocked_quantity") +} + +export const applyEntityHooks = () => { + applyHook() + applyFormulas() +}