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:
17 files changed
+410
-181
No files matched your search
@@ -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 { 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 { AbstractEventBusModuleService } from "@medusajs/utils"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
|
import { ulid } from "ulid"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
@@ -65,6 +66,8 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||||
|
const randId = ulid()
|
||||||
|
this.storeSubscribers({ event, subscriberId: randId, subscriber })
|
||||||
this.eventEmitter_.on(event, async (...args) => {
|
this.eventEmitter_.on(event, async (...args) => {
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -78,7 +81,23 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
|||||||
return this
|
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)
|
this.eventEmitter_.off(event, subscriber)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
IInventoryService,
|
IInventoryService,
|
||||||
InventoryItemDTO,
|
InventoryItemDTO,
|
||||||
InventoryLevelDTO,
|
InventoryLevelDTO,
|
||||||
|
MODULE_RESOURCE_TYPE,
|
||||||
ReservationItemDTO,
|
ReservationItemDTO,
|
||||||
SharedContext,
|
SharedContext,
|
||||||
UpdateInventoryLevelInput,
|
UpdateInventoryLevelInput,
|
||||||
@@ -46,7 +47,7 @@ export default class InventoryService implements IInventoryService {
|
|||||||
reservationItemService,
|
reservationItemService,
|
||||||
}: InjectedDependencies,
|
}: InjectedDependencies,
|
||||||
options?: unknown,
|
options?: unknown,
|
||||||
moduleDeclaration?: InternalModuleDeclaration
|
protected readonly moduleDeclaration?: InternalModuleDeclaration
|
||||||
) {
|
) {
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.inventoryItemService_ = inventoryItemService
|
this.inventoryItemService_ = inventoryItemService
|
||||||
@@ -60,7 +61,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param config - the find configuration to use
|
* @param config - the find configuration to use
|
||||||
* @return A tuple of inventory items and their total count
|
* @return A tuple of inventory items and their total count
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async listInventoryItems(
|
async listInventoryItems(
|
||||||
selector: FilterableInventoryItemProps,
|
selector: FilterableInventoryItemProps,
|
||||||
config: FindConfig<InventoryItemDTO> = { relations: [], skip: 0, take: 10 },
|
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
|
* @param config - the find configuration to use
|
||||||
* @return A tuple of inventory levels and their total count
|
* @return A tuple of inventory levels and their total count
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async listInventoryLevels(
|
async listInventoryLevels(
|
||||||
selector: FilterableInventoryLevelProps,
|
selector: FilterableInventoryLevelProps,
|
||||||
config: FindConfig<InventoryLevelDTO> = {
|
config: FindConfig<InventoryLevelDTO> = {
|
||||||
@@ -102,7 +109,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param config - the find configuration to use
|
* @param config - the find configuration to use
|
||||||
* @return A tuple of reservation items and their total count
|
* @return A tuple of reservation items and their total count
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async listReservationItems(
|
async listReservationItems(
|
||||||
selector: FilterableReservationItemProps,
|
selector: FilterableReservationItemProps,
|
||||||
config: FindConfig<ReservationItemDTO> = {
|
config: FindConfig<ReservationItemDTO> = {
|
||||||
@@ -125,7 +135,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param config - the find configuration to use
|
* @param config - the find configuration to use
|
||||||
* @return The retrieved inventory item
|
* @return The retrieved inventory item
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async retrieveInventoryItem(
|
async retrieveInventoryItem(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
config?: FindConfig<InventoryItemDTO>,
|
config?: FindConfig<InventoryItemDTO>,
|
||||||
@@ -145,7 +158,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param locationId - the id of the location
|
* @param locationId - the id of the location
|
||||||
* @return the retrieved inventory level
|
* @return the retrieved inventory level
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async retrieveInventoryLevel(
|
async retrieveInventoryLevel(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@@ -170,7 +186,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param inventoryItemId - the id of the reservation item
|
* @param inventoryItemId - the id of the reservation item
|
||||||
* @return the retrieved reservation level
|
* @return the retrieved reservation level
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async retrieveReservationItem(
|
async retrieveReservationItem(
|
||||||
reservationId: string,
|
reservationId: string,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -187,7 +206,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @return The created reservation item
|
* @return The created reservation item
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async createReservationItem(
|
async createReservationItem(
|
||||||
input: CreateReservationItemInput,
|
input: CreateReservationItemInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -222,7 +244,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @return The created inventory item
|
* @return The created inventory item
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async createInventoryItem(
|
async createInventoryItem(
|
||||||
input: CreateInventoryItemInput,
|
input: CreateInventoryItemInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -239,7 +264,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @return The created inventory level
|
* @return The created inventory level
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async createInventoryLevel(
|
async createInventoryLevel(
|
||||||
input: CreateInventoryLevelInput,
|
input: CreateInventoryLevelInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -253,7 +281,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @return The updated inventory item
|
* @return The updated inventory item
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async updateInventoryItem(
|
async updateInventoryItem(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
input: Partial<CreateInventoryItemInput>,
|
input: Partial<CreateInventoryItemInput>,
|
||||||
@@ -271,7 +302,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* Deletes an inventory item
|
* Deletes an inventory item
|
||||||
* @param inventoryItemId - the id of the inventory item to delete
|
* @param inventoryItemId - the id of the inventory item to delete
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async deleteInventoryItem(
|
async deleteInventoryItem(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -284,7 +318,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
return await this.inventoryItemService_.delete(inventoryItemId, context)
|
return await this.inventoryItemService_.delete(inventoryItemId, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async deleteInventoryItemLevelByLocationId(
|
async deleteInventoryItemLevelByLocationId(
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@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(
|
async deleteReservationItemByLocationId(
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@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 inventoryItemId - the id of the inventory item associated with the level
|
||||||
* @param locationId - the id of the location 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(
|
async deleteInventoryLevel(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@@ -337,7 +380,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @return The updated inventory level
|
* @return The updated inventory level
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async updateInventoryLevel(
|
async updateInventoryLevel(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@@ -370,7 +416,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param input - the input object
|
* @param input - the input object
|
||||||
* @return The updated inventory level
|
* @return The updated inventory level
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async updateReservationItem(
|
async updateReservationItem(
|
||||||
reservationItemId: string,
|
reservationItemId: string,
|
||||||
input: UpdateReservationItemInput,
|
input: UpdateReservationItemInput,
|
||||||
@@ -387,7 +436,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* Deletes reservation items by line item
|
* Deletes reservation items by line item
|
||||||
* @param lineItemId - the id of the line item associated with the reservation 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(
|
async deleteReservationItemsByLineItem(
|
||||||
lineItemId: string,
|
lineItemId: string,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -402,7 +454,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* Deletes a reservation item
|
* Deletes a reservation item
|
||||||
* @param reservationItemId - the id of the reservation item to delete
|
* @param reservationItemId - the id of the reservation item to delete
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async deleteReservationItem(
|
async deleteReservationItem(
|
||||||
reservationItemId: string | string[],
|
reservationItemId: string | string[],
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -418,7 +473,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @return The updated inventory level
|
* @return The updated inventory level
|
||||||
* @throws when the inventory level is not found
|
* @throws when the inventory level is not found
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async adjustInventory(
|
async adjustInventory(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
@@ -455,7 +513,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @return The available quantity
|
* @return The available quantity
|
||||||
* @throws when the inventory item is not found
|
* @throws when the inventory item is not found
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async retrieveAvailableQuantity(
|
async retrieveAvailableQuantity(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[],
|
locationIds: string[],
|
||||||
@@ -491,7 +552,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @return The stocked quantity
|
* @return The stocked quantity
|
||||||
* @throws when the inventory item is not found
|
* @throws when the inventory item is not found
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async retrieveStockedQuantity(
|
async retrieveStockedQuantity(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[],
|
locationIds: string[],
|
||||||
@@ -527,7 +591,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @return The reserved quantity
|
* @return The reserved quantity
|
||||||
* @throws when the inventory item is not found
|
* @throws when the inventory item is not found
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async retrieveReservedQuantity(
|
async retrieveReservedQuantity(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[],
|
locationIds: string[],
|
||||||
@@ -563,7 +630,10 @@ export default class InventoryService implements IInventoryService {
|
|||||||
* @param quantity - the quantity to check
|
* @param quantity - the quantity to check
|
||||||
* @return Whether there is sufficient inventory
|
* @return Whether there is sufficient inventory
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async confirmInventory(
|
async confirmInventory(
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationIds: string[],
|
locationIds: string[],
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export default class ReservationItemService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ops: Promise<unknown>[] = [
|
const ops: Promise<unknown>[] = [
|
||||||
itemRepository.softDelete({ line_item_id: lineItemId })
|
itemRepository.softDelete({ line_item_id: lineItemId }),
|
||||||
]
|
]
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
ops.push(
|
ops.push(
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe("POST /admin/products", () => {
|
|||||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
|
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
|
||||||
IdMap.getId("productWithOptions"),
|
IdMap.getId("productWithOptions"),
|
||||||
|
[
|
||||||
{
|
{
|
||||||
title: "Test",
|
title: "Test",
|
||||||
variant_rank: 0,
|
variant_rank: 0,
|
||||||
@@ -71,7 +72,8 @@ describe("POST /admin/products", () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
inventory_quantity: 0,
|
inventory_quantity: 0,
|
||||||
}
|
},
|
||||||
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ describe("POST /admin/products/:id/variants", () => {
|
|||||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
|
expect(ProductVariantServiceMock.create).toHaveBeenCalledWith(
|
||||||
IdMap.getId("productWithOptions"),
|
IdMap.getId("productWithOptions"),
|
||||||
|
[
|
||||||
{
|
{
|
||||||
inventory_quantity: 0,
|
inventory_quantity: 0,
|
||||||
manage_inventory: true,
|
manage_inventory: true,
|
||||||
@@ -48,7 +49,8 @@ describe("POST /admin/products/:id/variants", () => {
|
|||||||
amount: 1234,
|
amount: 1234,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("POST /admin/products/:id", () => {
|
|||||||
|
|
||||||
it("successfully updates variants and create new ones", async () => {
|
it("successfully updates variants and create new ones", async () => {
|
||||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
|
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 { DistributedTransaction } from "../../../../utils/transaction"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import {
|
import {
|
||||||
createVariantTransaction,
|
createVariantsTransaction,
|
||||||
revertVariantTransaction,
|
revertVariantTransaction,
|
||||||
} from "./transaction/create-product-variant"
|
} from "./transaction/create-product-variant"
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|
||||||
const newProduct = await entityManager.transaction(async (manager) => {
|
const product = await entityManager.transaction(async (manager) => {
|
||||||
const { variants } = validated
|
const { variants } = validated
|
||||||
delete validated.variants
|
delete validated.variants
|
||||||
|
|
||||||
@@ -185,27 +185,25 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
const variantsInputData = variants.map((variant) => {
|
||||||
variants.map(async (variant) => {
|
|
||||||
const options =
|
const options =
|
||||||
variant?.options?.map((option, index) => ({
|
variant?.options?.map((option, index) => ({
|
||||||
...option,
|
...option,
|
||||||
option_id: optionIds[index],
|
option_id: optionIds[index],
|
||||||
})) || []
|
})) || []
|
||||||
|
|
||||||
const input = {
|
return {
|
||||||
...variant,
|
...variant,
|
||||||
options,
|
options,
|
||||||
}
|
} as CreateProductVariantInput
|
||||||
|
})
|
||||||
|
|
||||||
const varTransation = await createVariantTransaction(
|
const varTransaction = await createVariantsTransaction(
|
||||||
transactionDependencies,
|
transactionDependencies,
|
||||||
newProduct.id,
|
newProduct.id,
|
||||||
input as CreateProductVariantInput
|
variantsInputData
|
||||||
)
|
|
||||||
allVariantTransactions.push(varTransation)
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
allVariantTransactions.push(varTransaction)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
allVariantTransactions.map(async (transaction) => {
|
allVariantTransactions.map(async (transaction) => {
|
||||||
@@ -220,15 +218,19 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newProduct
|
const rawProduct = await productService
|
||||||
})
|
.withTransaction(manager)
|
||||||
|
.retrieve(newProduct.id, {
|
||||||
const rawProduct = await productService.retrieve(newProduct.id, {
|
|
||||||
select: defaultAdminProductFields,
|
select: defaultAdminProductFields,
|
||||||
relations: defaultAdminProductRelations,
|
relations: defaultAdminProductRelations,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [product] = await pricingService.setProductPrices([rawProduct])
|
const [product] = await pricingService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.setProductPrices([rawProduct])
|
||||||
|
|
||||||
|
return product
|
||||||
|
})
|
||||||
|
|
||||||
res.json({ product })
|
res.json({ product })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
ProductVariantPricesCreateReq,
|
ProductVariantPricesCreateReq,
|
||||||
} from "../../../../types/product-variant"
|
} from "../../../../types/product-variant"
|
||||||
import { validator } from "../../../../utils/validator"
|
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
|
* @oas [post] /admin/products/{id}/variants
|
||||||
@@ -131,7 +131,7 @@ export default async (req, res) => {
|
|||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|
||||||
await manager.transaction(async (transactionManager) => {
|
await manager.transaction(async (transactionManager) => {
|
||||||
await createVariantTransaction(
|
await createVariantsTransaction(
|
||||||
{
|
{
|
||||||
manager: transactionManager,
|
manager: transactionManager,
|
||||||
inventoryService,
|
inventoryService,
|
||||||
@@ -139,7 +139,7 @@ export default async (req, res) => {
|
|||||||
productVariantService,
|
productVariantService,
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
validated as CreateProductVariantInput
|
[validated as CreateProductVariantInput]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+72
-42
@@ -18,26 +18,26 @@ import {
|
|||||||
} from "../../../../../utils/transaction"
|
} from "../../../../../utils/transaction"
|
||||||
|
|
||||||
enum actions {
|
enum actions {
|
||||||
createVariant = "createVariant",
|
createVariants = "createVariants",
|
||||||
createInventoryItem = "createInventoryItem",
|
createInventoryItems = "createInventoryItems",
|
||||||
attachInventoryItem = "attachInventoryItem",
|
attachInventoryItems = "attachInventoryItems",
|
||||||
}
|
}
|
||||||
|
|
||||||
const simpleFlow: TransactionStepsDefinition = {
|
const simpleFlow: TransactionStepsDefinition = {
|
||||||
next: {
|
next: {
|
||||||
action: actions.createVariant,
|
action: actions.createVariants,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const flowWithInventory: TransactionStepsDefinition = {
|
const flowWithInventory: TransactionStepsDefinition = {
|
||||||
next: {
|
next: {
|
||||||
action: actions.createVariant,
|
action: actions.createVariants,
|
||||||
saveResponse: true,
|
saveResponse: true,
|
||||||
next: {
|
next: {
|
||||||
action: actions.createInventoryItem,
|
action: actions.createInventoryItems,
|
||||||
saveResponse: true,
|
saveResponse: true,
|
||||||
next: {
|
next: {
|
||||||
action: actions.attachInventoryItem,
|
action: actions.attachInventoryItems,
|
||||||
noCompensation: true,
|
noCompensation: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -61,10 +61,10 @@ type InjectedDependencies = {
|
|||||||
inventoryService?: IInventoryService
|
inventoryService?: IInventoryService
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createVariantTransaction = async (
|
export const createVariantsTransaction = async (
|
||||||
dependencies: InjectedDependencies,
|
dependencies: InjectedDependencies,
|
||||||
productId: string,
|
productId: string,
|
||||||
input: CreateProductVariantInput
|
input: CreateProductVariantInput[]
|
||||||
): Promise<DistributedTransaction> => {
|
): Promise<DistributedTransaction> => {
|
||||||
const {
|
const {
|
||||||
manager,
|
manager,
|
||||||
@@ -78,22 +78,27 @@ export const createVariantTransaction = async (
|
|||||||
|
|
||||||
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
const productVariantServiceTx = productVariantService.withTransaction(manager)
|
||||||
|
|
||||||
async function createVariant(variantInput: CreateProductVariantInput) {
|
async function createVariants(variantInput: CreateProductVariantInput[]) {
|
||||||
return await productVariantServiceTx.create(productId, variantInput)
|
return await productVariantServiceTx.create(productId, variantInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeVariant(variant: ProductVariant) {
|
async function removeVariants(variants: ProductVariant[]) {
|
||||||
if (variant) {
|
if (variants.length) {
|
||||||
await productVariantServiceTx.delete(variant.id)
|
await productVariantServiceTx.delete(variants.map((v) => v.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createInventoryItem(variant: ProductVariant) {
|
async function createInventoryItems(variants: ProductVariant[] = []) {
|
||||||
|
const context = { transactionManager: manager }
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
variants.map(async (variant) => {
|
||||||
if (!variant.manage_inventory) {
|
if (!variant.manage_inventory) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await inventoryService!.createInventoryItem({
|
const inventoryItem = await inventoryService!.createInventoryItem(
|
||||||
|
{
|
||||||
sku: variant.sku,
|
sku: variant.sku,
|
||||||
origin_country: variant.origin_country,
|
origin_country: variant.origin_country,
|
||||||
hs_code: variant.hs_code,
|
hs_code: variant.hs_code,
|
||||||
@@ -103,27 +108,49 @@ export const createVariantTransaction = async (
|
|||||||
length: variant.length,
|
length: variant.length,
|
||||||
height: variant.height,
|
height: variant.height,
|
||||||
width: variant.width,
|
width: variant.width,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
return { variant, inventoryItem }
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeInventoryItem(inventoryItem: InventoryItemDTO) {
|
async function removeInventoryItems(
|
||||||
if (inventoryItem) {
|
data: {
|
||||||
await inventoryService!.deleteInventoryItem(inventoryItem.id)
|
variant: ProductVariant
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function attachInventoryItem(
|
|
||||||
variant: ProductVariant,
|
|
||||||
inventoryItem: InventoryItemDTO
|
inventoryItem: InventoryItemDTO
|
||||||
|
}[] = []
|
||||||
) {
|
) {
|
||||||
if (!variant.manage_inventory) {
|
const context = { transactionManager: manager }
|
||||||
return
|
|
||||||
|
return await Promise.all(
|
||||||
|
data.map(async ({ inventoryItem }) => {
|
||||||
|
return await inventoryService!.deleteInventoryItem(
|
||||||
|
inventoryItem.id,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await productVariantInventoryServiceTx.attachInventoryItem(
|
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,
|
variant.id,
|
||||||
inventoryItem.id
|
inventoryItem.id
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transactionHandler(
|
async function transactionHandler(
|
||||||
@@ -132,46 +159,49 @@ export const createVariantTransaction = async (
|
|||||||
payload: TransactionPayload
|
payload: TransactionPayload
|
||||||
) {
|
) {
|
||||||
const command = {
|
const command = {
|
||||||
[actions.createVariant]: {
|
[actions.createVariants]: {
|
||||||
[TransactionHandlerType.INVOKE]: async (
|
[TransactionHandlerType.INVOKE]: async (
|
||||||
data: CreateProductVariantInput
|
data: CreateProductVariantInput[]
|
||||||
) => {
|
) => {
|
||||||
return await createVariant(data)
|
return await createVariants(data)
|
||||||
},
|
},
|
||||||
[TransactionHandlerType.COMPENSATE]: async (
|
[TransactionHandlerType.COMPENSATE]: async (
|
||||||
data: CreateProductVariantInput,
|
data: CreateProductVariantInput[],
|
||||||
{ invoke }
|
{ invoke }
|
||||||
) => {
|
) => {
|
||||||
await removeVariant(invoke[actions.createVariant])
|
await removeVariants(invoke[actions.createVariants])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[actions.createInventoryItem]: {
|
[actions.createInventoryItems]: {
|
||||||
[TransactionHandlerType.INVOKE]: async (
|
[TransactionHandlerType.INVOKE]: async (
|
||||||
data: CreateProductVariantInput,
|
data: CreateProductVariantInput[],
|
||||||
{ invoke }
|
{ invoke }
|
||||||
) => {
|
) => {
|
||||||
const { [actions.createVariant]: variant } = invoke
|
const { [actions.createVariants]: variants } = invoke
|
||||||
|
|
||||||
return await createInventoryItem(variant)
|
return await createInventoryItems(variants)
|
||||||
},
|
},
|
||||||
[TransactionHandlerType.COMPENSATE]: async (
|
[TransactionHandlerType.COMPENSATE]: async (
|
||||||
data: CreateProductVariantInput,
|
data: {
|
||||||
|
variant: ProductVariant
|
||||||
|
inventoryItem: InventoryItemDTO
|
||||||
|
}[],
|
||||||
{ invoke }
|
{ invoke }
|
||||||
) => {
|
) => {
|
||||||
await removeInventoryItem(invoke[actions.createInventoryItem])
|
await removeInventoryItems(invoke[actions.createInventoryItems])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[actions.attachInventoryItem]: {
|
[actions.attachInventoryItems]: {
|
||||||
[TransactionHandlerType.INVOKE]: async (
|
[TransactionHandlerType.INVOKE]: async (
|
||||||
data: CreateProductVariantInput,
|
data: CreateProductVariantInput[],
|
||||||
{ invoke }
|
{ invoke }
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
[actions.createVariant]: variant,
|
[actions.createVariants]: variants,
|
||||||
[actions.createInventoryItem]: inventoryItem,
|
[actions.createInventoryItems]: inventoryItemsResult,
|
||||||
} = invoke
|
} = 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 { DistributedTransaction } from "../../../../utils/transaction"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import {
|
import {
|
||||||
createVariantTransaction,
|
createVariantsTransaction,
|
||||||
revertVariantTransaction,
|
revertVariantTransaction,
|
||||||
} from "./transaction/create-product-variant"
|
} from "./transaction/create-product-variant"
|
||||||
|
|
||||||
@@ -224,13 +224,11 @@ export default async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variantsToCreate.length) {
|
if (variantsToCreate.length) {
|
||||||
await Promise.all(
|
|
||||||
variantsToCreate.map(async (data) => {
|
|
||||||
try {
|
try {
|
||||||
const varTransaction = await createVariantTransaction(
|
const varTransaction = await createVariantsTransaction(
|
||||||
transactionDependencies,
|
transactionDependencies,
|
||||||
product.id,
|
product.id,
|
||||||
data as CreateProductVariantInput
|
variantsToCreate as CreateProductVariantInput[]
|
||||||
)
|
)
|
||||||
allVariantTransactions.push(varTransaction)
|
allVariantTransactions.push(varTransaction)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -245,8 +243,6 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default class EventBusService
|
|||||||
protected readonly config_: ConfigModule
|
protected readonly config_: ConfigModule
|
||||||
protected readonly stagedJobService_: StagedJobService
|
protected readonly stagedJobService_: StagedJobService
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly eventBusModuleService_: EventBusUtils.AbstractEventBusModuleService
|
protected readonly eventBusModuleService_: EventBusTypes.IEventBusModuleService
|
||||||
|
|
||||||
protected shouldEnqueuerRun: boolean
|
protected shouldEnqueuerRun: boolean
|
||||||
protected enqueue_: Promise<void>
|
protected enqueue_: Promise<void>
|
||||||
@@ -137,7 +137,7 @@ export default class EventBusService
|
|||||||
): Promise<TResult | void> {
|
): Promise<TResult | void> {
|
||||||
const manager = this.activeManager_
|
const manager = this.activeManager_
|
||||||
const isBulkEmit = !isString(eventNameOrData)
|
const isBulkEmit = !isString(eventNameOrData)
|
||||||
const events = isBulkEmit
|
let events: EventBusTypes.EmitData[] = isBulkEmit
|
||||||
? eventNameOrData.map((event) => ({
|
? eventNameOrData.map((event) => ({
|
||||||
eventName: event.eventName,
|
eventName: event.eventName,
|
||||||
data: event.data,
|
data: event.data,
|
||||||
@@ -151,6 +151,14 @@ export default class EventBusService
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
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.
|
* We store events in the database when in an ongoing transaction.
|
||||||
*
|
*
|
||||||
@@ -164,9 +172,11 @@ export default class EventBusService
|
|||||||
* In case of a failing transaction, jobs stored in the database are removed
|
* In case of a failing transaction, jobs stored in the database are removed
|
||||||
* as part of the rollback.
|
* as part of the rollback.
|
||||||
*/
|
*/
|
||||||
const stagedJobs = await this.stagedJobService_
|
|
||||||
|
stagedJobs = await this.stagedJobService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.create(events)
|
.create(events)
|
||||||
|
}
|
||||||
|
|
||||||
return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult
|
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
|
* Creates an unpublished product variant. Will validate against parent product
|
||||||
* to ensure that the variant can in fact be created.
|
* to ensure that the variant can in fact be created.
|
||||||
* @param productOrProductId - the product the variant will be added to
|
* @param productOrProductId - the product the variant will be added to
|
||||||
* @param variant - the variant to create
|
* @param variants
|
||||||
* @return resolves to the creation result.
|
* @return resolves to the creation result.
|
||||||
*/
|
*/
|
||||||
async create(
|
async create<
|
||||||
|
TVariants extends CreateProductVariantInput | CreateProductVariantInput[],
|
||||||
|
TOutput = TVariants extends CreateProductVariantInput[]
|
||||||
|
? CreateProductVariantInput[]
|
||||||
|
: CreateProductVariantInput
|
||||||
|
>(
|
||||||
productOrProductId: string | Product,
|
productOrProductId: string | Product,
|
||||||
variant: CreateProductVariantInput
|
variants: CreateProductVariantInput | CreateProductVariantInput[]
|
||||||
): Promise<ProductVariant> {
|
): Promise<TOutput> {
|
||||||
|
const isVariantsArray = Array.isArray(variants)
|
||||||
|
const variants_ = isVariantsArray ? variants : [variants]
|
||||||
|
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const productRepo = manager.withRepository(this.productRepository_)
|
const productRepo = manager.withRepository(this.productRepository_)
|
||||||
const variantRepo = manager.withRepository(this.productVariantRepository_)
|
const variantRepo = manager.withRepository(this.productVariantRepository_)
|
||||||
|
|
||||||
const { prices, ...rest } = variant
|
let product = productOrProductId as Product
|
||||||
|
|
||||||
let product = productOrProductId
|
|
||||||
|
|
||||||
if (isString(product)) {
|
if (isString(product)) {
|
||||||
product = (await productRepo.findOne({
|
product = (await productRepo.findOne({
|
||||||
@@ -195,42 +201,22 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (product.options.length !== variant.options.length) {
|
this.validateVariantsToCreate_(product, variants_)
|
||||||
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) => {
|
let computedRank = product.variants.length
|
||||||
if (!variant.options.find((vo) => option.id === vo.option_id)) {
|
const variantPricesToUpdate: {
|
||||||
throw new MedusaError(
|
id: string
|
||||||
MedusaError.Types.INVALID_DATA,
|
prices: ProductVariantPrice[]
|
||||||
`Variant options do not contain value for ${option.title}`
|
}[] = []
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const variantExists = product.variants.find((v) => {
|
const results = await Promise.all(
|
||||||
return v.options.every((option) => {
|
variants_.map(async (variant) => {
|
||||||
const variantOption = variant.options.find(
|
const { prices, ...rest } = variant
|
||||||
(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`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rest.variant_rank) {
|
if (!rest.variant_rank) {
|
||||||
rest.variant_rank = product.variants.length
|
rest.variant_rank = computedRank
|
||||||
}
|
}
|
||||||
|
++computedRank
|
||||||
|
|
||||||
const toCreate = {
|
const toCreate = {
|
||||||
...rest,
|
...rest,
|
||||||
@@ -241,23 +227,34 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
|
|
||||||
const result = await variantRepo.save(productVariant)
|
const result = await variantRepo.save(productVariant)
|
||||||
|
|
||||||
if (prices) {
|
if (prices?.length) {
|
||||||
await this.updateVariantPrices([
|
variantPricesToUpdate.push({ id: result.id, prices })
|
||||||
{
|
|
||||||
variantId: result.id,
|
|
||||||
prices,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.eventBus_
|
return result
|
||||||
.withTransaction(manager)
|
})
|
||||||
.emit(ProductVariantService.Events.CREATED, {
|
)
|
||||||
|
|
||||||
|
if (variantPricesToUpdate.length) {
|
||||||
|
await this.updateVariantPrices(
|
||||||
|
variantPricesToUpdate.map((v) => ({
|
||||||
|
variantId: v.id,
|
||||||
|
prices: v.prices,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToEmit = results.map((result) => ({
|
||||||
|
eventName: ProductVariantService.Events.CREATED,
|
||||||
|
data: {
|
||||||
id: result.id,
|
id: result.id,
|
||||||
product_id: result.product_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
|
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
|
export default ProductVariantService
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
FilterableStockLocationProps,
|
FilterableStockLocationProps,
|
||||||
FindConfig,
|
FindConfig,
|
||||||
IEventBusService,
|
IEventBusService,
|
||||||
|
MODULE_RESOURCE_TYPE,
|
||||||
SharedContext,
|
SharedContext,
|
||||||
StockLocationAddressInput,
|
StockLocationAddressInput,
|
||||||
UpdateStockLocationInput,
|
UpdateStockLocationInput,
|
||||||
@@ -41,7 +42,7 @@ export default class StockLocationService {
|
|||||||
constructor(
|
constructor(
|
||||||
{ eventBusService, manager }: InjectedDependencies,
|
{ eventBusService, manager }: InjectedDependencies,
|
||||||
options?: unknown,
|
options?: unknown,
|
||||||
moduleDeclaration?: InternalModuleDeclaration
|
protected readonly moduleDeclaration?: InternalModuleDeclaration
|
||||||
) {
|
) {
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.eventBusService_ = eventBusService
|
this.eventBusService_ = eventBusService
|
||||||
@@ -53,7 +54,10 @@ export default class StockLocationService {
|
|||||||
* @param config - Additional configuration for the query.
|
* @param config - Additional configuration for the query.
|
||||||
* @return A list of stock locations.
|
* @return A list of stock locations.
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async list(
|
async list(
|
||||||
selector: FilterableStockLocationProps = {},
|
selector: FilterableStockLocationProps = {},
|
||||||
config: FindConfig<StockLocation> = { relations: [], skip: 0, take: 10 },
|
config: FindConfig<StockLocation> = { relations: [], skip: 0, take: 10 },
|
||||||
@@ -72,7 +76,10 @@ export default class StockLocationService {
|
|||||||
* @param config - Additional configuration for the query.
|
* @param config - Additional configuration for the query.
|
||||||
* @return A list of stock locations and the count of matching stock locations.
|
* @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(
|
async listAndCount(
|
||||||
selector: FilterableStockLocationProps = {},
|
selector: FilterableStockLocationProps = {},
|
||||||
config: FindConfig<StockLocation> = { relations: [], skip: 0, take: 10 },
|
config: FindConfig<StockLocation> = { relations: [], skip: 0, take: 10 },
|
||||||
@@ -92,7 +99,10 @@ export default class StockLocationService {
|
|||||||
* @return The stock location.
|
* @return The stock location.
|
||||||
* @throws If the stock location ID is not definedor the stock location with the given ID was not found.
|
* @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(
|
async retrieve(
|
||||||
stockLocationId: string,
|
stockLocationId: string,
|
||||||
config: FindConfig<StockLocation> = {},
|
config: FindConfig<StockLocation> = {},
|
||||||
@@ -126,7 +136,10 @@ export default class StockLocationService {
|
|||||||
* @param data - The input data for creating a Stock Location.
|
* @param data - The input data for creating a Stock Location.
|
||||||
* @returns The created stock location.
|
* @returns The created stock location.
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async create(
|
async create(
|
||||||
data: CreateStockLocationInput,
|
data: CreateStockLocationInput,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
@@ -170,7 +183,10 @@ export default class StockLocationService {
|
|||||||
* @param updateData - The update data for the stock location.
|
* @param updateData - The update data for the stock location.
|
||||||
* @returns The updated stock location.
|
* @returns The updated stock location.
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async update(
|
async update(
|
||||||
stockLocationId: string,
|
stockLocationId: string,
|
||||||
updateData: UpdateStockLocationInput,
|
updateData: UpdateStockLocationInput,
|
||||||
@@ -216,7 +232,10 @@ export default class StockLocationService {
|
|||||||
* @param address - The update data for the address.
|
* @param address - The update data for the address.
|
||||||
* @returns The updated stock location address.
|
* @returns The updated stock location address.
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
protected async updateAddress(
|
protected async updateAddress(
|
||||||
addressId: string,
|
addressId: string,
|
||||||
address: StockLocationAddressInput,
|
address: StockLocationAddressInput,
|
||||||
@@ -257,7 +276,10 @@ export default class StockLocationService {
|
|||||||
* @param id - The ID of the stock location to delete.
|
* @param id - The ID of the stock location to delete.
|
||||||
* @returns An empty promise.
|
* @returns An empty promise.
|
||||||
*/
|
*/
|
||||||
@InjectEntityManager()
|
@InjectEntityManager(
|
||||||
|
(target) =>
|
||||||
|
target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||||
|
)
|
||||||
async delete(
|
async delete(
|
||||||
id: string,
|
id: string,
|
||||||
@MedusaContext() context: SharedContext = {}
|
@MedusaContext() context: SharedContext = {}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { EmitData, Subscriber, SubscriberContext } from "./common"
|
import {
|
||||||
|
EmitData,
|
||||||
|
Subscriber,
|
||||||
|
SubscriberContext,
|
||||||
|
SubscriberDescriptor,
|
||||||
|
} from "./common"
|
||||||
|
|
||||||
export interface IEventBusModuleService {
|
export interface IEventBusModuleService {
|
||||||
|
retrieveSubscribers(
|
||||||
|
event: string | symbol
|
||||||
|
): SubscriberDescriptor[] | undefined
|
||||||
|
|
||||||
emit<T>(
|
emit<T>(
|
||||||
eventName: string,
|
eventName: string,
|
||||||
data: T,
|
data: T,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { SharedContext } from "@medusajs/types"
|
import { SharedContext } from "@medusajs/types"
|
||||||
|
|
||||||
export function InjectEntityManager(
|
export function InjectEntityManager(
|
||||||
managerProperty = "manager_"
|
shouldForceTransaction: (target: any) => boolean = () => false,
|
||||||
|
managerProperty: string = "manager_"
|
||||||
): MethodDecorator {
|
): MethodDecorator {
|
||||||
return function (
|
return function (
|
||||||
target: any,
|
target: any,
|
||||||
@@ -18,9 +19,10 @@ export function InjectEntityManager(
|
|||||||
|
|
||||||
const argIndex = target.MedusaContextIndex_[propertyKey]
|
const argIndex = target.MedusaContextIndex_[propertyKey]
|
||||||
descriptor.value = async function (...args: any[]) {
|
descriptor.value = async function (...args: any[]) {
|
||||||
|
const shouldForceTransactionRes = shouldForceTransaction(target)
|
||||||
const context: SharedContext = args[argIndex] ?? {}
|
const context: SharedContext = args[argIndex] ?? {}
|
||||||
|
|
||||||
if (context?.transactionManager) {
|
if (!shouldForceTransactionRes && context?.transactionManager) {
|
||||||
return await originalMethod.apply(this, args)
|
return await originalMethod.apply(this, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,24 +23,15 @@ export abstract class AbstractEventBusModuleService
|
|||||||
): Promise<void>
|
): Promise<void>
|
||||||
abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
|
abstract emit<T>(data: EventBusTypes.EmitData<T>[]): Promise<void>
|
||||||
|
|
||||||
public subscribe(
|
protected storeSubscribers({
|
||||||
eventName: string | symbol,
|
event,
|
||||||
subscriber: EventBusTypes.Subscriber,
|
subscriberId,
|
||||||
context?: EventBusTypes.SubscriberContext
|
subscriber,
|
||||||
): this {
|
}: {
|
||||||
if (typeof subscriber !== `function`) {
|
event: string | symbol
|
||||||
throw new Error("Subscriber must be a function")
|
subscriberId: string
|
||||||
}
|
subscriber: EventBusTypes.Subscriber
|
||||||
/**
|
}) {
|
||||||
* 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}`
|
|
||||||
|
|
||||||
const newSubscriberDescriptor = { subscriber, id: subscriberId }
|
const newSubscriberDescriptor = { subscriber, id: subscriberId }
|
||||||
|
|
||||||
const existingSubscribers = this.eventToSubscribersMap_.get(event) ?? []
|
const existingSubscribers = this.eventToSubscribersMap_.get(event) ?? []
|
||||||
@@ -57,6 +48,33 @@ export abstract class AbstractEventBusModuleService
|
|||||||
...existingSubscribers,
|
...existingSubscribers,
|
||||||
newSubscriberDescriptor,
|
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
|
return this
|
||||||
}
|
}
|
||||||
@@ -70,7 +88,7 @@ export abstract class AbstractEventBusModuleService
|
|||||||
throw new Error("Subscriber must be a function")
|
throw new Error("Subscriber must be a function")
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingSubscribers = this.eventToSubscribersMap_.get(eventName)
|
const existingSubscribers = this.retrieveSubscribers(eventName)
|
||||||
|
|
||||||
if (existingSubscribers?.length) {
|
if (existingSubscribers?.length) {
|
||||||
const subIndex = existingSubscribers?.findIndex(
|
const subIndex = existingSubscribers?.findIndex(
|
||||||
|
|||||||
Reference in new issue
Block a user