chore(inventory): convert to dml (#10569)

Fixes: FRMW-2848

Co-authored-by: Harminder Virk <1706381+thetutlage@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2024-12-13 09:51:26 -03:00
committed by GitHub
parent ae1d875fcf
commit 729eb5da7b
33 changed files with 662 additions and 593 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/inventory": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
chore: Inventory DML

View File

@@ -1353,7 +1353,7 @@ medusaIntegrationTestRunner({
{ title: "variant two", options: { color: "blue" } },
],
})
console.log(product)
const [variantOne, variantTwo] = product.variants
const [itemOne, itemTwo, itemThree] =

View File

@@ -72,6 +72,7 @@ export type PropertyMetadata = {
fieldName: string
defaultValue?: any
nullable: boolean
computed: boolean
dataType: {
name: KnownDataTypes
options?: Record<string, any>

View File

@@ -12,6 +12,7 @@ describe("Array property", () => {
name: "array",
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

@@ -13,6 +13,7 @@ describe("Autoincrement property", () => {
options: {},
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

@@ -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<string> {
protected dataType: PropertyMetadata["dataType"] = {
name: "text",
}
}
const property = new StringProperty().computed()
const property2 = new StringProperty().nullable().computed()
expectTypeOf(property["$dataType"]).toEqualTypeOf<string | null>()
expect(property.parse("username")).toEqual({
fieldName: "username",
dataType: {
name: "text",
},
nullable: false,
computed: true,
indexes: [],
relationships: [],
})
expectTypeOf(property2["$dataType"]).toEqualTypeOf<string | null>()
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: [],
})

View File

@@ -12,6 +12,7 @@ describe("Big Number property", () => {
name: "bigNumber",
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

@@ -12,6 +12,7 @@ describe("Boolean property", () => {
name: "boolean",
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

@@ -12,6 +12,7 @@ describe("DateTime property", () => {
name: "dateTime",
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

@@ -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: [],
})

View File

@@ -12,6 +12,7 @@ describe("Float property", () => {
name: "float",
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

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

View File

@@ -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: [],
})

View File

@@ -13,6 +13,7 @@ describe("Number property", () => {
options: {},
},
nullable: false,
computed: false,
indexes: [],
relationships: [],
})

View File

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

View File

@@ -41,10 +41,16 @@ export function createBigNumberProperties<Schema extends DMLSchema>(
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<Record<string, unknown>, JSONProperty>
}
schemaWithBigNumber[`raw_${key}`] = jsonProperty
}
}

View File

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

View File

@@ -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<T> implements PropertyType<T> {
return new NullableModifier<T, this>(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<T | null, this>(this)
}
/**
* This method defines an index on a property.
*
@@ -132,6 +154,7 @@ export abstract class BaseProperty<T> implements PropertyType<T> {
fieldName,
dataType: this.dataType,
nullable: false,
computed: false,
defaultValue: this.#defaultValue,
indexes: this.#indexes,
relationships: this.#relationships,

View File

@@ -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<T, Schema extends PropertyType<T>>
implements PropertyType<T | null>
{
[IsComputedProperty]: true = true
static isComputedProperty(obj: any): obj is ComputedProperty<any, any> {
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
}
}

View File

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

View File

@@ -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<T, Schema extends PropertyType<T>>
this.#schema = schema
}
/**
* This method indicated that the property is a computed property.
*/
computed() {
return new ComputedProperty<T | null, this>(this)
}
/**
* Returns the serialized metadata
*/

View File

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

View File

@@ -229,6 +229,29 @@ moduleIntegrationTestRunner<IInventoryService>({
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 () => {

View File

@@ -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,6 +226,90 @@
"nullable": false,
"mappedType": "text"
},
"location_id": {
"name": "location_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"stocked_quantity": {
"name": "stocked_quantity",
"type": "numeric",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "decimal"
},
"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,
"mappedType": "json"
},
"inventory_item_id": {
"name": "inventory_item_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"raw_stocked_quantity": {
"name": "raw_stocked_quantity",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "json"
},
"raw_reserved_quantity": {
"name": "raw_reserved_quantity",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "json"
},
"raw_incoming_quantity": {
"name": "raw_incoming_quantity",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -245,91 +341,26 @@
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"inventory_item_id": {
"name": "inventory_item_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"location_id": {
"name": "location_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"stocked_quantity": {
"name": "stocked_quantity",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"reserved_quantity": {
"name": "reserved_quantity",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"incoming_quantity": {
"name": "incoming_quantity",
"type": "int",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "inventory_level",
"schema": "public",
"indexes": [
{
"keyName": "IDX_inventory_level_deleted_at",
"columnNames": ["deleted_at"],
"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"
},
{
"keyName": "IDX_inventory_level_inventory_item_id",
"columnNames": ["inventory_item_id"],
"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_inventory_item_id\" ON \"inventory_level\" (inventory_item_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_inventory_level_location_id",
"columnNames": ["location_id"],
"keyName": "IDX_inventory_level_deleted_at",
"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_deleted_at\" ON \"inventory_level\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_inventory_level_location_id",
@@ -337,11 +368,21 @@
"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_inventory_item_id",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"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_deleted_at",
"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 NULL"
},
{
"keyName": "IDX_reservation_item_line_item_id",
"columnNames": ["line_item_id"],
"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_line_item_id\" ON \"reservation_item\" (line_item_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_reservation_item_location_id",
"columnNames": ["location_id"],
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_reservation_item_location_id\" ON \"reservation_item\" (location_id)"
},
{
"keyName": "IDX_reservation_item_inventory_item_id",
"columnNames": ["inventory_item_id"],
"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"

View File

@@ -0,0 +1,72 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20241213063611 extends Migration {
async up(): Promise<void> {
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<void> {
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";'
)
}
}

View File

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

View File

@@ -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 { model } from "@medusajs/framework/utils"
import InventoryLevel from "./inventory-level"
import ReservationItem from "./reservation-item"
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()",
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<string, unknown> | null = null
@OneToMany(
() => InventoryLevel,
(inventoryLevel) => inventoryLevel.inventory_item,
.indexes([
{
cascade: ["soft-remove" as any],
}
)
location_levels = new Collection<Rel<InventoryLevel>>(this)
name: "IDX_inventory_item_sku",
on: ["sku"],
unique: true,
where: "deleted_at IS NULL",
},
])
@OneToMany(
() => ReservationItem,
(reservationItem) => reservationItem.inventory_item,
{
cascade: ["soft-remove" as any],
}
)
reservation_items = new Collection<Rel<ReservationItem>>(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")
}
}
export default InventoryItem

View File

@@ -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 { model } from "@medusajs/framework/utils"
import InventoryItem from "./inventory-item"
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",
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(),
})
.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",
},
])
@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()",
})
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<string, unknown> | null
@ManyToOne(() => InventoryItem, {
persist: false,
})
inventory_item: Rel<InventoryItem>
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)
)
}
}
}
export default InventoryLevel

