feat(prduct, utils, types): Create soft delete pattern for link module (#4649)
* feat(prouct, utils, types): Create soft delete pattern for link module * add comment * add comment * finalise * remove linkable keys * cleanup and tests * cleanup * add some comments and renaming * re work * fix tests --------- Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
This commit is contained in:
committed by
GitHub
parent
fc6c9df035
commit
ce3326c5fb
@@ -230,7 +230,6 @@ describe("Product module", function () {
|
||||
const products = await module.create([data])
|
||||
|
||||
await module.softDelete([products[0].id])
|
||||
|
||||
const deletedProducts = await module.list(
|
||||
{ id: products[0].id },
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Image, Product, ProductCategory, ProductVariant } from "@models"
|
||||
import {
|
||||
assignCategoriesToProduct,
|
||||
buildProductOnlyData,
|
||||
createImages,
|
||||
createProductAndTags,
|
||||
createProductVariants,
|
||||
@@ -16,7 +17,6 @@ import { ProductRepository } from "@repositories"
|
||||
import { ProductService } from "@services"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { TestDatabase } from "../../../utils"
|
||||
import { buildProductOnlyData } from "../../../__fixtures__/product/data/create-product"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
|
||||
@@ -81,15 +81,19 @@ describe("Product Service", () => {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual('Product with id: does-not-exist was not found')
|
||||
expect(error.message).toEqual(
|
||||
"Product with id: does-not-exist was not found"
|
||||
)
|
||||
})
|
||||
|
||||
it("should return a product when product with an id exists", async () => {
|
||||
const result = await service.retrieve(productOne.id)
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
id: productOne.id
|
||||
}))
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: productOne.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -150,18 +154,22 @@ describe("Product Service", () => {
|
||||
})
|
||||
|
||||
it("should update a product and its allowed relations", async () => {
|
||||
const updateData = [{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
images: images,
|
||||
thumbnail: images[0].url,
|
||||
}]
|
||||
const updateData = [
|
||||
{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
images: images,
|
||||
thumbnail: images[0].url,
|
||||
},
|
||||
]
|
||||
|
||||
const products = await service.update(updateData)
|
||||
|
||||
expect(products.length).toEqual(1)
|
||||
|
||||
let result = await service.retrieve(productOne.id, {relations: ["images", "thumbnail"]})
|
||||
let result = await service.retrieve(productOne.id, {
|
||||
relations: ["images", "thumbnail"],
|
||||
})
|
||||
let serialized = JSON.parse(JSON.stringify(result))
|
||||
|
||||
expect(serialized).toEqual(
|
||||
@@ -183,13 +191,16 @@ describe("Product Service", () => {
|
||||
|
||||
it("should throw an error when id is not present", async () => {
|
||||
let error
|
||||
const updateData = [{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
}, {
|
||||
id: undefined as unknown as string,
|
||||
title: "update test 2",
|
||||
}]
|
||||
const updateData = [
|
||||
{
|
||||
id: productOne.id,
|
||||
title: "update test 1",
|
||||
},
|
||||
{
|
||||
id: undefined as unknown as string,
|
||||
title: "update test 2",
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
await service.update(updateData)
|
||||
@@ -206,10 +217,12 @@ describe("Product Service", () => {
|
||||
|
||||
it("should throw an error when product with id does not exist", async () => {
|
||||
let error
|
||||
const updateData = [{
|
||||
id: "does-not-exist",
|
||||
title: "update test 1",
|
||||
}]
|
||||
const updateData = [
|
||||
{
|
||||
id: "does-not-exist",
|
||||
title: "update test 1",
|
||||
},
|
||||
]
|
||||
|
||||
try {
|
||||
await service.update(updateData)
|
||||
@@ -217,13 +230,14 @@ describe("Product Service", () => {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(`Product with id "does-not-exist" not found`)
|
||||
expect(error.message).toEqual(
|
||||
`Product with id "does-not-exist" not found`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
describe("soft deleted", function () {
|
||||
let deletedProduct
|
||||
let product
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -232,7 +246,7 @@ describe("Product Service", () => {
|
||||
const products = await createProductAndTags(testManager, productsData)
|
||||
|
||||
product = products[1]
|
||||
deletedProduct = await service.softDelete([products[0].id])
|
||||
await service.softDelete([products[0].id])
|
||||
})
|
||||
|
||||
it("should list all products that are not deleted", async () => {
|
||||
@@ -462,7 +476,19 @@ describe("Product Service", () => {
|
||||
})
|
||||
|
||||
const products = await service.create([data])
|
||||
const deleteProducts = await service.softDelete(products.map((p) => p.id))
|
||||
await service.softDelete(products.map((p) => p.id))
|
||||
const deleteProducts = await service.list(
|
||||
{ id: products.map((p) => p.id) },
|
||||
{
|
||||
relations: [
|
||||
"variants",
|
||||
"variants.options",
|
||||
"options",
|
||||
"options.values",
|
||||
],
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
expect(deleteProducts).toHaveLength(1)
|
||||
expect(deleteProducts[0].deleted_at).not.toBeNull()
|
||||
|
||||
@@ -1,5 +1,59 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { JoinerServiceConfig } from "@medusajs/types"
|
||||
import {
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import ProductImage from "./models/product-image"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
|
||||
export enum LinkableKeys {
|
||||
PRODUCT_ID = "product_id",
|
||||
PRODUCT_HANDLE = "product_handle",
|
||||
VARIANT_ID = "variant_id",
|
||||
VARIANT_SKU = "variant_sku",
|
||||
PRODUCT_OPTION_ID = "product_option_id",
|
||||
PRODUCT_TYPE_ID = "product_type_id",
|
||||
PRODUCT_CATEGORY_ID = "product_category_id",
|
||||
PRODUCT_COLLECTION_ID = "product_collection_id",
|
||||
PRODUCT_TAG_ID = "product_tag_id",
|
||||
PRODUCT_IMAGE_ID = "product_image_id",
|
||||
}
|
||||
|
||||
export const entityNameToLinkableKeysMap: MapToConfig = {
|
||||
[Product.name]: [
|
||||
{ mapTo: LinkableKeys.PRODUCT_ID, valueFrom: "id" },
|
||||
{
|
||||
mapTo: LinkableKeys.PRODUCT_HANDLE,
|
||||
valueFrom: "handle",
|
||||
},
|
||||
],
|
||||
[ProductVariant.name]: [
|
||||
{ mapTo: LinkableKeys.VARIANT_ID, valueFrom: "id" },
|
||||
{ mapTo: LinkableKeys.VARIANT_SKU, valueFrom: "sku" },
|
||||
],
|
||||
[ProductOption.name]: [
|
||||
{ mapTo: LinkableKeys.PRODUCT_OPTION_ID, valueFrom: "id" },
|
||||
],
|
||||
[ProductType.name]: [
|
||||
{ mapTo: LinkableKeys.PRODUCT_TYPE_ID, valueFrom: "id" },
|
||||
],
|
||||
[ProductCategory.name]: [
|
||||
{ mapTo: LinkableKeys.PRODUCT_CATEGORY_ID, valueFrom: "id" },
|
||||
],
|
||||
[ProductCollection.name]: [
|
||||
{ mapTo: LinkableKeys.PRODUCT_COLLECTION_ID, valueFrom: "id" },
|
||||
],
|
||||
[ProductTag.name]: [{ mapTo: LinkableKeys.PRODUCT_TAG_ID, valueFrom: "id" }],
|
||||
[ProductImage.name]: [
|
||||
{ mapTo: LinkableKeys.PRODUCT_IMAGE_ID, valueFrom: "id" },
|
||||
],
|
||||
}
|
||||
|
||||
export const joinerConfig: JoinerServiceConfig = {
|
||||
serviceName: Modules.PRODUCT,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { ProductCategory } from "@models"
|
||||
import { ProductCategoryRepository } from "@repositories"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
ModulesSdkUtils,
|
||||
MedusaError,
|
||||
isDefined,
|
||||
InjectTransactionManager,
|
||||
InjectManager,
|
||||
MedusaContext
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
@@ -39,9 +39,12 @@ export default class ProductCategoryService<
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>({
|
||||
id: productCategoryId,
|
||||
}, config)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
{
|
||||
id: productCategoryId,
|
||||
},
|
||||
config
|
||||
)
|
||||
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: true,
|
||||
@@ -111,31 +114,37 @@ export default class ProductCategoryService<
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
|
||||
@InjectTransactionManager(
|
||||
shouldForceTransaction,
|
||||
"productCategoryRepository_"
|
||||
)
|
||||
async create(
|
||||
data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
return (await (this.productCategoryRepository_ as ProductCategoryRepository).create(
|
||||
data,
|
||||
sharedContext
|
||||
)) as TEntity
|
||||
return (await (
|
||||
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||
).create(data, sharedContext)) as TEntity
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
|
||||
@InjectTransactionManager(
|
||||
shouldForceTransaction,
|
||||
"productCategoryRepository_"
|
||||
)
|
||||
async update(
|
||||
id: string,
|
||||
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
return (await (this.productCategoryRepository_ as ProductCategoryRepository).update(
|
||||
id,
|
||||
data,
|
||||
sharedContext
|
||||
)) as TEntity
|
||||
return (await (
|
||||
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||
).update(id, data, sharedContext)) as TEntity
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
|
||||
@InjectTransactionManager(
|
||||
shouldForceTransaction,
|
||||
"productCategoryRepository_"
|
||||
)
|
||||
async delete(
|
||||
id: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
ModulesSdkUtils,
|
||||
retrieveEntity,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
InjectManager,
|
||||
ModulesSdkUtils,
|
||||
retrieveEntity,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
@@ -85,29 +85,36 @@ export default class ProductCollectionService<
|
||||
return queryOptions
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
|
||||
@InjectTransactionManager(
|
||||
shouldForceTransaction,
|
||||
"productCollectionRepository_"
|
||||
)
|
||||
async create(
|
||||
data: ProductTypes.CreateProductCollectionDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productCollectionRepository_ as ProductCollectionRepository).create(
|
||||
data,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
return (await (
|
||||
this.productCollectionRepository_ as ProductCollectionRepository
|
||||
).create(data, sharedContext)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
|
||||
@InjectTransactionManager(
|
||||
shouldForceTransaction,
|
||||
"productCollectionRepository_"
|
||||
)
|
||||
async update(
|
||||
data: ProductTypes.UpdateProductCollectionDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productCollectionRepository_ as ProductCollectionRepository).update(
|
||||
data,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
return (await (
|
||||
this.productCollectionRepository_ as ProductCollectionRepository
|
||||
).update(data, sharedContext)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
|
||||
@InjectTransactionManager(
|
||||
shouldForceTransaction,
|
||||
"productCollectionRepository_"
|
||||
)
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
|
||||
@@ -38,12 +38,17 @@ import {
|
||||
isDefined,
|
||||
isString,
|
||||
kebabCase,
|
||||
mapObjectTo,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
import { joinerConfig } from "./../joiner-config"
|
||||
import {
|
||||
entityNameToLinkableKeysMap,
|
||||
joinerConfig,
|
||||
LinkableKeys,
|
||||
} from "./../joiner-config"
|
||||
import { ProductCategoryServiceTypes } from "../types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -958,7 +963,7 @@ export default class ProductModuleService<
|
||||
) {
|
||||
if (isDefined(productData.type)) {
|
||||
const productType = await this.productTypeService_.upsert(
|
||||
[productData.type],
|
||||
[productData.type!],
|
||||
sharedContext
|
||||
)
|
||||
|
||||
@@ -974,22 +979,41 @@ export default class ProductModuleService<
|
||||
await this.productService_.delete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
async softDelete(
|
||||
async softDelete<
|
||||
TReturnableLinkableKeys extends string = Lowercase<
|
||||
keyof typeof LinkableKeys
|
||||
>
|
||||
>(
|
||||
productIds: string[],
|
||||
{
|
||||
returnLinkableKeys,
|
||||
}: { returnLinkableKeys?: TReturnableLinkableKeys[] } = {
|
||||
returnLinkableKeys: [],
|
||||
},
|
||||
sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.softDelete_(productIds, sharedContext)
|
||||
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||
let [, cascadedEntitiesMap] = await this.softDelete_(
|
||||
productIds,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
||||
populate: true,
|
||||
})
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
mappedCascadedEntitiesMap = mapObjectTo<
|
||||
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
||||
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
||||
pick: returnLinkableKeys as string[],
|
||||
})
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async softDelete_(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
): Promise<[TProduct[], Record<string, unknown[]>]> {
|
||||
return await this.productService_.softDelete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
isDefined,
|
||||
} from "@medusajs/utils"
|
||||
import { ProductRepository } from "@repositories"
|
||||
|
||||
@@ -44,9 +44,12 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
||||
id: productId,
|
||||
}, config)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>(
|
||||
{
|
||||
id: productId,
|
||||
},
|
||||
config
|
||||
)
|
||||
|
||||
const product = await this.productRepository_.find(
|
||||
queryOptions,
|
||||
@@ -140,7 +143,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
data: ProductServiceTypes.UpdateProductDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await (this.productRepository_ as ProductRepository).update(
|
||||
return (await (this.productRepository_ as ProductRepository).update(
|
||||
data as WithRequiredProperty<
|
||||
ProductServiceTypes.UpdateProductDTO,
|
||||
"id"
|
||||
@@ -148,7 +151,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
{
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}
|
||||
) as TEntity[]
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
@@ -165,7 +168,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
async softDelete(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this.productRepository_.softDelete(productIds, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
|
||||
@@ -41,7 +41,18 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
|
||||
|
||||
delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
softDelete(ids: string[], context?: Context): Promise<T[]>
|
||||
/**
|
||||
* Soft delete entities and cascade to related entities if configured.
|
||||
*
|
||||
* @param ids
|
||||
* @param context
|
||||
*
|
||||
* @returns [T[], Record<string, string[]>] the second value being the map of the entity names and ids that were soft deleted
|
||||
*/
|
||||
softDelete(
|
||||
ids: string[],
|
||||
context?: Context
|
||||
): Promise<[T[], Record<string, unknown[]>]>
|
||||
|
||||
restore(ids: string[], context?: Context): Promise<T[]>
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import {
|
||||
CreateProductCategoryDTO,
|
||||
CreateProductCollectionDTO,
|
||||
CreateProductDTO,
|
||||
CreateProductOptionDTO,
|
||||
CreateProductTagDTO,
|
||||
CreateProductTypeDTO,
|
||||
CreateProductOptionDTO,
|
||||
CreateProductCategoryDTO,
|
||||
UpdateProductTagDTO,
|
||||
UpdateProductTypeDTO,
|
||||
UpdateProductOptionDTO,
|
||||
UpdateProductCategoryDTO,
|
||||
UpdateProductDTO,
|
||||
FilterableProductCategoryProps,
|
||||
FilterableProductCollectionProps,
|
||||
FilterableProductOptionProps,
|
||||
FilterableProductProps,
|
||||
FilterableProductTagProps,
|
||||
FilterableProductTypeProps,
|
||||
FilterableProductOptionProps,
|
||||
FilterableProductVariantProps,
|
||||
ProductCategoryDTO,
|
||||
ProductCollectionDTO,
|
||||
ProductDTO,
|
||||
ProductOptionDTO,
|
||||
ProductTagDTO,
|
||||
ProductTypeDTO,
|
||||
ProductOptionDTO,
|
||||
ProductVariantDTO,
|
||||
CreateProductCollectionDTO,
|
||||
UpdateProductCategoryDTO,
|
||||
UpdateProductCollectionDTO,
|
||||
UpdateProductDTO,
|
||||
UpdateProductOptionDTO,
|
||||
UpdateProductTagDTO,
|
||||
UpdateProductTypeDTO,
|
||||
} from "./common"
|
||||
|
||||
import { Context } from "../shared-context"
|
||||
@@ -72,18 +72,15 @@ export interface IProductModuleService {
|
||||
|
||||
createTags(
|
||||
data: CreateProductTagDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTagDTO[]>
|
||||
|
||||
updateTags(
|
||||
data: UpdateProductTagDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTagDTO[]>
|
||||
|
||||
deleteTags(
|
||||
productTagIds: string[],
|
||||
sharedContext?: Context,
|
||||
): Promise<void>
|
||||
deleteTags(productTagIds: string[], sharedContext?: Context): Promise<void>
|
||||
|
||||
retrieveType(
|
||||
typeId: string,
|
||||
@@ -105,18 +102,15 @@ export interface IProductModuleService {
|
||||
|
||||
createTypes(
|
||||
data: CreateProductTypeDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypeDTO[]>
|
||||
|
||||
updateTypes(
|
||||
data: UpdateProductTypeDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypeDTO[]>
|
||||
|
||||
deleteTypes(
|
||||
productTypeIds: string[],
|
||||
sharedContext?: Context,
|
||||
): Promise<void>
|
||||
deleteTypes(productTypeIds: string[], sharedContext?: Context): Promise<void>
|
||||
|
||||
retrieveOption(
|
||||
optionId: string,
|
||||
@@ -138,17 +132,17 @@ export interface IProductModuleService {
|
||||
|
||||
createOptions(
|
||||
data: CreateProductOptionDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductOptionDTO[]>
|
||||
|
||||
updateOptions(
|
||||
data: UpdateProductOptionDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductOptionDTO[]>
|
||||
|
||||
deleteOptions(
|
||||
productOptionIds: string[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
retrieveVariant(
|
||||
@@ -189,17 +183,17 @@ export interface IProductModuleService {
|
||||
|
||||
createCollections(
|
||||
data: CreateProductCollectionDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCollectionDTO[]>
|
||||
|
||||
updateCollections(
|
||||
data: UpdateProductCollectionDTO[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCollectionDTO[]>
|
||||
|
||||
deleteCollections(
|
||||
productCollectionIds: string[],
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
retrieveCategory(
|
||||
@@ -222,19 +216,16 @@ export interface IProductModuleService {
|
||||
|
||||
createCategory(
|
||||
data: CreateProductCategoryDTO,
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCategoryDTO>
|
||||
|
||||
updateCategory(
|
||||
categoryId: string,
|
||||
data: UpdateProductCategoryDTO,
|
||||
sharedContext?: Context,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductCategoryDTO>
|
||||
|
||||
deleteCategory(
|
||||
categoryId: string,
|
||||
sharedContext?: Context,
|
||||
): Promise<void>
|
||||
deleteCategory(categoryId: string, sharedContext?: Context): Promise<void>
|
||||
|
||||
create(
|
||||
data: CreateProductDTO[],
|
||||
@@ -248,10 +239,11 @@ export interface IProductModuleService {
|
||||
|
||||
delete(productIds: string[], sharedContext?: Context): Promise<void>
|
||||
|
||||
softDelete(
|
||||
softDelete<TReturnableLinkableKeys extends string = string>(
|
||||
productIds: string[],
|
||||
config?: { returnLinkableKeys?: TReturnableLinkableKeys[] },
|
||||
sharedContext?: Context
|
||||
): Promise<ProductDTO[]>
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
restore(productIds: string[], sharedContext?: Context): Promise<ProductDTO[]>
|
||||
}
|
||||
|
||||
43
packages/utils/src/common/__tests__/map-object-to.ts
Normal file
43
packages/utils/src/common/__tests__/map-object-to.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { mapObjectTo, MapToConfig } from "../map-object-to"
|
||||
|
||||
const input = {
|
||||
a: [{ id: "1" }, { id: "2" }],
|
||||
b: [{ id: "3" }, { id: "4", handle: "handle1" }],
|
||||
c: [{ id: "5", sku: "sku1" }, { id: "6" }],
|
||||
}
|
||||
|
||||
const mapToConfig: MapToConfig = {
|
||||
a: [{ mapTo: "a.id", valueFrom: "id" }],
|
||||
b: [
|
||||
{ mapTo: "b.id", valueFrom: "id" },
|
||||
{ mapTo: "b.handle", valueFrom: "handle" },
|
||||
],
|
||||
c: [
|
||||
{ mapTo: "c.id", valueFrom: "id" },
|
||||
{ mapTo: "c.sku", valueFrom: "sku" },
|
||||
],
|
||||
}
|
||||
|
||||
describe("mapObjectTo", function () {
|
||||
it("should return a new object with the keys remapped and the values picked from the original object based on the map config", function () {
|
||||
const remappedObject = mapObjectTo(input, mapToConfig)
|
||||
|
||||
expect(remappedObject).toEqual({
|
||||
"a.id": ["1", "2"],
|
||||
"b.id": ["3", "4"],
|
||||
"b.handle": ["handle1"],
|
||||
"c.id": ["5", "6"],
|
||||
"c.sku": ["sku1"],
|
||||
})
|
||||
})
|
||||
|
||||
it("should return a new object with only the picked properties", function () {
|
||||
const remappedObject = mapObjectTo(input, mapToConfig, {
|
||||
pick: ["a.id"],
|
||||
})
|
||||
|
||||
expect(remappedObject).toEqual({
|
||||
"a.id": ["1", "2"],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -22,3 +22,4 @@ export * from "./stringify-circular"
|
||||
export * from "./to-kebab-case"
|
||||
export * from "./to-pascal-case"
|
||||
export * from "./wrap-handler"
|
||||
export * from "./map-object-to"
|
||||
|
||||
53
packages/utils/src/common/map-object-to.ts
Normal file
53
packages/utils/src/common/map-object-to.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
type RemapInputObject = Record<string, unknown[]>
|
||||
type RemapConfig = { mapTo: string; valueFrom: string }
|
||||
export type MapToConfig = {
|
||||
[key: string]: RemapConfig[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object with the keys remapped and the values picked from the original object based
|
||||
* on the map config
|
||||
*
|
||||
* @param object input object
|
||||
* @param mapTo configuration to map the output object
|
||||
* @param removeIfNotRemapped if true, the keys that are not remapped will be removed from the output object
|
||||
* @param pick if provided, only the keys in the array will be picked from the output object
|
||||
*/
|
||||
export function mapObjectTo<
|
||||
TResult = any,
|
||||
T extends RemapInputObject = RemapInputObject
|
||||
>(
|
||||
object: T,
|
||||
mapTo: MapToConfig,
|
||||
{
|
||||
removeIfNotRemapped,
|
||||
pick,
|
||||
}: { removeIfNotRemapped?: boolean; pick?: string[] } = {}
|
||||
): TResult {
|
||||
removeIfNotRemapped ??= false
|
||||
|
||||
const newObject: Record<string, any> = {}
|
||||
|
||||
for (const key in object) {
|
||||
const remapConfig = mapTo[key as string]!
|
||||
|
||||
if (!remapConfig) {
|
||||
if (!removeIfNotRemapped) {
|
||||
newObject[key] = object[key]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
remapConfig.forEach((config) => {
|
||||
if (pick?.length && !pick.includes(config.mapTo)) {
|
||||
return
|
||||
}
|
||||
|
||||
newObject[config.mapTo] = object[key]
|
||||
.map((obj: any) => obj[config.valueFrom])
|
||||
.filter(Boolean)
|
||||
})
|
||||
}
|
||||
|
||||
return newObject as TResult
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./mikro-orm/mikro-orm-repository"
|
||||
export * from "./repository"
|
||||
export * from "./utils"
|
||||
export * from "./mikro-orm/utils"
|
||||
export * from "./mikro-orm/mikro-orm-create-connection"
|
||||
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
|
||||
|
||||
@@ -2,10 +2,10 @@ import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import { MedusaContext } from "../../decorators"
|
||||
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
||||
import {
|
||||
mikroOrmSerializer,
|
||||
mikroOrmUpdateDeletedAtRecursively,
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||
transactionWrapper,
|
||||
} from "../utils"
|
||||
import { mikroOrmSerializer, mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
||||
|
||||
class MikroOrmBase<T = any> {
|
||||
protected readonly manager_: any
|
||||
@@ -71,13 +71,17 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<T[]> {
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
||||
const date = new Date()
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
||||
|
||||
return entities
|
||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
})
|
||||
|
||||
return [entities, softDeletedEntitiesMap]
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
|
||||
54
packages/utils/src/dal/mikro-orm/utils.ts
Normal file
54
packages/utils/src/dal/mikro-orm/utils.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { SoftDeletableFilterKey } from "./mikro-orm-soft-deletable-filter"
|
||||
|
||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
||||
T extends object = any
|
||||
>(
|
||||
manager: any,
|
||||
entities: (T & { id: string; deleted_at?: string | Date | null })[],
|
||||
value: Date | null
|
||||
) => {
|
||||
for (const entity of entities) {
|
||||
if (!("deleted_at" in entity)) continue
|
||||
|
||||
entity.deleted_at = value
|
||||
|
||||
const relations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.constructor.name).relations
|
||||
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
let collectionRelation = entity[relation.name]
|
||||
|
||||
if (!collectionRelation.isInitialized()) {
|
||||
await collectionRelation.init()
|
||||
}
|
||||
|
||||
const relationEntities = await collectionRelation.getItems({
|
||||
filters: {
|
||||
[SoftDeletableFilterKey]: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
||||
}
|
||||
|
||||
await manager.persist(entity)
|
||||
}
|
||||
}
|
||||
|
||||
export const mikroOrmSerializer = async <TOutput extends object>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput> => {
|
||||
options ??= {}
|
||||
const { serialize } = await import("@mikro-orm/core")
|
||||
const result = serialize(data, options)
|
||||
return result as unknown as Promise<TOutput>
|
||||
}
|
||||
@@ -49,7 +49,10 @@ export abstract class AbstractBaseRepository<T = any>
|
||||
|
||||
abstract delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
abstract softDelete(ids: string[], context?: Context): Promise<T[]>
|
||||
abstract softDelete(
|
||||
ids: string[],
|
||||
context?: Context
|
||||
): Promise<[T[], Record<string, unknown[]>]>
|
||||
|
||||
abstract restore(ids: string[], context?: Context): Promise<T[]>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SoftDeletableFilterKey } from "../dal"
|
||||
import { isObject } from "../common"
|
||||
|
||||
export async function transactionWrapper<TManager = unknown>(
|
||||
this: any,
|
||||
@@ -33,54 +33,52 @@ export async function transactionWrapper<TManager = unknown>(
|
||||
return await transactionMethod.bind(this.manager_)(task, options)
|
||||
}
|
||||
|
||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
||||
T extends object = any
|
||||
>(
|
||||
manager: any,
|
||||
entities: T[],
|
||||
value: Date | null
|
||||
) => {
|
||||
/**
|
||||
* Can be used to create a new Object that collect the entities
|
||||
* based on the columnLookup. This is useful when you want to soft delete entities and return
|
||||
* an object where the keys are the entities name and the values are the entities
|
||||
* that were soft deleted.
|
||||
*
|
||||
* @param entities
|
||||
* @param deletedEntitiesMap
|
||||
* @param getEntityName
|
||||
*/
|
||||
export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
deletedEntitiesMap,
|
||||
getEntityName,
|
||||
}: {
|
||||
entities: any[]
|
||||
deletedEntitiesMap?: Map<string, any[]>
|
||||
getEntityName?: (entity: any) => string
|
||||
}): Record<string, any[]> {
|
||||
deletedEntitiesMap ??= new Map<string, any[]>()
|
||||
getEntityName ??= (entity) => entity.constructor.name
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!("deleted_at" in entity)) continue
|
||||
;(entity as any).deleted_at = value
|
||||
const entityName = getEntityName(entity)
|
||||
const shouldSkip = !!deletedEntitiesMap
|
||||
.get(entityName)
|
||||
?.some((e) => e.id === entity.id)
|
||||
|
||||
const relations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.constructor.name).relations
|
||||
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
let collectionRelation = entity[relation.name]
|
||||
|
||||
if (!collectionRelation.isInitialized()) {
|
||||
await collectionRelation.init()
|
||||
}
|
||||
|
||||
const relationEntities = await collectionRelation.getItems({
|
||||
filters: {
|
||||
[SoftDeletableFilterKey]: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
||||
if (!entity.deleted_at || shouldSkip) {
|
||||
continue
|
||||
}
|
||||
|
||||
await manager.persist(entity)
|
||||
}
|
||||
}
|
||||
const values = deletedEntitiesMap.get(entityName) ?? []
|
||||
values.push(entity)
|
||||
deletedEntitiesMap.set(entityName, values)
|
||||
|
||||
export const mikroOrmSerializer = async <TOutput extends object>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput> => {
|
||||
options ??= {}
|
||||
const { serialize } = await import("@mikro-orm/core")
|
||||
const result = serialize(data, options)
|
||||
return result as unknown as Promise<TOutput>
|
||||
Object.values(entity).forEach((propValue: any) => {
|
||||
if (propValue != null && isObject(propValue[0])) {
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities: propValue,
|
||||
deletedEntitiesMap,
|
||||
getEntityName,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Object.fromEntries(deletedEntitiesMap)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user