test(): Test the create product workflow compensation (#4716)
**What** Integration tests to validate the workflow compensation. Also, fix the transaction state when the workflow is compensating and some steps does not have any compensation
This commit is contained in:
1 parent
86c314eb2a
commit
ac866ebb51
21 files changed
+391
-193
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": patch
|
||||||
|
"@medusajs/workflows": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
test(): Test the create product workflow compensation
|
||||||
|
fix(orchestration): Fix the transaction state after compensating with no compensation steps in the middle
|
||||||
|
chore(workflows): Export and naming
|
||||||
|
feat(types): Update product workflow input types
|
||||||
|
feat(medusa): Update product workflow usage and cleanup endpoint
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:integration": "jest --silent=false --runInBand --bail --detectOpenHandles --forceExit",
|
"test:integration": "jest --silent=false --maxWorkers=50% --bail --detectOpenHandles --forceExit",
|
||||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import path from "path"
|
||||||
|
import { initDb, useDb } from "../../../../environment-helpers/use-db"
|
||||||
|
import { bootstrapApp } from "../../../../environment-helpers/bootstrap-app"
|
||||||
|
import {
|
||||||
|
createProducts,
|
||||||
|
CreateProductsActions,
|
||||||
|
Handlers,
|
||||||
|
pipe,
|
||||||
|
} from "@medusajs/workflows"
|
||||||
|
import { IProductModuleService, WorkflowTypes } from "@medusajs/types"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||||
|
|
||||||
|
describe("CreateProduct workflow", function () {
|
||||||
|
let medusaProcess
|
||||||
|
let medusaContainer
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
|
||||||
|
await initDb({ cwd } as any)
|
||||||
|
const { container } = await bootstrapApp({ cwd })
|
||||||
|
medusaContainer = container
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
const db = useDb()
|
||||||
|
await db.shutdown()
|
||||||
|
|
||||||
|
medusaProcess.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compensate all the invoke if something fails", async () => {
|
||||||
|
const workflow = createProducts(medusaContainer)
|
||||||
|
|
||||||
|
workflow.appendAction(
|
||||||
|
"fail_step",
|
||||||
|
CreateProductsActions.attachInventoryItems,
|
||||||
|
{
|
||||||
|
invoke: pipe({}, async function failStep() {
|
||||||
|
throw new Error(`Failed to create products`)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noCompensation: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const input: WorkflowTypes.ProductWorkflow.CreateProductsWorkflowInputDTO =
|
||||||
|
{
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
title: "Test product",
|
||||||
|
type: { value: "physical" },
|
||||||
|
tags: [{ value: "test" }],
|
||||||
|
subtitle: "Test subtitle",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
title: "Test variant",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
amount: 100,
|
||||||
|
currency_code: "usd",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
title: "Test option",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = medusaContainer.resolve("manager")
|
||||||
|
const context = {
|
||||||
|
manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result, errors, transaction } = await workflow.run({
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
throwOnError: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(errors).toEqual([
|
||||||
|
{
|
||||||
|
action: "fail_step",
|
||||||
|
handlerType: "invoke",
|
||||||
|
error: new Error(`Failed to create products`),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(transaction.getState()).toEqual("reverted")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
expect(result[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const productId = result[0].id
|
||||||
|
|
||||||
|
let [product] = await Handlers.ProductHandlers.listProducts({
|
||||||
|
container: medusaContainer,
|
||||||
|
context,
|
||||||
|
data: {
|
||||||
|
ids: [productId],
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(product).toBeUndefined()
|
||||||
|
|
||||||
|
const productModule = medusaContainer.resolve(
|
||||||
|
ModuleRegistrationName.PRODUCT
|
||||||
|
) as IProductModuleService
|
||||||
|
|
||||||
|
;[product] = await productModule.list(
|
||||||
|
{
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withDeleted: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(product).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
deleted_at: expect.any(String),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
globals: {
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
|
tsconfig: "tsconfig.spec.json",
|
||||||
diagnostics: false,
|
diagnostics: false,
|
||||||
isolatedModules: true,
|
isolatedModules: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["test"],
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||||
"build": "rimraf dist && tsc --build",
|
"build": "rimraf dist && tsc --build",
|
||||||
"serve": "node dist/app.js",
|
"serve": "node dist/app.js",
|
||||||
"test": "jest"
|
"test": "jest --maxWorkers=50% --detectOpenHandles --forceExit"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"medusa-interfaces": "^1.3.7",
|
"medusa-interfaces": "^1.3.7",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IdMap } from "medusa-test-utils"
|
|||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
import { PriceListServiceMock } from "../../../../../services/__mocks__/price-list"
|
||||||
|
|
||||||
|
jest.setTimeout(10000)
|
||||||
describe("POST /price-lists", () => {
|
describe("POST /price-lists", () => {
|
||||||
describe("successfully creates a price list", () => {
|
describe("successfully creates a price list", () => {
|
||||||
let subject
|
let subject
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
import { DistributedTransaction } from "@medusajs/orchestration"
|
import { DistributedTransaction } from "@medusajs/orchestration"
|
||||||
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
import { IInventoryService, WorkflowTypes } from "@medusajs/types"
|
||||||
import { FlagRouter } from "@medusajs/utils"
|
import { FlagRouter } from "@medusajs/utils"
|
||||||
import { Workflows, createProducts } from "@medusajs/workflows"
|
import { createProducts, Workflows } from "@medusajs/workflows"
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
|
||||||
@@ -142,6 +142,8 @@ export default async (req, res) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let product
|
||||||
|
|
||||||
if (isWorkflowEnabled && !!productModuleService) {
|
if (isWorkflowEnabled && !!productModuleService) {
|
||||||
const createProductWorkflow = createProducts(req.scope)
|
const createProductWorkflow = createProducts(req.scope)
|
||||||
|
|
||||||
@@ -157,119 +159,114 @@ export default async (req, res) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const { result: products } = await createProductWorkflow.run({
|
const { result } = await createProductWorkflow.run({
|
||||||
input,
|
input,
|
||||||
context: {
|
context: {
|
||||||
manager: entityManager,
|
manager: entityManager,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
product = result[0]
|
||||||
|
} else {
|
||||||
|
product = await entityManager.transaction(async (manager) => {
|
||||||
|
const { variants } = validated
|
||||||
|
delete validated.variants
|
||||||
|
|
||||||
return res.json({ product: products[0] })
|
if (!validated.thumbnail && validated.images && validated.images.length) {
|
||||||
|
validated.thumbnail = validated.images[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
let shippingProfile
|
||||||
|
// Get default shipping profile
|
||||||
|
if (validated.is_giftcard) {
|
||||||
|
shippingProfile = await shippingProfileService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.retrieveGiftCardDefault()
|
||||||
|
} else {
|
||||||
|
shippingProfile = await shippingProfileService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.retrieveDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provided that the feature flag is enabled and
|
||||||
|
// no sales channels are available, set the default one
|
||||||
|
if (
|
||||||
|
featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key) &&
|
||||||
|
!validated?.sales_channels?.length
|
||||||
|
) {
|
||||||
|
const defaultSalesChannel = await salesChannelService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.retrieveDefault()
|
||||||
|
validated.sales_channels = [defaultSalesChannel]
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProduct = await productService
|
||||||
|
.withTransaction(manager)
|
||||||
|
.create({ ...validated, profile_id: shippingProfile.id })
|
||||||
|
|
||||||
|
if (variants) {
|
||||||
|
for (const [index, variant] of variants.entries()) {
|
||||||
|
variant["variant_rank"] = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionIds =
|
||||||
|
validated?.options?.map(
|
||||||
|
(o) => newProduct.options.find((newO) => newO.title === o.title)?.id
|
||||||
|
) || []
|
||||||
|
|
||||||
|
const allVariantTransactions: DistributedTransaction[] = []
|
||||||
|
const transactionDependencies = {
|
||||||
|
manager,
|
||||||
|
inventoryService,
|
||||||
|
productVariantInventoryService,
|
||||||
|
productVariantService,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const variantsInputData = variants.map((variant) => {
|
||||||
|
const options =
|
||||||
|
variant?.options?.map((option, index) => ({
|
||||||
|
...option,
|
||||||
|
option_id: optionIds[index],
|
||||||
|
})) || []
|
||||||
|
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
options,
|
||||||
|
} as CreateProductVariantInput
|
||||||
|
})
|
||||||
|
|
||||||
|
const varTransaction = await createVariantsTransaction(
|
||||||
|
transactionDependencies,
|
||||||
|
newProduct.id,
|
||||||
|
variantsInputData
|
||||||
|
)
|
||||||
|
allVariantTransactions.push(varTransaction)
|
||||||
|
} catch (e) {
|
||||||
|
await Promise.all(
|
||||||
|
allVariantTransactions.map(async (transaction) => {
|
||||||
|
await revertVariantTransaction(
|
||||||
|
transactionDependencies,
|
||||||
|
transaction
|
||||||
|
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newProduct
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = await entityManager.transaction(async (manager) => {
|
const rawProduct = await productService.retrieve(product.id, {
|
||||||
const { variants } = validated
|
select: defaultAdminProductFields,
|
||||||
delete validated.variants
|
relations: defaultAdminProductRelations,
|
||||||
|
|
||||||
if (!validated.thumbnail && validated.images && validated.images.length) {
|
|
||||||
validated.thumbnail = validated.images[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
let shippingProfile
|
|
||||||
// Get default shipping profile
|
|
||||||
if (validated.is_giftcard) {
|
|
||||||
shippingProfile = await shippingProfileService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.retrieveGiftCardDefault()
|
|
||||||
} else {
|
|
||||||
shippingProfile = await shippingProfileService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.retrieveDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provided that the feature flag is enabled and
|
|
||||||
// no sales channels are available, set the default one
|
|
||||||
if (
|
|
||||||
featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key) &&
|
|
||||||
!validated?.sales_channels?.length
|
|
||||||
) {
|
|
||||||
const defaultSalesChannel = await salesChannelService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.retrieveDefault()
|
|
||||||
validated.sales_channels = [defaultSalesChannel]
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProduct = await productService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.create({ ...validated, profile_id: shippingProfile.id })
|
|
||||||
|
|
||||||
if (variants) {
|
|
||||||
for (const [index, variant] of variants.entries()) {
|
|
||||||
variant["variant_rank"] = index
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionIds =
|
|
||||||
validated?.options?.map(
|
|
||||||
(o) => newProduct.options.find((newO) => newO.title === o.title)?.id
|
|
||||||
) || []
|
|
||||||
|
|
||||||
const allVariantTransactions: DistributedTransaction[] = []
|
|
||||||
const transactionDependencies = {
|
|
||||||
manager,
|
|
||||||
inventoryService,
|
|
||||||
productVariantInventoryService,
|
|
||||||
productVariantService,
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const variantsInputData = variants.map((variant) => {
|
|
||||||
const options =
|
|
||||||
variant?.options?.map((option, index) => ({
|
|
||||||
...option,
|
|
||||||
option_id: optionIds[index],
|
|
||||||
})) || []
|
|
||||||
|
|
||||||
return {
|
|
||||||
...variant,
|
|
||||||
options,
|
|
||||||
} as CreateProductVariantInput
|
|
||||||
})
|
|
||||||
|
|
||||||
const varTransaction = await createVariantsTransaction(
|
|
||||||
transactionDependencies,
|
|
||||||
newProduct.id,
|
|
||||||
variantsInputData
|
|
||||||
)
|
|
||||||
allVariantTransactions.push(varTransaction)
|
|
||||||
} catch (e) {
|
|
||||||
await Promise.all(
|
|
||||||
allVariantTransactions.map(async (transaction) => {
|
|
||||||
await revertVariantTransaction(
|
|
||||||
transactionDependencies,
|
|
||||||
transaction
|
|
||||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawProduct = await productService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.retrieve(newProduct.id, {
|
|
||||||
select: defaultAdminProductFields,
|
|
||||||
relations: defaultAdminProductRelations,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [product] = await pricingService
|
|
||||||
.withTransaction(manager)
|
|
||||||
.setProductPrices([rawProduct])
|
|
||||||
|
|
||||||
return product
|
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json({ product })
|
const [pricedProduct] = await pricingService.setProductPrices([rawProduct])
|
||||||
|
|
||||||
|
res.json({ product: pricedProduct })
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProductVariantOptionReq {
|
class ProductVariantOptionReq {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"prepare": "cross-env NODE_ENV=production yarn run release",
|
"prepare": "cross-env NODE_ENV=production yarn run release",
|
||||||
"build": "rollup --config --environment NODE_ENV:development",
|
"build": "rollup --config --environment NODE_ENV:development",
|
||||||
"release": "rollup --config --environment NODE_ENV:production",
|
"release": "rollup --config --environment NODE_ENV:production",
|
||||||
"test": "jest src"
|
"test": "jest src --passWithNoTests"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"camelcase": "^6.3.0",
|
"camelcase": "^6.3.0",
|
||||||
|
|||||||
@@ -787,6 +787,88 @@ describe("Transaction Orchestrator", () => {
|
|||||||
expect(resumedTransaction.getState()).toBe(TransactionState.REVERTED)
|
expect(resumedTransaction.getState()).toBe(TransactionState.REVERTED)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Should hold the status REVERTED if the steps failed and the compensation succeed and has some no compensations step set", async () => {
|
||||||
|
const mocks = {
|
||||||
|
one: jest.fn().mockImplementation((payload) => {
|
||||||
|
return
|
||||||
|
}),
|
||||||
|
compensateOne: jest.fn().mockImplementation((payload) => {
|
||||||
|
return
|
||||||
|
}),
|
||||||
|
two: jest.fn().mockImplementation((payload) => {
|
||||||
|
return
|
||||||
|
}),
|
||||||
|
compensateTwo: jest.fn().mockImplementation((payload) => {
|
||||||
|
return
|
||||||
|
}),
|
||||||
|
three: jest.fn().mockImplementation((payload) => {
|
||||||
|
throw new Error("Third step error")
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
actionId: string,
|
||||||
|
functionHandlerType: TransactionHandlerType,
|
||||||
|
payload: TransactionPayload
|
||||||
|
) {
|
||||||
|
const command = {
|
||||||
|
firstMethod: {
|
||||||
|
[TransactionHandlerType.INVOKE]: () => {
|
||||||
|
mocks.one(payload)
|
||||||
|
},
|
||||||
|
[TransactionHandlerType.COMPENSATE]: () => {
|
||||||
|
mocks.compensateOne(payload)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secondMethod: {
|
||||||
|
[TransactionHandlerType.INVOKE]: () => {
|
||||||
|
mocks.two(payload)
|
||||||
|
},
|
||||||
|
[TransactionHandlerType.COMPENSATE]: () => {
|
||||||
|
mocks.compensateTwo(payload)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thirdMethod: {
|
||||||
|
[TransactionHandlerType.INVOKE]: () => {
|
||||||
|
mocks.three(payload)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return command[actionId][functionHandlerType](payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flow: TransactionStepsDefinition = {
|
||||||
|
next: {
|
||||||
|
action: "firstMethod",
|
||||||
|
next: {
|
||||||
|
action: "secondMethod",
|
||||||
|
next: {
|
||||||
|
action: "thirdMethod",
|
||||||
|
noCompensation: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategy = new TransactionOrchestrator("transaction-name", flow)
|
||||||
|
|
||||||
|
const transaction = await strategy.beginTransaction(
|
||||||
|
"transaction_id_123",
|
||||||
|
handler
|
||||||
|
)
|
||||||
|
|
||||||
|
await strategy.resume(transaction)
|
||||||
|
|
||||||
|
expect(mocks.one).toBeCalledTimes(1)
|
||||||
|
expect(mocks.compensateOne).toBeCalledTimes(1)
|
||||||
|
expect(mocks.two).toBeCalledTimes(1)
|
||||||
|
expect(mocks.compensateTwo).toBeCalledTimes(1)
|
||||||
|
expect(mocks.three).toBeCalledTimes(1)
|
||||||
|
|
||||||
|
expect(transaction.getState()).toBe(TransactionState.REVERTED)
|
||||||
|
})
|
||||||
|
|
||||||
it("Should revert a transaction when .cancelTransaction() is called", async () => {
|
it("Should revert a transaction when .cancelTransaction() is called", async () => {
|
||||||
const mocks = {
|
const mocks = {
|
||||||
one: jest.fn().mockImplementation((payload) => {
|
one: jest.fn().mockImplementation((payload) => {
|
||||||
|
|||||||
@@ -147,8 +147,7 @@ describe("WorkflowManager", () => {
|
|||||||
expect(handlers.get("bar").invoke).toHaveBeenCalledTimes(0)
|
expect(handlers.get("bar").invoke).toHaveBeenCalledTimes(0)
|
||||||
expect(handlers.get("bar").compensate).toHaveBeenCalledTimes(0)
|
expect(handlers.get("bar").compensate).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
// Failed because the async is flagged as noCompensation
|
expect(continuation.getState()).toBe(TransactionState.REVERTED)
|
||||||
expect(continuation.getState()).toBe(TransactionState.FAILED)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update an existing global flow with a new step and a new handler", async () => {
|
it("should update an existing global flow with a new step and a new handler", async () => {
|
||||||
|
|||||||
@@ -146,8 +146,7 @@ describe("WorkflowManager", () => {
|
|||||||
expect(handlers.get("bar").invoke).toHaveBeenCalledTimes(0)
|
expect(handlers.get("bar").invoke).toHaveBeenCalledTimes(0)
|
||||||
expect(handlers.get("bar").compensate).toHaveBeenCalledTimes(0)
|
expect(handlers.get("bar").compensate).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
// Failed because the async is flagged as noCompensation
|
expect(continuation.getState()).toBe(TransactionState.REVERTED)
|
||||||
expect(continuation.getState()).toBe(TransactionState.FAILED)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should update a flow with a new step and a new handler", async () => {
|
it("should update a flow with a new step and a new handler", async () => {
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ export class DistributedTransaction {
|
|||||||
public modelId: string
|
public modelId: string
|
||||||
public transactionId: string
|
public transactionId: string
|
||||||
|
|
||||||
private errors: TransactionStepError[] = []
|
private readonly errors: TransactionStepError[] = []
|
||||||
|
|
||||||
private context: TransactionContext = new TransactionContext()
|
private readonly context: TransactionContext = new TransactionContext()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private flow: TransactionFlow,
|
private flow: TransactionFlow,
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { TransactionStep, TransactionStepHandler } from "./transaction-step"
|
|||||||
import {
|
import {
|
||||||
TransactionHandlerType,
|
TransactionHandlerType,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionStepStatus,
|
|
||||||
TransactionStepsDefinition,
|
TransactionStepsDefinition,
|
||||||
|
TransactionStepStatus,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
@@ -233,9 +233,8 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
const stepDef = flow.steps[step]
|
const stepDef = flow.steps[step]
|
||||||
const curState = stepDef.getStates()
|
const curState = stepDef.getStates()
|
||||||
if (
|
if (
|
||||||
(curState.state === TransactionState.DONE ||
|
curState.state === TransactionState.DONE ||
|
||||||
curState.status === TransactionStepStatus.PERMANENT_FAILURE) &&
|
curState.status === TransactionStepStatus.PERMANENT_FAILURE
|
||||||
!stepDef.definition.noCompensation
|
|
||||||
) {
|
) {
|
||||||
stepDef.beginCompensation()
|
stepDef.beginCompensation()
|
||||||
stepDef.changeState(TransactionState.NOT_STARTED)
|
stepDef.changeState(TransactionState.NOT_STARTED)
|
||||||
@@ -334,6 +333,11 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
if (curState.state === TransactionState.NOT_STARTED) {
|
if (curState.state === TransactionState.NOT_STARTED) {
|
||||||
if (step.isCompensating()) {
|
if (step.isCompensating()) {
|
||||||
step.changeState(TransactionState.COMPENSATING)
|
step.changeState(TransactionState.COMPENSATING)
|
||||||
|
|
||||||
|
if (step.definition.noCompensation) {
|
||||||
|
step.changeState(TransactionState.REVERTED)
|
||||||
|
continue
|
||||||
|
}
|
||||||
} else if (flow.state === TransactionState.INVOKING) {
|
} else if (flow.state === TransactionState.INVOKING) {
|
||||||
step.changeState(TransactionState.INVOKING)
|
step.changeState(TransactionState.INVOKING)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ProductStatus } from "../../product"
|
import { ProductStatus } from "../../product"
|
||||||
import { WorkflowInputConfig } from "../common"
|
|
||||||
|
|
||||||
export interface CreateProductTypeInputDTO {
|
export interface CreateProductTypeInputDTO {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -92,5 +91,4 @@ export interface CreateProductInputDTO {
|
|||||||
|
|
||||||
export interface CreateProductsWorkflowInputDTO {
|
export interface CreateProductsWorkflowInputDTO {
|
||||||
products: CreateProductInputDTO[]
|
products: CreateProductInputDTO[]
|
||||||
config?: WorkflowInputConfig
|
|
||||||
}
|
}
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./create-products"
|
|
||||||
export * from "./cart"
|
export * from "./cart"
|
||||||
|
export * from "./product"
|
||||||
+38
-70
@@ -1,19 +1,18 @@
|
|||||||
import { InputAlias, Workflows } from "../definitions"
|
import { InputAlias, Workflows } from "../../definitions"
|
||||||
import {
|
import {
|
||||||
TransactionStepsDefinition,
|
TransactionStepsDefinition,
|
||||||
WorkflowManager,
|
WorkflowManager,
|
||||||
} from "@medusajs/orchestration"
|
} from "@medusajs/orchestration"
|
||||||
import { exportWorkflow, pipe } from "../helper"
|
import { aggregateData, exportWorkflow, pipe } from "../../helper"
|
||||||
|
|
||||||
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
|
import { ProductTypes, WorkflowTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
InventoryHandlers,
|
InventoryHandlers,
|
||||||
MiddlewaresHandlers,
|
MiddlewaresHandlers,
|
||||||
ProductHandlers,
|
ProductHandlers,
|
||||||
} from "../handlers"
|
} from "../../handlers"
|
||||||
import { aggregateData } from "../helper/aggregate"
|
|
||||||
|
|
||||||
export enum Actions {
|
export enum CreateProductsActions {
|
||||||
prepare = "prepare",
|
prepare = "prepare",
|
||||||
createProducts = "createProducts",
|
createProducts = "createProducts",
|
||||||
attachToSalesChannel = "attachToSalesChannel",
|
attachToSalesChannel = "attachToSalesChannel",
|
||||||
@@ -21,34 +20,29 @@ export enum Actions {
|
|||||||
createPrices = "createPrices",
|
createPrices = "createPrices",
|
||||||
createInventoryItems = "createInventoryItems",
|
createInventoryItems = "createInventoryItems",
|
||||||
attachInventoryItems = "attachInventoryItems",
|
attachInventoryItems = "attachInventoryItems",
|
||||||
result = "result",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workflowSteps: TransactionStepsDefinition = {
|
export const workflowSteps: TransactionStepsDefinition = {
|
||||||
next: {
|
next: {
|
||||||
action: Actions.prepare,
|
action: CreateProductsActions.prepare,
|
||||||
noCompensation: true,
|
noCompensation: true,
|
||||||
next: {
|
next: {
|
||||||
action: Actions.createProducts,
|
action: CreateProductsActions.createProducts,
|
||||||
next: [
|
next: [
|
||||||
{
|
{
|
||||||
action: Actions.attachShippingProfile,
|
action: CreateProductsActions.attachShippingProfile,
|
||||||
saveResponse: false,
|
saveResponse: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: Actions.attachToSalesChannel,
|
action: CreateProductsActions.attachToSalesChannel,
|
||||||
saveResponse: false,
|
saveResponse: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: Actions.createPrices,
|
action: CreateProductsActions.createPrices,
|
||||||
next: {
|
next: {
|
||||||
action: Actions.createInventoryItems,
|
action: CreateProductsActions.createInventoryItems,
|
||||||
next: {
|
next: {
|
||||||
action: Actions.attachInventoryItems,
|
action: CreateProductsActions.attachInventoryItems,
|
||||||
next: {
|
|
||||||
action: Actions.result,
|
|
||||||
noCompensation: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -59,7 +53,7 @@ export const workflowSteps: TransactionStepsDefinition = {
|
|||||||
|
|
||||||
const handlers = new Map([
|
const handlers = new Map([
|
||||||
[
|
[
|
||||||
Actions.prepare,
|
CreateProductsActions.prepare,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
@@ -69,17 +63,17 @@ const handlers = new Map([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
aggregateData(),
|
aggregateData(),
|
||||||
MiddlewaresHandlers.createProductsPrepareData
|
ProductHandlers.createProductsPrepareData
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Actions.createProducts,
|
CreateProductsActions.createProducts,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: {
|
invoke: {
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
aggregateData(),
|
aggregateData(),
|
||||||
@@ -88,7 +82,7 @@ const handlers = new Map([
|
|||||||
compensate: pipe(
|
compensate: pipe(
|
||||||
{
|
{
|
||||||
invoke: {
|
invoke: {
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias: ProductHandlers.removeProducts.aliases.products,
|
alias: ProductHandlers.removeProducts.aliases.products,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -98,16 +92,16 @@ const handlers = new Map([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Actions.attachShippingProfile,
|
CreateProductsActions.attachShippingProfile,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
{
|
{
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias:
|
alias:
|
||||||
ProductHandlers.attachShippingProfileToProducts.aliases
|
ProductHandlers.attachShippingProfileToProducts.aliases
|
||||||
.products,
|
.products,
|
||||||
@@ -121,10 +115,10 @@ const handlers = new Map([
|
|||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
{
|
{
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias:
|
alias:
|
||||||
ProductHandlers.detachShippingProfileFromProducts.aliases
|
ProductHandlers.detachShippingProfileFromProducts.aliases
|
||||||
.products,
|
.products,
|
||||||
@@ -137,16 +131,16 @@ const handlers = new Map([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Actions.attachToSalesChannel,
|
CreateProductsActions.attachToSalesChannel,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
{
|
{
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias:
|
alias:
|
||||||
ProductHandlers.attachSalesChannelToProducts.aliases.products,
|
ProductHandlers.attachSalesChannelToProducts.aliases.products,
|
||||||
},
|
},
|
||||||
@@ -159,10 +153,10 @@ const handlers = new Map([
|
|||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
{
|
{
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias:
|
alias:
|
||||||
ProductHandlers.detachSalesChannelFromProducts.aliases.products,
|
ProductHandlers.detachSalesChannelFromProducts.aliases.products,
|
||||||
},
|
},
|
||||||
@@ -174,12 +168,12 @@ const handlers = new Map([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Actions.createInventoryItems,
|
CreateProductsActions.createInventoryItems,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: {
|
invoke: {
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias: InventoryHandlers.createInventoryItems.aliases.products,
|
alias: InventoryHandlers.createInventoryItems.aliases.products,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -189,7 +183,7 @@ const handlers = new Map([
|
|||||||
compensate: pipe(
|
compensate: pipe(
|
||||||
{
|
{
|
||||||
invoke: {
|
invoke: {
|
||||||
from: Actions.createInventoryItems,
|
from: CreateProductsActions.createInventoryItems,
|
||||||
alias:
|
alias:
|
||||||
InventoryHandlers.removeInventoryItems.aliases.inventoryItems,
|
InventoryHandlers.removeInventoryItems.aliases.inventoryItems,
|
||||||
},
|
},
|
||||||
@@ -200,12 +194,12 @@ const handlers = new Map([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Actions.attachInventoryItems,
|
CreateProductsActions.attachInventoryItems,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: {
|
invoke: {
|
||||||
from: Actions.createInventoryItems,
|
from: CreateProductsActions.createInventoryItems,
|
||||||
alias:
|
alias:
|
||||||
InventoryHandlers.attachInventoryItems.aliases.inventoryItems,
|
InventoryHandlers.attachInventoryItems.aliases.inventoryItems,
|
||||||
},
|
},
|
||||||
@@ -216,7 +210,7 @@ const handlers = new Map([
|
|||||||
compensate: pipe(
|
compensate: pipe(
|
||||||
{
|
{
|
||||||
invoke: {
|
invoke: {
|
||||||
from: Actions.createInventoryItems,
|
from: CreateProductsActions.createInventoryItems,
|
||||||
alias:
|
alias:
|
||||||
InventoryHandlers.detachInventoryItems.aliases.inventoryItems,
|
InventoryHandlers.detachInventoryItems.aliases.inventoryItems,
|
||||||
},
|
},
|
||||||
@@ -227,16 +221,16 @@ const handlers = new Map([
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
Actions.createPrices,
|
CreateProductsActions.createPrices,
|
||||||
{
|
{
|
||||||
invoke: pipe(
|
invoke: pipe(
|
||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
{
|
{
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias:
|
alias:
|
||||||
ProductHandlers.updateProductsVariantsPrices.aliases.products,
|
ProductHandlers.updateProductsVariantsPrices.aliases.products,
|
||||||
},
|
},
|
||||||
@@ -249,10 +243,10 @@ const handlers = new Map([
|
|||||||
{
|
{
|
||||||
invoke: [
|
invoke: [
|
||||||
{
|
{
|
||||||
from: Actions.prepare,
|
from: CreateProductsActions.prepare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
from: Actions.createProducts,
|
from: CreateProductsActions.createProducts,
|
||||||
alias:
|
alias:
|
||||||
ProductHandlers.updateProductsVariantsPrices.aliases.products,
|
ProductHandlers.updateProductsVariantsPrices.aliases.products,
|
||||||
},
|
},
|
||||||
@@ -264,32 +258,6 @@ const handlers = new Map([
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
|
||||||
Actions.result,
|
|
||||||
{
|
|
||||||
invoke: pipe(
|
|
||||||
{
|
|
||||||
invoke: [
|
|
||||||
{
|
|
||||||
from: Actions.prepare,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: Actions.createProducts,
|
|
||||||
alias: "products",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
async ({ data }) => {
|
|
||||||
return {
|
|
||||||
alias: ProductHandlers.listProducts.aliases.ids,
|
|
||||||
value: data.products.map((product) => product.id),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
aggregateData(),
|
|
||||||
ProductHandlers.listProducts
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
])
|
])
|
||||||
|
|
||||||
WorkflowManager.register(Workflows.CreateProducts, workflowSteps, handlers)
|
WorkflowManager.register(Workflows.CreateProducts, workflowSteps, handlers)
|
||||||
@@ -297,4 +265,4 @@ WorkflowManager.register(Workflows.CreateProducts, workflowSteps, handlers)
|
|||||||
export const createProducts = exportWorkflow<
|
export const createProducts = exportWorkflow<
|
||||||
WorkflowTypes.ProductWorkflow.CreateProductsWorkflowInputDTO,
|
WorkflowTypes.ProductWorkflow.CreateProductsWorkflowInputDTO,
|
||||||
ProductTypes.ProductDTO[]
|
ProductTypes.ProductDTO[]
|
||||||
>(Workflows.CreateProducts, Actions.result)
|
>(Workflows.CreateProducts, CreateProductsActions.createProducts)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-products"
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from "./create-products-prepare-create-prices-compensation"
|
export * from "./create-products-prepare-create-prices-compensation"
|
||||||
export * from "./create-products-prepare-data"
|
|
||||||
-2
@@ -28,7 +28,6 @@ export type CreateProductsPreparedData = {
|
|||||||
ProductHandle,
|
ProductHandle,
|
||||||
VariantIndexAndPrices[]
|
VariantIndexAndPrices[]
|
||||||
>
|
>
|
||||||
config?: WorkflowTypes.CommonWorkflow.WorkflowInputConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProductsPrepareData({
|
export async function createProductsPrepareData({
|
||||||
@@ -145,7 +144,6 @@ export async function createProductsPrepareData({
|
|||||||
productsHandleShippingProfileIdMap,
|
productsHandleShippingProfileIdMap,
|
||||||
productsHandleSalesChannelsMap,
|
productsHandleSalesChannelsMap,
|
||||||
productsHandleVariantsIndexPricesMap,
|
productsHandleVariantsIndexPricesMap,
|
||||||
config: data.config,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./create-products-prepare-data"
|
||||||
export * from "./create-products"
|
export * from "./create-products"
|
||||||
export * from "./detach-sales-channel-from-products"
|
export * from "./detach-sales-channel-from-products"
|
||||||
export * from "./attach-sales-channel-to-products"
|
export * from "./attach-sales-channel-to-products"
|
||||||
|
|||||||
Reference in new issue
Block a user