View File

@@ -1,125 +1,40 @@
import {
BeforeCreate,
Entity,
Filter,
ManyToOne,
OnInit,
PrimaryKey,
Property,
Rel,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import InventoryItem from "./inventory-item"
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()",
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(),
})
created_at: Date
.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",
},
])
@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<string, unknown> | 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,
})
inventory_item: Rel<InventoryItem>
@BeforeCreate()
beforeCreate(): void {
this.id = generateEntityId(this.id, "resitem")
}
@OnInit()
onInit(): void {
this.id = generateEntityId(this.id, "resitem")
}
}
export default ReservationItem

View File

@@ -1,2 +1,2 @@
export * from "./inventory-level"
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/framework/utils"
export * from "./inventory-level"

View File

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

View File

@@ -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<InventoryItem>
protected readonly reservationItemService_: ModulesSdkTypes.IMedusaInternalService<ReservationItem>
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<ReservationItem[]> {
): Promise<InferEntityType<typeof ReservationItem>[]> {
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<InventoryLevel[]> {
): Promise<InferEntityType<typeof InventoryLevel>[]> {
return await this.inventoryLevelService_.create(input, context)
}
@@ -473,7 +481,7 @@ export default class InventoryModuleService
id: string
})[],
@MedusaContext() context: Context = {}
): Promise<InventoryItem[]> {
): Promise<InferEntityType<typeof InventoryItem>[]> {
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<ReservationItem[]> {
): Promise<InferEntityType<typeof ReservationItem>[]> {
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<typeof InventoryLevel>[] = []
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<InventoryLevel> {
): Promise<InferEntityType<typeof InventoryLevel>> {
const inventoryLevel = await this.retrieveInventoryLevelByItemAndLocation(
inventoryItemId,
locationId,

View File

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