fix(medusa): Bulk create variant + pass transaction to the inventory service context methods (#3835)
* fix(medusa): Bulk create variant and pass transaction where needed * Create fair-penguins-stare.md * fix unit tests * event * transaction orchestration * revert options * Prevent isolated module to use the given transaction if any in the exposed service * Use enum * remove changeset to re do it * Create thick-ants-tickle.md * update event bus local * remove changeset to re do it * Create thick-kings-wonder.md * remove changeset to re do it * Create slimy-bees-eat.md * Update packages/utils/src/event-bus/index.ts Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --------- Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
366b12fcea
commit
af710f1b48
10
.changeset/slimy-bees-eat.md
Normal file
10
.changeset/slimy-bees-eat.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
"@medusajs/event-bus-local": patch
|
||||
"@medusajs/inventory": patch
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/stock-location": patch
|
||||
"@medusajs/types": patch
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
fix(medusa): Bulk create variant and pass transaction to the inventory service context methods
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Logger, MedusaContainer } from "@medusajs/modules-sdk"
|
||||
import { EmitData, Subscriber } from "@medusajs/types"
|
||||
import { EmitData, EventBusTypes, Subscriber } from "@medusajs/types"
|
||||
import { AbstractEventBusModuleService } from "@medusajs/utils"
|
||||
import { EventEmitter } from "events"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
type InjectedDependencies = {
|
||||
logger: Logger
|
||||
@@ -65,6 +66,8 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
||||
}
|
||||
|
||||
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||
const randId = ulid()
|
||||
this.storeSubscribers({ event, subscriberId: randId, subscriber })
|
||||
this.eventEmitter_.on(event, async (...args) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
@@ -78,7 +81,23 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
||||
return this
|
||||
}
|
||||
|
||||
unsubscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||
unsubscribe(
|
||||
event: string | symbol,
|
||||
subscriber: Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
const existingSubscribers = this.retrieveSubscribers(event)
|
||||
|
||||
if (existingSubscribers?.length) {
|
||||
const subIndex = existingSubscribers?.findIndex(
|
||||
(sub) => sub.id === context?.subscriberId
|
||||
)
|
||||
|
||||
if (subIndex !== -1) {
|
||||
this.eventToSubscribersMap_.get(event)?.splice(subIndex as number, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.eventEmitter_.off(event, subscriber)
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IInventoryService,
|
||||
InventoryItemDTO,
|
||||
InventoryLevelDTO,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
ReservationItemDTO,
|
||||
SharedContext,
|
||||
UpdateInventoryLevelInput,
|
||||
@@ -46,7 +47,7 @@ export default class InventoryService implements IInventoryService {
|
||||
reservationItemService,
|
||||
}: InjectedDependencies,
|
||||
options?: unknown,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
protected readonly moduleDeclaration?: InternalModuleDeclaration
|
||||
) {
|
||||
this.manager_ = manager
|
||||
this.inventoryItemService_ = inventoryItemService
|
||||
@@ -60,7 +61,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param config - the find configuration to use
|
||||
* @return A tuple of inventory items and their total count
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async listInventoryItems(
|
||||
selector: FilterableInventoryItemProps,
|
||||
config: FindConfig<InventoryItemDTO> = { relations: [], skip: 0, take: 10 },
|
||||
@@ -79,7 +83,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param config - the find configuration to use
|
||||
* @return A tuple of inventory levels and their total count
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async listInventoryLevels(
|
||||
selector: FilterableInventoryLevelProps,
|
||||
config: FindConfig<InventoryLevelDTO> = {
|
||||
@@ -102,7 +109,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param config - the find configuration to use
|
||||
* @return A tuple of reservation items and their total count
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async listReservationItems(
|
||||
selector: FilterableReservationItemProps,
|
||||
config: FindConfig<ReservationItemDTO> = {
|
||||
@@ -125,7 +135,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param config - the find configuration to use
|
||||
* @return The retrieved inventory item
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieveInventoryItem(
|
||||
inventoryItemId: string,
|
||||
config?: FindConfig<InventoryItemDTO>,
|
||||
@@ -145,7 +158,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param locationId - the id of the location
|
||||
* @return the retrieved inventory level
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieveInventoryLevel(
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
@@ -170,7 +186,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param inventoryItemId - the id of the reservation item
|
||||
* @return the retrieved reservation level
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieveReservationItem(
|
||||
reservationId: string,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -187,7 +206,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param input - the input object
|
||||
* @return The created reservation item
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async createReservationItem(
|
||||
input: CreateReservationItemInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -222,7 +244,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param input - the input object
|
||||
* @return The created inventory item
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async createInventoryItem(
|
||||
input: CreateInventoryItemInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -239,7 +264,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param input - the input object
|
||||
* @return The created inventory level
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async createInventoryLevel(
|
||||
input: CreateInventoryLevelInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -253,7 +281,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param input - the input object
|
||||
* @return The updated inventory item
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async updateInventoryItem(
|
||||
inventoryItemId: string,
|
||||
input: Partial<CreateInventoryItemInput>,
|
||||
@@ -271,7 +302,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* Deletes an inventory item
|
||||
* @param inventoryItemId - the id of the inventory item to delete
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteInventoryItem(
|
||||
inventoryItemId: string,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -284,7 +318,10 @@ export default class InventoryService implements IInventoryService {
|
||||
return await this.inventoryItemService_.delete(inventoryItemId, context)
|
||||
}
|
||||
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteInventoryItemLevelByLocationId(
|
||||
locationId: string,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -295,7 +332,10 @@ export default class InventoryService implements IInventoryService {
|
||||
)
|
||||
}
|
||||
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteReservationItemByLocationId(
|
||||
locationId: string,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -311,7 +351,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param inventoryItemId - the id of the inventory item associated with the level
|
||||
* @param locationId - the id of the location associated with the level
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteInventoryLevel(
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
@@ -337,7 +380,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param input - the input object
|
||||
* @return The updated inventory level
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async updateInventoryLevel(
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
@@ -370,7 +416,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param input - the input object
|
||||
* @return The updated inventory level
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async updateReservationItem(
|
||||
reservationItemId: string,
|
||||
input: UpdateReservationItemInput,
|
||||
@@ -387,7 +436,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* Deletes reservation items by line item
|
||||
* @param lineItemId - the id of the line item associated with the reservation item
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteReservationItemsByLineItem(
|
||||
lineItemId: string,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -402,7 +454,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* Deletes a reservation item
|
||||
* @param reservationItemId - the id of the reservation item to delete
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async deleteReservationItem(
|
||||
reservationItemId: string | string[],
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -418,7 +473,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @return The updated inventory level
|
||||
* @throws when the inventory level is not found
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async adjustInventory(
|
||||
inventoryItemId: string,
|
||||
locationId: string,
|
||||
@@ -455,7 +513,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @return The available quantity
|
||||
* @throws when the inventory item is not found
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieveAvailableQuantity(
|
||||
inventoryItemId: string,
|
||||
locationIds: string[],
|
||||
@@ -491,7 +552,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @return The stocked quantity
|
||||
* @throws when the inventory item is not found
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieveStockedQuantity(
|
||||
inventoryItemId: string,
|
||||
locationIds: string[],
|
||||
@@ -527,7 +591,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @return The reserved quantity
|
||||
* @throws when the inventory item is not found
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieveReservedQuantity(
|
||||
inventoryItemId: string,
|
||||
locationIds: string[],
|
||||
@@ -563,7 +630,10 @@ export default class InventoryService implements IInventoryService {
|
||||
* @param quantity - the quantity to check
|
||||
* @return Whether there is sufficient inventory
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async confirmInventory(
|
||||
inventoryItemId: string,
|
||||
locationIds: string[],
|
||||
|
||||
@@ -243,7 +243,7 @@ export default class ReservationItemService {
|
||||
)
|
||||
|
||||
const ops: Promise<unknown>[] = [
|
||||
itemRepository.softDelete({ line_item_id: lineItemId })
|
||||
itemRepository.softDelete({ line_item_id: lineItemId }),
|
||||
]
|
||||
for (const item of items) {
|
||||
ops.push(
|
||||
|
||||
@@ -55,23 +55,25 @@ describe("POST /admin/products", () => {
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
|
||||
IdMap.getId("productWithOptions"),
|
||||
{
|
||||
title: "Test",
|
||||
variant_rank: 0,
|
||||
prices: [
|
||||
{
|
||||
currency_code: "USD",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("option1"),
|
||||
value: "100",
|
||||
},
|
||||
],
|
||||
inventory_quantity: 0,
|
||||
}
|
||||
[
|
||||
{
|
||||
title: "Test",
|
||||
variant_rank: 0,
|
||||
prices: [
|
||||
{
|
||||
currency_code: "USD",
|
||||
amount: 100,
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
option_id: IdMap.getId("option1"),
|
||||
value: "100",
|
||||
},
|
||||
],
|
||||
inventory_quantity: 0,
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,18 +37,20 @@ describe("POST /admin/products/:id/variants", () => {
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
|
||||
IdMap.getId("productWithOptions"),
|
||||
{
|
||||
inventory_quantity: 0,
|
||||
manage_inventory: true,
|
||||
title: "Test Product Variant",
|
||||
options: [],
|
||||
prices: [
|
||||
{
|
||||
currency_code: "DKK",
|
||||
amount: 1234,
|
||||
},
|
||||
],
|
||||
}
|
||||
[
|
||||
{
|
||||
inventory_quantity: 0,
|
||||
manage_inventory: true,
|
||||
title: "Test Product Variant",
|
||||
options: [],
|
||||
prices: [
|
||||
{
|
||||
currency_code: "DKK",
|
||||
amount: 1234,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ describe("POST /admin/products/:id", () => {
|
||||
|
||||
it("successfully updates variants and create new ones", async () => {
|
||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(2)
|
||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { FlagRouter } from "../../../../utils/flag-router"
|
||||
import { DistributedTransaction } from "../../../../utils/transaction"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import {
|
||||
createVariantTransaction,
|
||||
createVariantsTransaction,
|
||||
revertVariantTransaction,
|
||||
} from "./transaction/create-product-variant"
|
||||
|
||||
@@ -130,7 +130,7 @@ export default async (req, res) => {
|
||||
|
||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
const newProduct = await entityManager.transaction(async (manager) => {
|
||||
const product = await entityManager.transaction(async (manager) => {
|
||||
const { variants } = validated
|
||||
delete validated.variants
|
||||
|
||||
@@ -185,27 +185,25 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
variants.map(async (variant) => {
|
||||
const options =
|
||||
variant?.options?.map((option, index) => ({
|
||||
...option,
|
||||
option_id: optionIds[index],
|
||||
})) || []
|
||||
const variantsInputData = variants.map((variant) => {
|
||||
const options =
|
||||
variant?.options?.map((option, index) => ({
|
||||
...option,
|
||||
option_id: optionIds[index],
|
||||
})) || []
|
||||
|
||||
const input = {
|
||||
...variant,
|
||||
options,
|
||||
}
|
||||
return {
|
||||
...variant,
|
||||
options,
|
||||
} as CreateProductVariantInput
|
||||
})
|
||||
|
||||
const varTransation = await createVariantTransaction(
|
||||
transactionDependencies,
|
||||
newProduct.id,
|
||||
input as CreateProductVariantInput
|
||||
)
|
||||
allVariantTransactions.push(varTransation)
|
||||
})
|
||||
const varTransaction = await createVariantsTransaction(
|
||||
transactionDependencies,
|
||||
newProduct.id,
|
||||
variantsInputData
|
||||
)
|
||||
allVariantTransactions.push(varTransaction)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
@@ -220,15 +218,19 @@ export default async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
return newProduct
|
||||
})
|
||||
const rawProduct = await productService
|
||||
.withTransaction(manager)
|
||||
.retrieve(newProduct.id, {
|
||||
select: defaultAdminProductFields,
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
|
||||
const rawProduct = await productService.retrieve(newProduct.id, {
|
||||
select: defaultAdminProductFields,
|
||||
relations: defaultAdminProductRelations,
|
||||
})
|
||||
const [product] = await pricingService
|
||||
.withTransaction(manager)
|
||||
.setProductPrices([rawProduct])
|
||||
|
||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
||||
return product
|
||||
})
|
||||
|
||||
res.json({ product })
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
ProductVariantPricesCreateReq,
|
||||
} from "../../../../types/product-variant"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { createVariantTransaction } from "./transaction/create-product-variant"
|
||||
import { createVariantsTransaction } from "./transaction/create-product-variant"
|
||||
|
||||
/**
|
||||
* @oas [post] /admin/products/{id}/variants
|
||||
@@ -131,7 +131,7 @@ export default async (req, res) => {
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await createVariantTransaction(
|
||||
await createVariantsTransaction(
|
||||
{
|
||||
manager: transactionManager,
|
||||
inventoryService,
|
||||
@@ -139,7 +139,7 @@ export default async (req, res) => {
|
||||
productVariantService,
|
||||
},
|
||||
id,
|
||||
validated as CreateProductVariantInput
|
||||
[validated as CreateProductVariantInput]
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -18,26 +18,26 @@ import {
|
||||
} from "../../../../../utils/transaction"
|
||||
|
||||
enum actions {
|
||||
createVariant = "createVariant",
|
||||
createInventoryItem = "createInventoryItem",
|
||||
attachInventoryItem = "attachInventoryItem",
|
||||
createVariants = "createVariants",
|
||||
createInventoryItems = "createInventoryItems",
|
||||
attachInventoryItems = "attachInventoryItems",
|
||||
}
|
||||
|
||||
const simpleFlow: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: actions.createVariant,
|
||||
action: actions.createVariants,
|
||||
},
|
||||
}
|
||||
|
||||
const flowWithInventory: TransactionStepsDefinition = {
|
||||
next: {
|
||||
action: actions.createVariant,
|
||||
action: actions.createVariants,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: actions.createInventoryItem,
|
||||
action: actions.createInventoryItems,
|
||||
saveResponse: true,
|
||||
next: {
|
||||
action: actions.attachInventoryItem,
|
||||
action: actions.attachInventoryItems,
|
||||
noCompensation: true,
|
||||
},
|
||||
},
|
||||
@@ -61,10 +61,10 @@ type InjectedDependencies = {
|
||||
inventoryService?: IInventoryService
|
||||
}
|
||||
|
||||
export const createVariantTransaction = async (
|
||||
export const createVariantsTransaction = async (
|
||||
dependencies: InjectedDependencies,
|
||||
productId: string,
|
||||
input: CreateProductVariantInput
|
||||
input: CreateProductVariantInput[]
|
||||
): Promise<DistributedTransaction> => {
|
||||
const {
|
||||
manager,
|
||||
@@ -78,51 +78,78 @@ export const createVariantTransaction = async (
|
||||
|
||||
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
||||
|
||||
async function createVariant(variantInput: CreateProductVariantInput) {
|
||||
async function createVariants(variantInput: CreateProductVariantInput[]) {
|
||||
return await productVariantServiceTx.create(productId, variantInput)
|
||||
}
|
||||
|
||||
async function removeVariant(variant: ProductVariant) {
|
||||
if (variant) {
|
||||
await productVariantServiceTx.delete(variant.id)
|
||||
async function removeVariants(variants: ProductVariant[]) {
|
||||
if (variants.length) {
|
||||
await productVariantServiceTx.delete(variants.map((v) => v.id))
|
||||
}
|
||||
}
|
||||
|
||||
async function createInventoryItem(variant: ProductVariant) {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
async function createInventoryItems(variants: ProductVariant[] = []) {
|
||||
const context = { transactionManager: manager }
|
||||
|
||||
return await inventoryService!.createInventoryItem({
|
||||
sku: variant.sku,
|
||||
origin_country: variant.origin_country,
|
||||
hs_code: variant.hs_code,
|
||||
mid_code: variant.mid_code,
|
||||
material: variant.material,
|
||||
weight: variant.weight,
|
||||
length: variant.length,
|
||||
height: variant.height,
|
||||
width: variant.width,
|
||||
})
|
||||
return await Promise.all(
|
||||
variants.map(async (variant) => {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
|
||||
const inventoryItem = await inventoryService!.createInventoryItem(
|
||||
{
|
||||
sku: variant.sku,
|
||||
origin_country: variant.origin_country,
|
||||
hs_code: variant.hs_code,
|
||||
mid_code: variant.mid_code,
|
||||
material: variant.material,
|
||||
weight: variant.weight,
|
||||
length: variant.length,
|
||||
height: variant.height,
|
||||
width: variant.width,
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
return { variant, inventoryItem }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
|
||||
if (inventoryItem) {
|
||||
await inventoryService!.deleteInventoryItem(inventoryItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function attachInventoryItem(
|
||||
variant: ProductVariant,
|
||||
inventoryItem: InventoryItemDTO
|
||||
async function removeInventoryItems(
|
||||
data: {
|
||||
variant: ProductVariant
|
||||
inventoryItem: InventoryItemDTO
|
||||
}[] = []
|
||||
) {
|
||||
if (!variant.manage_inventory) {
|
||||
return
|
||||
}
|
||||
const context = { transactionManager: manager }
|
||||
|
||||
await productVariantInventoryServiceTx.attachInventoryItem(
|
||||
variant.id,
|
||||
inventoryItem.id
|
||||
return await Promise.all(
|
||||
data.map(async ({ inventoryItem }) => {
|
||||
return await inventoryService!.deleteInventoryItem(
|
||||
inventoryItem.id,
|
||||
context
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async function attachInventoryItems(
|
||||
data: {
|
||||
variant: ProductVariant
|
||||
inventoryItem: InventoryItemDTO
|
||||
}[]
|
||||
) {
|
||||
return await Promise.all(
|
||||
data
|
||||
.filter((d) => d)
|
||||
.map(async ({ variant, inventoryItem }) => {
|
||||
return productVariantInventoryServiceTx.attachInventoryItem(
|
||||
variant.id,
|
||||
inventoryItem.id
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,46 +159,49 @@ export const createVariantTransaction = async (
|
||||
payload: TransactionPayload
|
||||
) {
|
||||
const command = {
|
||||
[actions.createVariant]: {
|
||||
[actions.createVariants]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput
|
||||
data: CreateProductVariantInput[]
|
||||
) => {
|
||||
return await createVariant(data)
|
||||
return await createVariants(data)
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
data: CreateProductVariantInput[],
|
||||
{ invoke }
|
||||
) => {
|
||||
await removeVariant(invoke[actions.createVariant])
|
||||
await removeVariants(invoke[actions.createVariants])
|
||||
},
|
||||
},
|
||||
[actions.createInventoryItem]: {
|
||||
[actions.createInventoryItems]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
data: CreateProductVariantInput[],
|
||||
{ invoke }
|
||||
) => {
|
||||
const { [actions.createVariant]: variant } = invoke
|
||||
const { [actions.createVariants]: variants } = invoke
|
||||
|
||||
return await createInventoryItem(variant)
|
||||
return await createInventoryItems(variants)
|
||||
},
|
||||
[TransactionHandlerType.COMPENSATE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
data: {
|
||||
variant: ProductVariant
|
||||
inventoryItem: InventoryItemDTO
|
||||
}[],
|
||||
{ invoke }
|
||||
) => {
|
||||
await removeInventoryItem(invoke[actions.createInventoryItem])
|
||||
await removeInventoryItems(invoke[actions.createInventoryItems])
|
||||
},
|
||||
},
|
||||
[actions.attachInventoryItem]: {
|
||||
[actions.attachInventoryItems]: {
|
||||
[TransactionHandlerType.INVOKE]: async (
|
||||
data: CreateProductVariantInput,
|
||||
data: CreateProductVariantInput[],
|
||||
{ invoke }
|
||||
) => {
|
||||
const {
|
||||
[actions.createVariant]: variant,
|
||||
[actions.createInventoryItem]: inventoryItem,
|
||||
[actions.createVariants]: variants,
|
||||
[actions.createInventoryItems]: inventoryItemsResult,
|
||||
} = invoke
|
||||
|
||||
return await attachInventoryItem(variant, inventoryItem)
|
||||
return await attachInventoryItems(inventoryItemsResult)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators
|
||||
import { DistributedTransaction } from "../../../../utils/transaction"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import {
|
||||
createVariantTransaction,
|
||||
createVariantsTransaction,
|
||||
revertVariantTransaction,
|
||||
} from "./transaction/create-product-variant"
|
||||
|
||||
@@ -224,29 +224,25 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
if (variantsToCreate.length) {
|
||||
await Promise.all(
|
||||
variantsToCreate.map(async (data) => {
|
||||
try {
|
||||
const varTransaction = await createVariantTransaction(
|
||||
try {
|
||||
const varTransaction = await createVariantsTransaction(
|
||||
transactionDependencies,
|
||||
product.id,
|
||||
variantsToCreate as CreateProductVariantInput[]
|
||||
)
|
||||
allVariantTransactions.push(varTransaction)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
await revertVariantTransaction(
|
||||
transactionDependencies,
|
||||
product.id,
|
||||
data as CreateProductVariantInput
|
||||
)
|
||||
allVariantTransactions.push(varTransaction)
|
||||
} catch (e) {
|
||||
await Promise.all(
|
||||
allVariantTransactions.map(async (transaction) => {
|
||||
await revertVariantTransaction(
|
||||
transactionDependencies,
|
||||
transaction
|
||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||
})
|
||||
)
|
||||
transaction
|
||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||
})
|
||||
)
|
||||
|
||||
throw e
|
||||
}
|
||||
})
|
||||
)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class EventBusService
|
||||
protected readonly config_: ConfigModule
|
||||
protected readonly stagedJobService_: StagedJobService
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly eventBusModuleService_: EventBusUtils.AbstractEventBusModuleService
|
||||
protected readonly eventBusModuleService_: EventBusTypes.IEventBusModuleService
|
||||
|
||||
protected shouldEnqueuerRun: boolean
|
||||
protected enqueue_: Promise<void>
|
||||
@@ -137,7 +137,7 @@ export default class EventBusService
|
||||
): Promise<TResult | void> {
|
||||
const manager = this.activeManager_
|
||||
const isBulkEmit = !isString(eventNameOrData)
|
||||
const events = isBulkEmit
|
||||
let events: EventBusTypes.EmitData[] = isBulkEmit
|
||||
? eventNameOrData.map((event) => ({
|
||||
eventName: event.eventName,
|
||||
data: event.data,
|
||||
@@ -151,22 +151,32 @@ export default class EventBusService
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* We store events in the database when in an ongoing transaction.
|
||||
*
|
||||
* If we are in a long-running transaction, the ACID properties of a
|
||||
* transaction ensure, that events are kept invisible to the enqueuer
|
||||
* until the transaction has committed.
|
||||
*
|
||||
* This patterns also gives us at-least-once delivery of events, as events
|
||||
* are only removed from the database, if they are successfully delivered.
|
||||
*
|
||||
* In case of a failing transaction, jobs stored in the database are removed
|
||||
* as part of the rollback.
|
||||
*/
|
||||
const stagedJobs = await this.stagedJobService_
|
||||
.withTransaction(manager)
|
||||
.create(events)
|
||||
events = events.filter(
|
||||
(event) =>
|
||||
this.eventBusModuleService_?.retrieveSubscribers(event.eventName)
|
||||
?.length ?? false
|
||||
)
|
||||
|
||||
let stagedJobs: StagedJob[] = []
|
||||
if (events.length) {
|
||||
/**
|
||||
* We store events in the database when in an ongoing transaction.
|
||||
*
|
||||
* If we are in a long-running transaction, the ACID properties of a
|
||||
* transaction ensure, that events are kept invisible to the enqueuer
|
||||
* until the transaction has committed.
|
||||
*
|
||||
* This patterns also gives us at-least-once delivery of events, as events
|
||||
* are only removed from the database, if they are successfully delivered.
|
||||
*
|
||||
* In case of a failing transaction, jobs stored in the database are removed
|
||||
* as part of the rollback.
|
||||
*/
|
||||
|
||||
stagedJobs = await this.stagedJobService_
|
||||
.withTransaction(manager)
|
||||
.create(events)
|
||||
}
|
||||
|
||||
return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult
|
||||
}
|
||||
|
||||
@@ -162,20 +162,26 @@ class ProductVariantService extends TransactionBaseService {
|
||||
* Creates an unpublished product variant. Will validate against parent product
|
||||
* to ensure that the variant can in fact be created.
|
||||
* @param productOrProductId - the product the variant will be added to
|
||||
* @param variant - the variant to create
|
||||
* @param variants
|
||||
* @return resolves to the creation result.
|
||||
*/
|
||||
async create(
|
||||
async create<
|
||||
TVariants extends CreateProductVariantInput | CreateProductVariantInput[],
|
||||
TOutput = TVariants extends CreateProductVariantInput[]
|
||||
? CreateProductVariantInput[]
|
||||
: CreateProductVariantInput
|
||||
>(
|
||||
productOrProductId: string | Product,
|
||||
variant: CreateProductVariantInput
|
||||
): Promise<ProductVariant> {
|
||||
variants: CreateProductVariantInput | CreateProductVariantInput[]
|
||||
): Promise<TOutput> {
|
||||
const isVariantsArray = Array.isArray(variants)
|
||||
const variants_ = isVariantsArray ? variants : [variants]
|
||||
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const productRepo = manager.withRepository(this.productRepository_)
|
||||
const variantRepo = manager.withRepository(this.productVariantRepository_)
|
||||
|
||||
const { prices, ...rest } = variant
|
||||
|
||||
let product = productOrProductId
|
||||
let product = productOrProductId as Product
|
||||
|
||||
if (isString(product)) {
|
||||
product = (await productRepo.findOne({
|
||||
@@ -195,69 +201,60 @@ class ProductVariantService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
if (product.options.length !== variant.options.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Product options length does not match variant options length. Product has ${product.options.length} and variant has ${variant.options.length}.`
|
||||
)
|
||||
}
|
||||
this.validateVariantsToCreate_(product, variants_)
|
||||
|
||||
product.options.forEach((option) => {
|
||||
if (!variant.options.find((vo) => option.id === vo.option_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variant options do not contain value for ${option.title}`
|
||||
)
|
||||
}
|
||||
})
|
||||
let computedRank = product.variants.length
|
||||
const variantPricesToUpdate: {
|
||||
id: string
|
||||
prices: ProductVariantPrice[]
|
||||
}[] = []
|
||||
|
||||
const variantExists = product.variants.find((v) => {
|
||||
return v.options.every((option) => {
|
||||
const variantOption = variant.options.find(
|
||||
(o) => option.option_id === o.option_id
|
||||
)
|
||||
const results = await Promise.all(
|
||||
variants_.map(async (variant) => {
|
||||
const { prices, ...rest } = variant
|
||||
|
||||
return option.value === variantOption?.value
|
||||
if (!rest.variant_rank) {
|
||||
rest.variant_rank = computedRank
|
||||
}
|
||||
++computedRank
|
||||
|
||||
const toCreate = {
|
||||
...rest,
|
||||
product_id: product.id,
|
||||
}
|
||||
|
||||
const productVariant = variantRepo.create(toCreate)
|
||||
|
||||
const result = await variantRepo.save(productVariant)
|
||||
|
||||
if (prices?.length) {
|
||||
variantPricesToUpdate.push({ id: result.id, prices })
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
if (variantExists) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
`Variant with title ${variantExists.title} with provided options already exists`
|
||||
if (variantPricesToUpdate.length) {
|
||||
await this.updateVariantPrices(
|
||||
variantPricesToUpdate.map((v) => ({
|
||||
variantId: v.id,
|
||||
prices: v.prices,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
if (!rest.variant_rank) {
|
||||
rest.variant_rank = product.variants.length
|
||||
}
|
||||
|
||||
const toCreate = {
|
||||
...rest,
|
||||
product_id: product.id,
|
||||
}
|
||||
|
||||
const productVariant = variantRepo.create(toCreate)
|
||||
|
||||
const result = await variantRepo.save(productVariant)
|
||||
|
||||
if (prices) {
|
||||
await this.updateVariantPrices([
|
||||
{
|
||||
variantId: result.id,
|
||||
prices,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ProductVariantService.Events.CREATED, {
|
||||
const eventsToEmit = results.map((result) => ({
|
||||
eventName: ProductVariantService.Events.CREATED,
|
||||
data: {
|
||||
id: result.id,
|
||||
product_id: result.product_id,
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
return result
|
||||
await this.eventBus_.withTransaction(manager).emit(eventsToEmit)
|
||||
|
||||
return (isVariantsArray ? results : results[0]) as unknown as TOutput
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1104,6 +1101,46 @@ class ProductVariantService extends TransactionBaseService {
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
protected validateVariantsToCreate_(
|
||||
product: Product,
|
||||
variants: CreateProductVariantInput[]
|
||||
): void {
|
||||
for (const variant of variants) {
|
||||
if (product.options.length !== variant.options.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Product options length does not match variant options length. Product has ${product.options.length} and variant has ${variant.options.length}.`
|
||||
)
|
||||
}
|
||||
|
||||
product.options.forEach((option) => {
|
||||
if (!variant.options.find((vo) => option.id === vo.option_id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variant options do not contain value for ${option.title}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const variantExists = product.variants.find((v) => {
|
||||
return v.options.every((option) => {
|
||||
const variantOption = variant.options.find(
|
||||
(o) => option.option_id === o.option_id
|
||||
)
|
||||
|
||||
return option.value === variantOption?.value
|
||||
})
|
||||
})
|
||||
|
||||
if (variantExists) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
`Variant with title ${variantExists.title} with provided options already exists`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductVariantService
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
FilterableStockLocationProps,
|
||||
FindConfig,
|
||||
IEventBusService,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
SharedContext,
|
||||
StockLocationAddressInput,
|
||||
UpdateStockLocationInput,
|
||||
@@ -41,7 +42,7 @@ export default class StockLocationService {
|
||||
constructor(
|
||||
{ eventBusService, manager }: InjectedDependencies,
|
||||
options?: unknown,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
protected readonly moduleDeclaration?: InternalModuleDeclaration
|
||||
) {
|
||||
this.manager_ = manager
|
||||
this.eventBusService_ = eventBusService
|
||||
@@ -53,7 +54,10 @@ export default class StockLocationService {
|
||||
* @param config - Additional configuration for the query.
|
||||
* @return A list of stock locations.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async list(
|
||||
selector: FilterableStockLocationProps = {},
|
||||
config: FindConfig<StockLocation> = { relations: [], skip: 0, take: 10 },
|
||||
@@ -72,7 +76,10 @@ export default class StockLocationService {
|
||||
* @param config - Additional configuration for the query.
|
||||
* @return A list of stock locations and the count of matching stock locations.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async listAndCount(
|
||||
selector: FilterableStockLocationProps = {},
|
||||
config: FindConfig<StockLocation> = { relations: [], skip: 0, take: 10 },
|
||||
@@ -92,7 +99,10 @@ export default class StockLocationService {
|
||||
* @return The stock location.
|
||||
* @throws If the stock location ID is not definedor the stock location with the given ID was not found.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async retrieve(
|
||||
stockLocationId: string,
|
||||
config: FindConfig<StockLocation> = {},
|
||||
@@ -126,7 +136,10 @@ export default class StockLocationService {
|
||||
* @param data - The input data for creating a Stock Location.
|
||||
* @returns The created stock location.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async create(
|
||||
data: CreateStockLocationInput,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
@@ -170,7 +183,10 @@ export default class StockLocationService {
|
||||
* @param updateData - The update data for the stock location.
|
||||
* @returns The updated stock location.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async update(
|
||||
stockLocationId: string,
|
||||
updateData: UpdateStockLocationInput,
|
||||
@@ -216,7 +232,10 @@ export default class StockLocationService {
|
||||
* @param address - The update data for the address.
|
||||
* @returns The updated stock location address.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
protected async updateAddress(
|
||||
addressId: string,
|
||||
address: StockLocationAddressInput,
|
||||
@@ -257,7 +276,10 @@ export default class StockLocationService {
|
||||
* @param id - The ID of the stock location to delete.
|
||||
* @returns An empty promise.
|
||||
*/
|
||||
@InjectEntityManager()
|
||||
@InjectEntityManager(
|
||||
(target) =>
|
||||
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
)
|
||||
async delete(
|
||||
id: string,
|
||||
@MedusaContext() context: SharedContext = {}
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { EmitData, Subscriber, SubscriberContext } from "./common"
|
||||
import {
|
||||
EmitData,
|
||||
Subscriber,
|
||||
SubscriberContext,
|
||||
SubscriberDescriptor,
|
||||
} from "./common"
|
||||
|
||||
export interface IEventBusModuleService {
|
||||
retrieveSubscribers(
|
||||
event: string | symbol
|
||||
): SubscriberDescriptor[] | undefined
|
||||
|
||||
emit<T>(
|
||||
eventName: string,
|
||||
data: T,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { SharedContext } from "@medusajs/types"
|
||||
|
||||
export function InjectEntityManager(
|
||||
managerProperty = "manager_"
|
||||
shouldForceTransaction: (target: any) => boolean = () => false,
|
||||
managerProperty: string = "manager_"
|
||||
): MethodDecorator {
|
||||
return function (
|
||||
target: any,
|
||||
@@ -18,9 +19,10 @@ export function InjectEntityManager(
|
||||
|
||||
const argIndex = target.MedusaContextIndex_[propertyKey]
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const shouldForceTransactionRes = shouldForceTransaction(target)
|
||||
const context: SharedContext = args[argIndex] ?? {}
|
||||
|
||||
if (context?.transactionManager) {
|
||||
if (!shouldForceTransactionRes && context?.transactionManager) {
|
||||
return await originalMethod.apply(this, args)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,24 +23,15 @@ export abstract class AbstractEventBusModuleService
|
||||
): Promise<void>
|
||||
abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
|
||||
|
||||
public subscribe(
|
||||
eventName: string | symbol,
|
||||
subscriber: EventBusTypes.Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (typeof subscriber !== `function`) {
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
/**
|
||||
* If context is provided, we use the subscriberId from it
|
||||
* otherwise we generate a random using a ulid
|
||||
*/
|
||||
|
||||
const randId = ulid()
|
||||
const event = eventName.toString()
|
||||
|
||||
const subscriberId = context?.subscriberId ?? `${event}-${randId}`
|
||||
|
||||
protected storeSubscribers({
|
||||
event,
|
||||
subscriberId,
|
||||
subscriber,
|
||||
}: {
|
||||
event: string | symbol
|
||||
subscriberId: string
|
||||
subscriber: EventBusTypes.Subscriber
|
||||
}) {
|
||||
const newSubscriberDescriptor = { subscriber, id: subscriberId }
|
||||
|
||||
const existingSubscribers = this.eventToSubscribersMap_.get(event) ?? []
|
||||
@@ -57,6 +48,33 @@ export abstract class AbstractEventBusModuleService
|
||||
...existingSubscribers,
|
||||
newSubscriberDescriptor,
|
||||
])
|
||||
}
|
||||
|
||||
public retrieveSubscribers(event: string | symbol) {
|
||||
return this.eventToSubscribersMap_.get(event)
|
||||
}
|
||||
|
||||
public subscribe(
|
||||
eventName: string | symbol,
|
||||
subscriber: EventBusTypes.Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (typeof subscriber !== `function`) {
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
/**
|
||||
* If context is provided, we use the subscriberId from it
|
||||
* otherwise we generate a random using a ulid
|
||||
*/
|
||||
|
||||
const randId = ulid()
|
||||
const event = eventName.toString()
|
||||
|
||||
this.storeSubscribers({
|
||||
event,
|
||||
subscriberId: context?.subscriberId ?? `${event}-${randId}`,
|
||||
subscriber,
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -70,7 +88,7 @@ export abstract class AbstractEventBusModuleService
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
|
||||
const existingSubscribers = this.eventToSubscribersMap_.get(eventName)
|
||||
const existingSubscribers = this.retrieveSubscribers(eventName)
|
||||
|
||||
if (existingSubscribers?.length) {
|
||||
const subIndex = existingSubscribers?.findIndex(
|
||||
|
||||
Reference in New Issue
Block a user