feat: convert MikroORM entities to DML entities (#10043)

* feat: convert MikroORM entities to DML entities

* feat: wip on repository changes

* continue repositories and types rework

* fix order repository usage

* continue to update product category repository

* Add foreign key as part of the inferred DML type

* ../../core/types/src/dml/index.ts

* ../../core/types/src/dml/index.ts

* fix: relationships mapping

* handle nullable foreign keys types

* handle nullable foreign keys types

* handle nullable foreign keys types

* continue to update product category repository

* fix all product category repositories issues

* fix product category service types

* fix product module service types

* fix product module service types

* fix repository template type

* refactor: use a singleton DMLToMikroORM factory instance

Since the MikroORM MetadataStorage is global, we will also have to turn DML
to MikroORM entities conversion use a global bucket as well

* refactor: update product module to use DML in tests

* wip: tests

* WIP product linkable fixes

* continue type fixing and start test fixing

* test: fix more tests

* fix repository

* fix pivot table computaion + fix mikro orm repository

* fix many to many management and configuration

* fix many to many management and configuration

* fix many to many management and configuration

* update product tag relation configuration

* Introduce experimental dml hooks to fix some issues with categories

* more fixes

* fix product tests

* add missing id prefixes

* fix product category handle management

* test: fix more failing tests

* test: make it all green

* test: fix breaking tests

* fix: build issues

* fix: build issues

* fix: more breaking tests

* refactor: fix issues after merge

* refactor: fix issues after merge

* refactor: surpress types error

* test: fix DML failing tests

* improve many to many inference + tests

* Wip fix columns from product entity

* remove product model before create hook and manage handle validation and transformation at the service level

* test: fix breaking unit tests

* fix: product module service to not update handle on product update

* fix define link and joiner config

* test: fix joiner config test

* test: fix joiner config test

* fix joiner config primary keys

* Fix joiner config builder

* Fix joiner config builder

* test: remove only modifier from test

* refactor: remove hooks usage from product collection

* refactor: remove hooks usage from product-option

* refactor: remove hooks usage for computing category handle

* refactor: remove hooks usage from productCategory model

* refactor: remove hooks from DML

* refactor: remove cruft

* cleanup

* re add foerign key indexes

* chore: remove unused types

* refactor: cleanup

* migration and models configuration adjustments

* cleanup

* fix random ordering

* fix

* test: fix product-category tests

* test: update breaking DML tests

* test: array assertion to not care about ordering

* fix: temporarily apply id ordering for products

* fix ordering

* fix ordering remove logs

---------

Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2024-11-26 20:01:02 +05:30
1 parent d6fa912b22
commit 9f204817b0
61 files changed
+2315 -1939

No files matched your search

@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { OrderClaim } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory<OrderClaim>(
export class OrderClaimRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
OrderClaim
) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { OrderExchange } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory<OrderExchange>(
export class OrderExchangeRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
OrderExchange
) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { Order } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Order>(
export class OrderRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Order
) {}
@@ -2,7 +2,7 @@ import { DALUtils } from "@medusajs/framework/utils"
import { Return } from "@models"
import { setFindMethods } from "../utils/base-repository-find"
export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Return>(
export class ReturnRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Return
) {}
@@ -118,7 +118,7 @@ export function setFindMethods<T>(klass: Constructor<T>, entity: any) {
klass.prototype.findAndCount = async function findAndCount(
this: any,
findOptions: DAL.FindOptions<T> = { where: {} },
findOptions: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
context: Context = {}
): Promise<[T[], number]> {
const manager = this.getActiveManager(context)
@@ -1,3 +1,5 @@
import { kebabCase } from "@medusajs/framework/utils"
export const productCategoriesData = [
{
id: "category-0",
@@ -25,7 +27,12 @@ export const productCategoriesData = [
name: "category 1 b 1",
parent_category_id: "category-1-b",
},
]
].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
export const productCategoriesRankData = [
{
@@ -64,7 +71,12 @@ export const productCategoriesRankData = [
parent_category_id: "category-0-0",
rank: 2,
},
]
].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
export const eletronicsCategoriesData = [
{
@@ -188,4 +200,9 @@ export const eletronicsCategoriesData = [
name: "Mini Models",
parent_category_id: "iphones",
},
]
].map((entry) => {
return {
handle: kebabCase(entry.name),
...entry,
}
})
@@ -2,16 +2,19 @@ export const categoriesData = [
{
id: "category-0",
name: "category 0",
handle: "category-0",
parent_category_id: null,
},
{
id: "category-1",
name: "category 1",
handle: "category-1",
parent_category_id: "category-0",
},
{
id: "category-1-a",
name: "category 1 a",
handle: "category-1-a",
parent_category_id: "category-1",
},
]
@@ -1,10 +1,11 @@
import { ProductTypes } from "@medusajs/framework/types"
import { ProductStatus } from "@medusajs/framework/utils"
import { Image } from "@models"
import { ProductStatus, toHandle } from "@medusajs/framework/utils"
import { ProductImage } from "@models"
import faker from "faker"
export const buildProductOnlyData = ({
title,
handle,
description,
subtitle,
is_giftcard,
@@ -14,6 +15,7 @@ export const buildProductOnlyData = ({
status,
}: {
title?: string
handle?: string
description?: string
subtitle?: string
is_giftcard?: boolean
@@ -22,15 +24,17 @@ export const buildProductOnlyData = ({
images?: { id?: string; url: string }[]
status?: ProductStatus
} = {}) => {
title ??= faker.commerce.productName()
return {
title: title ?? faker.commerce.productName(),
title: title as string,
handle: handle ?? toHandle(title!),
description: description ?? faker.commerce.productName(),
subtitle: subtitle ?? faker.commerce.productName(),
is_giftcard: is_giftcard ?? false,
discountable: discountable ?? true,
thumbnail: thumbnail as string,
status: status ?? ProductStatus.PUBLISHED,
images: (images ?? []) as Image[],
images: (images ?? []) as ProductImage[],
}
}
@@ -48,7 +52,7 @@ export const buildProductAndRelationsData = ({
options,
variants,
collection_id,
}: Partial<ProductTypes.CreateProductDTO>) => {
}: Partial<ProductTypes.CreateProductDTO> & { tags: { value: string }[] }) => {
const defaultOptionTitle = "test-option"
const defaultOptionValue = "test-value"
@@ -60,7 +64,7 @@ export const buildProductAndRelationsData = ({
discountable: discountable ?? true,
thumbnail: thumbnail as string,
status: status ?? ProductStatus.PUBLISHED,
images: (images ?? []) as Image[],
images: (images ?? []) as ProductImage[],
type_id,
tags: tags ?? [{ value: "tag-1" }],
collection_id,
@@ -4,6 +4,7 @@ export const productsData = [
{
id: "test-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
tags: [
{
@@ -15,6 +16,7 @@ export const productsData = [
{
id: "test-2",
title: "product",
handle: "product",
status: ProductStatus.PUBLISHED,
tags: [
{
@@ -26,6 +28,7 @@ export const productsData = [
{
id: "test-3",
title: "product 3",
handle: "product-3",
status: ProductStatus.PUBLISHED,
tags: [
{
@@ -1,4 +1,8 @@
import { ProductStatus } from "@medusajs/framework/utils"
import {
kebabCase,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
Image,
@@ -10,6 +14,7 @@ import {
} from "@models"
import ProductOption from "../../../src/models/product-option"
import { InferEntityType } from "@medusajs/types"
export * from "./data/create-product"
@@ -24,7 +29,7 @@ export async function createProductAndTags(
}[]
) {
const products: any[] = data.map((productData) => {
return manager.create(Product, productData)
return manager.create(toMikroORMEntity(Product), productData)
})
await manager.persistAndFlush(products)
@@ -42,7 +47,7 @@ export async function createProductAndTypes(
}[]
) {
const products: any[] = data.map((productData) => {
return manager.create(Product, productData)
return manager.create(toMikroORMEntity(Product), productData)
})
await manager.persistAndFlush(products)
@@ -55,7 +60,7 @@ export async function createProductVariants(
data: any[]
) {
const variants: any[] = data.map((variantsData) => {
return manager.create(ProductVariant, variantsData)
return manager.create(toMikroORMEntity(ProductVariant), variantsData)
})
await manager.persistAndFlush(variants)
@@ -72,7 +77,10 @@ export async function createCollections(
}[]
) {
const collections: any[] = collectionData.map((collectionData) => {
return manager.create(ProductCollection, collectionData)
if (!collectionData.handle && collectionData.title) {
collectionData.handle = kebabCase(collectionData.title)
}
return manager.create(toMikroORMEntity(ProductCollection), collectionData)
})
await manager.persistAndFlush(collections)
@@ -88,7 +96,7 @@ export async function createTypes(
}[]
) {
const types: any[] = typesData.map((typesData) => {
return manager.create(ProductType, typesData)
return manager.create(toMikroORMEntity(ProductType), typesData)
})
await manager.persistAndFlush(types)
@@ -112,7 +120,7 @@ export async function createOptions(
}[]
) {
const options: any[] = optionsData.map((option) => {
return manager.create(ProductOption, option)
return manager.create(toMikroORMEntity(ProductOption), option)
})
await manager.persistAndFlush(options)
@@ -125,7 +133,7 @@ export async function createImages(
imagesData: string[]
) {
const images: any[] = imagesData.map((img) => {
return manager.create(Image, { url: img })
return manager.create(toMikroORMEntity(Image), { url: img })
})
await manager.persistAndFlush(images)
@@ -135,8 +143,8 @@ export async function createImages(
export async function assignCategoriesToProduct(
manager: SqlEntityManager,
product: Product,
categories: ProductCategory[]
product: InferEntityType<Product>,
categories: InferEntityType<ProductCategory>[]
) {
product.categories.add(categories)
@@ -175,7 +175,7 @@ moduleIntegrationTestRunner<Service>({
include_descendants_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -190,12 +190,14 @@ moduleIntegrationTestRunner<Service>({
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
category_children: [],
rank: 0,
},
{
id: "category-1-b",
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
rank: 1,
category_children: [
expect.objectContaining({
id: "category-1-b-1",
@@ -220,7 +222,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -235,8 +237,10 @@ moduleIntegrationTestRunner<Service>({
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
rank: 1,
parent_category: expect.objectContaining({
id: "high-performance",
rank: 1,
parent_category_id: "gaming-laptops",
handle: "high-performance-gaming-laptops",
mpath:
@@ -245,6 +249,7 @@ moduleIntegrationTestRunner<Service>({
id: "gaming-laptops",
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops",
rank: 0,
parent_category_id: "laptops",
parent_category: expect.objectContaining({
id: "laptops",
@@ -282,7 +287,7 @@ moduleIntegrationTestRunner<Service>({
include_descendants_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -296,6 +301,7 @@ moduleIntegrationTestRunner<Service>({
handle: "gaming-laptops",
mpath: "electronics.computers.laptops.gaming-laptops",
parent_category_id: "laptops",
rank: 0,
category_children: [
expect.objectContaining({
id: "budget-gaming",
@@ -304,6 +310,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.budget-gaming",
parent_category_id: "gaming-laptops",
category_children: [],
rank: 0,
}),
expect.objectContaining({
id: "high-performance",
@@ -311,6 +318,7 @@ moduleIntegrationTestRunner<Service>({
mpath:
"electronics.computers.laptops.gaming-laptops.high-performance",
parent_category_id: "gaming-laptops",
rank: 1,
category_children: expect.arrayContaining([
expect.objectContaining({
id: "vr-ready",
@@ -319,6 +327,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.high-performance.vr-ready",
parent_category_id: "high-performance",
category_children: [],
rank: 0,
}),
expect.objectContaining({
id: "4k-gaming",
@@ -327,6 +336,7 @@ moduleIntegrationTestRunner<Service>({
"electronics.computers.laptops.gaming-laptops.high-performance.4k-gaming",
parent_category_id: "high-performance",
category_children: [],
rank: 1,
}),
]),
}),
@@ -407,7 +417,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -421,12 +431,15 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
rank: 0,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
rank: 0,
id: "category-0",
handle: "category-0",
mpath: "category-0",
@@ -440,17 +453,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
rank: 1,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
rank: 0,
}),
}),
},
@@ -465,7 +481,7 @@ moduleIntegrationTestRunner<Service>({
include_ancestors_tree: true,
},
{
select: ["id", "handle"],
select: ["id", "handle", "rank"],
}
)
@@ -475,17 +491,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-a",
mpath: "category-0.category-1.category-1-a",
parent_category_id: "category-1",
rank: 0,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
rank: 0,
}),
}),
category_children: [],
@@ -495,17 +514,20 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b",
mpath: "category-0.category-1.category-1-b",
parent_category_id: "category-1",
rank: 1,
parent_category: expect.objectContaining({
id: "category-1",
handle: "category-1",
mpath: "category-0.category-1",
parent_category_id: "category-0",
rank: 0,
parent_category: expect.objectContaining({
id: "category-0",
handle: "category-0",
mpath: "category-0",
parent_category_id: null,
parent_category: null,
rank: 0,
}),
}),
category_children: [
@@ -514,6 +536,7 @@ moduleIntegrationTestRunner<Service>({
handle: "category-1-b-1",
mpath: "category-0.category-1.category-1-b.category-1-b-1",
parent_category_id: "category-1-b",
rank: 0,
}),
],
},
@@ -879,6 +902,7 @@ moduleIntegrationTestRunner<Service>({
await service.create([
{
name: "New Category",
handle: "new-category",
parent_category_id: null,
},
])
@@ -904,11 +928,13 @@ moduleIntegrationTestRunner<Service>({
await service.create([
{
name: "New Category",
handle: "new-category",
parent_category_id: null,
rank: 0,
},
{
name: "New Category 2",
handle: "new-category-2",
parent_category_id: null,
},
])
@@ -932,6 +958,7 @@ moduleIntegrationTestRunner<Service>({
await service.create([
{
name: "New Category 2.1",
handle: "new-category-2-1",
parent_category_id: productCategoryNew.id,
},
])
@@ -5,6 +5,7 @@ import {
Modules,
ProductEvents,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductCategory } from "@models"
import {
@@ -31,15 +32,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
@@ -5,6 +5,7 @@ import {
Modules,
ProductEvents,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductCollection } from "@models"
import {
@@ -31,15 +32,17 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
@@ -47,11 +50,13 @@ moduleIntegrationTestRunner<IProductModuleService>({
{
id: "test-1",
title: "collection 1",
handle: "collection-1",
products: [productOne],
},
{
id: "test-2",
title: "collection",
handle: "collection",
products: [productTwo],
},
]
@@ -1,5 +1,9 @@
import { IProductModuleService } from "@medusajs/framework/types"
import { Modules, ProductStatus } from "@medusajs/framework/utils"
import {
Modules,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductOption } from "@models"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
@@ -16,25 +20,27 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
optionOne = testManager.create(ProductOption, {
optionOne = testManager.create(toMikroORMEntity(ProductOption), {
id: "option-1",
title: "option 1",
product: productOne,
})
optionTwo = testManager.create(ProductOption, {
optionTwo = testManager.create(toMikroORMEntity(ProductOption), {
id: "option-2",
title: "option 1",
product: productTwo,
@@ -198,7 +204,7 @@ moduleIntegrationTestRunner<IProductModuleService>({
it("should return requested attributes when requested through config", async () => {
const option = await service.retrieveProductOption(optionOne.id, {
select: ["id", "product.title"],
select: ["id", "product.handle", "product.title"],
relations: ["product"],
})
@@ -5,6 +5,7 @@ import {
Modules,
ProductEvents,
ProductStatus,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { Product, ProductTag } from "@models"
import {
@@ -35,25 +36,27 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
productTwo = testManager.create(Product, {
productTwo = testManager.create(toMikroORMEntity(Product), {
id: "product-2",
title: "product 2",
handle: "product-2",
status: ProductStatus.PUBLISHED,
})
tagOne = testManager.create(ProductTag, {
tagOne = testManager.create(toMikroORMEntity(ProductTag), {
id: "tag-1",
value: "tag 1",
products: [productOne],
})
tagTwo = testManager.create(ProductTag, {
tagTwo = testManager.create(toMikroORMEntity(ProductTag), {
id: "tag-2",
value: "tag",
products: [productTwo],
@@ -1,7 +1,7 @@
import { IProductModuleService } from "@medusajs/framework/types"
import { ProductType } from "@models"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { Modules } from "@medusajs/framework/utils"
import { Modules, toMikroORMEntity } from "@medusajs/framework/utils"
jest.setTimeout(30000)
@@ -15,12 +15,12 @@ moduleIntegrationTestRunner<IProductModuleService>({
beforeEach(async () => {
const testManager = await MikroOrmWrapper.forkManager()
typeOne = testManager.create(ProductType, {
typeOne = testManager.create(toMikroORMEntity(ProductType), {
id: "type-1",
value: "type 1",
})
typeTwo = testManager.create(ProductType, {
typeTwo = testManager.create(toMikroORMEntity(ProductType), {
id: "type-2",
value: "type",
})
@@ -125,7 +125,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
productCategoryTwo = categories[1]
productOne = service.createProducts({
id: "product-1",
title: "product 1",
status: ProductStatus.PUBLISHED,
options: [
@@ -136,7 +135,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
],
variants: [
{
id: "variant-1",
title: "variant 1",
options: { "opt-title": "val-1" },
},
@@ -144,12 +142,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
productTwo = service.createProducts({
id: "product-2",
title: "product 2",
status: ProductStatus.PUBLISHED,
categories: [{ id: productCategoryOne.id }],
collection_id: productCollectionOne.id,
tags: [{ id: tags[0].id }],
category_ids: [productCategoryOne.id],
tag_ids: [tags[0].id],
options: [
{
title: "size",
@@ -162,7 +159,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
],
variants: [
{
id: "variant-2",
title: "variant 2",
options: {
size: "large",
@@ -170,7 +166,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
},
},
{
id: "variant-3",
title: "variant 3",
options: {
size: "small",
@@ -389,11 +384,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
const existingVariant1 = product.variants.find(
(v) => v.title === "new variant 1"
)
)!
const existingVariant2 = product.variants.find(
(v) => v.title === "new variant 2"
)
)!
await service.upsertProductVariants([
{
@@ -793,10 +788,14 @@ moduleIntegrationTestRunner<IProductModuleService>({
})
it("should do a partial update on the options of a variant successfully", async () => {
const variantToUpdate = productTwo.variants.find(
(variant) => variant.title === "variant 3"
)!
await service.updateProducts(productTwo.id, {
variants: [
{
id: "variant-3",
id: variantToUpdate.id,
options: { size: "small", color: "blue" },
},
],
@@ -7,12 +7,17 @@ import {
createProductVariants,
} from "../__fixtures__/product"
import { IProductModuleService, ProductDTO } from "@medusajs/framework/types"
import {
InferEntityType,
IProductModuleService,
ProductDTO,
} from "@medusajs/framework/types"
import {
Module,
Modules,
ProductStatus,
kebabCase,
toMikroORMEntity,
} from "@medusajs/framework/utils"
import { moduleIntegrationTestRunner } from "@medusajs/test-utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
@@ -27,7 +32,7 @@ import {
variantsData,
} from "../__fixtures__/product/data"
jest.setTimeout(30000)
jest.setTimeout(300000)
type Service = IProductModuleService & {
productService_: ProductService
@@ -50,17 +55,19 @@ moduleIntegrationTestRunner<Service>({
service: ProductModuleService,
}).linkable
expect(Object.keys(linkable)).toEqual([
"product",
"productVariant",
"productOption",
"productOptionValue",
"productType",
"productImage",
"productTag",
"productCollection",
"productCategory",
])
expect(Object.keys(linkable)).toHaveLength(8)
expect(Object.keys(linkable)).toEqual(
expect.arrayContaining([
"product",
"productVariant",
"productOption",
"productOptionValue",
"productType",
"productTag",
"productCollection",
"productCategory",
])
)
Object.keys(linkable).forEach((key) => {
delete linkable[key].toJSON
@@ -119,15 +126,6 @@ moduleIntegrationTestRunner<Service>({
field: "productType",
},
},
productImage: {
id: {
linkable: "product_image_id",
entity: "ProductImage",
primaryKey: "id",
serviceName: "product",
field: "productImage",
},
},
productTag: {
id: {
linkable: "product_tag_id",
@@ -160,16 +158,17 @@ moduleIntegrationTestRunner<Service>({
describe("Product Service", () => {
let testManager: SqlEntityManager
let products!: Product[]
let productOne: Product
let categories!: ProductCategory[]
let products!: InferEntityType<typeof Product>[]
let productOne: InferEntityType<typeof Product>
let categories!: InferEntityType<typeof ProductCategory>[]
describe("retrieve", () => {
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
@@ -243,9 +242,10 @@ moduleIntegrationTestRunner<Service>({
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
productOne = testManager.create(Product, {
productOne = testManager.create(toMikroORMEntity(Product), {
id: "product-1",
title: "product 1",
handle: "product-1",
status: ProductStatus.PUBLISHED,
})
@@ -435,23 +435,23 @@ moduleIntegrationTestRunner<Service>({
})
describe("relation: categories", () => {
let workingProduct: Product
let workingCategory: ProductCategory
let workingProduct: InferEntityType<typeof Product>
let workingCategory: InferEntityType<typeof ProductCategory>
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
products = await createProductAndTags(testManager, productsData)
workingProduct = products.find((p) => p.id === "test-1") as Product
workingProduct = products.find((p) => p.id === "test-1")!
categories = []
for (const entry of categoriesData) {
categories.push((await categoryService.create([entry]))[0])
}
workingCategory = (await testManager.findOne(
ProductCategory,
toMikroORMEntity(ProductCategory),
"category-1"
)) as ProductCategory
))!
workingProduct = await assignCategoriesToProduct(
testManager,
@@ -477,9 +477,7 @@ moduleIntegrationTestRunner<Service>({
}
)
const product = products.find(
(p) => p.id === workingProduct.id
) as unknown as Product
const product = products.find((p) => p.id === workingProduct.id)!
expect(product).toEqual(
expect.objectContaining({
@@ -534,32 +532,34 @@ moduleIntegrationTestRunner<Service>({
})
describe("relation: collections", () => {
let workingProduct: Product
let workingProductTwo: Product
let workingCollection: ProductCollection
let workingCollectionTwo: ProductCollection
let workingProduct: InferEntityType<typeof Product>
let workingProductTwo: InferEntityType<typeof Product>
let workingCollection: InferEntityType<typeof ProductCollection>
let workingCollectionTwo: InferEntityType<typeof ProductCollection>
const collectionData = [
{
id: "test-1",
title: "col 1",
handle: "col-1",
},
{
id: "test-2",
title: "col 2",
handle: "col-2",
},
]
beforeEach(async () => {
testManager = await MikroOrmWrapper.forkManager()
await createCollections(testManager, collectionData)
workingCollection = (await testManager.findOne(
ProductCollection,
workingCollection = await testManager.findOne(
toMikroORMEntity(ProductCollection),
"test-1"
)) as ProductCollection
)
workingCollectionTwo = (await testManager.findOne(
ProductCollection,
toMikroORMEntity(ProductCollection),
"test-2"
)) as ProductCollection
))!
products = await createProductAndTags(testManager, [
{
@@ -575,10 +575,8 @@ moduleIntegrationTestRunner<Service>({
},
])
workingProduct = products.find((p) => p.id === "test-1") as Product
workingProductTwo = products.find(
(p) => p.id === "test-2"
) as Product
workingProduct = products.find((p) => p.id === "test-1")!
workingProductTwo = products.find((p) => p.id === "test-2")!
})
it("should filter by collection relation and scope fields", async () => {
@@ -588,7 +586,12 @@ moduleIntegrationTestRunner<Service>({
collection_id: workingCollection.id,
},
{
select: ["title", "collection.title"],
select: [
"title",
"handle",
"collection.title",
"collection.handle",
],
relations: ["collection"],
}
)
@@ -618,7 +621,12 @@ moduleIntegrationTestRunner<Service>({
collection_id: [workingCollection.id, workingCollectionTwo.id],
},
{
select: ["title", "collection.title"],
select: [
"title",
"handle",
"collection.title",
"collection.handle",
],
relations: ["collection"],
}
)
@@ -9,7 +9,6 @@ import {
ProductType,
ProductVariant,
} from "@models"
import ProductImage from "./models/product-image"
import { default as schema } from "./schema"
export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, {
@@ -20,7 +19,6 @@ export const joinerConfig = defineJoinerConfig(Modules.PRODUCT, {
ProductOption,
ProductOptionValue,
ProductType,
ProductImage,
ProductTag,
ProductCollection,
ProductCategory,
@@ -82,6 +82,15 @@
"default": "0",
"mappedType": "integer"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"parent_category_id": {
"name": "parent_category_id",
"type": "text",
@@ -122,28 +131,42 @@
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "product_category",
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_category_parent_category_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_category_deleted_at",
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_parent_category_id\" ON \"product_category\" (parent_category_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_category_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_deleted_at\" ON \"product_category\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_category_path",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_category_path\" ON \"product_category\" (mpath) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_category_handle_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_category_handle_unique\" ON \"product_category\" (handle) WHERE deleted_at IS NULL"
},
{
"keyName": "product_category_pkey",
@@ -247,13 +270,20 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_collection_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_collection_deleted_at\" ON \"product_collection\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_collection_handle_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_collection_handle_unique\" ON \"product_collection\" (handle) WHERE deleted_at IS NULL"
},
{
"keyName": "product_collection_pkey",
@@ -334,13 +364,20 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_tag_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_tag_deleted_at\" ON \"product_tag\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_tag_value_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_tag_value_unique\" ON \"product_tag\" (value) WHERE deleted_at IS NULL"
},
{
"keyName": "product_tag_pkey",
@@ -377,7 +414,7 @@
},
"metadata": {
"name": "metadata",
"type": "json",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
@@ -421,13 +458,20 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"composite": false,
"keyName": "IDX_product_type_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_type_deleted_at\" ON \"product_type\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_type_value_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_type_value_unique\" ON \"product_type\" (value) WHERE deleted_at IS NULL"
},
{
"keyName": "product_type_pkey",
@@ -506,6 +550,7 @@
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'draft'",
"enumItems": [
"draft",
"proposed",
@@ -595,24 +640,6 @@
"nullable": true,
"mappedType": "text"
},
"collection_id": {
"name": "collection_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"type_id": {
"name": "type_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"discountable": {
"name": "discountable",
"type": "boolean",
@@ -632,6 +659,33 @@
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"type_id": {
"name": "type_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"collection_id": {
"name": "collection_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -663,28 +717,42 @@
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
}
},
"name": "product",
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_type_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_deleted_at",
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_type_id\" ON \"product\" (type_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_collection_id",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_collection_id\" ON \"product\" (collection_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_deleted_at\" ON \"product\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_handle_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_handle_unique\" ON \"product\" (handle) WHERE deleted_at IS NULL"
},
{
"keyName": "product_pkey",
@@ -698,19 +766,6 @@
],
"checks": [],
"foreignKeys": {
"product_collection_id_foreign": {
"constraintName": "product_collection_id_foreign",
"columnNames": [
"collection_id"
],
"localTableName": "public.product",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_collection",
"deleteRule": "set null",
"updateRule": "cascade"
},
"product_type_id_foreign": {
"constraintName": "product_type_id_foreign",
"columnNames": [
@@ -723,6 +778,19 @@
"referencedTableName": "public.product_type",
"deleteRule": "set null",
"updateRule": "cascade"
},
"product_collection_id_foreign": {
"constraintName": "product_collection_id_foreign",
"columnNames": [
"collection_id"
],
"localTableName": "public.product",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_collection",
"deleteRule": "set null",
"updateRule": "cascade"
}
}
},
@@ -746,15 +814,6 @@
"nullable": false,
"mappedType": "text"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
@@ -764,6 +823,15 @@
"nullable": true,
"mappedType": "json"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -801,13 +869,28 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_option_product_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_option_deleted_at",
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_product_id\" ON \"product_option\" (product_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_option_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_deleted_at\" ON \"product_option\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_option_product_id_title_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_option_product_id_title_unique\" ON \"product_option\" (product_id, title) WHERE deleted_at IS NULL"
},
{
"keyName": "product_option_pkey",
@@ -856,15 +939,6 @@
"nullable": false,
"mappedType": "text"
},
"option_id": {
"name": "option_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"metadata": {
"name": "metadata",
"type": "jsonb",
@@ -874,6 +948,15 @@
"nullable": true,
"mappedType": "json"
},
"option_id": {
"name": "option_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -911,13 +994,28 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_option_value_option_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_option_value_deleted_at",
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_value_option_id\" ON \"product_option_value\" (option_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_option_value_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_option_value_deleted_at\" ON \"product_option_value\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_option_value_option_id_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_option_value_option_id_unique\" ON \"product_option_value\" (option_id, value) WHERE deleted_at IS NULL"
},
{
"keyName": "product_option_value_pkey",
@@ -975,6 +1073,25 @@
"nullable": true,
"mappedType": "json"
},
"rank": {
"name": "rank",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
@@ -1006,38 +1123,34 @@
"nullable": true,
"length": 6,
"mappedType": "datetime"
},
"rank": {
"name": "rank",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "0",
"mappedType": "integer"
},
"product_id": {
"name": "product_id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
}
},
"name": "image",
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_image_product_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_image_deleted_at",
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_image_product_id\" ON \"image\" (product_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_image_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_image_deleted_at\" ON \"image\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_image_url",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_image_url\" ON \"image\" (url) WHERE deleted_at IS NULL"
},
{
"keyName": "image_pkey",
@@ -1310,39 +1423,39 @@
},
"weight": {
"name": "weight",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"length": {
"name": "length",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"height": {
"name": "height",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"width": {
"name": "width",
"type": "numeric",
"type": "integer",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "decimal"
"mappedType": "integer"
},
"metadata": {
"name": "metadata",
@@ -1409,13 +1522,52 @@
"schema": "public",
"indexes": [
{
"columnNames": [
"deleted_at"
],
"keyName": "IDX_product_variant_product_id",
"columnNames": [],
"composite": false,
"keyName": "IDX_product_variant_deleted_at",
"primary": false,
"unique": false
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_variant_product_id\" ON \"product_variant\" (product_id) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_deleted_at",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_product_variant_deleted_at\" ON \"product_variant\" (deleted_at) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_sku_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_sku_unique\" ON \"product_variant\" (sku) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_barcode_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_barcode_unique\" ON \"product_variant\" (barcode) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_ean_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_ean_unique\" ON \"product_variant\" (ean) WHERE deleted_at IS NULL"
},
{
"keyName": "IDX_product_variant_upc_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_product_variant_upc_unique\" ON \"product_variant\" (upc) WHERE deleted_at IS NULL"
},
{
"keyName": "product_variant_pkey",
@@ -0,0 +1,173 @@
import { Migration } from "@mikro-orm/migrations"
export class Migration20241125090957 extends Migration {
async up(): Promise<void> {
this.addSql(
'alter table if exists "product" drop constraint if exists "product_status_check";'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_category_parent_category_id" ON "product_category" (parent_category_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_category_path" ON "product_category" (mpath) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_category_handle_unique" ON "product_category" (handle) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_collection_handle_unique" ON "product_collection" (handle) WHERE deleted_at IS NULL;'
)
this.addSql('drop index if exists "IDX_product_image_deleted_at";')
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_image_deleted_at" ON "image" (deleted_at) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_image_url" ON "image" (url) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_tag_value_unique" ON "product_tag" (value) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_type_value_unique" ON "product_type" (value) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "product" alter column "status" type text using ("status"::text);'
)
this.addSql(
"alter table if exists \"product\" add constraint \"product_status_check\" check (\"status\" in ('draft', 'proposed', 'published', 'rejected'));"
)
this.addSql(
'alter table if exists "product" alter column "status" set default \'draft\';'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_type_id" ON "product" (type_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_collection_id" ON "product" (collection_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_handle_unique" ON "product" (handle) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "product_option" alter column "product_id" type text using ("product_id"::text);'
)
this.addSql(
'alter table if exists "product_option" alter column "product_id" set not null;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_option_product_id" ON "product_option" (product_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_option_product_id_title_unique" ON "product_option" (product_id, title) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_option_value_option_id" ON "product_option_value" (option_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_option_value_option_id_unique" ON "product_option_value" (option_id, value) WHERE deleted_at IS NULL;'
)
this.addSql(
'alter table if exists "product_variant" alter column "weight" type integer using ("weight"::integer);'
)
this.addSql(
'alter table if exists "product_variant" alter column "length" type integer using ("length"::integer);'
)
this.addSql(
'alter table if exists "product_variant" alter column "height" type integer using ("height"::integer);'
)
this.addSql(
'alter table if exists "product_variant" alter column "width" type integer using ("width"::integer);'
)
this.addSql(
'CREATE INDEX IF NOT EXISTS "IDX_product_variant_product_id" ON "product_variant" (product_id) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_sku_unique" ON "product_variant" (sku) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_barcode_unique" ON "product_variant" (barcode) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_ean_unique" ON "product_variant" (ean) WHERE deleted_at IS NULL;'
)
this.addSql(
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_product_variant_upc_unique" ON "product_variant" (upc) WHERE deleted_at IS NULL;'
)
}
async down(): Promise<void> {
this.addSql(
'alter table if exists "product" drop constraint if exists "product_status_check";'
)
this.addSql(
'drop index if exists "IDX_product_category_parent_category_id";'
)
this.addSql('drop index if exists "IDX_product_category_path";')
this.addSql('drop index if exists "IDX_category_handle_unique";')
this.addSql('drop index if exists "IDX_collection_handle_unique";')
this.addSql('drop index if exists "IDX_image_deleted_at";')
this.addSql('drop index if exists "IDX_product_image_url";')
this.addSql(
'create index if not exists "IDX_product_image_deleted_at" on "image" ("deleted_at");'
)
this.addSql('drop index if exists "IDX_tag_value_unique";')
this.addSql('drop index if exists "IDX_type_value_unique";')
this.addSql(
'alter table if exists "product" alter column "status" drop default;'
)
this.addSql(
'alter table if exists "product" alter column "status" type text using ("status"::text);'
)
this.addSql(
"alter table if exists \"product\" add constraint \"product_status_check\" check (\"status\" in ('draft', 'proposed', 'published', 'rejected'));"
)
this.addSql('drop index if exists "IDX_product_type_id";')
this.addSql('drop index if exists "IDX_product_collection_id";')
this.addSql('drop index if exists "IDX_product_handle_unique";')
this.addSql(
'alter table if exists "product_option" alter column "product_id" type text using ("product_id"::text);'
)
this.addSql(
'alter table if exists "product_option" alter column "product_id" drop not null;'
)
this.addSql('drop index if exists "IDX_product_option_product_id";')
this.addSql('drop index if exists "IDX_option_product_id_title_unique";')
this.addSql('drop index if exists "IDX_product_option_value_option_id";')
this.addSql('drop index if exists "IDX_option_value_option_id_unique";')
this.addSql(
'alter table if exists "product_variant" alter column "weight" type numeric using ("weight"::numeric);'
)
this.addSql(
'alter table if exists "product_variant" alter column "length" type numeric using ("length"::numeric);'
)
this.addSql(
'alter table if exists "product_variant" alter column "height" type numeric using ("height"::numeric);'
)
this.addSql(
'alter table if exists "product_variant" alter column "width" type numeric using ("width"::numeric);'
)
this.addSql('drop index if exists "IDX_product_variant_product_id";')
this.addSql('drop index if exists "IDX_product_variant_sku_unique";')
this.addSql('drop index if exists "IDX_product_variant_barcode_unique";')
this.addSql('drop index if exists "IDX_product_variant_ean_unique";')
this.addSql('drop index if exists "IDX_product_variant_upc_unique";')
}
}
@@ -1,140 +1,45 @@
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
kebabCase,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
EventArgs,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
OneToMany,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const categoryHandleIndexName = "IDX_category_handle_unique"
const categoryHandleIndexStatement = createPsqlIndexStatementHelper({
name: categoryHandleIndexName,
tableName: "product_category",
columns: ["handle"],
unique: true,
where: "deleted_at IS NULL",
})
const categoryMpathIndexName = "IDX_product_category_path"
const categoryMpathIndexStatement = createPsqlIndexStatementHelper({
name: categoryMpathIndexName,
tableName: "product_category",
columns: ["mpath"],
unique: false,
where: "deleted_at IS NULL",
})
categoryMpathIndexStatement.MikroORMIndex()
categoryHandleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_category" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductCategory {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text", nullable: false })
name?: string
@Searchable()
@Property({ columnType: "text", default: "", nullable: false })
description?: string
@Searchable()
@Property({ columnType: "text", nullable: false })
handle?: string
@Property({ columnType: "text", nullable: false })
mpath?: string
@Property({ columnType: "boolean", default: false })
is_active?: boolean
@Property({ columnType: "boolean", default: false })
is_internal?: boolean
@Property({
columnType: "integer",
nullable: false,
default: 0,
const ProductCategory = model
.define("ProductCategory", {
id: model.id({ prefix: "pcat" }).primaryKey(),
name: model.text().searchable(),
description: model.text().searchable().default(""),
handle: model.text().searchable(),
mpath: model.text(),
is_active: model.boolean().default(false),
is_internal: model.boolean().default(false),
rank: model.number().default(0),
metadata: model.json().nullable(),
parent_category: model
.belongsTo(() => ProductCategory, {
mappedBy: "category_children",
})
.nullable(),
category_children: model.hasMany(() => ProductCategory, {
mappedBy: "parent_category",
}),
products: model.manyToMany(() => Product, {
mappedBy: "categories",
}),
})
rank: number
@ManyToOne(() => ProductCategory, {
columnType: "text",
fieldName: "parent_category_id",
nullable: true,
mapToPk: true,
onDelete: "cascade",
.cascades({
delete: ["category_children"],
})
parent_category_id?: string | null
@ManyToOne(() => ProductCategory, { nullable: true, persist: false })
parent_category?: ProductCategory
@OneToMany({
entity: () => ProductCategory,
mappedBy: (productCategory) => productCategory.parent_category,
})
category_children = new Collection<ProductCategory>(this)
@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
@Index({ name: "IDX_product_category_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@ManyToMany(() => Product, (product) => product.categories)
products = new Collection<Product>(this)
@OnInit()
async onInit() {
this.id = generateEntityId(this.id, "pcat")
this.parent_category_id ??= this.parent_category?.id ?? null
}
@BeforeCreate()
async onCreate(args: EventArgs<ProductCategory>) {
this.id = generateEntityId(this.id, "pcat")
this.parent_category_id ??= this.parent_category?.id ?? null
if (!this.handle && this.name) {
this.handle = kebabCase(this.name)
}
this.mpath = `${this.mpath ? this.mpath + "." : ""}${this.id}`
}
}
.indexes([
{
name: "IDX_product_category_path",
on: ["mpath"],
unique: false,
where: "deleted_at IS NULL",
},
{
name: "IDX_category_handle_unique",
on: ["handle"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductCategory
@@ -1,81 +1,23 @@
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
OneToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
kebabCase,
Searchable,
} from "@medusajs/framework/utils"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const collectionHandleIndexName = "IDX_collection_handle_unique"
const collectionHandleIndexStatement = createPsqlIndexStatementHelper({
name: collectionHandleIndexName,
tableName: "product_collection",
columns: ["handle"],
unique: true,
where: "deleted_at IS NULL",
})
collectionHandleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_collection" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductCollection {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
handle?: string
@OneToMany(() => Product, (product) => product.collection)
products = new Collection<Product>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
const ProductCollection = model
.define("ProductCollection", {
id: model.id({ prefix: "pcol" }).primaryKey(),
title: model.text().searchable(),
handle: model.text(),
metadata: model.json().nullable(),
products: model.hasMany(() => Product, {
mappedBy: "collection",
}),
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_collection_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "pcol")
if (!this.handle && this.title) {
this.handle = kebabCase(this.title)
}
}
}
.indexes([
{
name: "IDX_collection_handle_unique",
on: ["handle"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductCollection
@@ -1,88 +1,26 @@
import {
BeforeCreate,
Entity,
Filter,
Index,
ManyToOne,
OnInit,
PrimaryKey,
Property,
Rel,
} from "@mikro-orm/core"
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const imageUrlIndexName = "IDX_product_image_url"
const imageUrlIndexStatement = createPsqlIndexStatementHelper({
name: imageUrlIndexName,
tableName: "image",
columns: ["url"],
unique: false,
where: "deleted_at IS NULL",
})
imageUrlIndexStatement.MikroORMIndex()
@Entity({ tableName: "image" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductImage {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
url: string
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@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
@Index({ name: "IDX_product_image_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@Property({ columnType: "integer", default: 0 })
rank: number
@ManyToOne(() => Product, {
columnType: "text",
onDelete: "cascade",
fieldName: "product_id",
mapToPk: true,
})
product_id: string
@ManyToOne(() => Product, {
persist: false,
})
product: Rel<Product>
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "img")
}
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "img")
}
}
const ProductImage = model
.define(
{ tableName: "image", name: "ProductImage" },
{
id: model.id({ prefix: "img" }).primaryKey(),
url: model.text(),
metadata: model.json().nullable(),
rank: model.number().default(0),
product: model.belongsTo(() => Product, {
mappedBy: "images",
}),
}
)
.indexes([
{
name: "IDX_product_image_url",
on: ["url"],
unique: false,
where: "deleted_at IS NULL",
},
])
export default ProductImage
@@ -1,87 +1,27 @@
import {
DALUtils,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { ProductOption, ProductVariant } from "./index"
const optionValueOptionIdIndexName = "IDX_option_value_option_id_unique"
const optionValueOptionIdIndexStatement = createPsqlIndexStatementHelper({
name: optionValueOptionIdIndexName,
tableName: "product_option_value",
columns: ["option_id", "value"],
unique: true,
where: "deleted_at IS NULL",
})
optionValueOptionIdIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_option_value" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductOptionValue {
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
value: string
@ManyToOne(() => ProductOption, {
columnType: "text",
fieldName: "option_id",
mapToPk: true,
nullable: true,
onDelete: "cascade",
const ProductOptionValue = model
.define("ProductOptionValue", {
id: model.id({ prefix: "optval" }).primaryKey(),
value: model.text(),
metadata: model.json().nullable(),
option: model
.belongsTo(() => ProductOption, {
mappedBy: "values",
})
.nullable(),
variants: model.manyToMany(() => ProductVariant, {
mappedBy: "options",
}),
})
option_id: string | null
@ManyToOne(() => ProductOption, {
nullable: true,
persist: false,
})
option: ProductOption | null
@ManyToMany(() => ProductVariant, (variant) => variant.options)
variants = new Collection<ProductVariant>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@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
@Index({ name: "IDX_product_option_value_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "optval")
this.option_id ??= this.option?.id ?? null
}
}
.indexes([
{
name: "IDX_option_value_option_id_unique",
on: ["option_id", "value"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductOptionValue
@@ -1,93 +1,29 @@
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
Index,
ManyToOne,
OnInit,
OneToMany,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { Product } from "./index"
import ProductOptionValue from "./product-option-value"
const optionProductIdTitleIndexName = "IDX_option_product_id_title_unique"
const optionProductIdTitleIndexStatement = createPsqlIndexStatementHelper({
name: optionProductIdTitleIndexName,
tableName: "product_option",
columns: ["product_id", "title"],
unique: true,
where: "deleted_at IS NULL",
})
optionProductIdTitleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_option" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductOption {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@ManyToOne(() => Product, {
columnType: "text",
fieldName: "product_id",
mapToPk: true,
nullable: true,
onDelete: "cascade",
const ProductOption = model
.define("ProductOption", {
id: model.id({ prefix: "opt" }).primaryKey(),
title: model.text().searchable(),
metadata: model.json().nullable(),
product: model.belongsTo(() => Product, {
mappedBy: "options",
}),
values: model.hasMany(() => ProductOptionValue, {
mappedBy: "option",
}),
})
product_id: string | null
@ManyToOne(() => Product, {
persist: false,
nullable: true,
.cascades({
delete: ["values"],
})
product: Product | null
@OneToMany(() => ProductOptionValue, (value) => value.option, {
cascade: [Cascade.PERSIST, "soft-remove" as any],
})
values = new Collection<ProductOptionValue>(this)
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@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
@Index({ name: "IDX_product_option_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "opt")
this.product_id ??= this.product?.id ?? null
}
}
.indexes([
{
name: "IDX_option_product_id_title_unique",
on: ["product_id", "title"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductOption
@@ -1,73 +1,25 @@
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
import { model } from "@medusajs/framework/utils"
import Product from "./product"
const tagValueIndexName = "IDX_tag_value_unique"
const tagValueIndexStatement = createPsqlIndexStatementHelper({
name: tagValueIndexName,
tableName: "product_tag",
columns: ["value"],
unique: true,
where: "deleted_at IS NULL",
})
tagValueIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_tag" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductTag {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
value: string
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@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
@Index({ name: "IDX_product_tag_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@ManyToMany(() => Product, (product) => product.tags)
products = new Collection<Product>(this)
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "ptag")
}
}
const ProductTag = model
.define(
{ tableName: "product_tag", name: "ProductTag" },
{
id: model.id({ prefix: "ptag" }).primaryKey(),
value: model.text().searchable(),
metadata: model.json().nullable(),
products: model.manyToMany(() => Product, {
mappedBy: "tags",
}),
}
)
.indexes([
{
name: "IDX_tag_value_unique",
on: ["value"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductTag
@@ -1,67 +1,22 @@
import {
BeforeCreate,
Entity,
Filter,
Index,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { Product } from "@models"
import {
DALUtils,
Searchable,
createPsqlIndexStatementHelper,
generateEntityId,
} from "@medusajs/framework/utils"
const typeValueIndexName = "IDX_type_value_unique"
const typeValueIndexStatement = createPsqlIndexStatementHelper({
name: typeValueIndexName,
tableName: "product_type",
columns: ["value"],
unique: true,
where: "deleted_at IS NULL",
})
typeValueIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_type" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductType {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
value: string
@Property({ columnType: "json", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
const ProductType = model
.define("ProductType", {
id: model.id({ prefix: "ptyp" }).primaryKey(),
value: model.text().searchable(),
metadata: model.json().nullable(),
product: model.hasMany(() => Product, {
mappedBy: "type",
}),
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_type_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "ptyp")
}
}
.indexes([
{
name: "IDX_type_value_unique",
on: ["value"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductType
@@ -1,187 +1,69 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
Searchable,
} from "@medusajs/framework/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
Index,
ManyToMany,
ManyToOne,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model } from "@medusajs/framework/utils"
import { Product, ProductOptionValue } from "@models"
const variantSkuIndexName = "IDX_product_variant_sku_unique"
const variantSkuIndexStatement = createPsqlIndexStatementHelper({
name: variantSkuIndexName,
tableName: "product_variant",
columns: ["sku"],
unique: true,
where: "deleted_at IS NULL",
})
const variantBarcodeIndexName = "IDX_product_variant_barcode_unique"
const variantBarcodeIndexStatement = createPsqlIndexStatementHelper({
name: variantBarcodeIndexName,
tableName: "product_variant",
columns: ["barcode"],
unique: true,
where: "deleted_at IS NULL",
})
const variantEanIndexName = "IDX_product_variant_ean_unique"
const variantEanIndexStatement = createPsqlIndexStatementHelper({
name: variantEanIndexName,
tableName: "product_variant",
columns: ["ean"],
unique: true,
where: "deleted_at IS NULL",
})
const variantUpcIndexName = "IDX_product_variant_upc_unique"
const variantUpcIndexStatement = createPsqlIndexStatementHelper({
name: variantUpcIndexName,
tableName: "product_variant",
columns: ["upc"],
unique: true,
where: "deleted_at IS NULL",
})
const variantProductIdIndexName = "IDX_product_variant_product_id"
const variantProductIdIndexStatement = createPsqlIndexStatementHelper({
name: variantProductIdIndexName,
tableName: "product_variant",
columns: ["product_id"],
unique: false,
where: "deleted_at IS NULL",
})
variantProductIdIndexStatement.MikroORMIndex()
variantSkuIndexStatement.MikroORMIndex()
variantBarcodeIndexStatement.MikroORMIndex()
variantEanIndexStatement.MikroORMIndex()
variantUpcIndexStatement.MikroORMIndex()
@Entity({ tableName: "product_variant" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductVariant {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Searchable()
@Property({ columnType: "text", nullable: true })
sku?: string | null
@Searchable()
@Property({ columnType: "text", nullable: true })
barcode?: string | null
@Searchable()
@Property({ columnType: "text", nullable: true })
ean?: string | null
@Searchable()
@Property({ columnType: "text", nullable: true })
upc?: string | null
@Property({ columnType: "boolean", default: false })
allow_backorder?: boolean = false
@Property({ columnType: "boolean", default: true })
manage_inventory?: boolean = true
@Property({ columnType: "text", nullable: true })
hs_code?: string | null
@Property({ columnType: "text", nullable: true })
origin_country?: string | null
@Property({ columnType: "text", nullable: true })
mid_code?: string | null
@Property({ columnType: "text", nullable: true })
material?: string | null
@Property({ columnType: "numeric", nullable: true })
weight?: number | null
@Property({ columnType: "numeric", nullable: true })
length?: number | null
@Property({ columnType: "numeric", nullable: true })
height?: number | null
@Property({ columnType: "numeric", nullable: true })
width?: number | null
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@Property({
columnType: "integer",
nullable: true,
default: 0,
const ProductVariant = model
.define("ProductVariant", {
id: model.id({ prefix: "variant" }).primaryKey(),
title: model.text().searchable(),
sku: model.text().searchable().nullable(),
barcode: model.text().searchable().nullable(),
ean: model.text().searchable().nullable(),
upc: model.text().searchable().nullable(),
allow_backorder: model.boolean().default(false),
manage_inventory: model.boolean().default(true),
hs_code: model.text().nullable(),
origin_country: model.text().nullable(),
mid_code: model.text().nullable(),
material: model.text().nullable(),
weight: model.number().nullable(),
length: model.number().nullable(),
height: model.number().nullable(),
width: model.number().nullable(),
metadata: model.json().nullable(),
variant_rank: model.number().default(0).nullable(),
product: model
.belongsTo(() => Product, {
mappedBy: "variants",
})
.nullable(),
options: model.manyToMany(() => ProductOptionValue, {
pivotTable: "product_variant_option",
mappedBy: "variants",
joinColumn: "variant_id",
inverseJoinColumn: "option_value_id",
}),
})
variant_rank?: number | null
@ManyToOne(() => Product, {
columnType: "text",
nullable: true,
onDelete: "cascade",
fieldName: "product_id",
mapToPk: true,
})
product_id: string | null
@ManyToOne(() => Product, {
persist: false,
nullable: true,
})
product: Product | null
@ManyToMany(() => ProductOptionValue, "variants", {
owner: true,
pivotTable: "product_variant_option",
joinColumn: "variant_id",
inverseJoinColumn: "option_value_id",
})
options = new Collection<ProductOptionValue>(this)
@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
@Index({ name: "IDX_product_variant_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "variant")
this.product_id ??= this.product?.id ?? null
}
}
.indexes([
{
name: "IDX_product_variant_product_id",
on: ["product_id"],
unique: false,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_sku_unique",
on: ["sku"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_barcode_unique",
on: ["barcode"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_ean_unique",
on: ["ean"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_variant_upc_unique",
on: ["upc"],
unique: true,
where: "deleted_at IS NULL",
},
])
export default ProductVariant
+77 -213
View File
@@ -1,222 +1,86 @@
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Enum,
Filter,
Index,
ManyToMany,
ManyToOne,
OneToMany,
OnInit,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import { model, ProductUtils } from "@medusajs/framework/utils"
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
ProductUtils,
Searchable,
toHandle,
} from "@medusajs/framework/utils"
import ProductCategory from "./product-category"
import ProductCollection from "./product-collection"
import ProductImage from "./product-image"
import ProductOption from "./product-option"
import ProductTag from "./product-tag"
import ProductType from "./product-type"
import ProductImage from "./product-image"
import ProductOption from "./product-option"
import ProductVariant from "./product-variant"
import ProductCategory from "./product-category"
import ProductCollection from "./product-collection"
const productHandleIndexName = "IDX_product_handle_unique"
const productHandleIndexStatement = createPsqlIndexStatementHelper({
name: productHandleIndexName,
tableName: "product",
columns: ["handle"],
unique: true,
where: "deleted_at IS NULL",
})
const productTypeIndexName = "IDX_product_type_id"
const productTypeIndexStatement = createPsqlIndexStatementHelper({
name: productTypeIndexName,
tableName: "product",
columns: ["type_id"],
unique: false,
where: "deleted_at IS NULL",
})
const productCollectionIndexName = "IDX_product_collection_id"
const productCollectionIndexStatement = createPsqlIndexStatementHelper({
name: productCollectionIndexName,
tableName: "product",
columns: ["collection_id"],
unique: false,
where: "deleted_at IS NULL",
})
productTypeIndexStatement.MikroORMIndex()
productCollectionIndexStatement.MikroORMIndex()
productHandleIndexStatement.MikroORMIndex()
@Entity({ tableName: "product" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class Product {
@PrimaryKey({ columnType: "text" })
id!: string
@Searchable()
@Property({ columnType: "text" })
title: string
@Property({ columnType: "text" })
handle?: string
@Searchable()
@Property({ columnType: "text", nullable: true })
subtitle?: string | null
@Searchable()
@Property({
columnType: "text",
nullable: true,
const Product = model
.define("Product", {
id: model.id({ prefix: "prod" }).primaryKey(),
title: model.text().searchable(),
handle: model.text(),
subtitle: model.text().searchable().nullable(),
description: model.text().searchable().nullable(),
is_giftcard: model.boolean().default(false),
status: model
.enum(ProductUtils.ProductStatus)
.default(ProductUtils.ProductStatus.DRAFT),
thumbnail: model.text().nullable(),
weight: model.text().nullable(),
length: model.text().nullable(),
height: model.text().nullable(),
width: model.text().nullable(),
origin_country: model.text().nullable(),
hs_code: model.text().nullable(),
mid_code: model.text().nullable(),
material: model.text().nullable(),
discountable: model.boolean().default(true),
external_id: model.text().nullable(),
metadata: model.json().nullable(),
variants: model.hasMany(() => ProductVariant, {
mappedBy: "product",
}),
type: model
.belongsTo(() => ProductType, {
mappedBy: "product",
})
.nullable(),
tags: model.manyToMany(() => ProductTag, {
mappedBy: "products",
pivotTable: "product_tags",
}),
options: model.hasMany(() => ProductOption, {
mappedBy: "product",
}),
images: model.hasMany(() => ProductImage, {
mappedBy: "product",
}),
collection: model
.belongsTo(() => ProductCollection, {
mappedBy: "products",
})
.nullable(),
categories: model.manyToMany(() => ProductCategory, {
pivotTable: "product_category_product",
mappedBy: "products",
}),
})
description?: string | null
@Property({ columnType: "boolean", default: false })
is_giftcard!: boolean
@Enum(() => ProductUtils.ProductStatus)
@Property({ default: ProductUtils.ProductStatus.DRAFT })
status!: ProductUtils.ProductStatus
@Property({ columnType: "text", nullable: true })
thumbnail?: string | null
@OneToMany(() => ProductOption, (o) => o.product, {
cascade: ["soft-remove"] as any,
.cascades({
delete: ["variants", "options", "images"],
})
options = new Collection<ProductOption>(this)
@Searchable()
@OneToMany(() => ProductVariant, (variant) => variant.product, {
cascade: ["soft-remove"] as any,
})
variants = new Collection<ProductVariant>(this)
@Property({ columnType: "text", nullable: true })
weight?: number | null
@Property({ columnType: "text", nullable: true })
length?: number | null
@Property({ columnType: "text", nullable: true })
height?: number | null
@Property({ columnType: "text", nullable: true })
width?: number | null
@Property({ columnType: "text", nullable: true })
origin_country?: string | null
@Property({ columnType: "text", nullable: true })
hs_code?: string | null
@Property({ columnType: "text", nullable: true })
mid_code?: string | null
@Property({ columnType: "text", nullable: true })
material?: string | null
@Searchable()
@ManyToOne(() => ProductCollection, {
columnType: "text",
nullable: true,
fieldName: "collection_id",
mapToPk: true,
onDelete: "set null",
})
collection_id: string | null
@ManyToOne(() => ProductCollection, {
nullable: true,
persist: false,
})
collection: ProductCollection | null
@ManyToOne(() => ProductType, {
columnType: "text",
nullable: true,
fieldName: "type_id",
mapToPk: true,
onDelete: "set null",
})
type_id: string | null
@ManyToOne(() => ProductType, {
nullable: true,
persist: false,
})
type: ProductType | null
@ManyToMany(() => ProductTag, "products", {
owner: true,
pivotTable: "product_tags",
index: "IDX_product_tag_id",
})
tags = new Collection<ProductTag>(this)
@OneToMany(() => ProductImage, (image) => image.product_id, {
cascade: [Cascade.PERSIST, Cascade.REMOVE],
})
images = new Collection<ProductImage>(this)
@ManyToMany(() => ProductCategory, "products", {
owner: true,
pivotTable: "product_category_product",
})
categories = new Collection<ProductCategory>(this)
@Property({ columnType: "boolean", default: true })
discountable: boolean
@Property({ columnType: "text", nullable: true })
external_id?: string | null
@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
@Index({ name: "IDX_product_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null
@OnInit()
@BeforeCreate()
onInit() {
this.id = generateEntityId(this.id, "prod")
this.type_id ??= this.type?.id ?? null
this.collection_id ??= this.collection?.id ?? null
if (!this.handle && this.title) {
this.handle = toHandle(this.title)
}
}
}
.indexes([
{
name: "IDX_product_handle_unique",
on: ["handle"],
unique: true,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_type_id",
on: ["type_id"],
unique: false,
where: "deleted_at IS NULL",
},
{
name: "IDX_product_collection_id",
on: ["collection_id"],
unique: false,
where: "deleted_at IS NULL",
},
])
export default Product
@@ -1,30 +1,30 @@
import {
Context,
DAL,
InferEntityType,
ProductCategoryTransformOptions,
ProductTypes,
} from "@medusajs/framework/types"
import { DALUtils, isDefined, MedusaError } from "@medusajs/framework/utils"
import {
EntityDTO,
LoadStrategy,
FilterQuery as MikroFilterQuery,
FindOptions as MikroOptions,
RequiredEntityData,
} from "@mikro-orm/core"
import { LoadStrategy, FindOptions as MikroOptions } from "@mikro-orm/core"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { ProductCategory } from "@models"
import { UpdateCategoryInput } from "@types"
// eslint-disable-next-line max-len
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<ProductCategory> {
export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeRepository<
typeof ProductCategory
> {
buildFindOptions(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
familyOptions: ProductCategoryTransformOptions = {}
) {
const findOptions_ = { ...findOptions }
findOptions_.options ??= {
orderBy: { rank: "ASC" },
findOptions_.options ??= {}
findOptions_.options.orderBy = {
id: "ASC",
rank: "ASC",
...findOptions_.options.orderBy,
}
const fields = (findOptions_.options.fields ??= [])
@@ -69,17 +69,19 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}
async find(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {},
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
const productCategories = await manager.find(
ProductCategory,
findOptions_.where as MikroFilterQuery<ProductCategory>,
{ ...findOptions_.options } as MikroOptions<ProductCategory>
const productCategories = await manager.find<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
findOptions_.where,
{ ...findOptions_.options } as any // TODO
)
if (
@@ -102,7 +104,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
return this.sortCategoriesByRank(categoriesTree)
}
sortCategoriesByRank(categories: ProductCategory[]): ProductCategory[] {
sortCategoriesByRank(
categories: InferEntityType<typeof ProductCategory>[]
): InferEntityType<typeof ProductCategory>[] {
const sortedCategories = categories.sort((a, b) => a.rank - b.rank)
for (const category of sortedCategories) {
@@ -122,12 +126,12 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
descendants?: boolean
ancestors?: boolean
},
productCategories: ProductCategory[],
findOptions: DAL.FindOptions<ProductCategory> & {
productCategories: InferEntityType<typeof ProductCategory>[],
findOptions: DAL.FindOptions<typeof ProductCategory> & {
serialize?: boolean
} = { where: {} },
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const { serialize = true } = findOptions
delete findOptions.serialize
@@ -173,17 +177,17 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
...findOptions.options,
limit: undefined,
offset: 0,
} as MikroOptions<ProductCategory>
} as MikroOptions<any>
delete where.id
delete where.mpath
delete where.parent_category_id
const categoriesInTree = serialize
? await this.serialize<ProductCategory[]>(
await manager.find(ProductCategory, where, options)
? await this.serialize<InferEntityType<typeof ProductCategory>[]>(
await manager.find(ProductCategory.name, where, options)
)
: await manager.find(ProductCategory, where, options)
: await manager.find(ProductCategory.name, where, options)
const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat]))
@@ -235,18 +239,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}
async findAndCount(
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
findOptions: DAL.FindOptions<typeof ProductCategory> = { where: {} },
transformOptions: ProductCategoryTransformOptions = {},
context: Context = {}
): Promise<[ProductCategory[], number]> {
): Promise<[InferEntityType<typeof ProductCategory>[], number]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const findOptions_ = this.buildFindOptions(findOptions, transformOptions)
const [productCategories, count] = await manager.findAndCount(
ProductCategory,
findOptions_.where as MikroFilterQuery<ProductCategory>,
findOptions_.options as MikroOptions<ProductCategory>
)
const [productCategories, count] = (await manager.findAndCount(
ProductCategory.name,
findOptions_.where,
findOptions_.options as any
)) as unknown as [InferEntityType<typeof ProductCategory>[], number]
if (
!transformOptions.includeDescendantsTree &&
@@ -271,19 +275,23 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async delete(ids: string[], context: Context = {}): Promise<void> {
const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context)
await manager.nativeDelete(ProductCategory, { id: ids }, {})
await manager.nativeDelete(ProductCategory.name, { id: ids }, {})
}
async softDelete(
ids: string[],
context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> {
): Promise<
[InferEntityType<typeof ProductCategory>[], Record<string, unknown[]>]
> {
const manager = super.getActiveManager<SqlEntityManager>(context)
await this.baseDelete(ids, context)
const categories = await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOne(ProductCategory, {
const productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id,
})
@@ -306,11 +314,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async restore(
ids: string[],
context: Context = {}
): Promise<[ProductCategory[], Record<string, unknown[]>]> {
): Promise<
[InferEntityType<typeof ProductCategory>[], Record<string, unknown[]>]
> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(ProductCategory, {
const productCategory = await manager.findOneOrFail<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id,
})
manager.assign(productCategory, { deleted_at: null })
@@ -327,12 +339,14 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await Promise.all(
ids.map(async (id) => {
const productCategory = await manager.findOne(
ProductCategory,
const productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{ id },
{
populate: ["category_children"],
}
} as any // TODO
)
if (!productCategory) {
@@ -357,13 +371,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async create(
data: ProductTypes.CreateProductCategoryDTO[],
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
const siblingsCount = await manager.count(ProductCategory, {
const categoryData: Partial<InferEntityType<typeof ProductCategory>> = {
...entry,
}
const siblingsCount = await manager.count(ProductCategory.name, {
parent_category_id: categoryData?.parent_category_id || null,
})
@@ -378,14 +394,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}
// Set the base mpath if the category has a parent. The model `create` hook will append the own id to the base mpath.
let parentCategory: InferEntityType<typeof ProductCategory> | null =
null
const parentCategoryId =
categoryData.parent_category_id ?? categoryData.parent_category?.id
if (parentCategoryId) {
const parentCategory = await manager.findOne(
ProductCategory,
parentCategoryId
)
parentCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, parentCategoryId)
if (!parentCategory) {
throw new MedusaError(
@@ -393,14 +410,28 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
`Parent category with id: '${parentCategoryId}' does not exist`
)
}
categoryData.mpath = parentCategory.mpath
}
return manager.create(
ProductCategory,
categoryData as RequiredEntityData<ProductCategory>
const result = await manager.create<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
categoryData as unknown as InferEntityType<typeof ProductCategory>
)
/**
* Since "mpath" calculation relies on the id of the created
* category, we have to compute it after calling manager.create. So
* that we can access the "category.id" which is under the hood
* defined by DML.
*/
manager.assign(result, {
mpath: parentCategory
? `${parentCategory.mpath}.${result.id}`
: result.id,
})
return result
})
)
@@ -411,14 +442,18 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
async update(
data: UpdateCategoryInput[],
context: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const manager = super.getActiveManager<SqlEntityManager>(context)
const categories = await Promise.all(
data.map(async (entry, i) => {
const categoryData: Partial<EntityDTO<ProductCategory>> = { ...entry }
let productCategory = (await manager.findOne(ProductCategory, {
const categoryData: Partial<InferEntityType<typeof ProductCategory>> = {
...entry,
}
let productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
id: categoryData.id,
})) as ProductCategory
})
if (!productCategory) {
throw new MedusaError(
@@ -465,10 +500,9 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
)
)[0]
const newParentCategory = await manager.findOne(
ProductCategory,
categoryData.parent_category_id
)
const newParentCategory = await manager.findOne<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, categoryData.parent_category_id)
if (!newParentCategory) {
throw new MedusaError(
@@ -487,13 +521,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
)
function updateMpathRecursively(
category: ProductCategory,
category: InferEntityType<typeof ProductCategory>,
newBaseMpath: string
) {
const newMpath = `${newBaseMpath}.${category.id}`
category.mpath = newMpath
for (let child of category.category_children) {
child = manager.getReference(ProductCategory, child.id)
child = manager.getReference(ProductCategory.name, child.id)
manager.assign(
child,
categoryDataChildrenMap.get(child.id) ?? {}
@@ -502,12 +536,15 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}
}
updateMpathRecursively(productCategory!, newParentCategory.mpath!)
updateMpathRecursively(
productCategory!,
(newParentCategory as any).mpath!
)
// categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}`
}
// Rerank the siblings in the new parent
const siblingsCount = await manager.count(ProductCategory, {
const siblingsCount = await manager.count(ProductCategory.name, {
parent_category_id: categoryData.parent_category_id,
})
@@ -534,7 +571,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
return productCategory
// If only the rank changed, we need to rerank all siblings.
} else if (isDefined(categoryData.rank)) {
const siblingsCount = await manager.count(ProductCategory, {
const siblingsCount = await manager.count(ProductCategory.name, {
parent_category_id: productCategory.parent_category_id,
})
@@ -546,7 +583,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
await this.rerankAllSiblings(
manager,
productCategory,
categoryData as Partial<EntityDTO<ProductCategory>> & {
categoryData as Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
}
)
@@ -569,9 +606,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterDeletion(
manager: SqlEntityManager,
removedSibling: Partial<ProductCategory>
removedSibling: Partial<InferEntityType<typeof ProductCategory>>
) {
const affectedSiblings = await manager.find(ProductCategory, {
const affectedSiblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
parent_category_id: removedSibling.parent_category_id,
rank: { $gt: removedSibling.rank },
})
@@ -586,9 +625,11 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankSiblingsAfterCreation(
manager: SqlEntityManager,
addedSibling: Partial<EntityDTO<ProductCategory>>
addedSibling: Partial<InferEntityType<typeof ProductCategory>>
) {
const affectedSiblings = await manager.find(ProductCategory, {
const affectedSiblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(ProductCategory.name, {
parent_category_id: addedSibling.parent_category_id,
rank: { $gte: addedSibling.rank },
})
@@ -603,16 +644,22 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
protected async rerankAllSiblings(
manager: SqlEntityManager,
originalSibling: Partial<ProductCategory> & { rank: number },
updatedSibling: Partial<EntityDTO<ProductCategory>> & { rank: number }
originalSibling: Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
},
updatedSibling: Partial<InferEntityType<typeof ProductCategory>> & {
rank: number
}
) {
if (originalSibling.rank === updatedSibling.rank) {
return
}
if (originalSibling.rank < updatedSibling.rank) {
const siblings = await manager.find(
ProductCategory,
const siblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{
parent_category_id: originalSibling.parent_category_id,
rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank },
@@ -627,8 +674,10 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
manager.persist(updatedSiblings)
} else {
const siblings = await manager.find(
ProductCategory,
const siblings = await manager.find<
InferEntityType<typeof ProductCategory>
>(
ProductCategory.name,
{
parent_category_id: originalSibling.parent_category_id,
rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank },
@@ -1,11 +1,11 @@
import { Product } from "@models"
import { Context, DAL } from "@medusajs/framework/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { DALUtils } from "@medusajs/framework/utils"
import { SqlEntityManager } from "@mikro-orm/postgresql"
// eslint-disable-next-line max-len
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Product>(
export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Product
) {
constructor(...args: any[]) {
@@ -19,7 +19,9 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
* first find all products that are in the categories, and then exclude them
*/
protected async mutateNotInCategoriesConstraints(
findOptions: DAL.FindOptions<Product> = { where: {} },
findOptions: DAL.FindOptions<typeof Product> = {
where: {},
},
context: Context = {}
): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context)
@@ -29,7 +31,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<Pr
findOptions.where.categories?.id?.["$nin"]
) {
const productsInCategories = await manager.find(
Product,
this.entity,
{
categories: {
id: { $in: findOptions.where.categories.id["$nin"] },
@@ -2,6 +2,7 @@ import {
Context,
DAL,
FindConfig,
InferEntityType,
ProductTypes,
} from "@medusajs/framework/types"
import {
@@ -33,7 +34,7 @@ export default class ProductCategoryService {
productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory> {
): Promise<InferEntityType<typeof ProductCategory>> {
if (!isDefined(productCategoryId)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
@@ -41,7 +42,7 @@ export default class ProductCategoryService {
)
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
const queryOptions = ModulesSdkUtils.buildQuery(
{
id: productCategoryId,
},
@@ -67,7 +68,7 @@ export default class ProductCategoryService {
)
}
return productCategories[0] as ProductCategory
return productCategories[0]
}
@InjectManager("productCategoryRepository_")
@@ -75,7 +76,7 @@ export default class ProductCategoryService {
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> {
): Promise<InferEntityType<typeof ProductCategory>[]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
includeAncestorsTree: filters?.include_ancestors_tree || false,
@@ -94,17 +95,14 @@ export default class ProductCategoryService {
delete filters.q
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
const queryOptions = ModulesSdkUtils.buildQuery(filters, config)
queryOptions.where ??= {}
return (await this.productCategoryRepository_.find(
return await this.productCategoryRepository_.find(
queryOptions,
transformOptions,
sharedContext
)) as ProductCategory[]
)
}
@InjectManager("productCategoryRepository_")
@@ -112,7 +110,7 @@ export default class ProductCategoryService {
filters: ProductTypes.FilterableProductCategoryProps = {},
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[ProductCategory[], number]> {
): Promise<[InferEntityType<typeof ProductCategory>[], number]> {
const transformOptions = {
includeDescendantsTree: filters?.include_descendants_tree || false,
includeAncestorsTree: filters?.include_ancestors_tree || false,
@@ -131,37 +129,34 @@ export default class ProductCategoryService {
delete filters.q
}
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
filters,
config
)
const queryOptions = ModulesSdkUtils.buildQuery(filters, config)
queryOptions.where ??= {}
return (await this.productCategoryRepository_.findAndCount(
return await this.productCategoryRepository_.findAndCount(
queryOptions,
transformOptions,
sharedContext
)) as [ProductCategory[], number]
)
}
@InjectTransactionManager("productCategoryRepository_")
async create(
data: ProductTypes.CreateProductCategoryDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> {
return (await (
): Promise<InferEntityType<typeof ProductCategory>[]> {
return await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).create(data, sharedContext)) as ProductCategory[]
).create(data, sharedContext)
}
@InjectTransactionManager("productCategoryRepository_")
async update(
data: UpdateCategoryInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCategory[]> {
return (await (
): Promise<InferEntityType<typeof ProductCategory>[]> {
return await (
this.productCategoryRepository_ as unknown as ProductCategoryRepository
).update(data, sharedContext)) as ProductCategory[]
).update(data, sharedContext)
}
@InjectTransactionManager("productCategoryRepository_")
@@ -3,6 +3,7 @@ import {
DAL,
FindConfig,
IEventBusModuleService,
InferEntityType,
InternalModuleDeclaration,
ModuleJoinerConfig,
ModulesSdkTypes,
@@ -105,15 +106,31 @@ export default class ProductModuleService
implements ProductTypes.IProductModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<Product>
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<ProductVariant>
protected readonly productService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Product>
>
protected readonly productVariantService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductVariant>
>
protected readonly productCategoryService_: ProductCategoryService
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<ProductTag>
protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService<ProductCollection>
protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService<ProductImage>
protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService<ProductType>
protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService<ProductOption>
protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService<ProductOptionValue>
protected readonly productTagService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductTag>
>
protected readonly productCollectionService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductCollection>
>
protected readonly productImageService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductImage>
>
protected readonly productTypeService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductType>
>
protected readonly productOptionService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductOption>
>
protected readonly productOptionValueService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ProductOptionValue>
>
protected readonly eventBusModuleService_?: IEventBusModuleService
constructor(
@@ -211,12 +228,16 @@ export default class ProductModuleService
return {
...config,
order: {
id: "ASC",
...config?.order,
...(hasImagesRelation ? {
images: {
rank: "ASC",
},
} : {}),
...(hasImagesRelation
? {
images: {
rank: "ASC",
...((config?.order?.images as object) ?? {}),
},
}
: {}),
},
}
}
@@ -256,7 +277,7 @@ export default class ProductModuleService
protected async createVariants_(
data: ProductTypes.CreateProductVariantDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductVariant[]> {
): Promise<InferEntityType<typeof ProductVariant>[]> {
if (data.some((v) => !v.product_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -332,8 +353,8 @@ export default class ProductModuleService
(variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id
)
let created: ProductVariant[] = []
let updated: ProductVariant[] = []
let created: InferEntityType<typeof ProductVariant>[] = []
let updated: InferEntityType<typeof ProductVariant>[] = []
if (forCreate.length) {
created = await this.createVariants_(forCreate, sharedContext)
@@ -400,7 +421,7 @@ export default class ProductModuleService
protected async updateVariants_(
data: UpdateProductVariantInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductVariant[]> {
): Promise<InferEntityType<typeof ProductVariant>[]> {
// Validation step
const variantIdsToUpdate = data.map(({ id }) => id)
const variants = await this.productVariantService_.list(
@@ -535,8 +556,8 @@ export default class ProductModuleService
(tag): tag is ProductTypes.CreateProductTagDTO => !tag.id
)
let created: ProductTag[] = []
let updated: ProductTag[] = []
let created: InferEntityType<typeof ProductTag>[] = []
let updated: InferEntityType<typeof ProductTag>[] = []
if (forCreate.length) {
created = await this.productTagService_.create(forCreate, sharedContext)
@@ -665,8 +686,8 @@ export default class ProductModuleService
(type): type is ProductTypes.CreateProductTypeDTO => !type.id
)
let created: ProductType[] = []
let updated: ProductType[] = []
let created: InferEntityType<typeof ProductType>[] = []
let updated: InferEntityType<typeof ProductType>[] = []
if (forCreate.length) {
created = await this.productTypeService_.create(forCreate, sharedContext)
@@ -763,7 +784,7 @@ export default class ProductModuleService
protected async createOptions_(
data: ProductTypes.CreateProductOptionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductOption[]> {
): Promise<InferEntityType<typeof ProductOption>[]> {
if (data.some((v) => !v.product_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -810,8 +831,8 @@ export default class ProductModuleService
(option): option is ProductTypes.CreateProductOptionDTO => !option.id
)
let created: ProductOption[] = []
let updated: ProductOption[] = []
let created: InferEntityType<typeof ProductOption>[] = []
let updated: InferEntityType<typeof ProductOption>[] = []
if (forCreate.length) {
created = await this.createOptions_(forCreate, sharedContext)
@@ -876,7 +897,7 @@ export default class ProductModuleService
protected async updateOptions_(
data: UpdateProductOptionInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductOption[]> {
): Promise<InferEntityType<typeof ProductOption>[]> {
// Validation step
if (data.some((option) => !option.id)) {
throw new MedusaError(
@@ -985,7 +1006,7 @@ export default class ProductModuleService
async createCollections_(
data: ProductTypes.CreateProductCollectionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCollection[]> {
): Promise<InferEntityType<typeof ProductCollection>[]> {
const normalizedInput = data.map(
ProductModuleService.normalizeCreateProductCollectionInput
)
@@ -1030,8 +1051,8 @@ export default class ProductModuleService
!collection.id
)
let created: ProductCollection[] = []
let updated: ProductCollection[] = []
let created: InferEntityType<typeof ProductCollection>[] = []
let updated: InferEntityType<typeof ProductCollection>[] = []
if (forCreate.length) {
created = await this.createCollections_(forCreate, sharedContext)
@@ -1125,7 +1146,7 @@ export default class ProductModuleService
protected async updateCollections_(
data: UpdateCollectionInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<ProductCollection[]> {
): Promise<InferEntityType<typeof ProductCollection>[]> {
const normalizedInput = data.map(
ProductModuleService.normalizeUpdateProductCollectionInput
) as UpdateCollectionInput[]
@@ -1139,7 +1160,7 @@ export default class ProductModuleService
sharedContext
)
const collections: ProductCollection[] = []
const collections: InferEntityType<typeof ProductCollection>[] = []
const updateSelectorAndData = updatedCollections.flatMap(
(collectionData) => {
@@ -1178,7 +1199,7 @@ export default class ProductModuleService
collections.push({
...collectionData,
products: productsToUpdate ?? [],
} as ProductCollection)
} as InferEntityType<typeof ProductCollection>)
return result
}
@@ -1208,7 +1229,12 @@ export default class ProductModuleService
): Promise<
ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO
> {
const input = Array.isArray(data) ? data : [data]
const input = (Array.isArray(data) ? data : [data]).map(
(productCategory) => {
productCategory.handle ??= kebabCase(productCategory.name)
return productCategory
}
)
const categories = await this.productCategoryService_.create(
input,
@@ -1255,8 +1281,8 @@ export default class ProductModuleService
!category.id
)
let created: ProductCategory[] = []
let updated: ProductCategory[] = []
let created: InferEntityType<typeof ProductCategory>[] = []
let updated: InferEntityType<typeof ProductCategory>[] = []
if (forCreate.length) {
created = await this.productCategoryService_.create(
@@ -1406,8 +1432,8 @@ export default class ProductModuleService
(product): product is ProductTypes.CreateProductDTO => !product.id
)
let created: Product[] = []
let updated: Product[] = []
let created: InferEntityType<typeof Product>[] = []
let updated: InferEntityType<typeof Product>[] = []
if (forCreate.length) {
created = await this.createProducts_(forCreate, sharedContext)
@@ -1494,7 +1520,7 @@ export default class ProductModuleService
protected async createProducts_(
data: ProductTypes.CreateProductDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<Product[]> {
): Promise<InferEntityType<typeof Product>[]> {
const normalizedInput = await promiseAll(
data.map(async (d) => {
const normalized = await this.normalizeCreateProductInput(
@@ -1581,7 +1607,7 @@ export default class ProductModuleService
protected async updateProducts_(
data: UpdateProductInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<Product[]> {
): Promise<InferEntityType<typeof Product>[]> {
const normalizedInput = await promiseAll(
data.map(async (d) => {
const normalized = await this.normalizeUpdateProductInput(
@@ -1733,6 +1759,13 @@ export default class ProductModuleService
) {
this.validateProductPayload(productData)
if (!productData.title) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Product title is required`
)
}
const options = productData.options
const missingOptionsVariants: string[] = []
@@ -1872,7 +1905,7 @@ export default class ProductModuleService
variants:
| ProductTypes.CreateProductVariantDTO[]
| ProductTypes.UpdateProductVariantDTO[],
options: ProductOption[]
options: InferEntityType<typeof ProductOption>[]
):
| ProductTypes.CreateProductVariantDTO[]
| ProductTypes.UpdateProductVariantDTO[] {
@@ -1945,7 +1978,7 @@ export default class ProductModuleService
| ProductTypes.CreateProductVariantDTO
| UpdateProductVariantInput
) & { options: { id: string }[]; product_id: string })[],
variants: ProductVariant[]
variants: InferEntityType<typeof ProductVariant>[]
) {
for (const variantData of data) {
const existingVariant = variants.find((v) => {