chore: added TSDoc for workflow utility functions (#5674)

* feat(translation): Init plugin

* Create migration and entity

* Start service implementation

* fix typo

* typings

* config

* lang -> locale

* chnge config

* link

* update link

* update export

* workflow composer (wip)

* workflow step/workflow

* simplify api

* Add structured tests

* Add step name onto the step function for future usage

* new abstraction level of exported workflow

* cleanup

* rm step 4

* Add parallelize support to workflow composition

* add Symbols and support primitive types as input

* rem .only

* eslint

* cleanup + test

* resultFrom

* prevent undefined from crashing

* finalise tests

* chore: transform

* chore: tests and transform type

* move translation to my dummy

* chore: copy before transform

* yarn.lock

* chore: shortcut to property access

* Add type safety

* WIP typesafety

* WIP

* finalise typings

* remove extends unknown

* WIP

* finalise transform typings

* finalise transform typings

* workflow typs

* More typings in invoke and compensate

* rm comment

* Context as the last args

* fix step function type

* types

* fixes

* fixes compose

* chore: transform

* chore: fix tests and transform Proxy

* chore: args length

* uncomment

* chore: array fill

* WIP fix filler

* WIP fix filler

* chore: remove only

* apply gap filler to create step invoke

* context first

* fixes

* wofkrlow ts documentation

* wofkrlow ts documentation

* wofkrlow ts documentation

* wofkrlow ts documentation

* wofkrlow ts documentation

* chore: hook

* hook types

* update types

* don't loose previous iteration

* update implementation

* fix some tests part 1

* finalise typings

* rm new lines

* fixes

* wip

* fixes

* fix tests

* simplify types

* simplify types

* update export

* improve types exclusion compensateInput

* allow a workflow to return plain object composed of stepReturn properties

* only allow one handler for the hook registration

* only allow one handler for the hook registration

* workflow loading

* lint

* lint

* lint

* finalise tests

* try to fix ci

* try to fix ci

* remove corepack step

* cleanup

* cleanup

* cleanup

* chore: context as 2nd argumentq

* added tsdoc for some workflow functions

* Add support for StepResponse and re work the typings

* changeset

* chore: invoke output as default compensate input

* copy data

* copy data

* fix createWorkflow result

* added tsdoc to remaining utility functions

* rm test file

* proxify input and transformer as well

* transformer should re run + type update

* rework step response

* allow void return from steps

* updates to the TSDocs

* address comments

* address PR feedback

* add await for API Route examples

* ignore documenting hooks

---------

Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com>
Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Shahed Nasser
2023-11-23 16:38:43 +00:00
committed by GitHub
parent 1e39a95f8a
commit f3e20379b0
28 changed files with 687 additions and 175 deletions
@@ -16,10 +16,25 @@ import {
} from "./type"
import { proxify } from "./helpers/proxy"
/**
* The type of invocation function passed to a step.
*
* @typeParam TInput - The type of the input that the function expects.
* @typeParam TOutput - The type of the output that the function returns.
* @typeParam TCompensateInput - The type of the input that the compensation function expects.
*
* @returns The expected output based on the type parameter `TOutput`.
*/
type InvokeFn<TInput extends object, TOutput, TCompensateInput> = (
/**
* The input of the step.
*/
input: {
[Key in keyof TInput]: TInput[Key]
},
/**
* The step's context.
*/
context: StepExecutionContext
) =>
| void
@@ -32,8 +47,22 @@ type InvokeFn<TInput extends object, TOutput, TCompensateInput> = (
TCompensateInput extends undefined ? TOutput : TCompensateInput
>>
/**
* The type of compensation function passed to a step.
*
* @typeParam T -
* The type of the argument passed to the compensation function. If not specified, then it will be the same type as the invocation function's output.
*
* @returns There's no expected type to be returned by the compensation function.
*/
type CompensateFn<T> = (
/**
* The argument passed to the compensation function.
*/
input: T | undefined,
/**
* The step's context.
*/
context: StepExecutionContext
) => unknown | Promise<unknown>
@@ -56,6 +85,8 @@ interface ApplyStepOptions<
}
/**
* @internal
*
* Internal function to create the invoke and compensate handler for a step.
* This is where the inputs and context are passed to the underlying invoke and compensate function.
*
@@ -152,55 +183,77 @@ function applyStep<
}
/**
* Function which will create a StepFunction to be used inside a createWorkflow composer function.
* This function will return a function which can be used to bind the step to a workflow.
* The types of the input to be passed to the step function is defined by the generic of the invoke function provided.
* This function creates a {@link StepFunction} that can be used as a step in a workflow constructed by the {@link createWorkflow} function.
*
* @param name
* @param invokeFn
* @param compensateFn
* @typeParam TInvokeInput - The type of the expected input parameter to the invocation function.
* @typeParam TInvokeResultOutput - The type of the expected output parameter of the invocation function.
* @typeParam TInvokeResultCompensateInput - The type of the expected input parameter to the compensation function.
*
* @returns A step function to be used in a workflow.
*
* @example
* ```ts
* import {
* createStep,
* StepResponse,
* StepExecutionContext,
* WorkflowData
* } from "@medusajs/workflows"
*
* interface CreateProductInput {
* title: string
* }
*
* interface CreateProductOutput {
* product: { id: string; title: string }
* compensateInput: {
* product_id: string
* }
* }
*
* export const createProductStep = createStep(
* "createProductStep",
* async function (input: Step1Input, context: StepExecutionContext): Promise<CreateProductOutput> {
* const productService = context.container.resolve("productService")
* const product = await productService.create(input)
* return {
* product,
* compensateInput: {
* product_id: product.id
* }
* }
* },
* async function (input: { product_id: string }, context: StepExecutionContext) {
* const productService = context.container.resolve("productService")
* "createProductStep",
* async function (
* input: CreateProductInput,
* context
* ) {
* const productService = context.container.resolve(
* "productService"
* )
* const product = await productService.create(input)
* return new StepResponse({
* product
* }, {
* product_id: product.id
* })
* },
* async function (
* input,
* context
* ) {
* const productService = context.container.resolve(
* "productService"
* )
* await productService.delete(input.product_id)
* })
* }
* )
*/
export function createStep<
TInvokeInput extends object,
TInvokeResultOutput,
TInvokeResultCompensateInput
>(
/**
* The name of the step.
*/
name: string,
/**
* An invocation function that will be executed when the workflow is executed. The function must return an instance of {@link StepResponse}. The constructor of {@link StepResponse}
* accepts the output of the step as a first argument, and optionally as a second argument the data to be passed to the compensation function as a parameter.
*/
invokeFn: InvokeFn<
TInvokeInput,
TInvokeResultOutput,
TInvokeResultCompensateInput
>,
/**
* A compensation function that's executed if an error occurs in the workflow. It's used to roll-back actions when errors occur.
* It accepts as a parameter the second argument passed to the constructor of the {@link StepResponse} instance returned by the invocation function. If the
* invocation function doesn't pass the second argument to `StepResponse` constructor, the compensation function receives the first argument
* passed to the `StepResponse` constructor instead.
*/
compensateFn?: CompensateFn<TInvokeResultCompensateInput>
): StepFunction<TInvokeInput, TInvokeResultOutput> {
const stepName = name ?? invokeFn.name
@@ -20,6 +20,53 @@ import { proxify } from "./helpers/proxy"
global[SymbolMedusaWorkflowComposerContext] = null
/**
* An exported workflow, which is the type of a workflow constructed by the {@link createWorkflow} function. The exported workflow can be invoked to create
* an executable workflow, optionally within a specified container. So, to execute the workflow, you must invoke the exported workflow, then run the
* `run` method of the exported workflow.
*
* @example
* To execute a workflow:
*
* ```ts
* myWorkflow()
* .run({
* input: {
* name: "John"
* }
* })
* .then(({ result }) => {
* console.log(result)
* })
* ```
*
* To specify the container of the workflow, you can pass it as an argument to the call of the exported workflow. This is necessary when executing the workflow
* within a Medusa resource such as an API Route or a Subscriber.
*
* For example:
*
* ```ts
* import type {
* MedusaRequest,
* MedusaResponse
* } from "@medusajs/medusa";
* import myWorkflow from "../../../workflows/hello-world";
*
* export async function GET(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result } = await myWorkflow(req.scope)
* .run({
* input: {
* name: req.query.name as string
* }
* })
*
* res.send(result)
* }
* ```
*/
type ReturnWorkflow<TData, TResult, THooks extends Record<string, Function>> = {
<TDataOverride = undefined, TResultOverride = undefined>(
container?: LoadedModule[] | MedusaContainer
@@ -37,32 +84,58 @@ type ReturnWorkflow<TData, TResult, THooks extends Record<string, Function>> = {
} & THooks
/**
* Creates a new workflow with the given name and composer function.
* The composer function will compose the workflow by using the step, parallelize and other util functions that
* will allow to define the flow of event of a workflow.
* This function creates a workflow with the provided name and a constructor function.
* The constructor function builds the workflow from steps created by the {@link createStep} function.
* The returned workflow is an exported workflow of type {@link ReturnWorkflow}, meaning it's not executed right away. To execute it,
* invoke the exported workflow, then run its `run` method.
*
* @param name
* @param composer
* @typeParam TData - The type of the input passed to the composer function.
* @typeParam TResult - The type of the output returned by the composer function.
* @typeParam THooks - The type of hooks defined in the workflow.
*
* @returns The created workflow. You can later execute the workflow by invoking it, then using its `run` method.
*
* @example
* ```ts
* import { createWorkflow, WorkflowData } from "@medusajs/workflows"
* import { createProductStep, getProductStep, createPricesStep } from "./steps"
* import { createWorkflow } from "@medusajs/workflows"
* import { MedusaRequest, MedusaResponse, Product } from "@medusajs/medusa"
* import {
* createProductStep,
* getProductStep,
* createPricesStep
* } from "./steps"
*
* interface MyWorkflowData {
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow("my-workflow", (input: WorkflowData<MyWorkflowData>) => {
* // Everything here will be executed and resolved later during the execution. Including the data access.
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow", (input) => {
* // Everything here will be executed and resolved later
* // during the execution. Including the data access.
*
* const product = createProductStep(input)
* const prices = createPricesStep(product)
* const product = createProductStep(input)
* const prices = createPricesStep(product)
* return getProductStep(product.id)
* }
* )
*
* const id = product.id
* return getProductStep(product.id)
* })
* ```
* export async function GET(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result: product } = await myWorkflow(req.scope)
* .run({
* input: {
* title: "Shirt"
* }
* })
*
* res.json({
* product
* })
* }
*/
export function createWorkflow<
@@ -70,7 +143,15 @@ export function createWorkflow<
TResult,
THooks extends Record<string, Function> = Record<string, Function>
>(
/**
* The name of the workflow.
*/
name: string,
/**
* The constructor function that is executed when the `run` method in {@link ReturnWorkflow} is used.
* The function can't be an arrow function or an asynchronus function. It also can't directly manipulate data.
* You'll have to use the {@link transform} function if you need to directly manipulate data.
*/
composer: (input: WorkflowData<TData>) =>
| void
| WorkflowData<TResult>
@@ -30,6 +30,9 @@ async function resolveProperty(property, transactionContext) {
}
}
/**
* @internal
*/
export async function resolveValue(input, transactionContext) {
const unwrapInput = async (
inputTOUnwrap: Record<string, unknown>,
@@ -1,27 +1,64 @@
import { SymbolWorkflowStepResponse } from "./symbol"
/**
* This class is used to create the response returned by a step. A step return its data by returning an instance of `StepResponse`.
*
* @typeParam TOutput - The type of the output of the step.
* @typeParam TCompensateInput -
* The type of the compensation input. If the step doesn't specify any compensation input, then the type of `TCompensateInput` is the same
* as that of `TOutput`.
*/
export class StepResponse<TOutput, TCompensateInput = TOutput> {
readonly #__type = SymbolWorkflowStepResponse
readonly #output: TOutput
readonly #compensateInput?: TCompensateInput
constructor(output: TOutput, compensateInput?: TCompensateInput) {
/**
* The constructor of the StepResponse
*
* @typeParam TOutput - The type of the output of the step.
* @typeParam TCompensateInput -
* The type of the compensation input. If the step doesn't specify any compensation input, then the type of `TCompensateInput` is the same
* as that of `TOutput`.
*/
constructor(
/**
* The output of the step.
*/
output: TOutput,
/**
* The input to be passed as a parameter to the step's compensation function. If not provided, the `output` will be provided instead.
*/
compensateInput?: TCompensateInput
) {
this.#output = output
this.#compensateInput = (compensateInput ?? output) as TCompensateInput
}
/**
* @internal
*/
get __type() {
return this.#__type
}
/**
* @internal
*/
get output(): TOutput {
return this.#output
}
/**
* @internal
*/
get compensateInput(): TCompensateInput {
return this.#compensateInput as TCompensateInput
}
/**
* @internal
*/
toJSON() {
return {
__type: this.#__type,
+94 -1
View File
@@ -9,7 +9,100 @@ import {
WorkflowData,
} from "./type"
export function hook<TOutput>(name: string, value: any): WorkflowData<TOutput> {
/**
*
* @ignore
*
* This function allows you to add hooks in your workflow that provide access to some data. Then, consumers of that workflow can add a handler function that performs
* an action with the provided data or modify it.
*
* For example, in a "create product" workflow, you may add a hook after the product is created, providing access to the created product.
* Then, developers using that workflow can hook into that point to access the product, modify its attributes, then return the updated product.
*
* @typeParam TOutput - The expected output of the hook's handler function.
* @returns The output of handler functions of this hook. If there are no handler functions, the output is `undefined`.
*
* @example
* import {
* createWorkflow,
* StepExecutionContext,
* hook,
* transform
* } from "@medusajs/workflows"
* import {
* createProductStep,
* getProductStep,
* createPricesStep
* } from "./steps"
* import {
* MedusaRequest,
* MedusaResponse,
* Product, ProductService
* } from "@medusajs/medusa"
*
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow",
* function (input) {
* const product = createProductStep(input)
*
* const hookProduct = hook<Product>("createdProductHook", product)
*
* const newProduct = transform({
* product,
* hookProduct
* }, (input) => {
* return input.hookProduct || input.product
* })
*
* const prices = createPricesStep(newProduct)
*
* return getProductStep(product.id)
* }
* )
*
* myWorkflow.createdProductHook(
* async (product, context: StepExecutionContext) => {
* const productService: ProductService = context.container.resolve("productService")
*
* const updatedProduct = await productService.update(product.id, {
* description: "a cool shirt"
* })
*
* return updatedProduct
* })
*
* export async function POST(
* req: MedusaRequest,
* res: MedusaResponse
* ) {
* const { result: product } = await myWorkflow(req.scope)
* .run({
* input: {
* title: req.body.title
* }
* })
*
* res.json({
* product
* })
* }
*/
export function hook<TOutput>(
/**
* The name of the hook. This will be used by the consumer to add a handler method for the hook.
*/
name: string,
/**
* The data that a handler function receives as a parameter.
*/
value: any
): WorkflowData<TOutput> {
const hookBinder = (
global[SymbolMedusaWorkflowComposerContext] as CreateWorkflowComposerContext
).hookBinder
@@ -2,32 +2,44 @@ import { CreateWorkflowComposerContext, WorkflowData } from "./type"
import { SymbolMedusaWorkflowComposerContext } from "./helpers"
/**
* Parallelize multiple steps.
* The steps will be run in parallel. The result of each step will be returned as part of the result array.
* Each StepResult can be accessed from the resulted array in the order they were passed to the parallelize function.
* This function is used to run multiple steps in parallel. The result of each step will be returned as part of the result array.
*
* @param steps
* @typeParam TResult - The type of the expected result.
*
* @returns The step results. The results are ordered in the array by the order they're passed in the function's parameter.
*
* @example
* ```ts
* import { createWorkflow, WorkflowData, parallelize } from "@medusajs/workflows"
* import { createProductStep, getProductStep, createPricesStep, attachProductToSalesChannelStep } from "./steps"
* import {
* createWorkflow,
* parallelize
* } from "@medusajs/workflows"
* import {
* createProductStep,
* getProductStep,
* createPricesStep,
* attachProductToSalesChannelStep
* } from "./steps"
*
* interface MyWorkflowData {
* interface WorkflowInput {
* title: string
* }
*
* const myWorkflow = createWorkflow("my-workflow", (input: WorkflowData<MyWorkflowData>) => {
* const product = createProductStep(input)
* const myWorkflow = createWorkflow<
* WorkflowInput,
* Product
* >("my-workflow", (input) => {
* const product = createProductStep(input)
*
* const [prices, productSalesChannel] = parallelize(
* const [prices, productSalesChannel] = parallelize(
* createPricesStep(product),
* attachProductToSalesChannelStep(product)
* )
* )
*
* const id = product.id
* return getProductStep(product.id)
* })
* const id = product.id
* return getProductStep(product.id)
* }
* )
*/
export function parallelize<TResult extends WorkflowData[]>(
...steps: TResult
@@ -13,14 +13,67 @@ type Func1<T extends object | WorkflowData, U> = (
type Func<T, U> = (input: T, context: StepExecutionContext) => U | Promise<U>
/**
*
* This function transforms the output of other utility functions.
*
* For example, if you're using the value(s) of some step(s) as an input to a later step. As you can't directly manipulate data in the workflow constructor function passed to {@link createWorkflow},
* the `transform` function provides access to the runtime value of the step(s) output so that you can manipulate them.
*
* Another example is if you're using the runtime value of some step(s) as the output of a workflow.
*
* If you're also retrieving the output of a hook and want to check if its value is set, you must use a workflow to get the runtime value of that hook.
*
* @returns There's no expected value to be returned by the `transform` function.
*
* @example
* import {
* createWorkflow,
* transform
* } from "@medusajs/workflows"
* import { step1, step2 } from "./steps"
*
* type WorkflowInput = {
* name: string
* }
*
* type WorkflowOutput = {
* message: string
* }
*
* const myWorkflow = createWorkflow<
* WorkflowInput,
* WorkflowOutput
* >
* ("hello-world", (input) => {
* const str1 = step1(input)
* const str2 = step2(input)
*
* return transform({
* str1,
* str2
* }, (input) => ({
* message: `${input.str1}${input.str2}`
* }))
* })
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RFinal>(
/**
* The output(s) of other step functions.
*/
values: T,
/**
* The transform function used to perform action on the runtime values of the provided `values`.
*/
...func:
| [Func1<T, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RFinal>(
@@ -30,6 +83,9 @@ export function transform<T extends object | WorkflowData, RA, RFinal>(
| [Func1<T, RA>, Func<RA, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RFinal>(
@@ -40,6 +96,9 @@ export function transform<T extends object | WorkflowData, RA, RB, RFinal>(
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RFinal>(
@@ -51,6 +110,9 @@ export function transform<T extends object | WorkflowData, RA, RB, RC, RFinal>(
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RFinal>(
@@ -63,6 +125,9 @@ export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RFina
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RE, RFinal>(
@@ -76,6 +141,9 @@ export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RE, R
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RE>, Func<RE, RFinal>]
): WorkflowData<RFinal>
/**
* @internal
*/
// prettier-ignore
// eslint-disable-next-line max-len
export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RE, RF, RFinal>(
@@ -90,14 +158,6 @@ export function transform<T extends object | WorkflowData, RA, RB, RC, RD, RE, R
| [Func1<T, RA>, Func<RA, RB>, Func<RB, RC>, Func<RC, RD>, Func<RD, RE>, Func<RE, RF>, Func<RF, RFinal>]
): WorkflowData<RFinal>
/**
* Transforms the input value(s) using the provided functions.
* Allow to perform transformation on the future result of the step(s) to be passed
* to other steps later on at run time.
*
* @param values
* @param functions
*/
export function transform(
values: any | any[],
...functions: Function[]
@@ -15,6 +15,12 @@ export type StepFunctionResult<TOutput extends unknown | unknown[] = unknown> =
]
: WorkflowData<{ [K in keyof TOutput]: TOutput[K] }>
/**
* A step function to be used in a workflow.
*
* @typeParam TInput - The type of the input of the step.
* @typeParam TOutput - The type of the output of the step.
*/
export type StepFunction<TInput extends object = object, TOutput = unknown> = {
(input: { [K in keyof TInput]: WorkflowData<TInput[K]> }): WorkflowData<{
[K in keyof TOutput]: TOutput[K]
@@ -28,6 +34,11 @@ export type WorkflowDataProperties<T = unknown> = {
__step__: string
}
/**
* This type is used to encapsulate the input or output type of all utils.
*
* @typeParam T - The type of a step's input or result.
*/
export type WorkflowData<T = unknown> = (T extends object
? {
[Key in keyof T]: WorkflowData<T[Key]>
@@ -53,9 +64,21 @@ export type CreateWorkflowComposerContext = {
) => TOutput
}
/**
* The step's context.
*/
export interface StepExecutionContext {
/**
* The container used to access resources, such as services, in the step.
*/
container: MedusaContainer
/**
* Metadata passed in the input.
*/
metadata: TransactionPayload["metadata"]
/**
* {@inheritDoc Context}
*/
context: Context
}