diff --git a/packages/cli/oas/medusa-oas-cli/redocly/redocly-config.yaml b/packages/cli/oas/medusa-oas-cli/redocly/redocly-config.yaml index 1e49ac3858d83..38b152b8fecb0 100644 --- a/packages/cli/oas/medusa-oas-cli/redocly/redocly-config.yaml +++ b/packages/cli/oas/medusa-oas-cli/redocly/redocly-config.yaml @@ -16,6 +16,7 @@ decorators: - ProductCategoryResponse AdminShippingOption: - AdminShippingOption + - AdminServiceZone AdminProductCategory: - AdminProductCategory - AdminProduct @@ -41,6 +42,8 @@ decorators: AdminTaxRegion: - AdminTaxRegion - AdminTaxRate + AdminInventoryLevel: + - AdminInventoryItem theme: openapi: theme: diff --git a/packages/cli/oas/medusa-oas-cli/src/command-docs.ts b/packages/cli/oas/medusa-oas-cli/src/command-docs.ts index 4122c37ad46be..8e016f58b0d44 100644 --- a/packages/cli/oas/medusa-oas-cli/src/command-docs.ts +++ b/packages/cli/oas/medusa-oas-cli/src/command-docs.ts @@ -238,10 +238,13 @@ ${hint} ` const redoclyConfigPath = path.join(basePath, "redocly", "redocly-config.yaml") const originalContent = await readYaml(redoclyConfigPath) as CircularReferenceConfig - originalContent.decorators["medusa/circular-patch"].schemas = Object.assign( - originalContent.decorators["medusa/circular-patch"].schemas, - recommendation - ) + Object.keys(recommendation).forEach((recKey) => { + originalContent.decorators["medusa/circular-patch"].schemas[recKey] = [ + ...(originalContent.decorators["medusa/circular-patch"].schemas[recKey] || []), + ...recommendation[recKey] + ] + }) + await writeYaml(redoclyConfigPath, jsonObjectToYamlString(originalContent)) console.log(`🟡 Added the following unhandled circular references to redocly-config.ts:` + hintMessage) } diff --git a/packages/framework/framework/src/http/router.ts b/packages/framework/framework/src/http/router.ts index ff83dc0f39315..5761c5c585d54 100644 --- a/packages/framework/framework/src/http/router.ts +++ b/packages/framework/framework/src/http/router.ts @@ -10,6 +10,9 @@ import { } from "express" import { readdir } from "fs/promises" import { extname, join, parse, sep } from "path" +import { configManager } from "../config" +import { logger } from "../logger" +import { authenticate, AuthType, errorHandler } from "./middlewares" import { GlobalMiddlewareDescriptor, HTTP_METHODS, @@ -25,9 +28,6 @@ import { RouteHandler, RouteVerb, } from "./types" -import { authenticate, AuthType, errorHandler } from "./middlewares" -import { configManager } from "../config" -import { logger } from "../logger" const log = ({ activityId, @@ -486,43 +486,32 @@ class ApiRoutesLoader { const absolutePath = join(this.#sourceDir, middlewareFilePath) - try { - await import(absolutePath).then((import_) => { - const middlewaresConfig = import_.default as - | MiddlewaresConfig - | undefined - - if (!middlewaresConfig) { - log({ - activityId: this.#activityId, - message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`, - }) - return - } + await import(absolutePath).then((import_) => { + const middlewaresConfig = import_.default as MiddlewaresConfig | undefined - middlewaresConfig.routes = middlewaresConfig.routes?.map((route) => { - return { - ...route, - method: route.method ?? "USE", - } + if (!middlewaresConfig) { + log({ + activityId: this.#activityId, + message: `No middleware configuration found in ${absolutePath}. Skipping middleware configuration.`, }) + return + } - const descriptor: GlobalMiddlewareDescriptor = { - config: middlewaresConfig, + middlewaresConfig.routes = middlewaresConfig.routes?.map((route) => { + return { + ...route, + method: route.method ?? "USE", } + }) - this.validateMiddlewaresConfig(descriptor) + const descriptor: GlobalMiddlewareDescriptor = { + config: middlewaresConfig, + } - this.#globalMiddlewaresDescriptor = descriptor - }) - } catch (error) { - log({ - activityId: this.#activityId, - message: `Failed to load middleware configuration in ${absolutePath}. Skipping middleware configuration.`, - }) + this.validateMiddlewaresConfig(descriptor) - return - } + this.#globalMiddlewaresDescriptor = descriptor + }) } protected async createRoutesMap(): Promise { diff --git a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx index 0d18ea80ea306..3b6bd05d595d4 100644 --- a/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx +++ b/www/apps/api-reference/components/Tags/Operation/Parameters/Types/Object/index.tsx @@ -1,3 +1,5 @@ +"use client" + import type { SchemaObject } from "@/types/openapi" import TagOperationParametersDefault from "../Default" import dynamic from "next/dynamic" @@ -5,6 +7,7 @@ import type { TagOperationParametersProps } from "../.." import type { TagsOperationParametersNestedProps } from "../../Nested" import checkRequired from "@/utils/check-required" import { Loading, type DetailsProps } from "docs-ui" +import { useMemo } from "react" const TagOperationParameters = dynamic( async () => import("../.."), @@ -41,9 +44,22 @@ const TagOperationParametersObject = ({ isRequired, topLevel = false, }: TagOperationParametersObjectProps) => { + const isPropertiesEmpty = useMemo( + () => !schema.properties || !Object.values(schema.properties).length, + [schema] + ) + const isAdditionalPropertiesEmpty = useMemo( + () => + !schema.additionalProperties || + schema.additionalProperties.type !== "object" || + !schema.additionalProperties.properties || + !Object.values(schema.additionalProperties.properties).length, + [schema] + ) + if ( (schema.type !== "object" && schema.type !== undefined) || - (!schema.properties && !name) + (isPropertiesEmpty && isAdditionalPropertiesEmpty && !name) ) { return <> } @@ -65,22 +81,19 @@ const TagOperationParametersObject = ({ } const getPropertyParameterElms = (isNested = false) => { + const properties = isPropertiesEmpty + ? schema.additionalProperties!.properties + : schema.properties // sort properties to show required fields first - const sortedProperties = Object.keys(schema.properties).sort( + const sortedProperties = Object.keys(properties).sort( (property1, property2) => { - schema.properties[property1].isRequired = checkRequired( - schema, - property1 - ) - schema.properties[property2].isRequired = checkRequired( - schema, - property2 - ) + properties[property1].isRequired = checkRequired(schema, property1) + properties[property2].isRequired = checkRequired(schema, property2) - return schema.properties[property1].isRequired && - schema.properties[property2].isRequired + return properties[property1].isRequired && + properties[property2].isRequired ? 0 - : schema.properties[property1].isRequired + : properties[property1].isRequired ? -1 : 1 } @@ -90,13 +103,12 @@ const TagOperationParametersObject = ({ {sortedProperties.map((property, index) => ( ))} @@ -114,7 +126,7 @@ const TagOperationParametersObject = ({ ) } - if (!schema.properties || !Object.values(schema.properties).length) { + if (isPropertiesEmpty && isAdditionalPropertiesEmpty) { return getPropertyDescriptionElm() } diff --git a/www/apps/api-reference/types/openapi.ts b/www/apps/api-reference/types/openapi.ts index babfcf680193a..5e206e7330934 100644 --- a/www/apps/api-reference/types/openapi.ts +++ b/www/apps/api-reference/types/openapi.ts @@ -72,9 +72,15 @@ export type ArraySchemaObject = Omit< export type NonArraySchemaObject = Omit< OpenAPIV3.NonArraySchemaObject, - "properties" | "anyOf" | "allOf" | "oneOf" | "examples" + | "properties" + | "anyOf" + | "allOf" + | "oneOf" + | "examples" + | "additionalProperties" > & { properties: PropertiesObject + additionalProperties?: SchemaObject anyOf?: SchemaObject[] allOf?: SchemaObject[] oneOf?: SchemaObject[] @@ -90,6 +96,7 @@ export type SchemaObject = (ArraySchemaObject | NonArraySchemaObject) & { "x-featureFlag"?: string "x-expandable"?: string "x-schemaName"?: string + additionalProperties?: SchemaObject } export type PropertiesObject = { diff --git a/www/apps/book/app/advanced-development/api-routes/additional-data/page.mdx b/www/apps/book/app/advanced-development/api-routes/additional-data/page.mdx new file mode 100644 index 0000000000000..612597a01bd06 --- /dev/null +++ b/www/apps/book/app/advanced-development/api-routes/additional-data/page.mdx @@ -0,0 +1,201 @@ +import { Details } from "docs-ui" + +export const metadata = { + title: `${pageNumber} Pass Additional Data to Medusa's API Route`, +} + +# {metadata.title} + +In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. + +## Why Pass Additional Data? + +Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks. + +This is useful when you have a link from your custom module to a commerce module, and you want to perform an additional action when a request is sent to an existing API route. + +For example, the [Create Product API Route](!api!/admin#products_postproducts) accepts an `additional_data` parameter. If you have a data model linked to it, you consume the `productsCreated` hook to create a record of the data model using the custom data and link it to the product. + +### API Routes Accepting Additional Data + +
+ +- Campaigns + - [Create Campaign](!api!/admin#campaigns_postcampaigns) + - [Update Campaign](!api!/admin#campaigns_postcampaignsid) +- Cart + - [Create Cart](!api!/store#carts_postcarts) + - [Update Cart](!api!/store#carts_postcartsid) +- Customers + - [Create Customer](!api!/admin#customers_postcustomers) + - [Update Customer](!api!/admin#customers_postcustomersid) + - [Create Address](!api!/admin#customers_postcustomersidaddresses) + - [Update Address](!api!/admin#customers_postcustomersidaddressesaddress_id) +- Draft Orders + - [Create Draft Order](!api!/admin#draft-orders_postdraftorders) +- Orders + - [Complete Orders](!api!/admin#orders_postordersidcomplete) + - [Cancel Order's Fulfillment](!api!/admin#orders_postordersidfulfillmentsfulfillment_idcancel) + - [Create Shipment](!api!/admin#orders_postordersidfulfillmentsfulfillment_idshipments) + - [Create Fulfillment](!api!/admin#orders_postordersidfulfillments) +- Products + - [Create Product](!api!/admin#products_postproducts) + - [Update Product](!api!/admin#products_postproductsid) + - [Create Product Variant](!api!/admin#products_postproductsidvariants) + - [Update Product Variant](!api!/admin#products_postproductsidvariantsvariant_id) + - [Create Product Option](!api!/admin#products_postproductsidoptions) + - [Update Product Option](!api!/admin#products_postproductsidoptionsoption_id) +- Promotions + - [Create Promotion](!api!/admin#promotions_postpromotions) + - [Update Promotion](!api!/admin#promotions_postpromotionsid) + +
+ +--- + +## How to Pass Additional Data + +### 1. Specify Validation of Additional Data + +Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. + +To do that, use the middleware route object defined in `src/api/middlewares.ts`. + +For example, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/medusa" +import { z } from "zod" + +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional() + } + } + ] +}) +``` + +The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property. + +In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. + + + +Refer to [Zod's documentation](https://zod.dev) for all available validation rules. + + + +### 2. Pass the Additional Data in a Request + +You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. + +For example: + +```bash +curl -X POST 'http://localhost:9000/admin/products' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "title": "Product 1", + "additional_data": { + "brand": "Acme" + } +}' +``` + + + +Make sure to replace the `{token}` in the authorization header with an admin user's authentication token. + + + +In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. + +The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. + +--- + +## Use Additional Data in a Hook + + + +Learn about workflow hooks in [this guide](../../workflows/workflow-hooks/page.mdx). + + + +Step functions consuming the workflow hook can access the `additional_data` in the first parameter. + +For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. + +To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: + +```ts title="src/workflows/hooks/product-created.ts" +import { StepResponse } from "@medusajs/workflows-sdk" +import { createProductsWorkflow } from "@medusajs/core-flows" +import { Modules } from "@medusajs/utils" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + if (!additional_data.brand) { + return + } + + const productModuleService = container.resolve( + Modules.PRODUCT + ) + + await productModuleService.upsertProducts( + products.map((product) => ({ + ...product, + metadata: { + ...product.metadata, + brand: additional_data.brand + } + })) + ) + + return new StepResponse(products, { + products, + additional_data + }) + } +) +``` + +This consumes the `productsCreated` hook, which runs after the products are created. + +If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property. + +### Compensation Function + +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. + +For example, pass the following second parameter to the `productsCreated` hook: + +```ts title="src/workflows/hooks/product-created.ts" +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // ... + }, + async ({ products, additional_data }, { container }) => { + if (!additional_data.brand) { + return + } + + const productModuleService = container.resolve( + Modules.PRODUCT + ) + + await productModuleService.upsertProducts( + products + ) + } +) +``` + +This updates the product to their original state before adding the brand to their `metadata` property. diff --git a/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx b/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx index e9d66806e1927..31a9343215591 100644 --- a/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/access-workflow-errors/page.mdx @@ -47,6 +47,6 @@ export async function GET( ``` -The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of the `run`'s output. The value of `errors` is an array of error objects. +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. -Then, you can check the items in the `errors` array and handle them accordingly. Each error object has an `error` property, which holds the name or the text of the thrown error. \ No newline at end of file +The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. diff --git a/www/apps/book/app/advanced-development/workflows/add-workflow-hook/page.mdx b/www/apps/book/app/advanced-development/workflows/add-workflow-hook/page.mdx index f608435fef64d..2625de36e3c9f 100644 --- a/www/apps/book/app/advanced-development/workflows/add-workflow-hook/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/add-workflow-hook/page.mdx @@ -6,6 +6,8 @@ export const metadata = { In this chapter, you'll learn how to expose a hook in your workflow. +## When to Expose a Hook + Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. @@ -18,6 +20,8 @@ Your workflow isn't reusable by other applications. Use a step that performs wha +--- + ## How to Expose a Hook in a Workflow? To expose a hook in your workflow, use the `createHook` function imported from `@medusajs/workflows-sdk`. @@ -58,7 +62,7 @@ export const myWorkflow = createWorkflow( The `createHook` function accepts two parameters: -1. The first is a string indicating the hook's name. This is used to add a hook handler later. +1. The first is a string indicating the hook's name. You use this to consume the hook later. 2. The second is the input to pass to the hook handler. The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. @@ -81,4 +85,6 @@ myWorkflow.hooks.productCreated( ) ``` -The hook is available on the workflow's `hooks` property using its name `productCreated`. You invoke the hook, passing the handler as a parameter, which is a step function. +The hook is available on the workflow's `hooks` property using its name `productCreated`. + +You invoke the hook, passing a step function (the hook handler) as a parameter. diff --git a/www/apps/book/app/advanced-development/workflows/advanced-example/page.mdx b/www/apps/book/app/advanced-development/workflows/advanced-example/page.mdx deleted file mode 100644 index 68e49b43d98ae..0000000000000 --- a/www/apps/book/app/advanced-development/workflows/advanced-example/page.mdx +++ /dev/null @@ -1,272 +0,0 @@ -export const metadata = { - title: `${pageNumber} Advanced Workflow Example`, -} - -# {metadata.title} - -In this chapter, you’ll create an advanced workflow. - -Workflows are most useful when performing tasks across services and integrations: - -- You simplify complex flows or processes by splitting them into a series of steps. -- You avoid data inconsistency and loss through retry mechanisms and rollbacks. -- You execute the workflow from anywhere in your Medusa application. - -The workflow you’ll build is an update-product workflow. You'll use an example `ErpService` that has methods to manage the product in a third-party ERP system, and the `ProductService`. - ---- - -## 1. Create Workflow File - -Start by creating the file `src/workflows/update-product-erp/index.ts` that will hold the constructed workflow. - -In the file, add the type of the expected workflow input: - -```ts title="src/workflows/update-product-erp/index.ts" -import { UpsertProductDTO } from "@medusajs/types" - -export type UpdateProductAndErpWorkflowInput = UpsertProductDTO - -``` - -The expected input is the data to update in the product along with the product’s ID. - ---- - -## 2. Create Update Product Step - -The first step in the workflow receives the product’s ID and the data to update, then updates the product. - -Create the file `src/workflows/update-product-erp/steps/update-product.ts` with the following content: - -export const updateProductHighlights = [ - ["10", "resolve", "Resolve the `ProductService` from the Medusa container."], - ["13", "previousProductData", "Retrieve the `previousProductData` to pass it to the compensation function."], - ["16", "updateProducts", "Update the product."], - ["30", "updateProducts", "Revert the product’s data using the `previousProductData` passed from the step to the compensation function."] -] - -```ts title="src/workflows/update-product-erp/steps/update-product.ts" highlights={updateProductHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { createStep, StepResponse } from "@medusajs/workflows-sdk" -import { IProductModuleService } from "@medusajs/types" -import { Modules } from "@medusajs/utils" -import { UpdateProductAndErpWorkflowInput } from ".." - -const updateProduct = createStep( - "update-product", - async (input: UpdateProductAndErpWorkflowInput, context) => { - const productModuleService: IProductModuleService = - context.container.resolve(Modules.PRODUCT) - - const { id } = input - const previousProductData = - await productModuleService.retrieveProduct(id) - - const product = await productModuleService.updateProducts(id, input) - - return new StepResponse(product, { - // pass to compensation function - previousProductData, - }) - }, - // compensation function - async ({ previousProductData }, context) => { - const productModuleService: IProductModuleService = - context.container.resolve(Modules.PRODUCT) - - const { id, type, options, variants, ...previousData } = previousProductData - - await productModuleService.updateProducts( - id, - { - ...previousData, - variants: variants.map((variant) => { - const variantOptions = {} - - variant.options.forEach((option) => { - variantOptions[option.option.title] = option.value - }) - - return { - ...variant, - options: variantOptions, - } - }), - options: options.map((option) => ({ - ...option, - values: option.values.map((value) => value.value), - })), - type_id: type.id, - }) - } -) - -export default updateProduct -``` - -In the step: - -- You resolve the Product Module's main service from the Medusa container. -- You retrieve the `previousProductData` to pass it to the compensation function. -- You update and return the product. - -You also pass a compensation function as a second parameter to `createStep`. The compensation function runs if an error occurs during the workflow execution. - -In the compensation function, you revert the product’s data using the `previousProductData` passed from the step to the compensation function. - ---- - -## 3. Create Step 2: Update ERP - -The second step in the workflow receives the same input. It updates the product’s details in the ERP system. - - - -The `ErpModuleService` used is assumed to be created in a module. - - - -Create the file `src/workflows/update-product-erp/steps/update-erp.ts` with the following content: - -export const updateErpHighlights = [ - [ - "9", - "resolve", - "Resolve the `erpModuleService` from the Medusa container.", - ], - [ - "14", - "previousErpData", - "Retrieve the `previousErpData` to pass it to the compensation function.", - ], - [ - "16", - "updateProductErpData", - "Update the product’s ERP data and return the data from the ERP system.", - ], - [ - "31", - "updateProductErpData", - "Revert the product's data in the ERP system to its previous state using the `previousErpData`.", - ], -] - -```ts title="src/workflows/update-product-erp/steps/update-erp.ts" highlights={updateErpHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { createStep, StepResponse } from "@medusajs/workflows-sdk" -import { UpdateProductAndErpWorkflowInput } from ".." -import ErpModuleService from "../../../modules/erp/service" - -const updateErp = createStep( - "update-erp", - async (input: UpdateProductAndErpWorkflowInput, context) => { - const erpModuleService: ErpModuleService = - context.container.resolve("erpModuleService") - - const { id, ...updatedData } = input - - // get previous ERP data - const previousErpData = await erpModuleService.retrieveProductErpDetails(id) - - const updatedErpData = await erpModuleService.updateProductErpData( - id, - updatedData - ) - - return new StepResponse(updatedErpData, { - // pass to compensation function - previousErpData, - productId: id, - }) - }, - // compensation function - async ({ previousErpData, productId }, context) => { - const erpService: ErpModuleService = context.container.resolve("erpService") - - await erpService.updateProductErpData(productId, previousErpData) - } -) - -export default updateErp -``` - -In the step: - -- You resolve the `erpModuleService` from the Medusa container. -- You retrieve the `previousErpData` to pass it to the compensation function. -- You update the product’s ERP data and return the data from the ERP system. - -You also pass a compensation function as a second parameter to `createStep`. In the compensation function, you revert the product's data in the ERP system to its previous state. - ---- - -## 4. Create Workflow - -With the steps ready, you'll create the workflow that runs these steps to update the product’s data both in Medusa and the external ERP system. - -Change the content of `src/workflows/update-product-erp/index.ts` to the following: - -```ts title="src/workflows/update-product-erp/index.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/workflows-sdk" -import { UpsertProductDTO } from "@medusajs/types" -import updateProduct from "./steps/update-product" -import updateErp from "./steps/update-erp" - -export type UpdateProductAndErpWorkflowInput = UpsertProductDTO - -const updateProductAndErpWorkflow = createWorkflow( - "update-product-and-erp", - function (input: UpdateProductAndErpWorkflowInput) { - const product = updateProduct(input) - const erpData = updateErp(input) - - return new WorkflowResponse({ - product, - erpData, - }) -}) - -export default updateProductAndErpWorkflow -``` - -In the workflow construction function, you first run the `updateProduct` step, then the `updateErp` step. You return as the workflow’s result an object holding the updated product and ERP data. - ---- - -## 5. Execute Workflow - -You can now use and execute your workflow in your Medusa application. - -To execute the workflow in an API route, create the file `src/api/products/[id]/erp/route.ts` with the following content: - -```ts title="src/api/products/[id]/erp/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/medusa" -import updateProductAndErpWorkflow, { - UpdateProductAndErpWorkflowInput, -} from "../../../../../workflows/update-product-erp" - -type ProductErpReq = Omit - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - // skipping validation for simplicity - const productData: UpdateProductAndErpWorkflowInput = { - id: req.params.id, - ...req.body, - } - - const { result } = await updateProductAndErpWorkflow(req.scope).run({ - input: productData, - }) - - res.json(result) -} -``` - -In this `POST` API route, you retrieve the product’s ID from the path parameter and the data to update from the request body. You then execute the workflow by passing it the retrieved data as an input. - -The route returns the result of the workflow, which is an object holding both the update product’s details and the ERP details. diff --git a/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx b/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx index 98d790bec5fbe..27d900e2a7940 100644 --- a/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/compensation-function/page.mdx @@ -4,16 +4,25 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn how to add a compensation function to a step. +In this chapter, you'll learn what a compensation function is and how to add it to a step. -## Compensation Function +## What is a Compensation Function -To avoid data inconsistency when an error is thrown in a workflow, define a function (called a compensation function) and pass it as a second parameter to the `createStep` function. +A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. -For example: +For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. -```ts title="src/workflows/hello-world.ts" highlights={[["16"], ["17"], ["18"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -// other imports... +By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. + +--- + +## How to add a Compensation Function? + +A compensation function is passed as a second parameter to the `createStep` function. + +For example, create the file `src/workflows/hello-world.ts` with the following content: + +```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" import { createStep, StepResponse, @@ -34,16 +43,15 @@ const step1 = createStep( ) ``` -Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. It’s useful to undo or roll back actions you’ve performed in a step. +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. --- ## Test the Compensation Function -1. Add another step that throws an error: +Create a step in the same `src/workflows/hello-world.ts` file that throws an error: ```ts title="src/workflows/hello-world.ts" -// ... const step2 = createStep( "step-2", async () => { @@ -52,16 +60,16 @@ const step2 = createStep( ) ``` -2. Use the steps in a workflow. For example: +Then, create a workflow that uses the steps: ```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" import { - // other imports... createWorkflow, WorkflowResponse, } from "@medusajs/workflows-sdk" +// other imports... -// ... +// steps... const myWorkflow = createWorkflow( "hello-world", @@ -77,7 +85,7 @@ const myWorkflow = createWorkflow( export default myWorkflow ``` -3. Execute the workflow from a resource, such as an API route: +Finally, execute the workflow from an API route: ```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { @@ -97,13 +105,7 @@ export async function GET( } ``` -4. Run the Medusa application: - -```bash npm2yarn -npm run dev -``` - -5. Send a `GET` request to `/workflow`: +Run the Medusa application and send a `GET` request to `/workflow`: ```bash curl http://localhost:9000/workflow @@ -112,4 +114,85 @@ curl http://localhost:9000/workflow In the console, you'll see: - `Hello from step one!` logged in the terminal, indicating that the first step ran successfully. -- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. \ No newline at end of file +- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. + +--- + +## Pass Input to Compensation Function + +If a step creates a record, the compensation function must receive the ID of the record to remove it. + +To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. + +For example: + +export const inputHighlights = [ + ["11", "", "The data to pass as an input to the compensation function."], + ["14", "{ message }", "The data received as an input from `StepResponse`'s second parameter."] +] + +```ts highlights={inputHighlights} +import { + createStep, + StepResponse, +} from "@medusajs/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..."} + ) + }, + async ({ message }) => { + console.log(message) + } +) +``` + +In this example, the step passes an object as a second parameter to `StepResponse`. + +The compensation function receives the object and uses its `message` property to log a message. + +--- + +## Resolve Resources from the Medusa Container + +The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. + +For example: + +export const containerHighlights = [ + ["15", "container", "Access the container in the second parameter object."], + ["16", "resolve", "Use the container to resolve resources."] +] + +```ts +import { + createStep, + StepResponse, +} from "@medusajs/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/utils" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..."} + ) + }, + async ({ message }, { container }) => { + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + + logger.info(message) + } +) +``` + +In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. + +You then use the logger to log a message. diff --git a/www/apps/book/app/advanced-development/workflows/conditions/page.mdx b/www/apps/book/app/advanced-development/workflows/conditions/page.mdx index 9b4634ca66440..fe7155101e997 100644 --- a/www/apps/book/app/advanced-development/workflows/conditions/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/conditions/page.mdx @@ -1,27 +1,35 @@ export const metadata = { - title: `${pageNumber} Conditions in Workflow with When-Then`, + title: `${pageNumber} Conditions in Workflows with When-Then`, } # {metadata.title} In this chapter, you'll learn how to execute an action based on a condition in a workflow using the when-then utility. -## What is the When-Then Utility? +## Why If-Conditions Aren't Allowed in Workflows? + +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. -The when-then utility executes an action if a condition is satisfied. +At that point, variables in the workflow don't have any values. They only do when you execute the workflow. -Since if-conditions aren't allowed in the workflow constructor function, use the when-then utility if you want to execute a step based on a condition. +So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. + +Instead, use the when-then utility. --- -## How to Use the When-Then Utility? +## What is the When-Then Utility? + +The when-then utility functions execute an action if a condition is satisfied. + +The `when` function accepts as a parameter a function that returns a boolean value, and the `then` function is chained to `when`. `then` accepts as a parameter a function that's executed if `when`'s parameter function returns a `true` value. For example: export const highlights = [ - ["16", "input", "The data to pass as a parameter to the function in the second parameter"], - ["18", "return", "The function must return a boolean value indicating whether\nthe callback function passed to `then` should be executed."], - ["20", "() => {", "The function to execute if `when`'s second parameter returns a `true` value."] + ["15", "input", "The data to pass as a parameter to the function in the second parameter"], + ["17", "return", "The function must return a boolean value indicating whether\nthe callback function passed to `then` should be executed."], + ["19", "() => {", "The function to execute if `when`'s second parameter returns a `true` value."] ] ```ts highlights={highlights} @@ -32,13 +40,12 @@ import { } from "@medusajs/workflows-sdk" // step imports... -type WorkflowInput = { - is_active: boolean -} - const workflow = createWorkflow( "workflow", - function (input: WorkflowInput) { + function (input: { + is_active: boolean + }) { + const result = when( input, (input) => { @@ -59,9 +66,17 @@ const workflow = createWorkflow( ) ``` -Then `when` utility is a function imported from `@medusajs/workflows-sdk`. It accepts the following parameters: +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. + +### When Parameters + +`when` utility is a function imported from `@medusajs/workflows-sdk`. It accepts the following parameters: + +1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. + +### Then Parameters -1. An object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -2. A function that returns a boolean indicating whether to execute the action. +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. The callback function is only executed if `when`'s second parameter function returns a `true` value. \ No newline at end of file +The callback function is only executed if `when`'s second parameter function returns a `true` value. \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/workflows/constructor-constraints/page.mdx b/www/apps/book/app/advanced-development/workflows/constructor-constraints/page.mdx index 0592611de514b..1bee88f1b82b4 100644 --- a/www/apps/book/app/advanced-development/workflows/constructor-constraints/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/constructor-constraints/page.mdx @@ -4,13 +4,13 @@ export const metadata = { # {metadata.title} -This chapter lists some constraints to keep in mind when defining a workflow or its steps. +This chapter lists constraints of defining a workflow or its steps. ## Workflow Constraints ### No Async Functions -The function passed to the `createWorkflow` can’t be an async function: +The function passed to `createWorkflow` can’t be an async function: ```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]} // Don't @@ -28,13 +28,21 @@ const myWorkflow = createWorkflow( }) ``` -### No Direct Data Manipulation +### No Direct Variable Manipulation -Since the constructor function only defines how the workflow works, you can’t directly manipulate data within the function. Instead, use the `transform` function: +You can’t directly manipulate variables within the workflow's constructor function. + + + +Learn more about why you can't manipulate variables [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows) + + + +Instead, use the `transform` utility function imported from `@medusajs/workflows-sdk`: export const highlights = [ - ["9", "", "Don't manipulate data directly."], - ["20", "transform", "Use the `transform` function to manipulate data."] + ["9", "", "Don't manipulate variables directly."], + ["20", "transform", "Use the `transform` function to manipulate variables."] ] ```ts highlights={highlights} @@ -45,9 +53,9 @@ const myWorkflow = createWorkflow( const str1 = step1(input) const str2 = step2(input) - return { + return new WorkflowResponse({ message: `${str1}${str2}`, - } + }) }) // Do @@ -67,13 +75,21 @@ const myWorkflow = createWorkflow( }) ) - return result + return new WorkflowResponse(result) }) ``` ### No If Conditions -You can't use if-conditions in a workflow. Instead, use the when-then utility function explained in the next chapter: +You can't use if-conditions in a workflow. + + + +Learn more about why you can't use if-conditions [in this chapter](../conditions/page.mdx#why-if-conditions-arent-allowed-in-workflows) + + + +Instead, use the when-then utility function imported from `@medusajs/workflows-sdk`: ```ts // Don't @@ -104,7 +120,9 @@ const myWorkflow = createWorkflow( ### Returned Values -A step must only return [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object. Values of other types, such as Maps, aren't allowed. +A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object. + +Values of other types, such as Maps, aren't allowed. ```ts // Don't diff --git a/www/apps/book/app/advanced-development/workflows/execute-another-workflow/page.mdx b/www/apps/book/app/advanced-development/workflows/execute-another-workflow/page.mdx index f652d7e5a3c2a..0994482f6181e 100644 --- a/www/apps/book/app/advanced-development/workflows/execute-another-workflow/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/execute-another-workflow/page.mdx @@ -41,7 +41,9 @@ const workflow = createWorkflow( ) ``` -Instead of invoking the workflow, passing it the container, you instead use its `runAsStep` method and pass it an object as a parameter. The object has an `input` property to pass input to the workflow. +Instead of invoking the workflow, passing it the container, you use its `runAsStep` method and pass it an object as a parameter. + +The object has an `input` property to pass input to the workflow. --- @@ -49,6 +51,12 @@ Instead of invoking the workflow, passing it the container, you instead use its If you need to perform some data manipulation to prepare the other workflow's input data, use the `transform` utility function imported from `@medusajs/workflows-sdk`. + + +Learn about the transform utility in [this chapter](../variable-manipulation/page.mdx). + + + For example: export const transformHighlights = [ @@ -91,13 +99,19 @@ const workflow = createWorkflow( ) ``` -In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the variable created through `transform` as an input to the `createProductsWorkflow`. +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. --- ## Run Workflow Conditionally -To run a workflow in another based on a condition, use the `when` utility function imported from `@medusajs/workflows-sdk`. +To run a workflow in another based on a condition, use the when-then utility functions imported from `@medusajs/workflows-sdk`. + + + +Learn about the when-then utility in [this chapter](../conditions/page.mdx). + + For example: diff --git a/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx b/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx index 567a841f0178f..2e81b6c9ce1dc 100644 --- a/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/long-running-workflow/page.mdx @@ -10,10 +10,17 @@ In this chapter, you’ll learn what a long-running workflow is and how to confi ## What is a Long-Running Workflow? -By default, when you execute a workflow, you wait until the workflow finishes execution. Once you receive the workflow’s output, the rest of the code is executed. +When you execute a workflow, you wait until the workflow finishes execution to receive the output. A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished. +### Why use Long-Running Workflows? + +Long-running workflows are useful if: + +- A task takes too long. For example, you're importing data from a CSV file. +- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user. + --- ## Configure Long-Running Workflows @@ -71,9 +78,9 @@ So, when you execute the `hello-world` workflow, it continues its execution in t ## Change Step Status -Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the rest of the steps. +Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step. -To change fail or succeed a step, use the workflow engine's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key. +To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key. ### Retrieve Transaction ID @@ -90,7 +97,9 @@ const { transaction } = await myWorkflow(req.scope) ### Change Step Status to Successful -For example, the following workflow step (used in a different workflow) resolves the workflow engine's main service and sets `step-2` of the previous workflow as successful: +The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step. + +For example, consider the following step: export const successStatusHighlights = [ ["17", "transactionId", "Receive the workflow execution's transaction ID as an input to the step."], @@ -141,9 +150,7 @@ export const setStepSuccessStep = createStep( ); ``` -You use this step in another workflow that changes the long-running workflow's status. - -After change the async step's status to successful, the workflow execution continues to the next step. +In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful. The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: @@ -206,7 +213,9 @@ The `setStepSuccess` method of the workflow engine's main service accepts as a p ### Change Step Status to Failed -The workflow engine's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`. +The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`. + +After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. For example: @@ -259,15 +268,13 @@ export const setStepFailureStep = createStep( ); ``` -You use this step in another workflow that changes the long-running workflow's status. - -After change the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. +You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. --- ## Access Long-Running Workflow Status and Result -To access the status and result of a long-running workflow, use the workflow engine's main service' `subscribe` and `unsubscribe` methods. +To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service. For example: @@ -319,9 +326,11 @@ export async function GET(req: MedusaRequest, res: MedusaResponse) { } ``` -In the above example, you execute the long-running workflow `hello-world`. You then resolve the workflow engine from the Medusa container and use its `subscribe` method to listen to changes in the workflow execution’s status. +In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container. + +### subscribe Method -The `subscribe` method accepts an object having three properties: +The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties: Promise`", + description: + "The function executed when the workflow execution's status changes. The function receives a data object. It has an `eventType` property, which you use to check the status of the workflow execution.", }, ]} sectionTitle="Access Long-Running Workflow Status and Result" /> -Once the workflow execution finishes, the subscriber function is executed with the `eventType` of the received parameter set to `onFinish`. The workflow’s output is set in the `result` property of the parameter. +If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output. + +### unsubscribe Method You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. + +However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. + +--- + +## Example: Restaurant-Delivery Recipe + +To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](!resources!/recipes/marketplace/examples/restaurant-delivery). + +In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions. \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/workflows/page.mdx b/www/apps/book/app/advanced-development/workflows/page.mdx new file mode 100644 index 0000000000000..8b73c9a2e033b --- /dev/null +++ b/www/apps/book/app/advanced-development/workflows/page.mdx @@ -0,0 +1,14 @@ +export const metadata = { + title: `${pageNumber} Workflows Advanced Development`, +} + +# {metadata.title} + +In the next chapters, you'll learn about workflows in-depth and how to use them in your custom development. + +By the end of these chapters, you'll learn about: + +- Constructing a workflow and its constraints. +- Using a compensation function to undo a step's action when errors occur. +- Hooks and how to consume and expose them. +- Configurations to retry workflows or run them in the background. diff --git a/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx b/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx index a50f845d91048..31941584f19eb 100644 --- a/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/parallel-steps/page.mdx @@ -10,7 +10,7 @@ In this chapter, you’ll learn how to run workflow steps in parallel. If your workflow has steps that don’t rely on one another’s results, run them in parallel using the `parallelize` utility function imported from the `@medusajs/workflows-sdk`. -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing with the rest of its implementation. +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. For example: @@ -56,4 +56,6 @@ const myWorkflow = createWorkflow( The `parallelize` function accepts the steps to run in parallel as a parameter. -It returns an array of the steps' results. The results are ordered based on the `parallelize` parameters' order. \ No newline at end of file +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. + +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/workflows/variable-manipulation/page.mdx b/www/apps/book/app/advanced-development/workflows/variable-manipulation/page.mdx new file mode 100644 index 0000000000000..b2cd4f66fe6c6 --- /dev/null +++ b/www/apps/book/app/advanced-development/workflows/variable-manipulation/page.mdx @@ -0,0 +1,116 @@ +export const metadata = { + title: `${pageNumber} Variable Manipulation in Workflows with transform`, +} + +# {metadata.title} + +In this chapter, you'll learn how to manipulate variables in a workflow using the transform utility. + +## Why Variable Manipulation isn't Allowed in Worflows? + +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. + +At that point, variables in the workflow don't have any values. They only do when you execute the workflow. + +So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items. + +Instead, use the transform utility. + +--- + +## What is the transform Utility? + +The `transform` utility function creates a new variable as the result of manipulating other variables. + +For example, consider you have two strings as the output of two steps: + +```ts +const str1 = step1() +const str2 = step2() +``` + +To concatinate the strings, you create a new variable `str3` using the `transform` function: + +export const highlights = [ + ["14", "str3", "Holds the result returned by `transform`'s second parameter function."], + ["15", "", "Specify the data to pass as a parameter to the function in the second parameter."], + ["16", "data", "The data passed in the first parameter of `transform`."], + ["16", "`${data.str1}${data.str2}`", "Return the concatenated strings."] +] + +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/workflows-sdk" +// step imports... + +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1(input) + const str2 = step2(input) + + const str3 = transform( + { str1, str2 }, + (data) => `${data.str1}${data.str2}` + ) + + return new WorkflowResponse(str3) + } +) +``` + +The `transform` utility function is imported from `@medusajs/workflows-sdk`. It accepts two parameters: + +1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function. +2. The second parameter is the function performing the variable manipulation. + +The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. + +You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response. + +--- + +## Example: Looping Over Array + +Use `transform` to loop over arrays to create another variable from the array's items. + +For example: + +```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/workflows-sdk" +// step imports... + +type WorkflowInput = { + items: { + id: string + name: string + }[] +} + +const myWorkflow = createWorkflow( + "hello-world", + function ({ items }: WorkflowInput) { + const ids = transform( + { items }, + (data) => data.items.map((item) => item.id) + ) + + doSomethingStep(ids) + + // ... + } +) +``` + +This workflow receives an `items` array in its input. + +You use the `transform` utility to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. + +You then pass the `ids` variable as a parameter to the `doSomethingStep`. \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/workflows/workflow-hooks/page.mdx b/www/apps/book/app/advanced-development/workflows/workflow-hooks/page.mdx index 1e06ecde71dde..a90e7be0cacf7 100644 --- a/www/apps/book/app/advanced-development/workflows/workflow-hooks/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/workflow-hooks/page.mdx @@ -4,7 +4,7 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn what a workflow hook is, and an example of how to consume a workflow hook defined in Medusa. +In this chapter, you'll learn what a workflow hook is and how to consume them. ## What is a Workflow Hook? @@ -12,6 +12,12 @@ A workflow hook is a point in a workflow where you can inject custom functionali Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. + + +Refer to the [Workflows Reference](!resources!/medusa-workflows-reference) to view all workflows and their hooks. + + + You want to perform a custom action during a workflow's execution, such as when a product is created. @@ -22,17 +28,21 @@ You want to perform a custom action during a workflow's execution, such as when ## How to Consume a Hook? -You consume a hook by registering a hook handler on the workflow. A hook handler is registered in a TypeScript or JavaScript file created in the `src/workflows/hooks` directory. +A workflow has a special `hooks` property which is an object that holds its hooks. + +So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: -You'll find a workflow's exposed hooks in its `hooks` property. +- Import the workflow. +- Access its hook using the `hooks` property. +- Pass the hook a step function as a parameter to consume it. -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/my-workflow.ts` with the following content: +For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: export const handlerHighlights = [ ["3", "productsCreated", "Invoke the hook, passing it a step function as a parameter."], ] -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} import { createProductsWorkflow } from "@medusajs/core-flows" createProductsWorkflow.hooks.productsCreated( @@ -42,9 +52,11 @@ createProductsWorkflow.hooks.productsCreated( ) ``` -The hook is available on the workflow's `hooks` property using its name `productsCreated`. You invoke the hook, passing the handler as a parameter, which is a step function. +The `productsCreated` hook is available on the workflow's `hooks` property by its name. + +You invoke the hook, passing a step function (the hook handler) as a parameter. -Now, when a product is created using the Create Product API route, your hooks handler is executed. +Now, when a product is created using the [Create Product API route](!api!/admin#products_postproducts), your hook handler is executed after the product is created. @@ -52,20 +64,34 @@ A hook can have only one handler. -Similar to a step, the handler receives the hook's input as a first parameter, and the container in the object as a second parameter. + + +Refer to the [createProductsWorkflow reference](!resources!/references/medusa-workflows/createProductsWorkflow) to see at which point the hook handler is executed. + + + +### Hook Handler Parameter + +Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. + +Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. ### Hook Handler Compensation -You can also pass a compensation function as a second parameter: +Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. -```ts -import { myWorkflow } from "../my-workflow" +For example: -myWorkflow.hooks.productCreated( +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/core-flows" + +createProductsWorkflow.productCreated( async ({ productId }, { container }) => { // TODO perform an action + + return new StepResponse(undefined, { ids }) }, - async () => { + async ({ ids }, { container }) => { // undo the performed action } ) @@ -73,11 +99,15 @@ myWorkflow.hooks.productCreated( The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. -### Additional Data +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. -Medusa's workflow hooks pass to their handlers in the first parameter object an `additional_data` property: +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. -```ts highlights={[["4", "additional_data"]]} +### Additional Data Property + +Medusa's workflows pass in the hook's input an `additional_data` property: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} import { createProductsWorkflow } from "@medusajs/core-flows" createProductsWorkflow.hooks.productsCreated( @@ -87,13 +117,19 @@ createProductsWorkflow.hooks.productsCreated( ) ``` -This property is an object that holds additional data passed to the workflow. +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. -{/* TODO change to talk about validators once we can document them. */} + -To pass that additional data when executing the workflow, pass it as a parameter to the `.run` method of the workflow: +Learn how to pass `additional_data` in requests to API routes in [this chapter](../../api-routes/additional-data/page.mdx). -```ts highlights={[["10", "additional_data"]]} + + +### Pass Additional Data to Workflow + +You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} import type { MedusaRequest, MedusaResponse } from "@medusajs/medusa" import { createProductsWorkflow } from "@medusajs/core-flows" @@ -112,5 +148,3 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) { ``` Your hook handler then receives that passed data in the `additional_data` object. - -{/* TODO add a link to the workflows reference once available */} \ No newline at end of file diff --git a/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx b/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx index 9e38a379926ae..e7488827b94ca 100644 --- a/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx +++ b/www/apps/book/app/advanced-development/workflows/workflow-timeout/page.mdx @@ -6,17 +6,23 @@ export const metadata = { In this chapter, you’ll learn how to set a timeout for workflows and steps. -## Configure Workflow Timeout +## What is a Workflow Timeout? By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -You can configure a workflow’s timeout to indicate how long the workflow can run. Once the specified time is passed and the workflow is still running, the workflow is considered failed and an error is thrown. +You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - +### Timeout Doesn't Stop Step Execution -Timeout doesn't stop the execution of a running step. The timeout only affects the status of the workflow and its result. +Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. - +--- + +## Configure Workflow Timeout + +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. + +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. For example: @@ -49,7 +55,7 @@ export default myWorkflow ``` -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. +This workflow's executions fail if they run longer than two seconds. @@ -61,7 +67,7 @@ A workflow’s timeout error is returned in the `errors` property of the workflo ## Configure Step Timeout -Alternatively, you can configure timeout for a step rather than the entire workflow. +Alternatively, you can configure the timeout for a step rather than the entire workflow. @@ -69,6 +75,8 @@ As mentioned in the previous section, the timeout doesn't stop the execution of +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. + For example: ```tsx @@ -83,7 +91,7 @@ const step1 = createStep( ) ``` -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. +This step's executions fail if they run longer than two seconds. diff --git a/www/apps/book/app/basics/workflows/page.mdx b/www/apps/book/app/basics/workflows/page.mdx index 7a04688933ec2..f34a6279d8195 100644 --- a/www/apps/book/app/basics/workflows/page.mdx +++ b/www/apps/book/app/basics/workflows/page.mdx @@ -213,9 +213,7 @@ You’ll receive the following response: -- You're defining a flow with interactions across multiple systems and services. -- You're defining flows to be used across different resources. For example, if you want to invoke the flow manually through an API Router, but also want to automate its running through a scheduled job. -- You want to maintain data consistency and handle errors gracefully by rolling-back steps. This is explained more in later chapters. +You're implementing a custom feature exposed by an API route, or used in subscribers or scheduled jobs. diff --git a/www/apps/book/app/customization/next-steps/page.mdx b/www/apps/book/app/customization/next-steps/page.mdx index 063789cac1d54..e8a366f4245ac 100644 --- a/www/apps/book/app/customization/next-steps/page.mdx +++ b/www/apps/book/app/customization/next-steps/page.mdx @@ -20,7 +20,7 @@ While you can start playing around with Medusa and customize it, it's highly rec ## Helpful Resources Guides -The [Learning Resources](!resources!) documentation provides more helpful guides and references for your development journey. +The [Development Resources](!resources!) documentation provides more helpful guides and references for your development journey. Some of these guides and references are: @@ -33,6 +33,6 @@ Some of these guides and references are: ## More Examples in Recipes -In the Learning Resources documentation, you'll also find step-by-step guides of different use cases, such as building a marketplace, digital products, and more. +In the Development Resources documentation, you'll also find step-by-step guides of different use cases, such as building a marketplace, digital products, and more. Refer to the [Recipes](!resources!/recipes) documentation to learn more. diff --git a/www/apps/book/app/debugging-and-testing/testing-tools/integration-tests/page.mdx b/www/apps/book/app/debugging-and-testing/testing-tools/integration-tests/page.mdx index 681d272cd3ee0..30c658dd2f5e0 100644 --- a/www/apps/book/app/debugging-and-testing/testing-tools/integration-tests/page.mdx +++ b/www/apps/book/app/debugging-and-testing/testing-tools/integration-tests/page.mdx @@ -72,7 +72,7 @@ This runs your Medusa application and runs the tests available under the `src/in ## Other Options and Inputs -Refer to [this reference in the Learning Resources documentation](!resources!/test-tools-reference/medusaIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function. +Refer to [this reference in the Development Resources documentation](!resources!/test-tools-reference/medusaIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function. --- @@ -80,7 +80,7 @@ Refer to [this reference in the Learning Resources documentation](!resources!/te The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Learning Resources documentation](!resources!/test-tools-reference/medusaIntegrationTestRunner). +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](!resources!/test-tools-reference/medusaIntegrationTestRunner). --- diff --git a/www/apps/book/app/debugging-and-testing/testing-tools/modules-tests/page.mdx b/www/apps/book/app/debugging-and-testing/testing-tools/modules-tests/page.mdx index 7b657aba3528d..4ec365d0d4b2f 100644 --- a/www/apps/book/app/debugging-and-testing/testing-tools/modules-tests/page.mdx +++ b/www/apps/book/app/debugging-and-testing/testing-tools/modules-tests/page.mdx @@ -121,7 +121,7 @@ moduleIntegrationTestRunner({ ### Other Options and Inputs -Refer to [this reference in the Learning Resources documentation](!resources!/test-tools-reference/moduleIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function. +Refer to [this reference in the Development Resources documentation](!resources!/test-tools-reference/moduleIntegrationTestRunner) for other available parameter options and inputs of the `testSuite` function. --- @@ -129,4 +129,4 @@ Refer to [this reference in the Learning Resources documentation](!resources!/te The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Learning Resources documentation](!resources!/test-tools-reference/moduleIntegrationTestRunner). +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](!resources!/test-tools-reference/moduleIntegrationTestRunner). diff --git a/www/apps/book/app/more-resources/page.mdx b/www/apps/book/app/more-resources/page.mdx index 0ee6c2be227aa..5deb5a1ee983c 100644 --- a/www/apps/book/app/more-resources/page.mdx +++ b/www/apps/book/app/more-resources/page.mdx @@ -4,6 +4,6 @@ export const metadata = { # {metadata.title} -The Learning Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Learning Resources documentation where necessary. +The Development Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Development Resources documentation where necessary. -Check out the Learning Resources documentation [here](!resources!). \ No newline at end of file +Check out the Development Resources documentation [here](!resources!). \ No newline at end of file diff --git a/www/apps/book/app/not-found.mdx b/www/apps/book/app/not-found.mdx index c2bd96b6254f0..bc4143fb6d466 100644 --- a/www/apps/book/app/not-found.mdx +++ b/www/apps/book/app/not-found.mdx @@ -25,7 +25,7 @@ If you think this is a mistake, please [report this issue on GitHub](https://git icon: BookOpen }, { - title: "Learning Resources", + title: "Development Resources", href: "!resources!", icon: AcademicCapSolid }, diff --git a/www/apps/book/app/page.mdx b/www/apps/book/app/page.mdx index a84f66191885b..9838644f89874 100644 --- a/www/apps/book/app/page.mdx +++ b/www/apps/book/app/page.mdx @@ -104,5 +104,5 @@ By the end of this documentation, you’ll be an expert Medusa developer, leadin The documentation for Medusa v2 is split into the following sections: 1. The main documentation, which is the one you're currently viewing. It's highly recommended to follow all the chapters in this documentation before jumping into other documentation sections. -2. The [Learning Resources documentation](!resources!) provides guides and resources useful during your development, such as tools, API references, recipes, step-by-step guides and examples, and more. +2. The [Development Resources documentation](!resources!) provides guides and resources useful during your development, such as tools, API references, recipes, step-by-step guides and examples, and more. 3. The [Store](!api!/store) and [Admin](!api!/admin) API references provide a reference to the Medusa application's endpoints and instructions related to authentication, parameter types, and more. diff --git a/www/apps/book/app/storefront-development/page.mdx b/www/apps/book/app/storefront-development/page.mdx index 9c456e8961cdb..e19cf131e366f 100644 --- a/www/apps/book/app/storefront-development/page.mdx +++ b/www/apps/book/app/storefront-development/page.mdx @@ -10,7 +10,7 @@ You're free to choose how to build your storefront. You can start with our Next. -To learn how to build a storefront from scratch, check out the [Storefront Development guides](!resources!/storefront-development) in the Learning Resources documentation. +To learn how to build a storefront from scratch, check out the [Storefront Development guides](!resources!/storefront-development) in the Development Resources documentation. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 688a15069b364..9b3b3365d6da9 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -1,20 +1,21 @@ export const generatedEditDates = { "app/basics/scheduled-jobs/page.mdx": "2024-08-05T07:24:27+00:00", - "app/basics/workflows/page.mdx": "2024-09-11T10:48:38.853Z", + "app/basics/workflows/page.mdx": "2024-09-18T08:01:24.328Z", "app/deployment/page.mdx": "2024-08-05T07:24:05+00:00", "app/page.mdx": "2024-09-03T07:09:09.034Z", "app/basics/modules-and-services/page.mdx": "2024-09-11T10:48:35.195Z", "app/basics/commerce-modules/page.mdx": "2024-09-03T07:48:48.148Z", "app/advanced-development/workflows/retry-failed-steps/page.mdx": "2024-07-31T17:01:33+03:00", - "app/advanced-development/workflows/workflow-hooks/page.mdx": "2024-09-10T11:39:51.168Z", + "app/advanced-development/workflows/workflow-hooks/page.mdx": "2024-09-18T12:27:15.321Z", + "app/cheatsheet/page.mdx": "2024-07-11T13:53:40+03:00", "app/debugging-and-testing/logging/page.mdx": "2024-07-04T17:26:03+03:00", "app/more-resources/page.mdx": "2024-07-04T17:26:03+03:00", "app/storefront-development/page.mdx": "2024-09-11T10:58:59.290Z", "app/storefront-development/nextjs-starter/page.mdx": "2024-07-04T17:26:03+03:00", "app/basics/page.mdx": "2024-09-03T07:11:06.879Z", "app/basics/admin-customizations/page.mdx": "2024-09-03T08:07:35.584Z", - "app/advanced-development/workflows/workflow-timeout/page.mdx": "2024-07-31T17:01:33+03:00", - "app/advanced-development/workflows/parallel-steps/page.mdx": "2024-07-31T17:01:33+03:00", + "app/advanced-development/workflows/workflow-timeout/page.mdx": "2024-09-18T13:03:13.095Z", + "app/advanced-development/workflows/parallel-steps/page.mdx": "2024-09-18T12:56:50.436Z", "app/advanced-development/page.mdx": "2024-07-04T17:26:03+03:00", "app/first-customizations/page.mdx": "2024-09-11T10:48:42.374Z", "app/debugging-and-testing/page.mdx": "2024-05-03T17:36:38+03:00", @@ -23,32 +24,32 @@ export const generatedEditDates = { "app/basics/project-directories-files/page.mdx": "2024-07-04T17:26:03+03:00", "app/basics/api-routes/page.mdx": "2024-09-11T10:48:31.777Z", "app/basics/modules-directory-structure/page.mdx": "2024-05-07T18:00:28+02:00", - "app/advanced-development/workflows/access-workflow-errors/page.mdx": "2024-09-11T10:46:54.913Z", + "app/advanced-development/workflows/access-workflow-errors/page.mdx": "2024-09-18T12:54:04.695Z", "app/basics/events-and-subscribers/page.mdx": "2024-09-03T08:01:30.986Z", "app/advanced-development/modules/container/page.mdx": "2024-08-05T07:23:49+00:00", "app/basics/data-models/page.mdx": "2024-09-19T07:24:38.584Z", - "app/advanced-development/workflows/execute-another-workflow/page.mdx": "2024-07-21T21:19:23+02:00", + "app/advanced-development/workflows/execute-another-workflow/page.mdx": "2024-09-18T13:29:11.644Z", "app/basics/loaders/page.mdx": "2024-09-03T08:00:45.993Z", "app/advanced-development/admin/widgets/page.mdx": "2024-08-06T09:44:22+02:00", "app/advanced-development/data-models/page.mdx": "2024-09-19T07:26:43.535Z", "app/advanced-development/modules/remote-link/page.mdx": "2024-07-24T09:16:01+02:00", "app/advanced-development/api-routes/protected-routes/page.mdx": "2024-09-11T10:45:44.293Z", - "app/advanced-development/workflows/add-workflow-hook/page.mdx": "2024-08-13T09:55:37+03:00", + "app/advanced-development/workflows/add-workflow-hook/page.mdx": "2024-09-18T12:52:24.511Z", "app/advanced-development/events-and-subscribers/data-payload/page.mdx": "2024-07-16T17:12:05+01:00", "app/advanced-development/data-models/default-properties/page.mdx": "2024-09-19T07:32:06.118Z", "app/advanced-development/workflows/advanced-example/page.mdx": "2024-09-11T10:46:59.975Z", "app/advanced-development/events-and-subscribers/emit-event/page.mdx": "2024-09-10T11:39:51.168Z", - "app/advanced-development/workflows/conditions/page.mdx": "2024-07-31T17:01:33+03:00", + "app/advanced-development/workflows/conditions/page.mdx": "2024-09-18T08:52:40.755Z", "app/advanced-development/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00", "app/advanced-development/admin/page.mdx": "2024-05-29T13:50:19+03:00", - "app/advanced-development/workflows/long-running-workflow/page.mdx": "2024-09-11T11:29:58.203Z", - "app/advanced-development/workflows/constructor-constraints/page.mdx": "2024-07-17T13:19:51+01:00", - "app/advanced-development/data-models/write-migration/page.mdx": "2024-09-19T08:50:51.503Z", - "app/advanced-development/data-models/manage-relationships/page.mdx": "2024-09-19T12:13:08.980Z", + "app/advanced-development/workflows/long-running-workflow/page.mdx": "2024-09-18T13:26:19.706Z", + "app/advanced-development/workflows/constructor-constraints/page.mdx": "2024-09-18T08:58:08.705Z", + "app/advanced-development/data-models/write-migration/page.mdx": "2024-07-15T17:46:10+02:00", + "app/advanced-development/data-models/manage-relationships/page.mdx": "2024-09-10T11:39:51.167Z", "app/advanced-development/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", "app/advanced-development/modules/options/page.mdx": "2024-08-05T07:23:49+00:00", - "app/advanced-development/data-models/relationships/page.mdx": "2024-09-19T11:26:16.387Z", - "app/advanced-development/workflows/compensation-function/page.mdx": "2024-07-31T17:01:33+03:00", + "app/advanced-development/data-models/relationships/page.mdx": "2024-09-11T11:28:55.494Z", + "app/advanced-development/workflows/compensation-function/page.mdx": "2024-09-18T09:13:11.941Z", "app/advanced-development/modules/service-factory/page.mdx": "2024-07-26T14:40:56+00:00", "app/advanced-development/data-models/primary-key/page.mdx": "2024-07-02T12:34:44+03:00", "app/advanced-development/modules/module-links/page.mdx": "2024-07-24T09:16:01+02:00", @@ -80,6 +81,10 @@ export const generatedEditDates = { "app/advanced-development/modules/query/page.mdx": "2024-09-11T10:46:49.512Z", "app/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx": "2024-09-10T11:39:51.171Z", "app/debugging-and-testing/testing-tools/modules-tests/page.mdx": "2024-09-10T11:39:51.171Z", + "app/debugging-and-testing/instrumentation/page.mdx": "2024-09-17T08:53:15.910Z", + "app/advanced-development/api-routes/additional-data/page.mdx": "2024-09-18T12:22:26.063Z", + "app/advanced-development/workflows/page.mdx": "2024-09-18T08:00:57.364Z", + "app/advanced-development/workflows/variable-manipulation/page.mdx": "2024-09-18T09:03:20.805Z", "app/customization/custom-features/api-route/page.mdx": "2024-09-12T12:42:34.201Z", "app/customization/custom-features/module/page.mdx": "2024-09-12T12:39:37.928Z", "app/customization/custom-features/workflow/page.mdx": "2024-09-12T12:40:39.582Z", diff --git a/www/apps/book/providers/main-nav.tsx b/www/apps/book/providers/main-nav.tsx index d5c2fa86d0b29..670c9d574c15e 100644 --- a/www/apps/book/providers/main-nav.tsx +++ b/www/apps/book/providers/main-nav.tsx @@ -51,6 +51,9 @@ export const MainNavProvider = ({ children }: MainNavProviderProps) => { navItems={navigationDropdownItems} reportIssueLink={reportLink} editDate={editDate} + breadcrumbOptions={{ + showCategories: false, + }} > {children} diff --git a/www/apps/book/providers/search.tsx b/www/apps/book/providers/search.tsx index 75616006328a7..2836ae0c9a61d 100644 --- a/www/apps/book/providers/search.tsx +++ b/www/apps/book/providers/search.tsx @@ -21,8 +21,8 @@ const SearchProvider = ({ children }: SearchProviderProps) => { mainIndexName: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp", indices: [ - process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp", process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp", + process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp", ], }} searchProps={{ diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index c0c1a504ad822..daaa143921cf2 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -235,6 +235,11 @@ export const sidebar = numberSidebarItems( path: "/advanced-development/api-routes/cors", title: "Handling CORS", }, + { + type: "link", + path: "/advanced-development/api-routes/additional-data", + title: "Additional Data", + }, ], }, { @@ -367,18 +372,24 @@ export const sidebar = numberSidebarItems( ], }, { - type: "sub-category", + type: "link", + path: "/advanced-development/workflows", title: "Workflows", children: [ { type: "link", - path: "/advanced-development/workflows/constructor-constraints", - title: "Constraints", + path: "/advanced-development/workflows/variable-manipulation", + title: "Variable Manipulation", }, { type: "link", path: "/advanced-development/workflows/conditions", - title: "Conditions in Workflows", + title: "Using Conditions", + }, + { + type: "link", + path: "/advanced-development/workflows/constructor-constraints", + title: "Constructor Constraints", }, { type: "link", @@ -425,11 +436,6 @@ export const sidebar = numberSidebarItems( path: "/advanced-development/workflows/execute-another-workflow", title: "Execute Another Workflow", }, - { - type: "link", - path: "/advanced-development/workflows/advanced-example", - title: "Example: Advanced Workflow", - }, ], }, { diff --git a/www/apps/resources/app/contribution-guidelines/docs/page.mdx b/www/apps/resources/app/contribution-guidelines/docs/page.mdx index 18cf49007fe5e..a7a28ae3d24ef 100644 --- a/www/apps/resources/app/contribution-guidelines/docs/page.mdx +++ b/www/apps/resources/app/contribution-guidelines/docs/page.mdx @@ -20,7 +20,7 @@ The workspace has the following two directories: - `apps`: this directory holds the different documentation websites and projects. - `book`: includes the codebase for the Medusa Book. It's built with [Next.js 14](https://nextjs.org/). - - `resources`: includes the codebase for the Medusa Learning Resources documentation. It's built with [Next.js 14](https://nextjs.org/). + - `resources`: includes the codebase for the Medusa Development Resources documentation. It's built with [Next.js 14](https://nextjs.org/). - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 14](https://nextjs.org/). - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 14](https://nextjs.org/). - `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. @@ -37,7 +37,7 @@ All documentation projects are built with Next.js. The content is writtin in MDX The content of the Medusa Book is placed under the `www/apps/book/app` directory. -### Medusa Learning Resources Content +### Medusa Development Resources Content The content of the Medusa Resources documentation is placed under the `www/apps/resources/app` directory. diff --git a/www/apps/resources/app/layout.tsx b/www/apps/resources/app/layout.tsx index 795717e2fcfd1..7ee078e186bca 100644 --- a/www/apps/resources/app/layout.tsx +++ b/www/apps/resources/app/layout.tsx @@ -14,7 +14,7 @@ export const metadata: Metadata = { default: config.titleSuffix || "", }, description: - "Explore Medusa's recipes, API references, configurations, and more.", + "Explore Medusa's recipes, API references, configurations, storefront guides, and more.", metadataBase: new URL( process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000" ), diff --git a/www/apps/resources/app/not-found.mdx b/www/apps/resources/app/not-found.mdx index c2bd96b6254f0..bc4143fb6d466 100644 --- a/www/apps/resources/app/not-found.mdx +++ b/www/apps/resources/app/not-found.mdx @@ -25,7 +25,7 @@ If you think this is a mistake, please [report this issue on GitHub](https://git icon: BookOpen }, { - title: "Learning Resources", + title: "Development Resources", href: "!resources!", icon: AcademicCapSolid }, diff --git a/www/apps/resources/app/page.mdx b/www/apps/resources/app/page.mdx index 84b956e3922e2..40c102374552f 100644 --- a/www/apps/resources/app/page.mdx +++ b/www/apps/resources/app/page.mdx @@ -23,7 +23,7 @@ import { BuildingsSolid } from "@medusajs/icons" -# Medusa Learning Resources +# Medusa Development Resources Explore Medusa's commerce modules, SDKs, configurations, recipes, and more. diff --git a/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx b/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx index 6329bd35e9947..e7c887f1ee0a3 100644 --- a/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx +++ b/www/apps/resources/app/recipes/digital-products/examples/standard/page.mdx @@ -1817,79 +1817,80 @@ In a later step, you’ll add an API route to allow customers to view and downlo --- -## Step 12: Handle the Digital Product Order Event +## Step 12: Fulfill Digital Order Workflow -In this step, you'll create a subscriber that listens to the `digital_product_order.created` event and sends a customer an email with the digital products they purchased. +In this step, you'll create a workflow that fulfills a digital order by sending a notification to the customer. Later, you'll execute this workflow in a subscriber. -Create the file `digital-product/src/subscribers/handle-digital-order.ts` with the following content: +The workflow has the following steps: -export const subscriberHighlight = [ - ["20", "notificationModuleService", "Resolve the Notification Module's service to use it later to send a notification."], - ["22", "fileModuleService", "Resolve the File Module's service to use it later to retrieve a media's URL."], - ["26", "query", "Run the query to retrieve the digital product order."] -] +1. Retrieve the digital product order's details. For this, you'll use the `useRemoteQueryStep` imported from `@medusajs/core-flows`. +2. Send a notification to the customer with the digital products to download. -```ts title="digital-product/src/subscribers/handle-digital-order.ts" highlights={subscriberHighlight} collapsibleLines="1-14" expandMoreLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/medusa" -import { - INotificationModuleService, - IFileModuleService, -} from "@medusajs/types" -import { - Modules, - ContainerRegistrationKeys, -} from "@medusajs/utils" -import { MediaType } from "../modules/digital-product/types" +So, you only need to implement the second step. -async function digitalProductOrderCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - const notificationModuleService: INotificationModuleService = container - .resolve(Modules.NOTIFICATION) - const fileModuleService: IFileModuleService = container.resolve( - Modules.FILE - ) +### Add Types - const { data: [digitalProductOrder] } = await query.graph({ - entity: "digital_product_order", - fields: [ - "*", - "products.*", - "products.medias.*", - "order.*", - ], - filters: { - id: data.id, - }, - }) +Before creating the step, add to `src/modules/digital-product/types/index.ts` the following: + +```ts +import { OrderDTO } from "@medusajs/types" + +// ... - // TODO format and send notification +export type DigitalProductOrderData = { + id: string + status: OrderStatus + products?: DigitalProductData[] + order?: OrderDTO } +``` -export default digitalProductOrderCreatedHandler +This adds a type for a digital product order, which you'll use next. -export const config: SubscriberConfig = { - event: "digital_product_order.created", +### Create sendDigitalOrderNotificationStep + +To create the step, create the file `src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts` with the following content: + +```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts" collapsibleLines="1-11" expandMoreLabel="Show Imports" +import { + createStep, + StepResponse +} from "@medusajs/workflows-sdk" +import { + INotificationModuleService, + IFileModuleService +} from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { DigitalProductOrderData, MediaType } from "../../../modules/digital-product/types" + +type SendDigitalOrderNotificationStepInput = { + digital_product_order: DigitalProductOrderData } + +export const sendDigitalOrderNotificationStep = createStep( + "send-digital-order-notification", + async ({ + digital_product_order: digitalProductOrder + }: SendDigitalOrderNotificationStepInput, + { container }) => { + const notificationModuleService: INotificationModuleService = container + .resolve(ModuleRegistrationName.NOTIFICATION) + const fileModuleService: IFileModuleService = container.resolve( + ModuleRegistrationName.FILE + ) + + // TODO assemble notification + } +) ``` -This adds a subscriber that listens to the `digital_product_order.created` event. For now, it just resolves dependencies and retrieves the digital product order. +This creates the `sendDigitalOrderNotificationStep` step that receives a digital product order as an input. -Next, replace the `TODO` with the following: +In the step, so far you resolve the main services of the Notification and File Modules. -export const subscriber2Highlights = [ - ["1", "notificationData", "Format the data to be sent as a notification payload."], - ["10", "retrieveFile", "Retrieve the media's URL using the File Module's service."], - ["22", "createNotifications", "Send the notification to the customer."], - ["24", `"digital-order-template"`, "Replace with a real template ID."] -] +Replace the `TODO` with the following: -```ts highlights={subscriber2Highlights} +```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts" const notificationData = await Promise.all( digitalProductOrder.products.map(async (product) => { const medias = [] @@ -1906,44 +1907,169 @@ const notificationData = await Promise.all( return { name: product.name, - medias, + medias } }) ) -await notificationModuleService.createNotifications({ +// TODO send notification +``` + +In this snippet, you put together the data to send in the notification. You loop over the digital products in the order and retrieve the URL of their main files using the File Module. + +Finally, replace the new `TODO` with the following: + +```ts title="src/workflows/fulfill-digital-order/steps/send-digital-order-notification.ts" +const notification = await notificationModuleService.createNotifications({ to: digitalProductOrder.order.email, template: "digital-order-template", channel: "email", data: { - products: notificationData, - }, + products: notificationData + } }) + +return new StepResponse(notification) ``` -First, you format the data payload to send in the notification by retrieving the URLs of the purchased products' main medias. You use the File Module's service to retrieve the media URLs. +You use the `createNotifications` method of the Notification Module's main service to send an email using the installed provider. -Then, you use the Notification Module's service to send the notification as an email. +### Create Workflow - +Create the workflow in the file `src/workflows/fulfill-digital-order/index.ts`: -Replace the `digital-order-template` with a real template ID from your third-party notification service. +export const fulfillWorkflowHighlights = [ + ["17", "useRemoteQueryStep", "Retrieve the digital product order's details."], + ["33", "sendDigitalOrderNotificationStep", "Send a notification to the customer."] +] - +```ts title="src/workflows/fulfill-digital-order/index.ts" highlights={fulfillWorkflowHighlights} collapsibleLines="1-10" expandMoreLabel="Show Imports" +import { + createWorkflow, + WorkflowResponse +} from "@medusajs/workflows-sdk" +import { + useRemoteQueryStep +} from "@medusajs/core-flows" +import { sendDigitalOrderNotificationStep } from "./steps/send-digital-order-notification" -### Test Subscriber Out +type FulfillDigitalOrderWorkflowInput = { + id: string +} -To test out the subscriber, place an order with digital products. This triggers the `digital_product_order.created` event which executes the subscriber. +export const fulfillDigitalOrderWorkflow = createWorkflow( + "fulfill-digital-order", + ({ id }: FulfillDigitalOrderWorkflowInput) => { + const digitalProductOrder = useRemoteQueryStep({ + entry_point: "digital_product_order", + fields: [ + "*", + "products.*", + "products.medias.*", + "order.*" + ], + variables: { + filters: { + id, + }, + }, + list: false, + throw_if_key_not_found: true + }) - + sendDigitalOrderNotificationStep({ + digital_product_order: digitalProductOrder + }) -Check out the [integrations page](../../../../integrations/page.mdx) to find notification and file modules. + return new WorkflowResponse( + digitalProductOrder + ) + } +) +``` - +In the workflow, you: + +1. Retrieve the digital product order's details using the `useRemoteQueryStep` imported from `@medusajs/core-flows`. +2. Send a notification to the customer with the digital product download links using the `sendDigitalOrderNotificationStep`. + +### Configure Notification Module Provider + +In the `sendDigitalOrderNotificationStep`, you use a notification provider configured for the `email` channel to send the notification. + +Check out the [Integrations page](../../../../integrations/page.mdx) to find Notification Module Providers. + +For testing purposes, add to `medusa-config.js` the following to use the Local Notification Module Provider: + +```js title="medusa-config.js" +module.exports = defineConfig({ + // ... + modules: { + // ... + [Modules.NOTIFICATION]: { + resolve: "@medusajs/notification", + options: { + providers: [ + { + resolve: "@medusajs/notification-local", + id: "local", + options: { + name: "Local Notification Provider", + channels: ["email"], + }, + }, + ], + }, + }, + } +}) + +``` + +--- + +## Step 13: Handle the Digital Product Order Event + +In this step, you'll create a subscriber that listens to the `digital_product_order.created` event and executes the workflow from the above step. + +Create the file `src/subscribers/handle-digital-order.ts` with the following content: + +```ts title="src/subscribers/handle-digital-order.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/medusa" +import { + fulfillDigitalOrderWorkflow +} from "../workflows/fulfill-digital-order" + +async function digitalProductOrderCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await fulfillDigitalOrderWorkflow(container).run({ + input: { + id: data.id + } + }) +} + +export default digitalProductOrderCreatedHandler + +export const config: SubscriberConfig = { + event: "digital_product_order.created", +} +``` + +This adds a subscriber that listens to the `digital_product_order.created` event. It executes the `fulfillDigitalOrderWorkflow` to send the customer an email and mark the order's fulfillment as fulfilled. + +### Test Subscriber Out + +To test out the subscriber, place an order with digital products. This triggers the `digital_product_order.created` event which executes the subscriber. --- -## Step 13: Create Store API Routes +## Step 14: Create Store API Routes In this step, you’ll create three store API routes: @@ -2172,7 +2298,7 @@ You’ll test out these API routes in the next step. --- -## Step 14: Customize Next.js Starter +## Step 15: Customize Next.js Starter In this section, you’ll customize the [Next.js Starter storefront](../../../../nextjs-starter/page.mdx) to: diff --git a/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx b/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx index dabfca141f1b9..8f8cfad814c27 100644 --- a/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/examples/restaurant-delivery/page.mdx @@ -24,9 +24,6 @@ By following this example, you’ll have a restaurant-delivery platform with the 3. Delivery handling, from the restaurant accepting the order to the driver delivering the order to the customer. 4. Real-time tracking of the order’s delivery status. -- Example Repository -- OpenAPI Spec for Postman - - `price_list_type` + `calculated_price.price_list_type` The type of the variant price. diff --git a/www/apps/resources/config/index.ts b/www/apps/resources/config/index.ts index c9b7c3748a65f..d989fae0ec59a 100644 --- a/www/apps/resources/config/index.ts +++ b/www/apps/resources/config/index.ts @@ -5,7 +5,7 @@ import { generatedSidebar } from "../generated/sidebar.mjs" const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000" export const config: DocsConfig = { - titleSuffix: "Medusa Learning Resources", + titleSuffix: "Medusa Development Resources", baseUrl, basePath: process.env.NEXT_PUBLIC_BASE_PATH, sidebar: { diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 430a858e81f02..ad96b9d243274 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -141,7 +141,7 @@ export const generatedEditDates = { "app/nextjs-starter/page.mdx": "2024-07-01T10:21:19+03:00", "app/recipes/b2b/page.mdx": "2024-08-29T09:23:12.736Z", "app/recipes/commerce-automation/page.mdx": "2024-08-05T07:24:27+00:00", - "app/recipes/digital-products/examples/standard/page.mdx": "2024-09-11T10:50:14.310Z", + "app/recipes/digital-products/examples/standard/page.mdx": "2024-09-17T14:30:02.190Z", "app/recipes/digital-products/page.mdx": "2024-08-02T13:02:06+00:00", "app/recipes/ecommerce/page.mdx": "2024-06-09T15:18:43+02:00", "app/recipes/integrate-ecommerce-stack/page.mdx": "2024-08-05T07:24:27+00:00", diff --git a/www/apps/resources/providers/main-nav.tsx b/www/apps/resources/providers/main-nav.tsx index d5c2fa86d0b29..ea04fedd310e4 100644 --- a/www/apps/resources/providers/main-nav.tsx +++ b/www/apps/resources/providers/main-nav.tsx @@ -23,10 +23,14 @@ export const MainNavProvider = ({ children }: MainNavProviderProps) => { () => getNavDropdownItems({ basePath: config.baseUrl, - activePath: basePathUrl(), + activePath: basePathUrl( + pathname.startsWith("/commerce-modules") + ? "/commerce-modules" + : undefined + ), version: "v2", }), - [] + [pathname] ) const reportLink = useMemo( diff --git a/www/apps/resources/providers/search.tsx b/www/apps/resources/providers/search.tsx index da7390c22f591..a936ef6f11fef 100644 --- a/www/apps/resources/providers/search.tsx +++ b/www/apps/resources/providers/search.tsx @@ -21,8 +21,8 @@ const SearchProvider = ({ children }: SearchProviderProps) => { mainIndexName: process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp", indices: [ - process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp", process.env.NEXT_PUBLIC_DOCS_ALGOLIA_INDEX_NAME || "temp", + process.env.NEXT_PUBLIC_API_ALGOLIA_INDEX_NAME || "temp", ], }} searchProps={{ diff --git a/www/apps/ui/src/app/not-found.mdx b/www/apps/ui/src/app/not-found.mdx index c2bd96b6254f0..bc4143fb6d466 100644 --- a/www/apps/ui/src/app/not-found.mdx +++ b/www/apps/ui/src/app/not-found.mdx @@ -25,7 +25,7 @@ If you think this is a mistake, please [report this issue on GitHub](https://git icon: BookOpen }, { - title: "Learning Resources", + title: "Development Resources", href: "!resources!", icon: AcademicCapSolid }, diff --git a/www/apps/ui/src/app/not-found.tsx b/www/apps/ui/src/app/not-found.tsx index af661733cad63..a57b1bcdecdf3 100644 --- a/www/apps/ui/src/app/not-found.tsx +++ b/www/apps/ui/src/app/not-found.tsx @@ -35,7 +35,7 @@ export default function NotFound() { icon: BookOpen, }, { - title: "Learning Resources", + title: "Development Resources", href: "!resources!", icon: AcademicCapSolid, }, diff --git a/www/apps/user-guide/app/not-found.mdx b/www/apps/user-guide/app/not-found.mdx index c2bd96b6254f0..bc4143fb6d466 100644 --- a/www/apps/user-guide/app/not-found.mdx +++ b/www/apps/user-guide/app/not-found.mdx @@ -25,7 +25,7 @@ If you think this is a mistake, please [report this issue on GitHub](https://git icon: BookOpen }, { - title: "Learning Resources", + title: "Development Resources", href: "!resources!", icon: AcademicCapSolid }, diff --git a/www/packages/docs-ui/src/components/Icons/NavigationDropdown/Modules/index.tsx b/www/packages/docs-ui/src/components/Icons/NavigationDropdown/Modules/index.tsx new file mode 100644 index 0000000000000..0ae3bebb89aa9 --- /dev/null +++ b/www/packages/docs-ui/src/components/Icons/NavigationDropdown/Modules/index.tsx @@ -0,0 +1,33 @@ +import { IconProps } from "@medusajs/icons/dist/types" +import React from "react" + +export const NavigationDropdownModulesIcon = (props: IconProps) => { + return ( + + + + + + + + + + + + ) +} diff --git a/www/packages/docs-ui/src/components/MainNav/Breadcrumb/index.tsx b/www/packages/docs-ui/src/components/MainNav/Breadcrumb/index.tsx index 4af5df4be40fb..cccf1ad1edba4 100644 --- a/www/packages/docs-ui/src/components/MainNav/Breadcrumb/index.tsx +++ b/www/packages/docs-ui/src/components/MainNav/Breadcrumb/index.tsx @@ -1,13 +1,17 @@ "use client" import React, { useMemo } from "react" -import { Button, CurrentItemsState, useSidebar } from "../../.." +import { Button, CurrentItemsState, useMainNav, useSidebar } from "../../.." import clsx from "clsx" import Link from "next/link" import { SidebarItemLink } from "types" export const MainNavBreadcrumbs = () => { const { currentItems, getActiveItem } = useSidebar() + const { + activeItem: mainNavActiveItem, + breadcrumbOptions: { showCategories }, + } = useMainNav() const getLinkPath = (item?: SidebarItemLink): string | undefined => { if (!item) { @@ -27,14 +31,26 @@ export const MainNavBreadcrumbs = () => { const parentPath = item.parentItem?.type === "link" ? getLinkPath(item.parentItem) + : (item.parentItem?.type === "category" && showCategories) || + item.parentItem?.type === "sub-category" + ? "#" : undefined const firstItemPath = - item.default[0].type === "link" ? getLinkPath(item.default[0]) : undefined + item.default[0].type === "link" + ? getLinkPath(item.default[0]) + : (item.default[0].type === "category" && showCategories) || + item.default[0].type === "sub-category" + ? "#" + : undefined + + const breadcrumbPath = parentPath || firstItemPath || "/" - tempBreadcrumbItems.set( - parentPath || firstItemPath || "/", - item.parentItem?.childSidebarTitle || item.parentItem?.title || "" - ) + if (!mainNavActiveItem?.path.endsWith(breadcrumbPath)) { + tempBreadcrumbItems.set( + breadcrumbPath, + item.parentItem?.childSidebarTitle || item.parentItem?.title || "" + ) + } return tempBreadcrumbItems } @@ -48,10 +64,21 @@ export const MainNavBreadcrumbs = () => { } const activeItem = getActiveItem() - if (activeItem) { + if (activeItem && !mainNavActiveItem?.path.endsWith(activeItem.path)) { + if ( + activeItem.parentItem && + (activeItem.parentItem.type !== "category" || showCategories) + ) { + tempBreadcrumbItems.set( + activeItem.parentItem.type === "link" + ? getLinkPath(activeItem.parentItem) || "#" + : "#", + activeItem.parentItem.title || "" + ) + } tempBreadcrumbItems.set( getLinkPath(activeItem) || "/", - activeItem?.title || "" + activeItem.title || "" ) } @@ -70,9 +97,17 @@ export const MainNavBreadcrumbs = () => { / ))} diff --git a/www/packages/docs-ui/src/components/Search/Hits/index.tsx b/www/packages/docs-ui/src/components/Search/Hits/index.tsx index a7134c5cb8267..c5bed2bbb4a0b 100644 --- a/www/packages/docs-ui/src/components/Search/Hits/index.tsx +++ b/www/packages/docs-ui/src/components/Search/Hits/index.tsx @@ -97,7 +97,7 @@ export const SearchHits = ({ setNoResults, checkInternalPattern, }: SearchHitsProps) => { - const { hits } = useHits() + const { items: hits } = useHits() const { status } = useInstantSearch() const { setIsOpen } = useSearch() diff --git a/www/packages/docs-ui/src/constants.tsx b/www/packages/docs-ui/src/constants.tsx index 93257285ecace..974a27a818a4c 100644 --- a/www/packages/docs-ui/src/constants.tsx +++ b/www/packages/docs-ui/src/constants.tsx @@ -9,6 +9,7 @@ import { NavigationDropdownUiIcon } from "./components/Icons/NavigationDropdown/ import { NavigationDropdownDocV1Icon } from "./components/Icons/NavigationDropdown/DocV1" import { NavigationDropdownUserIcon } from "./components/Icons/NavigationDropdown/User" import { NavigationDropdownResourcesIcon } from "./components/Icons/NavigationDropdown/Resources" +import { NavigationDropdownModulesIcon } from "./components/Icons/NavigationDropdown/Modules" export const GITHUB_ISSUES_PREFIX = `https://github.com/medusajs/medusa/issues/new?assignees=&labels=type%3A+docs&template=docs.yml` export const GITHUB_UI_ISSUES_PREFIX = `https://github.com/medusajs/ui/issues/new?labels=documentation` @@ -20,11 +21,17 @@ export const navDropdownItemsV2: NavigationDropdownItem[] = [ icon: NavigationDropdownDocIcon, title: "Documentation", }, + { + type: "link", + path: `/v2/resources/commerce-modules`, + icon: NavigationDropdownModulesIcon, + title: "Commerce Modules", + }, { type: "link", path: `/v2/resources`, icon: NavigationDropdownResourcesIcon, - title: "Learning Resources", + title: "Development Resources", }, { type: "link", diff --git a/www/packages/docs-ui/src/providers/MainNav/index.tsx b/www/packages/docs-ui/src/providers/MainNav/index.tsx index 4037054c528b0..6b050e4943d36 100644 --- a/www/packages/docs-ui/src/providers/MainNav/index.tsx +++ b/www/packages/docs-ui/src/providers/MainNav/index.tsx @@ -1,13 +1,18 @@ "use client" import React, { createContext, useContext, useMemo } from "react" -import { NavigationDropdownItem } from "types" +import { + BreadcrumbOptions, + NavigationDropdownItem, + NavigationDropdownItemLink, +} from "types" export type MainNavContext = { navItems: NavigationDropdownItem[] - activeItem?: NavigationDropdownItem + activeItem?: NavigationDropdownItemLink reportIssueLink: string editDate?: string + breadcrumbOptions: BreadcrumbOptions } const MainNavContext = createContext(null) @@ -16,6 +21,7 @@ export type MainNavProviderProps = { navItems: NavigationDropdownItem[] reportIssueLink: string editDate?: string + breadcrumbOptions?: BreadcrumbOptions children?: React.ReactNode } @@ -24,9 +30,15 @@ export const MainNavProvider = ({ reportIssueLink, children, editDate, + breadcrumbOptions = { + showCategories: true, + }, }: MainNavProviderProps) => { const activeItem = useMemo( - () => navItems.find((item) => item.type === "link" && item.isActive), + () => + navItems.find( + (item) => item.type === "link" && item.isActive + ) as NavigationDropdownItemLink, [navItems] ) @@ -37,6 +49,7 @@ export const MainNavProvider = ({ activeItem, reportIssueLink, editDate, + breadcrumbOptions, }} > {children} diff --git a/www/packages/docs-ui/src/providers/Search/index.tsx b/www/packages/docs-ui/src/providers/Search/index.tsx index de69022c34f9c..2b9430f5704f4 100644 --- a/www/packages/docs-ui/src/providers/Search/index.tsx +++ b/www/packages/docs-ui/src/providers/Search/index.tsx @@ -13,6 +13,9 @@ import { checkArraySameElms } from "../../utils" import { liteClient as algoliasearch, LiteClient as SearchClient, + type SearchResponses, + type SearchHits, + SearchResponse, } from "algoliasearch/lite" import clsx from "clsx" import { CSSTransition, SwitchTransition } from "react-transition-group" @@ -78,11 +81,37 @@ export const SearchProvider = ({ async search(searchParams) { const requests = "requests" in searchParams ? searchParams.requests : searchParams - const noQueries = requests.every( + // always send this request, which is the main request with no filters + const mainRequest = requests[0] + + // retrieve only requests that have filters + // this is to ensure that we show no result if no filter is selected + const requestsWithFilters = requests.filter((item) => { + if ( + !item.params || + typeof item.params !== "object" || + !("tagFilters" in item.params) + ) { + return false + } + + const tagFilters = item.params.tagFilters as string[] + + // if no tag filters are specified, there's still one item, + // which is an empty array + return tagFilters.length >= 1 && tagFilters[0].length > 0 + }) + + // check whether a query is entered in the search box + const noQueries = requestsWithFilters.every( (item) => - ("facetQuery" in item && !item.facetQuery) || - ("query" in item && !item.query) + !item.facetQuery && + (!item.params || + typeof item.params !== "object" || + !("query" in item.params) || + !item.params.query) ) + if (noQueries) { return Promise.resolve({ results: requests.map(() => ({ @@ -99,7 +128,73 @@ export const SearchProvider = ({ }) } - return algoliaClient.search(searchParams) + // split requests per tags + const newRequests: typeof requestsWithFilters = [mainRequest] + for (const request of requestsWithFilters) { + const params = request.params as Record + const tagFilters = (params.tagFilters as string[][])[0] + + // if only one tag is selected, keep the request as-is + if (tagFilters.length === 1) { + newRequests.push(request) + + continue + } + + // if multiple tags are selected, split the tags + // to retrieve a small subset of results per each tag. + newRequests.push( + ...tagFilters.map((tag) => ({ + ...request, + params: { + ...params, + tagFilters: [tag], + }, + hitsPerPage: 4, + })) + ) + } + + return algoliaClient + .search(newRequests) + .then((response) => { + // combine results of the same index and return the results + const resultsByIndex: { + [indexName: string]: SearchResponse + } = {} + // extract the response of the main request + const mainResult = response.results[0] + + response.results.forEach((result, indexNum) => { + if (indexNum === 0) { + // ignore the main request's result + return + } + const resultIndex = "index" in result ? result.index : undefined + const resultHits = "hits" in result ? result.hits : [] + + if (!resultIndex) { + return + } + + resultsByIndex[resultIndex] = { + ...result, + ...(resultsByIndex[resultIndex] || {}), + hits: [ + ...(resultsByIndex[resultIndex]?.hits || []), + ...resultHits, + ], + nbHits: + (resultsByIndex[resultIndex]?.nbHits || 0) + + resultHits.length, + } + }) + + return { + // append the results with the main request's results + results: [mainResult, ...Object.values(resultsByIndex)], + } as SearchResponses + }) }, } }, [algolia.appId, algolia.apiKey]) diff --git a/www/packages/docs-ui/src/providers/Sidebar/index.tsx b/www/packages/docs-ui/src/providers/Sidebar/index.tsx index 2a6bfe1b2c96a..e9df8df7ad4c0 100644 --- a/www/packages/docs-ui/src/providers/Sidebar/index.tsx +++ b/www/packages/docs-ui/src/providers/Sidebar/index.tsx @@ -20,6 +20,7 @@ import { SidebarItemLink, InteractiveSidebarItem, SidebarItemCategory, + SidebarItemLinkWithParent, } from "types" export type CurrentItemsState = SidebarSectionItems & { @@ -34,7 +35,7 @@ export type SidebarContextType = { items: SidebarSectionItems currentItems: CurrentItemsState | undefined activePath: string | null - getActiveItem: () => SidebarItemLink | undefined + getActiveItem: () => SidebarItemLinkWithParent | undefined setActivePath: (path: string | null) => void isLinkActive: (item: SidebarItem, checkChildren?: boolean) => boolean isChildrenActive: (item: SidebarItemCategory) => boolean @@ -100,8 +101,8 @@ const findItem = ( section: SidebarItem[], item: Partial, checkChildren = true -): SidebarItemLink | undefined => { - let foundItem: SidebarItemLink | undefined +): SidebarItemLinkWithParent | undefined => { + let foundItem: SidebarItemLinkWithParent | undefined section.some((i) => { if (i.type === "separator") { return false @@ -110,6 +111,9 @@ const findItem = ( foundItem = i } else if (checkChildren && i.children) { foundItem = findItem(i.children, item) + if (foundItem && !foundItem.parentItem) { + foundItem.parentItem = i + } } return foundItem !== undefined diff --git a/www/packages/types/src/navigation-dropdown.ts b/www/packages/types/src/navigation-dropdown.ts index c7f1a8044e0a0..1fce274a6b7af 100644 --- a/www/packages/types/src/navigation-dropdown.ts +++ b/www/packages/types/src/navigation-dropdown.ts @@ -14,3 +14,7 @@ export type NavigationDropdownItem = | { type: "divider" } + +export type BreadcrumbOptions = { + showCategories?: boolean +} diff --git a/www/packages/types/src/sidebar.ts b/www/packages/types/src/sidebar.ts index b6fdbe4e88fd6..0ddea0bac3127 100644 --- a/www/packages/types/src/sidebar.ts +++ b/www/packages/types/src/sidebar.ts @@ -41,6 +41,10 @@ export type InteractiveSidebarItem = | SidebarItemCategory | SidebarItemSubCategory +export type SidebarItemLinkWithParent = SidebarItemLink & { + parentItem?: InteractiveSidebarItem +} + export type SidebarItem = InteractiveSidebarItem | SidebarItemSeparator export type SidebarSectionItems = { diff --git a/www/utils/generated/oas-output/schemas/AdminFulfillmentProvider.ts b/www/utils/generated/oas-output/schemas/AdminFulfillmentProvider.ts index cc49eec058462..4c036e5741337 100644 --- a/www/utils/generated/oas-output/schemas/AdminFulfillmentProvider.ts +++ b/www/utils/generated/oas-output/schemas/AdminFulfillmentProvider.ts @@ -1,7 +1,7 @@ /** * @schema AdminFulfillmentProvider * type: object - * description: The shipping option's provider. + * description: The fulfillment provider's details. * x-schemaName: AdminFulfillmentProvider * required: * - id diff --git a/www/utils/generated/oas-output/schemas/AdminShippingProfile.ts b/www/utils/generated/oas-output/schemas/AdminShippingProfile.ts index ae81604efaf1b..99d8edfe49e53 100644 --- a/www/utils/generated/oas-output/schemas/AdminShippingProfile.ts +++ b/www/utils/generated/oas-output/schemas/AdminShippingProfile.ts @@ -1,8 +1,47 @@ /** * @schema AdminShippingProfile * type: object - * description: The shipping option's shipping profile. + * description: The shipping profile's details.. * x-schemaName: AdminShippingProfile + * properties: + * id: + * type: string + * title: id + * description: The shipping profile's ID. + * name: + * type: string + * title: name + * description: The shipping profile's name. + * type: + * type: string + * title: type + * description: The shipping profile's type. + * metadata: + * type: object + * description: The shipping profile's metadata, holds custom key-value pairs. + * created_at: + * type: string + * format: date-time + * title: created_at + * description: The date the shipping profile was created. + * updated_at: + * type: string + * format: date-time + * title: updated_at + * description: The date the shipping profile was updated. + * deleted_at: + * type: string + * format: date-time + * title: deleted_at + * description: The date the shipping profile was deleted. + * required: + * - id + * - name + * - type + * - metadata + * - created_at + * - updated_at + * - deleted_at * */ diff --git a/www/utils/generated/oas-output/schemas/AdminShippingProfileDeleteResponse.ts b/www/utils/generated/oas-output/schemas/AdminShippingProfileDeleteResponse.ts index eab4f24896abd..3d9cc66f12821 100644 --- a/www/utils/generated/oas-output/schemas/AdminShippingProfileDeleteResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminShippingProfileDeleteResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminShippingProfileDeleteResponse * type: object - * description: SUMMARY + * description: The details of the shipping profile's deletion. * x-schemaName: AdminShippingProfileDeleteResponse * required: * - id @@ -20,7 +20,7 @@ * deleted: * type: boolean * title: deleted - * description: Whether the Shipping Profile was deleted. + * description: Whether the shipping profile was deleted. * */ diff --git a/www/utils/generated/oas-output/schemas/AdminShippingProfileResponse.ts b/www/utils/generated/oas-output/schemas/AdminShippingProfileResponse.ts index f5981cc31de4f..89efabb4ae24c 100644 --- a/www/utils/generated/oas-output/schemas/AdminShippingProfileResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminShippingProfileResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminShippingProfileResponse * type: object - * description: SUMMARY + * description: The shipping profile's details. * x-schemaName: AdminShippingProfileResponse * required: * - shipping_profile diff --git a/www/utils/generated/oas-output/schemas/AdminStockLocation.ts b/www/utils/generated/oas-output/schemas/AdminStockLocation.ts index 06956ca356a4c..7503b706e8a24 100644 --- a/www/utils/generated/oas-output/schemas/AdminStockLocation.ts +++ b/www/utils/generated/oas-output/schemas/AdminStockLocation.ts @@ -1,7 +1,7 @@ /** * @schema AdminStockLocation * type: object - * description: The parent's location. + * description: The stock location's details. * x-schemaName: AdminStockLocation * required: * - id @@ -19,22 +19,22 @@ * address_id: * type: string * title: address_id - * description: The location's address id. + * description: The ID of the associated address. * address: * $ref: "#/components/schemas/AdminStockLocationAddress" * sales_channels: * type: array - * description: The location's sales channels. + * description: The sales channels associated with the location. * items: * $ref: "#/components/schemas/AdminSalesChannel" * fulfillment_providers: * type: array - * description: The location's fulfillment providers. + * description: The fulfillment providers associated with the location. * items: * $ref: "#/components/schemas/AdminFulfillmentProvider" * fulfillment_sets: * type: array - * description: The location's fulfillment sets. + * description: The fulfillment sets associated with the location. * items: * $ref: "#/components/schemas/AdminFulfillmentSet" * diff --git a/www/utils/generated/oas-output/schemas/AdminStockLocationAddress.ts b/www/utils/generated/oas-output/schemas/AdminStockLocationAddress.ts index 35a5bb6b3999d..4917c534eb64b 100644 --- a/www/utils/generated/oas-output/schemas/AdminStockLocationAddress.ts +++ b/www/utils/generated/oas-output/schemas/AdminStockLocationAddress.ts @@ -1,8 +1,56 @@ /** * @schema AdminStockLocationAddress * type: object - * description: The location's address. + * description: The details of the stock location address. * x-schemaName: AdminStockLocationAddress + * properties: + * id: + * type: string + * title: id + * description: The address's ID. + * address_1: + * type: string + * title: address_1 + * description: The address's first line. + * address_2: + * type: string + * title: address_2 + * description: The address's second line. + * company: + * type: string + * title: company + * description: The address's company. + * country_code: + * type: string + * title: country_code + * description: The address's country code. + * example: us + * city: + * type: string + * title: city + * description: The address's city. + * phone: + * type: string + * title: phone + * description: The address's phone. + * postal_code: + * type: string + * title: postal_code + * description: The address's postal code. + * province: + * type: string + * title: province + * description: The address's province. + * required: + * - id + * - address_1 + * - address_2 + * - company + * - country_code + * - city + * - phone + * - postal_code + * - province * */ diff --git a/www/utils/generated/oas-output/schemas/AdminStockLocationDeleteResponse.ts b/www/utils/generated/oas-output/schemas/AdminStockLocationDeleteResponse.ts index 53ab03d2b80d6..25183b5f78ed0 100644 --- a/www/utils/generated/oas-output/schemas/AdminStockLocationDeleteResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminStockLocationDeleteResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminStockLocationDeleteResponse * type: object - * description: SUMMARY + * description: The details of the stock location's deletion. * x-schemaName: AdminStockLocationDeleteResponse * required: * - id @@ -20,7 +20,7 @@ * deleted: * type: boolean * title: deleted - * description: Whether the Stock Location was deleted. + * description: Whether the stock location was deleted. * */ diff --git a/www/utils/generated/oas-output/schemas/AdminStockLocationListResponse.ts b/www/utils/generated/oas-output/schemas/AdminStockLocationListResponse.ts index af12caa18f174..719e3d960985a 100644 --- a/www/utils/generated/oas-output/schemas/AdminStockLocationListResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminStockLocationListResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminStockLocationListResponse * type: object - * description: SUMMARY + * description: The paginated list of stock locations. * x-schemaName: AdminStockLocationListResponse * required: * - limit @@ -12,18 +12,18 @@ * limit: * type: number * title: limit - * description: The stock location's limit. + * description: The maximum number of items returned. * offset: * type: number * title: offset - * description: The stock location's offset. + * description: The number of items skipped before retrieving the returned items. * count: * type: number * title: count - * description: The stock location's count. + * description: The total number of items. * stock_locations: * type: array - * description: The stock location's stock locations. + * description: The list of stock locations. * items: * $ref: "#/components/schemas/AdminStockLocation" * diff --git a/www/utils/generated/oas-output/schemas/AdminStockLocationResponse.ts b/www/utils/generated/oas-output/schemas/AdminStockLocationResponse.ts index ae3f2ba1a0ed7..0386b303720fe 100644 --- a/www/utils/generated/oas-output/schemas/AdminStockLocationResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminStockLocationResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminStockLocationResponse * type: object - * description: SUMMARY + * description: The stock location's details. * x-schemaName: AdminStockLocationResponse * required: * - stock_location diff --git a/www/utils/generated/oas-output/schemas/AdminStore.ts b/www/utils/generated/oas-output/schemas/AdminStore.ts index 0fe1850546fe0..9052840e382d2 100644 --- a/www/utils/generated/oas-output/schemas/AdminStore.ts +++ b/www/utils/generated/oas-output/schemas/AdminStore.ts @@ -30,28 +30,28 @@ * default_sales_channel_id: * type: string * title: default_sales_channel_id - * description: The store's default sales channel id. + * description: The ID of the sales channel used by default in the store. * default_region_id: * type: string * title: default_region_id - * description: The store's default region id. + * description: The ID of the region used by default in the store. * default_location_id: * type: string * title: default_location_id - * description: The store's default location id. + * description: The ID of the stock location used by default in the store. * metadata: * type: object - * description: The store's metadata. + * description: The store's metadata, can hold custom key-value pairs. * created_at: * type: string * format: date-time * title: created_at - * description: The store's created at. + * description: The date the store was created. * updated_at: * type: string * format: date-time * title: updated_at - * description: The store's updated at. + * description: The date the store was updated. * */ diff --git a/www/utils/generated/oas-output/schemas/AdminStoreCurrency.ts b/www/utils/generated/oas-output/schemas/AdminStoreCurrency.ts index 3290080524b1f..91ae34b045bf8 100644 --- a/www/utils/generated/oas-output/schemas/AdminStoreCurrency.ts +++ b/www/utils/generated/oas-output/schemas/AdminStoreCurrency.ts @@ -1,7 +1,7 @@ /** * @schema AdminStoreCurrency * type: object - * description: The supported currency's supported currencies. + * description: The details of a store's currency. * x-schemaName: AdminStoreCurrency * required: * - id @@ -16,36 +16,37 @@ * id: * type: string * title: id - * description: The supported currency's ID. + * description: The currency's ID. * currency_code: * type: string * title: currency_code - * description: The supported currency's currency code. + * description: The currency's code. + * example: usd * store_id: * type: string * title: store_id - * description: The supported currency's store id. + * description: The ID of the store this currency belongs to. * is_default: * type: boolean * title: is_default - * description: The supported currency's is default. + * description: Whether this currency is the default in the store. * currency: * $ref: "#/components/schemas/AdminCurrency" * created_at: * type: string * format: date-time * title: created_at - * description: The supported currency's created at. + * description: The date the currency was created. * updated_at: * type: string * format: date-time * title: updated_at - * description: The supported currency's updated at. + * description: The date the currency was updated. * deleted_at: * type: string * format: date-time * title: deleted_at - * description: The supported currency's deleted at. + * description: The date the currency was deleted. * */ diff --git a/www/utils/generated/oas-output/schemas/AdminStoreListResponse.ts b/www/utils/generated/oas-output/schemas/AdminStoreListResponse.ts index 92ec2bf842fe8..b2ab6595e8011 100644 --- a/www/utils/generated/oas-output/schemas/AdminStoreListResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminStoreListResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminStoreListResponse * type: object - * description: SUMMARY + * description: The paginated list of stores. * x-schemaName: AdminStoreListResponse * required: * - limit @@ -12,18 +12,18 @@ * limit: * type: number * title: limit - * description: The store's limit. + * description: The maximum number of items returned. * offset: * type: number * title: offset - * description: The store's offset. + * description: The number of items skipped before retrieving the returned items. * count: * type: number * title: count - * description: The store's count. + * description: The total number of items. * stores: * type: array - * description: The store's stores. + * description: The list of stores. * items: * $ref: "#/components/schemas/AdminStore" * diff --git a/www/utils/generated/oas-output/schemas/AdminStoreResponse.ts b/www/utils/generated/oas-output/schemas/AdminStoreResponse.ts index 0fec32274ac07..daa349e2cb560 100644 --- a/www/utils/generated/oas-output/schemas/AdminStoreResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminStoreResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminStoreResponse * type: object - * description: SUMMARY + * description: The store's details. * x-schemaName: AdminStoreResponse * required: * - store diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRate.ts b/www/utils/generated/oas-output/schemas/AdminTaxRate.ts index fddfe277cb086..90525352d23b8 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRate.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRate.ts @@ -1,7 +1,7 @@ /** * @schema AdminTaxRate * type: object - * description: The tax rate's parent. + * description: The tax rate's details. * x-schemaName: AdminTaxRate * required: * - id @@ -22,58 +22,61 @@ * id: * type: string * title: id - * description: The parent's ID. + * description: The tax rate's ID. * rate: * type: number * title: rate - * description: The parent's rate. + * description: The rate to charge. + * example: 10 * code: * type: string * title: code - * description: The parent's code. + * description: The code the tax rate is identified by. * name: * type: string * title: name - * description: The parent's name. + * description: The tax rate's name. * metadata: * type: object - * description: The parent's metadata. + * description: The tax rate's metadata, can hold custom key-value pairs. * tax_region_id: * type: string * title: tax_region_id - * description: The parent's tax region id. + * description: The ID of the tax region this rate belongs to. * is_combinable: * type: boolean * title: is_combinable - * description: The parent's is combinable. + * description: Whether the tax rate should be combined with parent rates. + * externalDocs: + * url: https://docs.medusajs.com/v2/resources/commerce-modules/tax/tax-rates-and-rules#combinable-tax-rates * is_default: * type: boolean * title: is_default - * description: The parent's is default. + * description: Whether this tax rate is the default in the tax region. * created_at: * type: string * format: date-time * title: created_at - * description: The parent's created at. + * description: The date the tax rate was created. * updated_at: * type: string * format: date-time * title: updated_at - * description: The parent's updated at. + * description: The date the tax rate was updated. * deleted_at: * type: string * format: date-time * title: deleted_at - * description: The parent's deleted at. + * description: The date the tax rate was deleted. * created_by: * type: string * title: created_by - * description: The parent's created by. + * description: The ID of the user that created the tax rate. * tax_region: * $ref: "#/components/schemas/AdminTaxRegion" * rules: * type: array - * description: The parent's rules. + * description: The tax rate's rules. * items: * $ref: "#/components/schemas/AdminTaxRateRule" * diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRateDeleteResponse.ts b/www/utils/generated/oas-output/schemas/AdminTaxRateDeleteResponse.ts index d616a2b6e7a45..a9da583cb110e 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRateDeleteResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRateDeleteResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminTaxRateDeleteResponse * type: object - * description: SUMMARY + * description: The details of the tax rate deletion. * x-schemaName: AdminTaxRateDeleteResponse * required: * - id @@ -20,7 +20,7 @@ * deleted: * type: boolean * title: deleted - * description: Whether the Tax Rate was deleted. + * description: Whether the tax rate was deleted. * */ diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRateResponse.ts b/www/utils/generated/oas-output/schemas/AdminTaxRateResponse.ts index d360d93b0910a..3d91b8019db2e 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRateResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRateResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminTaxRateResponse * type: object - * description: SUMMARY + * description: The tax rate's details. * x-schemaName: AdminTaxRateResponse * required: * - tax_rate diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRateRule.ts b/www/utils/generated/oas-output/schemas/AdminTaxRateRule.ts index 1399a883cbfe7..dfcf998ac132d 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRateRule.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRateRule.ts @@ -1,7 +1,7 @@ /** * @schema AdminTaxRateRule * type: object - * description: The rule's rules. + * description: The tax rate rule's details. * x-schemaName: AdminTaxRateRule * required: * - reference @@ -10,11 +10,13 @@ * reference: * type: string * title: reference - * description: The rule's reference. + * description: The name of the table this rule references. + * example: product_type * reference_id: * type: string * title: reference_id - * description: The rule's reference id. + * description: The ID of a record in the table that this rule references. + * example: ptyp_1 * */ diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRegion.ts b/www/utils/generated/oas-output/schemas/AdminTaxRegion.ts index 6c39aeee90fa6..18976c9491d06 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRegion.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRegion.ts @@ -1,19 +1,13 @@ /** * @schema AdminTaxRegion * type: object - * description: The parent's tax region. + * description: The tax region's details. * x-schemaName: AdminTaxRegion * required: * - id - * - rate - * - code * - country_code * - province_code - * - name * - metadata - * - tax_region_id - * - is_combinable - * - is_default * - parent_id * - created_at * - updated_at @@ -27,67 +21,44 @@ * type: string * title: id * description: The tax region's ID. - * rate: - * type: number - * title: rate - * description: The tax region's rate. - * code: - * type: string - * title: code - * description: The tax region's code. * country_code: * type: string * title: country_code * description: The tax region's country code. + * example: us * province_code: * type: string * title: province_code * description: The tax region's province code. - * name: - * type: string - * title: name - * description: The tax region's name. * metadata: * type: object - * description: The tax region's metadata. - * tax_region_id: - * type: string - * title: tax_region_id - * description: The tax region's tax region id. - * is_combinable: - * type: boolean - * title: is_combinable - * description: The tax region's is combinable. - * is_default: - * type: boolean - * title: is_default - * description: The tax region's is default. + * description: The tax region's metadata, can hold custom key-value pairs. * parent_id: * type: string * title: parent_id - * description: The tax region's parent id. + * description: The ID of the parent tax region. * created_at: * type: string * format: date-time * title: created_at - * description: The tax region's created at. + * description: The date the tax region was created. * updated_at: * type: string * format: date-time * title: updated_at - * description: The tax region's updated at. + * description: The date the tax region was updated. * deleted_at: * type: string * format: date-time * title: deleted_at - * description: The tax region's deleted at. + * description: The date the tax region was deleted. * created_by: * type: string * title: created_by - * description: The tax region's created by. + * description: The ID of the user that created the tax region. * tax_rates: * type: array - * description: The tax region's tax rates. + * description: The tax region's rates. * items: * $ref: "#/components/schemas/AdminTaxRate" * parent: diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRegionDeleteResponse.ts b/www/utils/generated/oas-output/schemas/AdminTaxRegionDeleteResponse.ts index 9636a80cf7d5c..5db94e7382c98 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRegionDeleteResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRegionDeleteResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminTaxRegionDeleteResponse * type: object - * description: SUMMARY + * description: The details of the tax region deletion. * x-schemaName: AdminTaxRegionDeleteResponse * required: * - id @@ -20,7 +20,7 @@ * deleted: * type: boolean * title: deleted - * description: Whether the Tax Region was deleted. + * description: Whether the tax region was deleted. * */ diff --git a/www/utils/generated/oas-output/schemas/AdminTaxRegionResponse.ts b/www/utils/generated/oas-output/schemas/AdminTaxRegionResponse.ts index a4ad374475b80..9be8bc9db8c31 100644 --- a/www/utils/generated/oas-output/schemas/AdminTaxRegionResponse.ts +++ b/www/utils/generated/oas-output/schemas/AdminTaxRegionResponse.ts @@ -1,7 +1,7 @@ /** * @schema AdminTaxRegionResponse * type: object - * description: SUMMARY + * description: The tax region's details. * x-schemaName: AdminTaxRegionResponse * required: * - tax_region diff --git a/www/utils/generated/oas-output/schemas/AdminWorkflowExecutionExecution.ts b/www/utils/generated/oas-output/schemas/AdminWorkflowExecutionExecution.ts index e72e06d9aa966..337a014a1d860 100644 --- a/www/utils/generated/oas-output/schemas/AdminWorkflowExecutionExecution.ts +++ b/www/utils/generated/oas-output/schemas/AdminWorkflowExecutionExecution.ts @@ -1,14 +1,48 @@ /** * @schema AdminWorkflowExecutionExecution * type: object - * description: The workflow execution's execution. + * description: The workflow execution's steps details. * x-schemaName: AdminWorkflowExecutionExecution * required: * - steps * properties: * steps: * type: object - * description: The execution's steps. + * description: The execution's steps. Each object key is a step ID, and the value is the object whose properties are shown below. + * required: + * - id + * - invoke + * - definition + * - compensate + * - depth + * - startedAt + * additionalProperties: + * type: object + * properties: + * id: + * type: string + * title: id + * description: The step's ID. + * invoke: + * type: object + * description: The state of the step's invokation function. + * x-schemaName: WorkflowExecutionFn + * definition: + * type: object + * description: The step's definition details. + * x-schemaName: WorkflowExecutionDefinition + * compensate: + * type: object + * description: The state of the step's compensation function. + * x-schemaName: WorkflowExecutionFn + * depth: + * type: number + * title: depth + * description: The step's depth in the workflow's execution. + * startedAt: + * type: number + * title: startedAt + * description: The timestamp the step started executing. * */ diff --git a/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts b/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts index a026018e37ec0..3a32f7ed486c7 100644 --- a/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts +++ b/www/utils/packages/docs-generator/src/classes/helpers/oas-schema.ts @@ -69,9 +69,16 @@ class OasSchemaHelper { // check if schema has child schemas // and convert those - if (schema.properties) { - Object.keys(schema.properties).forEach((property) => { - const propertySchema = schema.properties![property] + const properties = schema.properties + ? schema.properties + : schema.additionalProperties && + typeof schema.additionalProperties !== "boolean" && + !this.isRefObject(schema.additionalProperties) + ? schema.additionalProperties.properties + : undefined + if (properties) { + Object.keys(properties).forEach((property) => { + const propertySchema = properties![property] if ("$ref" in propertySchema) { return } @@ -105,7 +112,7 @@ class OasSchemaHelper { }) } - schema.properties![property] = + properties![property] = this.namedSchemaToReference( propertySchema as OpenApiSchema, level + 1 @@ -178,6 +185,28 @@ class OasSchemaHelper { this.namedSchemaToReference(transformedProperty) || transformedProperty }) + } else if ( + clonedSchema.additionalProperties && + typeof clonedSchema.additionalProperties !== "boolean" && + !this.isRefObject(clonedSchema.additionalProperties) && + clonedSchema.additionalProperties.properties + ) { + const additionalProps = schema.additionalProperties as OpenApiSchema + Object.entries(clonedSchema.additionalProperties.properties).forEach( + ([key, property]) => { + if (this.isRefObject(property)) { + return + } + + const transformedProperty = this.schemaChildrenToRefs( + property, + level + 1 + ) + additionalProps.properties![key] = + this.namedSchemaToReference(transformedProperty) || + transformedProperty + } + ) } return clonedSchema @@ -186,10 +215,17 @@ class OasSchemaHelper { isSchemaEmpty(schema: OpenApiSchema): boolean { switch (schema.type) { case "object": - return ( + const isPropertiesEmpty = schema.properties === undefined || Object.keys(schema.properties).length === 0 - ) + const isAdditionalPropertiesEmpty = + schema.additionalProperties === undefined || + typeof schema.additionalProperties === "boolean" || + (!this.isRefObject(schema.additionalProperties) && + (schema.additionalProperties.properties === undefined || + Object.keys(schema.additionalProperties.properties).length == 0)) + + return isPropertiesEmpty && isAdditionalPropertiesEmpty case "array": return ( !this.isRefObject(schema.items) && this.isSchemaEmpty(schema.items) @@ -350,6 +386,7 @@ class OasSchemaHelper { | OpenApiSchema | OpenAPIV3.RequestBodyObject | OpenAPIV3.ResponseObject + | OpenAPIV3.ParameterObject | undefined ): schema is OpenAPIV3.ReferenceObject { return schema !== undefined && "$ref" in schema diff --git a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts index d7c19dc2d09ba..df50612cbbe5b 100644 --- a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts @@ -1512,10 +1512,6 @@ class OasKindGenerator extends FunctionKindGenerator { case itemType.isClassOrInterface() || itemType.isTypeParameter() || (itemType as ts.Type).flags === ts.TypeFlags.Object: - const properties: Record< - string, - OpenApiSchema | OpenAPIV3.ReferenceObject - > = {} const requiredProperties: string[] = [] const baseType = itemType.getBaseTypes()?.[0] @@ -1535,8 +1531,26 @@ class OasKindGenerator extends FunctionKindGenerator { required: undefined, } + const properties: Record< + string, + OpenApiSchema | OpenAPIV3.ReferenceObject + > = {} + let isAdditionalProperties = false + if (level + 1 <= this.MAX_LEVEL) { - itemType.getProperties().forEach((property) => { + let itemProperties = itemType.getProperties() + + if ( + !itemProperties.length && + itemType.aliasTypeArguments?.length === 2 && + itemType.aliasTypeArguments[0].flags === ts.TypeFlags.String + ) { + // object has dynamic keys, so put the properties under additionalProperties + itemProperties = itemType.aliasTypeArguments[1].getProperties() + isAdditionalProperties = true + } + + itemProperties.forEach((property) => { if ( (allowedChildren && !allowedChildren.includes(property.name)) || (disallowedChildren && disallowedChildren.includes(property.name)) @@ -1624,7 +1638,14 @@ class OasKindGenerator extends FunctionKindGenerator { } if (Object.values(properties).length) { - objSchema.properties = properties + if (isAdditionalProperties) { + objSchema.additionalProperties = { + type: "object", + properties, + } + } else { + objSchema.properties = properties + } } objSchema.required = @@ -1987,6 +2008,26 @@ class OasKindGenerator extends FunctionKindGenerator { oldSchemaObj!.properties = newSchemaObj.properties } else if (!newSchemaObj?.properties) { delete oldSchemaObj!.properties + + // check if additionalProperties should be updated + if ( + !oldSchemaObj!.additionalProperties && + newSchemaObj.additionalProperties + ) { + oldSchemaObj!.additionalProperties = newSchemaObj.additionalProperties + } else if (!newSchemaObj.additionalProperties) { + delete oldSchemaObj!.additionalProperties + } else if ( + typeof oldSchemaObj!.additionalProperties !== "boolean" && + typeof newSchemaObj!.additionalProperties !== "boolean" + ) { + oldSchemaObj!.additionalProperties = + this.updateSchema({ + oldSchema: oldSchemaObj!.additionalProperties, + newSchema: newSchemaObj.additionalProperties, + level: level + 1, + }) || oldSchemaObj!.additionalProperties + } } else { // update existing properties Object.entries(oldSchemaObj!.properties!).forEach( diff --git a/www/utils/packages/docs-generator/src/commands/clean-oas.ts b/www/utils/packages/docs-generator/src/commands/clean-oas.ts index e5ed643b81c56..3e511ab44d7c6 100644 --- a/www/utils/packages/docs-generator/src/commands/clean-oas.ts +++ b/www/utils/packages/docs-generator/src/commands/clean-oas.ts @@ -175,6 +175,30 @@ export default async function () { }) // collect schemas + oas.parameters?.forEach((parameter) => { + if (oasSchemaHelper.isRefObject(parameter)) { + referencedSchemas.add( + oasSchemaHelper.normalizeSchemaName(parameter.$ref) + ) + + return + } + + if (!parameter.schema) { + return + } + + if (oasSchemaHelper.isRefObject(parameter.schema)) { + referencedSchemas.add( + oasSchemaHelper.normalizeSchemaName(parameter.schema.$ref) + ) + + return + } + + testAndFindReferenceSchema(parameter.schema) + }) + if (oas.requestBody) { if (oasSchemaHelper.isRefObject(oas.requestBody)) { referencedSchemas.add(