chore: workflow internals improvementss (#9455)
This commit is contained in:
committed by
GitHub
parent
1b9379be62
commit
34d57870ad
@@ -1170,6 +1170,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
await deleteLineItemsWorkflow(appContainer).run({
|
||||
input: {
|
||||
cart_id: cart.id,
|
||||
ids: items.map((i) => i.id),
|
||||
},
|
||||
throwOnError: false,
|
||||
@@ -1211,6 +1212,7 @@ medusaIntegrationTestRunner({
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
cart_id: cart.id,
|
||||
ids: items.map((i) => i.id),
|
||||
},
|
||||
throwOnError: false,
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ProductStatus,
|
||||
PromotionRuleOperator,
|
||||
PromotionType,
|
||||
RuleOperator
|
||||
RuleOperator,
|
||||
} from "@medusajs/utils"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import {
|
||||
@@ -2131,6 +2131,7 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
|
||||
expect(response.status).toEqual(200)
|
||||
|
||||
expect(response.data.cart).toEqual(
|
||||
expect.objectContaining({
|
||||
id: cart.id,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MedusaError } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import {
|
||||
@@ -102,13 +103,14 @@ export const addShippingMethodToWorkflow = createWorkflow(
|
||||
return cart.shipping_methods.map((sm) => sm.id)
|
||||
})
|
||||
|
||||
removeShippingMethodFromCartStep({
|
||||
shipping_method_ids: currentShippingMethods,
|
||||
})
|
||||
|
||||
const shippingMethodsToAdd = addShippingMethodToCartStep({
|
||||
shipping_methods: shippingMethodInput,
|
||||
})
|
||||
const [, shippingMethodsToAdd] = parallelize(
|
||||
removeShippingMethodFromCartStep({
|
||||
shipping_method_ids: currentShippingMethods,
|
||||
}),
|
||||
addShippingMethodToCartStep({
|
||||
shipping_methods: shippingMethodInput,
|
||||
})
|
||||
)
|
||||
|
||||
updateTaxLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
|
||||
@@ -49,7 +49,6 @@ export const completeCartWorkflow = createWorkflow(
|
||||
name: completeCartWorkflowId,
|
||||
store: true,
|
||||
idempotent: true,
|
||||
// 3 days of retention time
|
||||
retentionTime: THREE_DAYS,
|
||||
},
|
||||
(
|
||||
|
||||
@@ -117,7 +117,10 @@ export const createCartWorkflow = createWorkflow(
|
||||
}
|
||||
|
||||
// If there is only one country in the region, we prepare a shipping address with that country's code.
|
||||
if (!data.input.shipping_address && data.region.countries.length === 1) {
|
||||
if (
|
||||
!data.input.shipping_address &&
|
||||
data.region.countries.length === 1
|
||||
) {
|
||||
data_.shipping_address = {
|
||||
country_code: data.region.countries[0].iso_2,
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
} from "@medusajs/framework/types"
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createStep,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { createRemoteLinkStep } from "../../common/steps/create-remote-links"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
@@ -52,9 +53,10 @@ export const createPaymentCollectionForCartWorkflow = createWorkflow(
|
||||
list: false,
|
||||
})
|
||||
|
||||
validateCartStep({ cart })
|
||||
|
||||
validateExistingPaymentCollectionStep({ cart })
|
||||
parallelize(
|
||||
validateCartStep({ cart }),
|
||||
validateExistingPaymentCollectionStep({ cart })
|
||||
)
|
||||
|
||||
const paymentData = transform({ cart }, ({ cart }) => {
|
||||
return {
|
||||
|
||||
@@ -4,13 +4,13 @@ import {
|
||||
} from "@medusajs/framework/types"
|
||||
import { MedusaError, PromotionActions } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
createHook,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../common"
|
||||
import {
|
||||
@@ -138,14 +138,13 @@ export const updateCartWorkflow = createWorkflow(
|
||||
list: false,
|
||||
}).config({ name: "refetch–cart" })
|
||||
|
||||
parallelize(
|
||||
refreshCartShippingMethodsStep({ cart }),
|
||||
updateTaxLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
cart_id: carts[0].id,
|
||||
},
|
||||
})
|
||||
)
|
||||
refreshCartShippingMethodsStep({ cart })
|
||||
|
||||
updateTaxLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
cart_id: carts[0].id,
|
||||
},
|
||||
})
|
||||
|
||||
updateCartPromotionsWorkflow.runAsStep({
|
||||
input: {
|
||||
|
||||
@@ -3,8 +3,12 @@ import {
|
||||
Logger,
|
||||
PaymentSessionDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
Modules,
|
||||
promiseAll,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export interface DeletePaymentSessionStepInput {
|
||||
ids: string[]
|
||||
@@ -29,32 +33,42 @@ export const deletePaymentSessionsStep = createStep(
|
||||
return new StepResponse([], null)
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
const select = [
|
||||
"provider_id",
|
||||
"currency_code",
|
||||
"amount",
|
||||
"data",
|
||||
"context",
|
||||
"payment_collection.id",
|
||||
]
|
||||
const select = [
|
||||
"provider_id",
|
||||
"currency_code",
|
||||
"amount",
|
||||
"data",
|
||||
"context",
|
||||
"payment_collection.id",
|
||||
]
|
||||
|
||||
const [session] = await service.listPaymentSessions({ id }, { select })
|
||||
const sessions = await service.listPaymentSessions({ id: ids }, { select })
|
||||
const sessionMap = new Map(sessions.map((s) => [s.id, s]))
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
for (const id of ids) {
|
||||
const session = sessionMap.get(id)!
|
||||
|
||||
// As this requires an external method call, we will try to delete as many successful calls
|
||||
// as possible and pass them over to the compensation step to be recreated if any of the
|
||||
// payment sessions fails to delete.
|
||||
try {
|
||||
await service.deletePaymentSession(id)
|
||||
const promise = service
|
||||
.deletePaymentSession(id)
|
||||
.then((res) => {
|
||||
deleted.push(session)
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(
|
||||
`Encountered an error when trying to delete payment session - ${id} - ${e}`
|
||||
)
|
||||
})
|
||||
|
||||
deleted.push(session)
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Encountered an error when trying to delete payment session - ${id} - ${e}`
|
||||
)
|
||||
}
|
||||
promises.push(promise)
|
||||
}
|
||||
|
||||
await promiseAll(promises)
|
||||
|
||||
return new StepResponse(
|
||||
deleted.map((d) => d.id),
|
||||
deleted
|
||||
|
||||
@@ -83,6 +83,14 @@ class DistributedTransaction extends EventEmitter {
|
||||
private readonly context: TransactionContext = new TransactionContext()
|
||||
private static keyValueStore: IDistributedTransactionStorage
|
||||
|
||||
/**
|
||||
* Store data during the life cycle of the current transaction execution.
|
||||
* Store non persistent data such as transformers results, temporary data, etc.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
#temporaryStorage = new Map<string, unknown>()
|
||||
|
||||
public static setStorage(storage: IDistributedTransactionStorage) {
|
||||
this.keyValueStore = storage
|
||||
}
|
||||
@@ -298,6 +306,18 @@ class DistributedTransaction extends EventEmitter {
|
||||
|
||||
await DistributedTransaction.keyValueStore.clearStepTimeout(this, step)
|
||||
}
|
||||
|
||||
public setTemporaryData(key: string, value: unknown) {
|
||||
this.#temporaryStorage.set(key, value)
|
||||
}
|
||||
|
||||
public getTemporaryData(key: string) {
|
||||
return this.#temporaryStorage.get(key)
|
||||
}
|
||||
|
||||
public hasTemporaryData(key: string) {
|
||||
return this.#temporaryStorage.has(key)
|
||||
}
|
||||
}
|
||||
|
||||
DistributedTransaction.setStorage(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
|
||||
import {
|
||||
createMedusaContainer,
|
||||
isDefined,
|
||||
isString,
|
||||
MedusaContext,
|
||||
MedusaContextType,
|
||||
MedusaError,
|
||||
MedusaModuleType,
|
||||
createMedusaContainer,
|
||||
isDefined,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import {
|
||||
@@ -107,9 +107,17 @@ export class LocalWorkflow {
|
||||
return resolved
|
||||
}
|
||||
|
||||
const wrappableMethods = Object.getOwnPropertyNames(resolved).filter(
|
||||
(key) => key !== "constructor"
|
||||
)
|
||||
|
||||
return new Proxy(resolved, {
|
||||
get: function (target, prop) {
|
||||
if (typeof target[prop] !== "function") {
|
||||
const shouldWrap =
|
||||
wrappableMethods.includes(prop as string) &&
|
||||
typeof target[prop] === "function"
|
||||
|
||||
if (!shouldWrap) {
|
||||
return target[prop]
|
||||
}
|
||||
|
||||
@@ -131,6 +139,7 @@ export class LocalWorkflow {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
@@ -369,9 +378,11 @@ export class LocalWorkflow {
|
||||
|
||||
await orchestrator.resume(transaction)
|
||||
|
||||
cleanUpEventListeners()
|
||||
|
||||
return transaction
|
||||
try {
|
||||
return transaction
|
||||
} finally {
|
||||
cleanUpEventListeners()
|
||||
}
|
||||
}
|
||||
|
||||
async getRunningTransaction(uniqueTransactionId: string, context?: Context) {
|
||||
@@ -406,9 +417,11 @@ export class LocalWorkflow {
|
||||
|
||||
await orchestrator.cancelTransaction(transaction)
|
||||
|
||||
cleanUpEventListeners()
|
||||
|
||||
return transaction
|
||||
try {
|
||||
return transaction
|
||||
} finally {
|
||||
cleanUpEventListeners()
|
||||
}
|
||||
}
|
||||
|
||||
async registerStepSuccess(
|
||||
@@ -433,9 +446,11 @@ export class LocalWorkflow {
|
||||
response
|
||||
)
|
||||
|
||||
cleanUpEventListeners()
|
||||
|
||||
return transaction
|
||||
try {
|
||||
return transaction
|
||||
} finally {
|
||||
cleanUpEventListeners()
|
||||
}
|
||||
}
|
||||
|
||||
async registerStepFailure(
|
||||
@@ -459,9 +474,11 @@ export class LocalWorkflow {
|
||||
handler(this.container_, context)
|
||||
)
|
||||
|
||||
cleanUpEventListeners()
|
||||
|
||||
return transaction
|
||||
try {
|
||||
return transaction
|
||||
} finally {
|
||||
cleanUpEventListeners()
|
||||
}
|
||||
}
|
||||
|
||||
setOptions(options: Partial<TransactionModelOptions>) {
|
||||
|
||||
@@ -1,36 +1,56 @@
|
||||
import { isObject } from "./is-object"
|
||||
import * as util from "node:util"
|
||||
|
||||
/**
|
||||
* In most casees, JSON.parse(JSON.stringify(obj)) is enough to deep copy an object.
|
||||
* But in some cases, it's not enough. For example, if the object contains a function or a proxy, it will be lost after JSON.parse(JSON.stringify(obj)).
|
||||
*
|
||||
* @param obj
|
||||
* @param cache
|
||||
*/
|
||||
export function deepCopy<
|
||||
T extends Record<any, any> | Record<any, any>[] = Record<any, any>,
|
||||
TOutput = T extends [] ? T[] : T
|
||||
>(obj: T): TOutput {
|
||||
>(obj: T, cache = new WeakMap()): TOutput {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj
|
||||
return obj as TOutput
|
||||
}
|
||||
|
||||
// Handle circular references with cache
|
||||
if (cache.has(obj)) {
|
||||
return cache.get(obj) as TOutput
|
||||
}
|
||||
|
||||
let copy: TOutput
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(obj)) {
|
||||
const copy: any[] = []
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
copy[i] = deepCopy(obj[i])
|
||||
}
|
||||
return copy as TOutput
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
const copy: Record<any, any> = {}
|
||||
for (let attr in obj) {
|
||||
if (obj.hasOwnProperty(attr)) {
|
||||
copy[attr] = deepCopy(obj[attr] as T)
|
||||
}
|
||||
}
|
||||
copy = [] as unknown as TOutput
|
||||
cache.set(obj, copy) // Add to cache before recursing
|
||||
;(obj as Array<any>).forEach((item, index) => {
|
||||
;(copy as Array<any>)[index] = deepCopy(item, cache)
|
||||
})
|
||||
return copy
|
||||
}
|
||||
|
||||
return obj
|
||||
// Handle objects
|
||||
if (isObject(obj)) {
|
||||
if (util.types.isProxy(obj)) {
|
||||
return obj as unknown as TOutput
|
||||
}
|
||||
|
||||
copy = {} as TOutput
|
||||
cache.set(obj, copy) // Add to cache before recursing
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
;(copy as Record<any, any>)[key] = deepCopy(
|
||||
(obj as Record<any, any>)[key],
|
||||
cache
|
||||
)
|
||||
})
|
||||
|
||||
return copy
|
||||
}
|
||||
|
||||
return obj as TOutput
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { EventBusTypes } from "@medusajs/types"
|
||||
import {
|
||||
EventBusTypes,
|
||||
InternalModuleDeclaration,
|
||||
MedusaContainer,
|
||||
} from "@medusajs/types"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
export abstract class AbstractEventBusModuleService
|
||||
implements EventBusTypes.IEventBusModuleService
|
||||
{
|
||||
protected isWorkerMode: boolean = true
|
||||
|
||||
protected eventToSubscribersMap_: Map<
|
||||
string | symbol,
|
||||
EventBusTypes.SubscriberDescriptor[]
|
||||
@@ -16,6 +22,14 @@ export abstract class AbstractEventBusModuleService
|
||||
return this.eventToSubscribersMap_
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
container: MedusaContainer,
|
||||
moduleOptions = {},
|
||||
moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
this.isWorkerMode = moduleDeclaration.worker_mode !== "server"
|
||||
}
|
||||
|
||||
abstract emit<T>(
|
||||
data: EventBusTypes.Message<T> | EventBusTypes.Message<T>[],
|
||||
options: Record<string, unknown>
|
||||
@@ -63,6 +77,10 @@ export abstract class AbstractEventBusModuleService
|
||||
subscriber: EventBusTypes.Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (!this.isWorkerMode) {
|
||||
return this
|
||||
}
|
||||
|
||||
if (typeof subscriber !== `function`) {
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
@@ -88,6 +106,10 @@ export abstract class AbstractEventBusModuleService
|
||||
subscriber: EventBusTypes.Subscriber,
|
||||
context: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (!this.isWorkerMode) {
|
||||
return this
|
||||
}
|
||||
|
||||
if (typeof subscriber !== `function`) {
|
||||
throw new Error("Subscriber must be a function")
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ function createContextualWorkflowRunner<
|
||||
events,
|
||||
flowMetadata,
|
||||
]
|
||||
const transaction = await method.apply(method, args)
|
||||
const transaction = await method.apply(method, args) as DistributedTransactionType
|
||||
|
||||
let errors = transaction.getErrors(TransactionHandlerType.INVOKE)
|
||||
|
||||
|
||||
@@ -150,55 +150,67 @@ export function applyStep<
|
||||
const ret = {
|
||||
__type: OrchestrationUtils.SymbolWorkflowStep,
|
||||
__step__: stepName,
|
||||
config: (localConfig: LocalStepConfig) => {
|
||||
const newStepName = localConfig.name ?? stepName
|
||||
const newConfig = {
|
||||
...stepConfig,
|
||||
...localConfig,
|
||||
}
|
||||
}
|
||||
|
||||
delete localConfig.name
|
||||
|
||||
this.handlers.set(newStepName, handler)
|
||||
|
||||
this.flow.replaceAction(stepConfig.uuid!, newStepName, newConfig)
|
||||
this.isAsync ||= !!(newConfig.async || newConfig.compensateAsync)
|
||||
|
||||
ret.__step__ = newStepName
|
||||
WorkflowManager.update(this.workflowId, this.flow, this.handlers)
|
||||
|
||||
const confRef = proxify(ret)
|
||||
|
||||
if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) {
|
||||
const flagSteps =
|
||||
global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]
|
||||
.steps
|
||||
|
||||
const idx = flagSteps.findIndex((a) => a.__step__ === ret.__step__)
|
||||
if (idx > -1) {
|
||||
flagSteps.splice(idx, 1)
|
||||
}
|
||||
flagSteps.push(confRef)
|
||||
}
|
||||
|
||||
return confRef as StepFunction<TInvokeInput, TInvokeResultOutput>
|
||||
},
|
||||
const refRet = proxify(ret) as WorkflowData<TInvokeResultOutput> & {
|
||||
if: (
|
||||
input: any,
|
||||
condition: (...args: any) => boolean | WorkflowData
|
||||
): WorkflowData<TInvokeResultOutput> => {
|
||||
if (typeof condition !== "function") {
|
||||
throw new Error("Condition must be a function")
|
||||
}
|
||||
|
||||
wrapConditionalStep(input, condition, handler)
|
||||
this.handlers.set(ret.__step__, handler)
|
||||
|
||||
return proxify(ret)
|
||||
},
|
||||
) => WorkflowData<TInvokeResultOutput>
|
||||
}
|
||||
|
||||
const refRet = proxify(ret) as WorkflowData<TInvokeResultOutput>
|
||||
refRet.config = (
|
||||
localConfig: { name?: string } & Omit<
|
||||
TransactionStepsDefinition,
|
||||
"next" | "uuid" | "action"
|
||||
>
|
||||
) => {
|
||||
const newStepName = localConfig.name ?? stepName
|
||||
const newConfig = {
|
||||
async: false,
|
||||
compensateAsync: false,
|
||||
...stepConfig,
|
||||
...localConfig,
|
||||
}
|
||||
|
||||
delete localConfig.name
|
||||
|
||||
this.handlers.set(newStepName, handler)
|
||||
|
||||
this.flow.replaceAction(stepConfig.uuid!, newStepName, newConfig)
|
||||
this.isAsync ||= !!(newConfig.async || newConfig.compensateAsync)
|
||||
|
||||
ret.__step__ = newStepName
|
||||
WorkflowManager.update(this.workflowId, this.flow, this.handlers)
|
||||
|
||||
//const confRef = proxify(ret)
|
||||
|
||||
if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) {
|
||||
const flagSteps =
|
||||
global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition].steps
|
||||
|
||||
const idx = flagSteps.findIndex((a) => a.__step__ === ret.__step__)
|
||||
if (idx > -1) {
|
||||
flagSteps.splice(idx, 1)
|
||||
}
|
||||
flagSteps.push(refRet)
|
||||
}
|
||||
|
||||
return refRet
|
||||
}
|
||||
refRet.if = (
|
||||
input: any,
|
||||
condition: (...args: any) => boolean | WorkflowData
|
||||
): WorkflowData<TInvokeResultOutput> => {
|
||||
if (typeof condition !== "function") {
|
||||
throw new Error("Condition must be a function")
|
||||
}
|
||||
|
||||
wrapConditionalStep(input, condition, handler)
|
||||
this.handlers.set(ret.__step__, handler)
|
||||
|
||||
return refRet
|
||||
}
|
||||
|
||||
if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) {
|
||||
global[
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WorkflowStepHandlerArguments } from "@medusajs/orchestration"
|
||||
import { OrchestrationUtils, deepCopy } from "@medusajs/utils"
|
||||
import { OrchestrationUtils } from "@medusajs/utils"
|
||||
import { ApplyStepOptions } from "../create-step"
|
||||
import {
|
||||
CreateWorkflowComposerContext,
|
||||
@@ -9,6 +9,37 @@ import {
|
||||
import { resolveValue } from "./resolve-value"
|
||||
import { StepResponse } from "./step-response"
|
||||
|
||||
function buildStepContext({
|
||||
action,
|
||||
stepArguments,
|
||||
}: {
|
||||
action: StepExecutionContext["action"]
|
||||
stepArguments: WorkflowStepHandlerArguments
|
||||
}) {
|
||||
const metadata = stepArguments.metadata
|
||||
const idempotencyKey = metadata.idempotency_key
|
||||
|
||||
stepArguments.context!.idempotencyKey = idempotencyKey
|
||||
|
||||
const flowMetadata = stepArguments.transaction.getFlow()?.metadata
|
||||
const executionContext: StepExecutionContext = {
|
||||
workflowId: metadata.model_id,
|
||||
stepName: metadata.action,
|
||||
action,
|
||||
idempotencyKey,
|
||||
attempt: metadata.attempt,
|
||||
container: stepArguments.container,
|
||||
metadata,
|
||||
eventGroupId:
|
||||
flowMetadata?.eventGroupId ?? stepArguments.context!.eventGroupId,
|
||||
parentStepIdempotencyKey: flowMetadata?.parentStepIdempotencyKey as string,
|
||||
transactionId: stepArguments.context!.transactionId,
|
||||
context: stepArguments.context!,
|
||||
}
|
||||
|
||||
return executionContext
|
||||
}
|
||||
|
||||
export function createStepHandler<
|
||||
TInvokeInput,
|
||||
TStepInput extends {
|
||||
@@ -32,27 +63,10 @@ export function createStepHandler<
|
||||
) {
|
||||
const handler = {
|
||||
invoke: async (stepArguments: WorkflowStepHandlerArguments) => {
|
||||
const metadata = stepArguments.metadata
|
||||
const idempotencyKey = metadata.idempotency_key
|
||||
|
||||
stepArguments.context!.idempotencyKey = idempotencyKey
|
||||
|
||||
const flowMetadata = stepArguments.transaction.getFlow()?.metadata
|
||||
const executionContext: StepExecutionContext = {
|
||||
workflowId: metadata.model_id,
|
||||
stepName: metadata.action,
|
||||
const executionContext = buildStepContext({
|
||||
action: "invoke",
|
||||
idempotencyKey,
|
||||
attempt: metadata.attempt,
|
||||
container: stepArguments.container,
|
||||
metadata,
|
||||
eventGroupId:
|
||||
flowMetadata?.eventGroupId ?? stepArguments.context!.eventGroupId,
|
||||
parentStepIdempotencyKey:
|
||||
flowMetadata?.parentStepIdempotencyKey as string,
|
||||
transactionId: stepArguments.context!.transactionId,
|
||||
context: stepArguments.context!,
|
||||
}
|
||||
stepArguments,
|
||||
})
|
||||
|
||||
const argInput = input ? await resolveValue(input, stepArguments) : {}
|
||||
const stepResponse: StepResponse<any, any> = await invokeFn.apply(this, [
|
||||
@@ -72,31 +86,16 @@ export function createStepHandler<
|
||||
},
|
||||
compensate: compensateFn
|
||||
? async (stepArguments: WorkflowStepHandlerArguments) => {
|
||||
const metadata = stepArguments.metadata
|
||||
const idempotencyKey = metadata.idempotency_key
|
||||
|
||||
stepArguments.context!.idempotencyKey = idempotencyKey
|
||||
|
||||
const flowMetadata = stepArguments.transaction.getFlow()?.metadata
|
||||
const executionContext: StepExecutionContext = {
|
||||
workflowId: metadata.model_id,
|
||||
stepName: metadata.action,
|
||||
const executionContext = buildStepContext({
|
||||
action: "compensate",
|
||||
idempotencyKey,
|
||||
parentStepIdempotencyKey:
|
||||
flowMetadata?.parentStepIdempotencyKey as string,
|
||||
attempt: metadata.attempt,
|
||||
container: stepArguments.container,
|
||||
metadata,
|
||||
context: stepArguments.context!,
|
||||
}
|
||||
stepArguments,
|
||||
})
|
||||
|
||||
const stepOutput = (stepArguments.invoke[stepName] as any)?.output
|
||||
const invokeResult =
|
||||
stepOutput?.__type === OrchestrationUtils.SymbolWorkflowStepResponse
|
||||
? stepOutput.compensateInput &&
|
||||
deepCopy(stepOutput.compensateInput)
|
||||
: stepOutput && deepCopy(stepOutput)
|
||||
? stepOutput.compensateInput
|
||||
: stepOutput
|
||||
|
||||
const args = [invokeResult, executionContext]
|
||||
const output = await compensateFn.apply(this, args)
|
||||
|
||||
@@ -10,7 +10,7 @@ export function proxify<T>(obj: WorkflowData<any>): T {
|
||||
return target[prop]
|
||||
}
|
||||
|
||||
return transform(target[prop], async function (input, context) {
|
||||
return transform({}, async function (_, context) {
|
||||
const { invoke } = context as WorkflowTransactionContext
|
||||
let output =
|
||||
target.__type === OrchestrationUtils.SymbolInputReference ||
|
||||
@@ -19,9 +19,8 @@ export function proxify<T>(obj: WorkflowData<any>): T {
|
||||
: invoke?.[obj.__step__]?.output
|
||||
|
||||
output = await resolveValue(output, context)
|
||||
output = output?.[prop]
|
||||
|
||||
return output && JSON.parse(JSON.stringify(output))
|
||||
return output?.[prop]
|
||||
})
|
||||
},
|
||||
}) as unknown as T
|
||||
|
||||
@@ -3,31 +3,35 @@ import { deepCopy, OrchestrationUtils, promiseAll } from "@medusajs/utils"
|
||||
async function resolveProperty(property, transactionContext) {
|
||||
const { invoke: invokeRes } = transactionContext
|
||||
|
||||
let res
|
||||
|
||||
if (property?.__type === OrchestrationUtils.SymbolInputReference) {
|
||||
return transactionContext.payload
|
||||
res = transactionContext.payload
|
||||
} else if (
|
||||
property?.__type === OrchestrationUtils.SymbolMedusaWorkflowResponse
|
||||
) {
|
||||
return resolveValue(property.$result, transactionContext)
|
||||
res = await resolveValue(property.$result, transactionContext)
|
||||
} else if (
|
||||
property?.__type === OrchestrationUtils.SymbolWorkflowStepTransformer
|
||||
) {
|
||||
return await property.__resolver(transactionContext)
|
||||
res = await property.__resolver(transactionContext)
|
||||
} else if (property?.__type === OrchestrationUtils.SymbolWorkflowStep) {
|
||||
const output =
|
||||
invokeRes[property.__step__]?.output ?? invokeRes[property.__step__]
|
||||
if (output?.__type === OrchestrationUtils.SymbolWorkflowStepResponse) {
|
||||
return output.output
|
||||
res = output.output
|
||||
} else {
|
||||
res = output
|
||||
}
|
||||
|
||||
return output
|
||||
} else if (
|
||||
property?.__type === OrchestrationUtils.SymbolWorkflowStepResponse
|
||||
) {
|
||||
return property.output
|
||||
res = property.output
|
||||
} else {
|
||||
return property
|
||||
res = property
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,9 +57,8 @@ export async function resolveValue(input, transactionContext) {
|
||||
}
|
||||
|
||||
for (const key of Object.keys(inputTOUnwrap)) {
|
||||
parentRef[key] = await resolveProperty(
|
||||
inputTOUnwrap[key],
|
||||
transactionContext
|
||||
parentRef[key] = deepCopy(
|
||||
await resolveProperty(inputTOUnwrap[key], transactionContext)
|
||||
)
|
||||
|
||||
if (typeof parentRef[key] === "object") {
|
||||
@@ -68,8 +71,8 @@ export async function resolveValue(input, transactionContext) {
|
||||
|
||||
const copiedInput =
|
||||
input?.__type === OrchestrationUtils.SymbolWorkflowWorkflowData
|
||||
? deepCopy(input.output)
|
||||
: deepCopy(input)
|
||||
? input.output
|
||||
: input
|
||||
|
||||
const result = copiedInput?.__type
|
||||
? await resolveProperty(copiedInput, transactionContext)
|
||||
|
||||
@@ -2,6 +2,11 @@ import { resolveValue } from "./helpers"
|
||||
import { StepExecutionContext, WorkflowData } from "./type"
|
||||
import { proxify } from "./helpers/proxy"
|
||||
import { OrchestrationUtils } from "@medusajs/utils"
|
||||
import { ulid } from "ulid"
|
||||
import {
|
||||
TransactionContext,
|
||||
WorkflowStepHandlerArguments,
|
||||
} from "@medusajs/orchestration"
|
||||
|
||||
type Func1<T extends object | WorkflowData, U> = (
|
||||
input: T extends WorkflowData<infer U>
|
||||
@@ -158,16 +163,26 @@ export function transform(
|
||||
values: any | any[],
|
||||
...functions: Function[]
|
||||
): unknown {
|
||||
const uniqId = ulid()
|
||||
|
||||
const ret = {
|
||||
__id: uniqId,
|
||||
__type: OrchestrationUtils.SymbolWorkflowStepTransformer,
|
||||
__resolver: undefined,
|
||||
}
|
||||
|
||||
const returnFn = async function (transactionContext): Promise<any> {
|
||||
const allValues = await resolveValue(values, transactionContext)
|
||||
const stepValue = allValues
|
||||
? JSON.parse(JSON.stringify(allValues))
|
||||
: allValues
|
||||
const returnFn = async function (
|
||||
// If a transformer is returned as the result of a workflow, then at this point the workflow is entirely done, in that case we have a TransactionContext
|
||||
transactionContext: WorkflowStepHandlerArguments | TransactionContext
|
||||
): Promise<any> {
|
||||
if ("transaction" in transactionContext) {
|
||||
const temporaryDataKey = `${transactionContext.transaction.modelId}_${transactionContext.transaction.transactionId}_${uniqId}`
|
||||
|
||||
if (transactionContext.transaction.hasTemporaryData(temporaryDataKey)) {
|
||||
return transactionContext.transaction.getTemporaryData(temporaryDataKey)
|
||||
}
|
||||
}
|
||||
|
||||
const stepValue = await resolveValue(values, transactionContext)
|
||||
|
||||
let finalResult
|
||||
for (let i = 0; i < functions.length; i++) {
|
||||
@@ -177,6 +192,15 @@ export function transform(
|
||||
finalResult = await fn.apply(fn, [arg, transactionContext])
|
||||
}
|
||||
|
||||
if ("transaction" in transactionContext) {
|
||||
const temporaryDataKey = `${transactionContext.transaction.modelId}_${transactionContext.transaction.transactionId}_${uniqId}`
|
||||
|
||||
transactionContext.transaction.setTemporaryData(
|
||||
temporaryDataKey,
|
||||
finalResult
|
||||
)
|
||||
}
|
||||
|
||||
return finalResult
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ export function when(input, condition) {
|
||||
if (ret?.__type !== OrchestrationUtils.SymbolWorkflowStep) {
|
||||
const retStep = createStep(
|
||||
"when-then-" + ulid(),
|
||||
() => new StepResponse(ret)
|
||||
({ input }: { input: any }) => new StepResponse(input)
|
||||
)
|
||||
returnStep = retStep()
|
||||
returnStep = retStep({ input: ret })
|
||||
}
|
||||
|
||||
for (const step of applyCondition) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const POST = async (
|
||||
|
||||
await addToCartWorkflow(req.scope).run({
|
||||
input: workflowInput,
|
||||
})
|
||||
} as any)
|
||||
|
||||
const updatedCart = await refetchCart(
|
||||
req.params.id,
|
||||
|
||||
@@ -2,6 +2,7 @@ export const defaultStoreCartFields = [
|
||||
"id",
|
||||
"currency_code",
|
||||
"email",
|
||||
"region_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"completed_at",
|
||||
@@ -33,6 +34,7 @@ export const defaultStoreCartFields = [
|
||||
"promotions.application_method.type",
|
||||
"promotions.application_method.currency_code",
|
||||
"items.id",
|
||||
"items.product.id",
|
||||
"items.variant_id",
|
||||
"items.product_id",
|
||||
"items.product.categories.id",
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("LocalEventBusService", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
eventBus = new LocalEventBusService(moduleDeps as any)
|
||||
eventBus = new LocalEventBusService(moduleDeps as any, {}, {} as any)
|
||||
eventEmitter = (eventBus as any).eventEmitter_
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Event,
|
||||
EventBusTypes,
|
||||
InternalModuleDeclaration,
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
Message,
|
||||
@@ -25,7 +26,11 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
||||
protected readonly eventEmitter_: EventEmitter
|
||||
protected groupedEventsMap_: StagingQueueType
|
||||
|
||||
constructor({ logger }: MedusaContainer & InjectedDependencies) {
|
||||
constructor(
|
||||
{ logger }: MedusaContainer & InjectedDependencies,
|
||||
moduleOptions = {},
|
||||
moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments)
|
||||
@@ -54,16 +59,16 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
||||
eventData.name
|
||||
)
|
||||
|
||||
if (eventListenersCount === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!options.internal && !eventData.options?.internal) {
|
||||
this.logger_?.info(
|
||||
`Processing ${eventData.name} which has ${eventListenersCount} subscribers`
|
||||
)
|
||||
}
|
||||
|
||||
if (eventListenersCount === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.groupOrEmitEvent(eventData)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +119,10 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
||||
}
|
||||
|
||||
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||
if (!this.isWorkerMode) {
|
||||
return this
|
||||
}
|
||||
|
||||
const randId = ulid()
|
||||
this.storeSubscribers({ event, subscriberId: randId, subscriber })
|
||||
this.eventEmitter_.on(event, async (data: Event) => {
|
||||
@@ -133,6 +142,10 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
|
||||
subscriber: Subscriber,
|
||||
context?: EventBusTypes.SubscriberContext
|
||||
): this {
|
||||
if (!this.isWorkerMode) {
|
||||
return this
|
||||
}
|
||||
|
||||
const existingSubscribers = this.eventToSubscribersMap_.get(event)
|
||||
|
||||
if (existingSubscribers?.length) {
|
||||
|
||||
@@ -60,8 +60,7 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
|
||||
})
|
||||
|
||||
// Register our worker to handle emit calls
|
||||
const shouldStartWorker = moduleDeclaration.worker_mode !== "server"
|
||||
if (shouldStartWorker) {
|
||||
if (this.isWorkerMode) {
|
||||
this.bullWorker_ = new Worker(
|
||||
moduleOptions.queueName ?? "events-queue",
|
||||
this.worker_,
|
||||
@@ -116,7 +115,7 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
|
||||
// options for a particular event
|
||||
...eventData.options,
|
||||
},
|
||||
}
|
||||
} as any
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -140,13 +140,10 @@ export class InMemoryDistributedTransactionStorage
|
||||
})
|
||||
}
|
||||
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
const parsedData = JSON.parse(stringifiedData)
|
||||
|
||||
if (hasFinished && !retentionTime && !idempotent) {
|
||||
await this.deleteFromDb(parsedData)
|
||||
await this.deleteFromDb(data)
|
||||
} else {
|
||||
await this.saveToDb(parsedData)
|
||||
await this.saveToDb(data)
|
||||
}
|
||||
|
||||
if (hasFinished) {
|
||||
|
||||
@@ -236,7 +236,6 @@ export class RedisDistributedTransactionStorage
|
||||
}
|
||||
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
const parsedData = JSON.parse(stringifiedData)
|
||||
|
||||
if (!hasFinished) {
|
||||
if (ttl) {
|
||||
@@ -247,9 +246,9 @@ export class RedisDistributedTransactionStorage
|
||||
}
|
||||
|
||||
if (hasFinished && !retentionTime && !idempotent) {
|
||||
await this.deleteFromDb(parsedData)
|
||||
await this.deleteFromDb(data)
|
||||
} else {
|
||||
await this.saveToDb(parsedData)
|
||||
await this.saveToDb(data)
|
||||
}
|
||||
|
||||
if (hasFinished) {
|
||||
|
||||
Reference in New Issue
Block a user