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:
1 parent
fc6c9df035
commit
ce3326c5fb
17 files changed
+433
-151
No files matched your search
@@ -230,7 +230,6 @@ describe("Product module", function () {
|
|||||||
const products = await module.create([data])
|
const products = await module.create([data])
|
||||||
|
|
||||||
await module.softDelete([products[0].id])
|
await module.softDelete([products[0].id])
|
||||||
|
|
||||||
const deletedProducts = await module.list(
|
const deletedProducts = await module.list(
|
||||||
{ id: products[0].id },
|
{ id: products[0].id },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Image, Product, ProductCategory, ProductVariant } from "@models"
|
import { Image, Product, ProductCategory, ProductVariant } from "@models"
|
||||||
import {
|
import {
|
||||||
assignCategoriesToProduct,
|
assignCategoriesToProduct,
|
||||||
|
buildProductOnlyData,
|
||||||
createImages,
|
createImages,
|
||||||
createProductAndTags,
|
createProductAndTags,
|
||||||
createProductVariants,
|
createProductVariants,
|
||||||
@@ -16,7 +17,6 @@ import { ProductRepository } from "@repositories"
|
|||||||
import { ProductService } from "@services"
|
import { ProductService } from "@services"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { TestDatabase } from "../../../utils"
|
import { TestDatabase } from "../../../utils"
|
||||||
import { buildProductOnlyData } from "../../../__fixtures__/product/data/create-product"
|
|
||||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||||
import { kebabCase } from "@medusajs/utils"
|
import { kebabCase } from "@medusajs/utils"
|
||||||
|
|
||||||
@@ -81,15 +81,19 @@ describe("Product Service", () => {
|
|||||||
error = e
|
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 () => {
|
it("should return a product when product with an id exists", async () => {
|
||||||
const result = await service.retrieve(productOne.id)
|
const result = await service.retrieve(productOne.id)
|
||||||
|
|
||||||
expect(result).toEqual(expect.objectContaining({
|
expect(result).toEqual(
|
||||||
id: productOne.id
|
expect.objectContaining({
|
||||||
}))
|
id: productOne.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -150,18 +154,22 @@ describe("Product Service", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should update a product and its allowed relations", async () => {
|
it("should update a product and its allowed relations", async () => {
|
||||||
const updateData = [{
|
const updateData = [
|
||||||
|
{
|
||||||
id: productOne.id,
|
id: productOne.id,
|
||||||
title: "update test 1",
|
title: "update test 1",
|
||||||
images: images,
|
images: images,
|
||||||
thumbnail: images[0].url,
|
thumbnail: images[0].url,
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const products = await service.update(updateData)
|
const products = await service.update(updateData)
|
||||||
|
|
||||||
expect(products.length).toEqual(1)
|
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))
|
let serialized = JSON.parse(JSON.stringify(result))
|
||||||
|
|
||||||
expect(serialized).toEqual(
|
expect(serialized).toEqual(
|
||||||
@@ -183,13 +191,16 @@ describe("Product Service", () => {
|
|||||||
|
|
||||||
it("should throw an error when id is not present", async () => {
|
it("should throw an error when id is not present", async () => {
|
||||||
let error
|
let error
|
||||||
const updateData = [{
|
const updateData = [
|
||||||
|
{
|
||||||
id: productOne.id,
|
id: productOne.id,
|
||||||
title: "update test 1",
|
title: "update test 1",
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
id: undefined as unknown as string,
|
id: undefined as unknown as string,
|
||||||
title: "update test 2",
|
title: "update test 2",
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.update(updateData)
|
await service.update(updateData)
|
||||||
@@ -206,10 +217,12 @@ describe("Product Service", () => {
|
|||||||
|
|
||||||
it("should throw an error when product with id does not exist", async () => {
|
it("should throw an error when product with id does not exist", async () => {
|
||||||
let error
|
let error
|
||||||
const updateData = [{
|
const updateData = [
|
||||||
|
{
|
||||||
id: "does-not-exist",
|
id: "does-not-exist",
|
||||||
title: "update test 1",
|
title: "update test 1",
|
||||||
}]
|
},
|
||||||
|
]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.update(updateData)
|
await service.update(updateData)
|
||||||
@@ -217,13 +230,14 @@ describe("Product Service", () => {
|
|||||||
error = e
|
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("list", () => {
|
||||||
describe("soft deleted", function () {
|
describe("soft deleted", function () {
|
||||||
let deletedProduct
|
|
||||||
let product
|
let product
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -232,7 +246,7 @@ describe("Product Service", () => {
|
|||||||
const products = await createProductAndTags(testManager, productsData)
|
const products = await createProductAndTags(testManager, productsData)
|
||||||
|
|
||||||
product = products[1]
|
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 () => {
|
it("should list all products that are not deleted", async () => {
|
||||||
@@ -462,7 +476,19 @@ describe("Product Service", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const products = await service.create([data])
|
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).toHaveLength(1)
|
||||||
expect(deleteProducts[0].deleted_at).not.toBeNull()
|
expect(deleteProducts[0].deleted_at).not.toBeNull()
|
||||||
|
|||||||
@@ -1,5 +1,59 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { JoinerServiceConfig } from "@medusajs/types"
|
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 = {
|
export const joinerConfig: JoinerServiceConfig = {
|
||||||
serviceName: Modules.PRODUCT,
|
serviceName: Modules.PRODUCT,
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { ProductCategory } from "@models"
|
|||||||
import { ProductCategoryRepository } from "@repositories"
|
import { ProductCategoryRepository } from "@repositories"
|
||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ModulesSdkUtils,
|
|
||||||
MedusaError,
|
|
||||||
isDefined,
|
|
||||||
InjectTransactionManager,
|
|
||||||
InjectManager,
|
InjectManager,
|
||||||
MedusaContext
|
InjectTransactionManager,
|
||||||
|
isDefined,
|
||||||
|
MedusaContext,
|
||||||
|
MedusaError,
|
||||||
|
ModulesSdkUtils,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
import { shouldForceTransaction } from "../utils"
|
import { shouldForceTransaction } from "../utils"
|
||||||
@@ -39,9 +39,12 @@ export default class ProductCategoryService<
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>({
|
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||||
|
{
|
||||||
id: productCategoryId,
|
id: productCategoryId,
|
||||||
}, config)
|
},
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const transformOptions = {
|
const transformOptions = {
|
||||||
includeDescendantsTree: true,
|
includeDescendantsTree: true,
|
||||||
@@ -111,31 +114,37 @@ export default class ProductCategoryService<
|
|||||||
)) as [TEntity[], number]
|
)) as [TEntity[], number]
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
|
@InjectTransactionManager(
|
||||||
|
shouldForceTransaction,
|
||||||
|
"productCategoryRepository_"
|
||||||
|
)
|
||||||
async create(
|
async create(
|
||||||
data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
|
data: ProductCategoryServiceTypes.CreateProductCategoryDTO,
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity> {
|
): Promise<TEntity> {
|
||||||
return (await (this.productCategoryRepository_ as ProductCategoryRepository).create(
|
return (await (
|
||||||
data,
|
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||||
sharedContext
|
).create(data, sharedContext)) as TEntity
|
||||||
)) as TEntity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
|
@InjectTransactionManager(
|
||||||
|
shouldForceTransaction,
|
||||||
|
"productCategoryRepository_"
|
||||||
|
)
|
||||||
async update(
|
async update(
|
||||||
id: string,
|
id: string,
|
||||||
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
|
data: ProductCategoryServiceTypes.UpdateProductCategoryDTO,
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity> {
|
): Promise<TEntity> {
|
||||||
return (await (this.productCategoryRepository_ as ProductCategoryRepository).update(
|
return (await (
|
||||||
id,
|
this.productCategoryRepository_ as unknown as ProductCategoryRepository
|
||||||
data,
|
).update(id, data, sharedContext)) as TEntity
|
||||||
sharedContext
|
|
||||||
)) as TEntity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "productCategoryRepository_")
|
@InjectTransactionManager(
|
||||||
|
shouldForceTransaction,
|
||||||
|
"productCategoryRepository_"
|
||||||
|
)
|
||||||
async delete(
|
async delete(
|
||||||
id: string,
|
id: string,
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ModulesSdkUtils,
|
InjectManager,
|
||||||
retrieveEntity,
|
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
InjectManager,
|
ModulesSdkUtils,
|
||||||
|
retrieveEntity,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
import { shouldForceTransaction } from "../utils"
|
import { shouldForceTransaction } from "../utils"
|
||||||
@@ -85,29 +85,36 @@ export default class ProductCollectionService<
|
|||||||
return queryOptions
|
return queryOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
|
@InjectTransactionManager(
|
||||||
|
shouldForceTransaction,
|
||||||
|
"productCollectionRepository_"
|
||||||
|
)
|
||||||
async create(
|
async create(
|
||||||
data: ProductTypes.CreateProductCollectionDTO[],
|
data: ProductTypes.CreateProductCollectionDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity[]> {
|
): Promise<TEntity[]> {
|
||||||
return (await (this.productCollectionRepository_ as ProductCollectionRepository).create(
|
return (await (
|
||||||
data,
|
this.productCollectionRepository_ as ProductCollectionRepository
|
||||||
sharedContext
|
).create(data, sharedContext)) as TEntity[]
|
||||||
)) as TEntity[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
|
@InjectTransactionManager(
|
||||||
|
shouldForceTransaction,
|
||||||
|
"productCollectionRepository_"
|
||||||
|
)
|
||||||
async update(
|
async update(
|
||||||
data: ProductTypes.UpdateProductCollectionDTO[],
|
data: ProductTypes.UpdateProductCollectionDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity[]> {
|
): Promise<TEntity[]> {
|
||||||
return (await (this.productCollectionRepository_ as ProductCollectionRepository).update(
|
return (await (
|
||||||
data,
|
this.productCollectionRepository_ as ProductCollectionRepository
|
||||||
sharedContext
|
).update(data, sharedContext)) as TEntity[]
|
||||||
)) as TEntity[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "productCollectionRepository_")
|
@InjectTransactionManager(
|
||||||
|
shouldForceTransaction,
|
||||||
|
"productCollectionRepository_"
|
||||||
|
)
|
||||||
async delete(
|
async delete(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
|||||||
@@ -38,12 +38,17 @@ import {
|
|||||||
isDefined,
|
isDefined,
|
||||||
isString,
|
isString,
|
||||||
kebabCase,
|
kebabCase,
|
||||||
|
mapObjectTo,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
import { shouldForceTransaction } from "../utils"
|
import { shouldForceTransaction } from "../utils"
|
||||||
import { joinerConfig } from "./../joiner-config"
|
import {
|
||||||
|
entityNameToLinkableKeysMap,
|
||||||
|
joinerConfig,
|
||||||
|
LinkableKeys,
|
||||||
|
} from "./../joiner-config"
|
||||||
import { ProductCategoryServiceTypes } from "../types"
|
import { ProductCategoryServiceTypes } from "../types"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
@@ -958,7 +963,7 @@ export default class ProductModuleService<
|
|||||||
) {
|
) {
|
||||||
if (isDefined(productData.type)) {
|
if (isDefined(productData.type)) {
|
||||||
const productType = await this.productTypeService_.upsert(
|
const productType = await this.productTypeService_.upsert(
|
||||||
[productData.type],
|
[productData.type!],
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -974,22 +979,41 @@ export default class ProductModuleService<
|
|||||||
await this.productService_.delete(productIds, sharedContext)
|
await this.productService_.delete(productIds, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
async softDelete(
|
async softDelete<
|
||||||
|
TReturnableLinkableKeys extends string = Lowercase<
|
||||||
|
keyof typeof LinkableKeys
|
||||||
|
>
|
||||||
|
>(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
|
{
|
||||||
|
returnLinkableKeys,
|
||||||
|
}: { returnLinkableKeys?: TReturnableLinkableKeys[] } = {
|
||||||
|
returnLinkableKeys: [],
|
||||||
|
},
|
||||||
sharedContext: Context = {}
|
sharedContext: Context = {}
|
||||||
): Promise<ProductTypes.ProductDTO[]> {
|
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||||
const products = await this.softDelete_(productIds, sharedContext)
|
let [, cascadedEntitiesMap] = await this.softDelete_(
|
||||||
|
productIds,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
let mappedCascadedEntitiesMap
|
||||||
populate: true,
|
if (returnLinkableKeys) {
|
||||||
|
mappedCascadedEntitiesMap = mapObjectTo<
|
||||||
|
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
||||||
|
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
||||||
|
pick: returnLinkableKeys as string[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||||
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||||
protected async softDelete_(
|
protected async softDelete_(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TProduct[]> {
|
): Promise<[TProduct[], Record<string, unknown[]>]> {
|
||||||
return await this.productService_.softDelete(productIds, sharedContext)
|
return await this.productService_.softDelete(productIds, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
|
isDefined,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
ModulesSdkUtils,
|
ModulesSdkUtils,
|
||||||
isDefined,
|
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { ProductRepository } from "@repositories"
|
import { ProductRepository } from "@repositories"
|
||||||
|
|
||||||
@@ -44,9 +44,12 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
const queryOptions = ModulesSdkUtils.buildQuery<Product>(
|
||||||
|
{
|
||||||
id: productId,
|
id: productId,
|
||||||
}, config)
|
},
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
const product = await this.productRepository_.find(
|
const product = await this.productRepository_.find(
|
||||||
queryOptions,
|
queryOptions,
|
||||||
@@ -140,7 +143,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
data: ProductServiceTypes.UpdateProductDTO[],
|
data: ProductServiceTypes.UpdateProductDTO[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity[]> {
|
): Promise<TEntity[]> {
|
||||||
return await (this.productRepository_ as ProductRepository).update(
|
return (await (this.productRepository_ as ProductRepository).update(
|
||||||
data as WithRequiredProperty<
|
data as WithRequiredProperty<
|
||||||
ProductServiceTypes.UpdateProductDTO,
|
ProductServiceTypes.UpdateProductDTO,
|
||||||
"id"
|
"id"
|
||||||
@@ -148,7 +151,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
{
|
{
|
||||||
transactionManager: sharedContext.transactionManager,
|
transactionManager: sharedContext.transactionManager,
|
||||||
}
|
}
|
||||||
) as TEntity[]
|
)) as TEntity[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||||
@@ -165,7 +168,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
async softDelete(
|
async softDelete(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity[]> {
|
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||||
return await this.productRepository_.softDelete(productIds, {
|
return await this.productRepository_.softDelete(productIds, {
|
||||||
transactionManager: sharedContext.transactionManager,
|
transactionManager: sharedContext.transactionManager,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,18 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
|
|||||||
|
|
||||||
delete(ids: string[], context?: Context): Promise<void>
|
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[]>
|
restore(ids: string[], context?: Context): Promise<T[]>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import {
|
import {
|
||||||
|
CreateProductCategoryDTO,
|
||||||
|
CreateProductCollectionDTO,
|
||||||
CreateProductDTO,
|
CreateProductDTO,
|
||||||
|
CreateProductOptionDTO,
|
||||||
CreateProductTagDTO,
|
CreateProductTagDTO,
|
||||||
CreateProductTypeDTO,
|
CreateProductTypeDTO,
|
||||||
CreateProductOptionDTO,
|
|
||||||
CreateProductCategoryDTO,
|
|
||||||
UpdateProductTagDTO,
|
|
||||||
UpdateProductTypeDTO,
|
|
||||||
UpdateProductOptionDTO,
|
|
||||||
UpdateProductCategoryDTO,
|
|
||||||
UpdateProductDTO,
|
|
||||||
FilterableProductCategoryProps,
|
FilterableProductCategoryProps,
|
||||||
FilterableProductCollectionProps,
|
FilterableProductCollectionProps,
|
||||||
|
FilterableProductOptionProps,
|
||||||
FilterableProductProps,
|
FilterableProductProps,
|
||||||
FilterableProductTagProps,
|
FilterableProductTagProps,
|
||||||
FilterableProductTypeProps,
|
FilterableProductTypeProps,
|
||||||
FilterableProductOptionProps,
|
|
||||||
FilterableProductVariantProps,
|
FilterableProductVariantProps,
|
||||||
ProductCategoryDTO,
|
ProductCategoryDTO,
|
||||||
ProductCollectionDTO,
|
ProductCollectionDTO,
|
||||||
ProductDTO,
|
ProductDTO,
|
||||||
|
ProductOptionDTO,
|
||||||
ProductTagDTO,
|
ProductTagDTO,
|
||||||
ProductTypeDTO,
|
ProductTypeDTO,
|
||||||
ProductOptionDTO,
|
|
||||||
ProductVariantDTO,
|
ProductVariantDTO,
|
||||||
CreateProductCollectionDTO,
|
UpdateProductCategoryDTO,
|
||||||
UpdateProductCollectionDTO,
|
UpdateProductCollectionDTO,
|
||||||
|
UpdateProductDTO,
|
||||||
|
UpdateProductOptionDTO,
|
||||||
|
UpdateProductTagDTO,
|
||||||
|
UpdateProductTypeDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
|
|
||||||
import { Context } from "../shared-context"
|
import { Context } from "../shared-context"
|
||||||
@@ -72,18 +72,15 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
createTags(
|
createTags(
|
||||||
data: CreateProductTagDTO[],
|
data: CreateProductTagDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductTagDTO[]>
|
): Promise<ProductTagDTO[]>
|
||||||
|
|
||||||
updateTags(
|
updateTags(
|
||||||
data: UpdateProductTagDTO[],
|
data: UpdateProductTagDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductTagDTO[]>
|
): Promise<ProductTagDTO[]>
|
||||||
|
|
||||||
deleteTags(
|
deleteTags(productTagIds: string[], sharedContext?: Context): Promise<void>
|
||||||
productTagIds: string[],
|
|
||||||
sharedContext?: Context,
|
|
||||||
): Promise<void>
|
|
||||||
|
|
||||||
retrieveType(
|
retrieveType(
|
||||||
typeId: string,
|
typeId: string,
|
||||||
@@ -105,18 +102,15 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
createTypes(
|
createTypes(
|
||||||
data: CreateProductTypeDTO[],
|
data: CreateProductTypeDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductTypeDTO[]>
|
): Promise<ProductTypeDTO[]>
|
||||||
|
|
||||||
updateTypes(
|
updateTypes(
|
||||||
data: UpdateProductTypeDTO[],
|
data: UpdateProductTypeDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductTypeDTO[]>
|
): Promise<ProductTypeDTO[]>
|
||||||
|
|
||||||
deleteTypes(
|
deleteTypes(productTypeIds: string[], sharedContext?: Context): Promise<void>
|
||||||
productTypeIds: string[],
|
|
||||||
sharedContext?: Context,
|
|
||||||
): Promise<void>
|
|
||||||
|
|
||||||
retrieveOption(
|
retrieveOption(
|
||||||
optionId: string,
|
optionId: string,
|
||||||
@@ -138,17 +132,17 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
createOptions(
|
createOptions(
|
||||||
data: CreateProductOptionDTO[],
|
data: CreateProductOptionDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductOptionDTO[]>
|
): Promise<ProductOptionDTO[]>
|
||||||
|
|
||||||
updateOptions(
|
updateOptions(
|
||||||
data: UpdateProductOptionDTO[],
|
data: UpdateProductOptionDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductOptionDTO[]>
|
): Promise<ProductOptionDTO[]>
|
||||||
|
|
||||||
deleteOptions(
|
deleteOptions(
|
||||||
productOptionIds: string[],
|
productOptionIds: string[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
retrieveVariant(
|
retrieveVariant(
|
||||||
@@ -189,17 +183,17 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
createCollections(
|
createCollections(
|
||||||
data: CreateProductCollectionDTO[],
|
data: CreateProductCollectionDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductCollectionDTO[]>
|
): Promise<ProductCollectionDTO[]>
|
||||||
|
|
||||||
updateCollections(
|
updateCollections(
|
||||||
data: UpdateProductCollectionDTO[],
|
data: UpdateProductCollectionDTO[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductCollectionDTO[]>
|
): Promise<ProductCollectionDTO[]>
|
||||||
|
|
||||||
deleteCollections(
|
deleteCollections(
|
||||||
productCollectionIds: string[],
|
productCollectionIds: string[],
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
retrieveCategory(
|
retrieveCategory(
|
||||||
@@ -222,19 +216,16 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
createCategory(
|
createCategory(
|
||||||
data: CreateProductCategoryDTO,
|
data: CreateProductCategoryDTO,
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductCategoryDTO>
|
): Promise<ProductCategoryDTO>
|
||||||
|
|
||||||
updateCategory(
|
updateCategory(
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
data: UpdateProductCategoryDTO,
|
data: UpdateProductCategoryDTO,
|
||||||
sharedContext?: Context,
|
sharedContext?: Context
|
||||||
): Promise<ProductCategoryDTO>
|
): Promise<ProductCategoryDTO>
|
||||||
|
|
||||||
deleteCategory(
|
deleteCategory(categoryId: string, sharedContext?: Context): Promise<void>
|
||||||
categoryId: string,
|
|
||||||
sharedContext?: Context,
|
|
||||||
): Promise<void>
|
|
||||||
|
|
||||||
create(
|
create(
|
||||||
data: CreateProductDTO[],
|
data: CreateProductDTO[],
|
||||||
@@ -248,10 +239,11 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
delete(productIds: string[], sharedContext?: Context): Promise<void>
|
delete(productIds: string[], sharedContext?: Context): Promise<void>
|
||||||
|
|
||||||
softDelete(
|
softDelete<TReturnableLinkableKeys extends string = string>(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
|
config?: { returnLinkableKeys?: TReturnableLinkableKeys[] },
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<ProductDTO[]>
|
): Promise<Record<string, string[]> | void>
|
||||||
|
|
||||||
restore(productIds: string[], sharedContext?: Context): Promise<ProductDTO[]>
|
restore(productIds: string[], sharedContext?: Context): Promise<ProductDTO[]>
|
||||||
}
|
}
|
||||||
@@ -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-kebab-case"
|
||||||
export * from "./to-pascal-case"
|
export * from "./to-pascal-case"
|
||||||
export * from "./wrap-handler"
|
export * from "./wrap-handler"
|
||||||
|
export * from "./map-object-to"
|
||||||
@@ -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 "./mikro-orm/mikro-orm-repository"
|
||||||
export * from "./repository"
|
export * from "./repository"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
|
export * from "./mikro-orm/utils"
|
||||||
export * from "./mikro-orm/mikro-orm-create-connection"
|
export * from "./mikro-orm/mikro-orm-create-connection"
|
||||||
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
|
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 { MedusaContext } from "../../decorators"
|
||||||
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
||||||
import {
|
import {
|
||||||
mikroOrmSerializer,
|
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||||
mikroOrmUpdateDeletedAtRecursively,
|
|
||||||
transactionWrapper,
|
transactionWrapper,
|
||||||
} from "../utils"
|
} from "../utils"
|
||||||
|
import { mikroOrmSerializer, mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
||||||
|
|
||||||
class MikroOrmBase<T = any> {
|
class MikroOrmBase<T = any> {
|
||||||
protected readonly manager_: any
|
protected readonly manager_: any
|
||||||
@@ -71,13 +71,17 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
|
|||||||
ids: string[],
|
ids: string[],
|
||||||
@MedusaContext()
|
@MedusaContext()
|
||||||
{ transactionManager: manager }: Context = {}
|
{ transactionManager: manager }: Context = {}
|
||||||
): Promise<T[]> {
|
): Promise<[T[], Record<string, unknown[]>]> {
|
||||||
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
|
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
||||||
|
|
||||||
return entities
|
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||||
|
entities,
|
||||||
|
})
|
||||||
|
|
||||||
|
return [entities, softDeletedEntitiesMap]
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectTransactionManager()
|
@InjectTransactionManager()
|
||||||
|
|||||||
@@ -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 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[]>
|
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>(
|
export async function transactionWrapper<TManager = unknown>(
|
||||||
this: any,
|
this: any,
|
||||||
@@ -33,54 +33,52 @@ export async function transactionWrapper<TManager = unknown>(
|
|||||||
return await transactionMethod.bind(this.manager_)(task, options)
|
return await transactionMethod.bind(this.manager_)(task, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
/**
|
||||||
T extends object = any
|
* 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
|
||||||
manager: any,
|
* an object where the keys are the entities name and the values are the entities
|
||||||
entities: T[],
|
* that were soft deleted.
|
||||||
value: Date | null
|
*
|
||||||
) => {
|
* @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) {
|
for (const entity of entities) {
|
||||||
if (!("deleted_at" in entity)) continue
|
const entityName = getEntityName(entity)
|
||||||
;(entity as any).deleted_at = value
|
const shouldSkip = !!deletedEntitiesMap
|
||||||
|
.get(entityName)
|
||||||
|
?.some((e) => e.id === entity.id)
|
||||||
|
|
||||||
const relations = manager
|
if (!entity.deleted_at || shouldSkip) {
|
||||||
.getDriver()
|
continue
|
||||||
.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({
|
const values = deletedEntitiesMap.get(entityName) ?? []
|
||||||
filters: {
|
values.push(entity)
|
||||||
[SoftDeletableFilterKey]: {
|
deletedEntitiesMap.set(entityName, values)
|
||||||
withDeleted: true,
|
|
||||||
},
|
Object.values(entity).forEach((propValue: any) => {
|
||||||
},
|
if (propValue != null && isObject(propValue[0])) {
|
||||||
|
getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||||
|
entities: propValue,
|
||||||
|
deletedEntitiesMap,
|
||||||
|
getEntityName,
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await manager.persist(entity)
|
return Object.fromEntries(deletedEntitiesMap)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user