From bd73716bb6ae99a1379cedafab0b559e6ecbb871 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 8 Jan 2025 21:40:46 +0200 Subject: [PATCH 1/8] docs-util: add warning on request body change (#10881) --- .../packages/docs-generator/package.json | 1 + .../docs-generator/src/classes/kinds/oas.ts | 212 +++++++++++++----- www/utils/yarn.lock | 8 + 3 files changed, 161 insertions(+), 60 deletions(-) diff --git a/www/utils/packages/docs-generator/package.json b/www/utils/packages/docs-generator/package.json index 184339e9fc7ce..3ce3a2a271c93 100644 --- a/www/utils/packages/docs-generator/package.json +++ b/www/utils/packages/docs-generator/package.json @@ -19,6 +19,7 @@ "dependencies": { "@faker-js/faker": "^8.4.0", "@octokit/core": "^5.0.2", + "chalk": "^5.4.1", "commander": "^11.1.0", "dotenv": "^16.3.1", "eslint": "8.56.0", 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 7978e0d00555c..0b5dac43ea9a1 100644 --- a/www/utils/packages/docs-generator/src/classes/kinds/oas.ts +++ b/www/utils/packages/docs-generator/src/classes/kinds/oas.ts @@ -1,3 +1,4 @@ +import chalk from "chalk" import { readFileSync, writeFileSync } from "fs" import { OpenAPIV3 } from "openapi-types" import { basename, join } from "path" @@ -473,6 +474,8 @@ class OasKindGenerator extends FunctionKindGenerator { oas["x-authenticated"] = isAuthenticated oas.security = this.getSecurity({ isAdminAuthenticated, isAuthenticated }) + let parametersUpdated = false + // update path parameters const newPathParameters = this.getPathParameters({ oasPath, tagName }) @@ -480,12 +483,15 @@ class OasKindGenerator extends FunctionKindGenerator { const headerParams = this.getHeaderParameters(oasPath) newPathParameters.push(...headerParams) - oas.parameters = this.updateParameters({ + let updateParameterResult = this.updateParameters({ oldParameters: oas.parameters as OpenAPIV3.ParameterObject[], newParameters: newPathParameters, types: ["path", "header"], }) + oas.parameters = updateParameterResult.parameters + parametersUpdated = updateParameterResult.wasUpdated + // retrieve updated query and request schemas const { queryParameters, requestSchema } = this.getRequestParameters({ node, @@ -495,12 +501,15 @@ class OasKindGenerator extends FunctionKindGenerator { }) // update query parameters - oas.parameters = this.updateParameters({ + updateParameterResult = this.updateParameters({ oldParameters: oas.parameters as OpenAPIV3.ParameterObject[], newParameters: queryParameters, types: ["query"], }) + oas.parameters = updateParameterResult.parameters + parametersUpdated = updateParameterResult.wasUpdated || parametersUpdated + if (!oas.parameters.length) { oas.parameters = undefined } @@ -514,9 +523,11 @@ class OasKindGenerator extends FunctionKindGenerator { newSchema: requestSchema, }) + parametersUpdated = updatedRequestSchema?.wasUpdated || parametersUpdated + if ( - !updatedRequestSchema || - Object.keys(updatedRequestSchema).length === 0 + !updatedRequestSchema?.schema || + Object.keys(updatedRequestSchema.schema).length === 0 ) { // if there's no request schema, remove it from the OAS delete oas.requestBody @@ -527,9 +538,11 @@ class OasKindGenerator extends FunctionKindGenerator { "application/json": { schema: this.oasSchemaHelper.namedSchemaToReference( - updatedRequestSchema + updatedRequestSchema.schema ) || - this.oasSchemaHelper.schemaChildrenToRefs(updatedRequestSchema), + this.oasSchemaHelper.schemaChildrenToRefs( + updatedRequestSchema.schema + ), }, }, } @@ -581,7 +594,7 @@ class OasKindGenerator extends FunctionKindGenerator { updatedResponseSchema = this.updateSchema({ oldSchema: oldResponseSchema, newSchema: newResponseSchema, - }) + })?.schema if (oldResponseStatus && oldResponseSchema !== newStatus) { // delete the old response schema if its status is different @@ -633,7 +646,7 @@ class OasKindGenerator extends FunctionKindGenerator { parameters: (oas.parameters as OpenAPIV3.ParameterObject[])?.filter( (parameter) => parameter.in === "path" ), - requestBody: updatedRequestSchema, + requestBody: updatedRequestSchema?.schema, responseBody: updatedResponseSchema, }) @@ -673,6 +686,13 @@ class OasKindGenerator extends FunctionKindGenerator { source: newCurlExample, }, ] + } else if (parametersUpdated) { + // show a warning if the request parameters have changed + console.warn( + chalk.yellow( + `[WARNING] The request parameters of ${methodName} ${oasPath} have changed. Please update the cURL example.` + ) + ) } // push new tags to the tags property @@ -1927,9 +1947,16 @@ class OasKindGenerator extends FunctionKindGenerator { * The type of parameters. */ types: ParameterType[] - }): OpenAPIV3.ParameterObject[] { + }): { + parameters: OpenAPIV3.ParameterObject[] + wasUpdated: boolean + } { + let wasUpdated = false if (!oldParameters) { - return newParameters || [] + return { + parameters: newParameters || [], + wasUpdated: !!newParameters?.length, + } } const oppositeParamType = ["path", "query", "header"].filter( (item) => !types.includes(item as ParameterType) @@ -1952,6 +1979,7 @@ class OasKindGenerator extends FunctionKindGenerator { if (!updatedParameter) { // remove the parameter paramsToRemove.add(parameter.name) + wasUpdated = true return } @@ -1964,6 +1992,7 @@ class OasKindGenerator extends FunctionKindGenerator { if (updatedParameter.required !== parameter.required) { parameter.required = updatedParameter.required + wasUpdated = true } if ( @@ -1972,19 +2001,24 @@ class OasKindGenerator extends FunctionKindGenerator { ) { // the entire schema should be updated if the type changes. parameter.schema = updatedParameter.schema + wasUpdated = true } else if ((updatedParameter.schema as OpenApiSchema).type === "array") { + const updateResult = this.updateSchema({ + oldSchema: (parameter.schema as OpenAPIV3.ArraySchemaObject).items, + newSchema: (updatedParameter.schema as OpenAPIV3.ArraySchemaObject) + .items, + }) ;(parameter.schema as OpenAPIV3.ArraySchemaObject).items = - this.updateSchema({ - oldSchema: (parameter.schema as OpenAPIV3.ArraySchemaObject).items, - newSchema: (updatedParameter.schema as OpenAPIV3.ArraySchemaObject) - .items, - }) || (updatedParameter.schema as OpenAPIV3.ArraySchemaObject).items + updateResult?.schema || + (updatedParameter.schema as OpenAPIV3.ArraySchemaObject).items + wasUpdated = updateResult?.wasUpdated || wasUpdated } else if ((updatedParameter.schema as OpenApiSchema).type === "object") { - parameter.schema = - this.updateSchema({ - oldSchema: parameter.schema, - newSchema: updatedParameter.schema, - }) || updatedParameter.schema + const updateResult = this.updateSchema({ + oldSchema: parameter.schema, + newSchema: updatedParameter.schema, + }) + parameter.schema = updateResult?.schema || updatedParameter.schema + wasUpdated = updateResult?.wasUpdated || wasUpdated } if ( @@ -1994,6 +2028,7 @@ class OasKindGenerator extends FunctionKindGenerator { ;(parameter.schema as OpenApiSchema).title = ( updatedParameter.schema as OpenApiSchema ).title + wasUpdated = true } if ( @@ -2014,19 +2049,23 @@ class OasKindGenerator extends FunctionKindGenerator { } existingParams?.push(parameter) + wasUpdated = true }) // remove parameters no longer existing - return [ - ...oppositeParams, - ...(existingParams?.filter( - (parameter) => - oppositeParamType.includes( - (parameter as OpenAPIV3.ParameterObject).in as ParameterType - ) || - !paramsToRemove.has((parameter as OpenAPIV3.ParameterObject).name) - ) || []), - ] + return { + parameters: [ + ...oppositeParams, + ...(existingParams?.filter( + (parameter) => + oppositeParamType.includes( + (parameter as OpenAPIV3.ParameterObject).in as ParameterType + ) || + !paramsToRemove.has((parameter as OpenAPIV3.ParameterObject).name) + ) || []), + ], + wasUpdated, + } } /** @@ -2053,7 +2092,13 @@ class OasKindGenerator extends FunctionKindGenerator { * maximum call stack size exceeded error */ level?: number - }): OpenApiSchema | undefined { + }): + | { + schema: OpenApiSchema | undefined + wasUpdated: boolean + } + | undefined { + let wasUpdated = false if (isLevelExceeded(level, this.MAX_LEVEL)) { return } @@ -2070,10 +2115,28 @@ class OasKindGenerator extends FunctionKindGenerator { : newSchema ) as OpenApiSchema | undefined - if (!oldSchemaObj && newSchemaObj) { - return newSchemaObj - } else if (!newSchemaObj || !Object.keys(newSchemaObj).length) { - return undefined + const oldSchemaKeys = oldSchemaObj ? Object.keys(oldSchemaObj) : [] + const hasOldSchemaObj = + oldSchemaObj !== undefined && oldSchemaKeys.length > 0 + const hasNewSchemaObj = + newSchemaObj !== undefined && Object.keys(newSchemaObj).length > 0 + + if (!hasOldSchemaObj || !hasNewSchemaObj) { + // if old schema is just made up of description, return it. + const useOldSchema = + oldSchemaKeys.length === 1 && + newSchemaObj !== undefined && + oldSchemaKeys[0] === "description" + return { + schema: hasNewSchemaObj + ? newSchemaObj + : useOldSchema + ? oldSchemaObj + : undefined, + wasUpdated: !hasNewSchemaObj + ? !useOldSchema && hasOldSchemaObj + : hasOldSchemaObj !== hasNewSchemaObj, + } } const oldSchemaType = this.inferOasSchemaType(oldSchemaObj) @@ -2085,28 +2148,36 @@ class OasKindGenerator extends FunctionKindGenerator { description: oldSchemaObj?.description, example: oldSchemaObj?.example || newSchemaObj.example, } + wasUpdated = true } else if ( oldSchemaObj?.allOf && newSchemaObj.allOf && oldSchemaObj.allOf.length !== newSchemaObj.allOf.length ) { oldSchemaObj.allOf = newSchemaObj.allOf + wasUpdated = true } else if ( oldSchemaObj?.oneOf && newSchemaObj.oneOf && oldSchemaObj.oneOf.length !== newSchemaObj.oneOf.length ) { oldSchemaObj.oneOf = newSchemaObj.oneOf + wasUpdated = true } else if ( oldSchemaObj?.anyOf && newSchemaObj.anyOf && oldSchemaObj.anyOf.length !== newSchemaObj.anyOf.length ) { oldSchemaObj.anyOf = newSchemaObj.anyOf + wasUpdated = true } else if (oldSchemaType === "object") { if (!oldSchemaObj?.properties && newSchemaObj?.properties) { oldSchemaObj!.properties = newSchemaObj.properties + wasUpdated = true } else if (!newSchemaObj?.properties) { + if (oldSchemaObj!.properties) { + wasUpdated = true + } delete oldSchemaObj!.properties // check if additionalProperties should be updated @@ -2121,12 +2192,13 @@ class OasKindGenerator extends FunctionKindGenerator { typeof oldSchemaObj!.additionalProperties !== "boolean" && typeof newSchemaObj!.additionalProperties !== "boolean" ) { + const updateResult = this.updateSchema({ + oldSchema: oldSchemaObj!.additionalProperties, + newSchema: newSchemaObj.additionalProperties, + level: maybeIncrementLevel(level, "object"), + }) oldSchemaObj!.additionalProperties = - this.updateSchema({ - oldSchema: oldSchemaObj!.additionalProperties, - newSchema: newSchemaObj.additionalProperties, - level: maybeIncrementLevel(level, "object"), - }) || oldSchemaObj!.additionalProperties + updateResult?.schema || oldSchemaObj!.additionalProperties } } else { // update existing properties @@ -2138,17 +2210,20 @@ class OasKindGenerator extends FunctionKindGenerator { if (!newPropertySchemaKey) { // remove property delete oldSchemaObj!.properties![propertyName] + wasUpdated = true return } + const updateResult = this.updateSchema({ + oldSchema: propertySchema as OpenApiSchema, + newSchema: newSchemaObj!.properties![ + propertyName + ] as OpenApiSchema, + level: maybeIncrementLevel(level, "object"), + }) oldSchemaObj!.properties![propertyName] = - this.updateSchema({ - oldSchema: propertySchema as OpenApiSchema, - newSchema: newSchemaObj!.properties![ - propertyName - ] as OpenApiSchema, - level: maybeIncrementLevel(level, "object"), - }) || propertySchema + updateResult?.schema || propertySchema + wasUpdated = updateResult?.wasUpdated || wasUpdated } ) // add new properties @@ -2177,12 +2252,13 @@ class OasKindGenerator extends FunctionKindGenerator { ) if (schemaToUpdate) { - updatedSchema = - this.updateSchema({ - oldSchema: schemaToUpdate.schema, - newSchema: schema, - level: maybeIncrementLevel(level, "object"), - }) || newProperty + const updateResult = this.updateSchema({ + oldSchema: schemaToUpdate.schema, + newSchema: schema, + level: maybeIncrementLevel(level, "object"), + }) + updatedSchema = updateResult?.schema || newProperty + wasUpdated = updateResult?.wasUpdated || wasUpdated } } @@ -2207,12 +2283,13 @@ class OasKindGenerator extends FunctionKindGenerator { oldSchemaObj?.type === "array" && newSchemaObj?.type === "array" ) { - oldSchemaObj.items = - this.updateSchema({ - oldSchema: oldSchemaObj.items as OpenApiSchema, - newSchema: newSchemaObj!.items as OpenApiSchema, - level: maybeIncrementLevel(level, "array"), - }) || oldSchemaObj.items + const updateResult = this.updateSchema({ + oldSchema: oldSchemaObj.items as OpenApiSchema, + newSchema: newSchemaObj!.items as OpenApiSchema, + level: maybeIncrementLevel(level, "array"), + }) + oldSchemaObj.items = updateResult?.schema || oldSchemaObj.items + wasUpdated = updateResult?.wasUpdated || wasUpdated } if ( @@ -2223,10 +2300,25 @@ class OasKindGenerator extends FunctionKindGenerator { newSchemaObj?.description || SUMMARY_PLACEHOLDER } + if (!wasUpdated) { + const requiredChanged = + oldSchemaObj!.required?.length !== newSchemaObj?.required?.length || + oldSchemaObj!.required?.some( + (item, index) => item !== newSchemaObj!.required![index] + ) || + false + + const schemaNameChanged = + oldSchemaObj!["x-schemaName"] !== newSchemaObj?.["x-schemaName"] + wasUpdated = requiredChanged || schemaNameChanged + } oldSchemaObj!.required = newSchemaObj?.required oldSchemaObj!["x-schemaName"] = newSchemaObj?.["x-schemaName"] - return oldSchemaObj + return { + schema: oldSchemaObj, + wasUpdated, + } } /** diff --git a/www/utils/yarn.lock b/www/utils/yarn.lock index dacf49531b9f5..9c6df4b85939e 100644 --- a/www/utils/yarn.lock +++ b/www/utils/yarn.lock @@ -2242,6 +2242,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.4.1": + version: 5.4.1 + resolution: "chalk@npm:5.4.1" + checksum: b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef + languageName: node + linkType: hard + "charenc@npm:0.0.2": version: 0.0.2 resolution: "charenc@npm:0.0.2" @@ -2605,6 +2612,7 @@ __metadata: "@types/eslint": 8.56.6 "@types/node": ^20.12.10 "@types/pluralize": ^0.0.33 + chalk: ^5.4.1 commander: ^11.1.0 dotenv: ^16.3.1 eslint: 8.56.0 From 67782350a9da278457c3280c300ebec65bdc6326 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 9 Jan 2025 10:56:08 +0530 Subject: [PATCH 2/8] feat: add default retry strategy for redis (#10880) Fixes: FRMW-2861 --- .changeset/afraid-experts-walk.md | 5 +++++ .../common/__tests__/define-config.spec.ts | 18 +++++++++++++++++ .../core/utils/src/common/define-config.ts | 20 ++++++++++++++++++- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 .changeset/afraid-experts-walk.md diff --git a/.changeset/afraid-experts-walk.md b/.changeset/afraid-experts-walk.md new file mode 100644 index 0000000000000..f734cb09fbd97 --- /dev/null +++ b/.changeset/afraid-experts-walk.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +feat: add default retry strategy for redis diff --git a/packages/core/utils/src/common/__tests__/define-config.spec.ts b/packages/core/utils/src/common/__tests__/define-config.spec.ts index 9919e3e6b9264..f49263b6065dc 100644 --- a/packages/core/utils/src/common/__tests__/define-config.spec.ts +++ b/packages/core/utils/src/common/__tests__/define-config.spec.ts @@ -142,6 +142,9 @@ describe("defineConfig", function () { }, "storeCors": "http://localhost:8000", }, + "redisOptions": { + "retryStrategy": [Function], + }, }, } `) @@ -298,6 +301,9 @@ describe("defineConfig", function () { }, "storeCors": "http://localhost:8000", }, + "redisOptions": { + "retryStrategy": [Function], + }, }, } `) @@ -462,6 +468,9 @@ describe("defineConfig", function () { }, "storeCors": "http://localhost:8000", }, + "redisOptions": { + "retryStrategy": [Function], + }, }, } `) @@ -627,6 +636,9 @@ describe("defineConfig", function () { }, "storeCors": "http://localhost:8000", }, + "redisOptions": { + "retryStrategy": [Function], + }, }, } `) @@ -780,6 +792,9 @@ describe("defineConfig", function () { }, "storeCors": "http://localhost:8000", }, + "redisOptions": { + "retryStrategy": [Function], + }, }, } `) @@ -933,6 +948,9 @@ describe("defineConfig", function () { }, "storeCors": "http://localhost:8000", }, + "redisOptions": { + "retryStrategy": [Function], + }, }, } `) diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index 25fe76a99205b..fda1b8ec51718 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -74,7 +74,8 @@ type Config = Partial< * to override configuration as needed. */ export function defineConfig(config: Config = {}): ConfigModule { - const { http, ...restOfProjectConfig } = config.projectConfig || {} + const { http, redisOptions, ...restOfProjectConfig } = + config.projectConfig || {} /** * The defaults to use for the project config. They are shallow merged @@ -93,6 +94,23 @@ export function defineConfig(config: Config = {}): ConfigModule { }, ...http, }, + redisOptions: { + retryStrategy(retries) { + /** + * Exponentially increase delay with every retry + * attempt. Max to 4s + */ + const delay = Math.min(Math.pow(2, retries) * 50, 4000) + + /** + * Add a random jitter to not choke the server when multiple + * clients are retrying at the same time + */ + const jitter = Math.floor(Math.random() * 200) + return delay + jitter + }, + ...redisOptions, + }, ...restOfProjectConfig, } From 28febfc6438351fddb5b214b86f96aff89db688e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 9 Jan 2025 14:52:10 +0530 Subject: [PATCH 3/8] feat: remove dead code and refactor the logic of resolving plugins (#10874) --- .changeset/tiny-moles-build.md | 8 + .../core/types/src/common/config-module.ts | 40 ++++ .../core/utils/src/common/define-config.ts | 44 +--- .../utils/src/common/read-dir-recursive.ts | 52 ++++- .../src/medusa-test-runner-utils/use-db.ts | 2 +- packages/medusa/package.json | 2 +- packages/medusa/src/commands/db/generate.ts | 2 +- packages/medusa/src/commands/db/migrate.ts | 2 +- packages/medusa/src/commands/db/rollback.ts | 2 +- packages/medusa/src/commands/db/sync-links.ts | 2 +- .../__tests__/get-resolved-plugins.spec.ts | 211 ++++++++++++++++++ .../src/loaders/helpers/resolve-plugins.ts | 181 ++++++--------- packages/medusa/src/loaders/index.ts | 2 +- 13 files changed, 379 insertions(+), 171 deletions(-) create mode 100644 .changeset/tiny-moles-build.md create mode 100644 packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts diff --git a/.changeset/tiny-moles-build.md b/.changeset/tiny-moles-build.md new file mode 100644 index 0000000000000..9d63ee6393dea --- /dev/null +++ b/.changeset/tiny-moles-build.md @@ -0,0 +1,8 @@ +--- +"@medusajs/medusa": patch +"@medusajs/test-utils": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat: remove dead code and refactor the logic of resolving plugins diff --git a/packages/core/types/src/common/config-module.ts b/packages/core/types/src/common/config-module.ts index c574f2f57e50f..4dac6def09b6e 100644 --- a/packages/core/types/src/common/config-module.ts +++ b/packages/core/types/src/common/config-module.ts @@ -922,10 +922,50 @@ export type ConfigModule = { featureFlags: Record> } +type InternalModuleDeclarationOverride = InternalModuleDeclaration & { + /** + * Optional key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name. + */ + key?: string + /** + * By default, modules are enabled, if provided as true, this will disable the module entirely. + */ + disable?: boolean +} + +type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & { + /** + * key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name. + */ + key: string + /** + * By default, modules are enabled, if provided as true, this will disable the module entirely. + */ + disable?: boolean +} + +/** + * The configuration accepted by the "defineConfig" helper + */ +export type InputConfig = Partial< + Omit & { + admin: Partial + modules: + | Partial< + InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride + >[] + /** + * @deprecated use the array instead + */ + | ConfigModule["modules"] + } +> + export type PluginDetails = { resolve: string name: string id: string options: Record version: string + modules?: InputConfig["modules"] } diff --git a/packages/core/utils/src/common/define-config.ts b/packages/core/utils/src/common/define-config.ts index fda1b8ec51718..b0bbef36d384c 100644 --- a/packages/core/utils/src/common/define-config.ts +++ b/packages/core/utils/src/common/define-config.ts @@ -1,6 +1,6 @@ import { ConfigModule, - ExternalModuleDeclaration, + InputConfig, InternalModuleDeclaration, } from "@medusajs/types" import { @@ -29,42 +29,6 @@ export const DEFAULT_STORE_RESTRICTED_FIELDS = [ "payment_collections"*/ ] -type InternalModuleDeclarationOverride = InternalModuleDeclaration & { - /** - * Optional key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name. - */ - key?: string - /** - * By default, modules are enabled, if provided as true, this will disable the module entirely. - */ - disable?: boolean -} - -type ExternalModuleDeclarationOverride = ExternalModuleDeclaration & { - /** - * key to be used to identify the module, if not provided, it will be inferred from the module joiner config service name. - */ - key: string - /** - * By default, modules are enabled, if provided as true, this will disable the module entirely. - */ - disable?: boolean -} - -type Config = Partial< - Omit & { - admin: Partial - modules: - | Partial< - InternalModuleDeclarationOverride | ExternalModuleDeclarationOverride - >[] - /** - * @deprecated use the array instead - */ - | ConfigModule["modules"] - } -> - /** * The "defineConfig" helper can be used to define the configuration * of a medusa application. @@ -73,7 +37,7 @@ type Config = Partial< * make an application work seamlessly, but still provide you the ability * to override configuration as needed. */ -export function defineConfig(config: Config = {}): ConfigModule { +export function defineConfig(config: InputConfig = {}): ConfigModule { const { http, redisOptions, ...restOfProjectConfig } = config.projectConfig || {} @@ -150,14 +114,14 @@ export function defineConfig(config: Config = {}): ConfigModule { * @param configModules */ function resolveModules( - configModules: Config["modules"] + configModules: InputConfig["modules"] ): ConfigModule["modules"] { /** * The default set of modules to always use. The end user can swap * the modules by providing an alternate implementation via their * config. But they can never remove a module from this list. */ - const modules: Config["modules"] = [ + const modules: InputConfig["modules"] = [ { resolve: MODULE_PACKAGE_NAMES[Modules.CACHE] }, { resolve: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS] }, { resolve: MODULE_PACKAGE_NAMES[Modules.WORKFLOW_ENGINE] }, diff --git a/packages/core/utils/src/common/read-dir-recursive.ts b/packages/core/utils/src/common/read-dir-recursive.ts index 7ad9d2a7b43f2..bc5c175c378c0 100644 --- a/packages/core/utils/src/common/read-dir-recursive.ts +++ b/packages/core/utils/src/common/read-dir-recursive.ts @@ -2,21 +2,51 @@ import { Dirent } from "fs" import { readdir } from "fs/promises" import { join } from "path" -export async function readDirRecursive(dir: string): Promise { - let allEntries: Dirent[] = [] - const readRecursive = async (dir) => { +const MISSING_NODE_ERRORS = ["ENOTDIR", "ENOENT"] + +export async function readDir( + dir: string, + options?: { + ignoreMissing?: boolean + } +) { + try { const entries = await readdir(dir, { withFileTypes: true }) + return entries + } catch (error) { + if (options?.ignoreMissing && MISSING_NODE_ERRORS.includes(error.code)) { + return [] + } + throw error + } +} - for (const entry of entries) { - const fullPath = join(dir, entry.name) - Object.defineProperty(entry, "path", { - value: dir, - }) - allEntries.push(entry) +export async function readDirRecursive( + dir: string, + options?: { + ignoreMissing?: boolean + } +): Promise { + let allEntries: Dirent[] = [] + const readRecursive = async (dir: string) => { + try { + const entries = await readdir(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = join(dir, entry.name) + Object.defineProperty(entry, "path", { + value: dir, + }) + allEntries.push(entry) - if (entry.isDirectory()) { - await readRecursive(fullPath) + if (entry.isDirectory()) { + await readRecursive(fullPath) + } + } + } catch (error) { + if (options?.ignoreMissing && error.code === "ENOENT") { + return } + throw error } } diff --git a/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts b/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts index eafc848cbe9d0..24f2f5c0acd58 100644 --- a/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts +++ b/packages/medusa-test-utils/src/medusa-test-runner-utils/use-db.ts @@ -61,7 +61,7 @@ async function loadCustomLinks(directory: string, container: MedusaContainer) { const configModule = container.resolve( ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 71f488ffb5a35..1a75509a76ce2 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -40,7 +40,7 @@ "watch": "tsc --build --watch", "build": "rimraf dist && tsc --build", "serve": "node dist/app.js", - "test": "jest --silent --bail --maxWorkers=50% --forceExit" + "test": "jest --silent=false --bail --maxWorkers=50% --forceExit" }, "devDependencies": { "@medusajs/framework": "^2.2.0", diff --git a/packages/medusa/src/commands/db/generate.ts b/packages/medusa/src/commands/db/generate.ts index aeddb2a07e3de..176fa7896ccb5 100644 --- a/packages/medusa/src/commands/db/generate.ts +++ b/packages/medusa/src/commands/db/generate.ts @@ -26,7 +26,7 @@ const main = async function ({ directory, modules }) { ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/commands/db/migrate.ts b/packages/medusa/src/commands/db/migrate.ts index b8b086280fff8..f7e7a4a356b97 100644 --- a/packages/medusa/src/commands/db/migrate.ts +++ b/packages/medusa/src/commands/db/migrate.ts @@ -37,7 +37,7 @@ export async function migrate({ ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/commands/db/rollback.ts b/packages/medusa/src/commands/db/rollback.ts index 61ad13118e9d7..2c09252ed2586 100644 --- a/packages/medusa/src/commands/db/rollback.ts +++ b/packages/medusa/src/commands/db/rollback.ts @@ -26,7 +26,7 @@ const main = async function ({ directory, modules }) { ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/commands/db/sync-links.ts b/packages/medusa/src/commands/db/sync-links.ts index a151c0f0a95dd..59d6da7008460 100644 --- a/packages/medusa/src/commands/db/sync-links.ts +++ b/packages/medusa/src/commands/db/sync-links.ts @@ -187,7 +187,7 @@ const main = async function ({ directory, executeSafe, executeAll }) { const medusaAppLoader = new MedusaAppLoader() - const plugins = getResolvedPlugins(directory, configModule, true) || [] + const plugins = await getResolvedPlugins(directory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) diff --git a/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts new file mode 100644 index 0000000000000..2aa6cbd8f2423 --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/get-resolved-plugins.spec.ts @@ -0,0 +1,211 @@ +import path from "path" +import { defineConfig, FileSystem } from "@medusajs/framework/utils" +import { getResolvedPlugins } from "../helpers/resolve-plugins" + +const BASE_DIR = path.join(__dirname, "sample-proj") +const fs = new FileSystem(BASE_DIR) + +afterEach(async () => { + await fs.cleanup() +}) + +describe("getResolvedPlugins | relative paths", () => { + test("resolve configured plugins", async () => { + await fs.createJson("plugins/dummy/package.json", { + name: "my-dummy-plugin", + version: "1.0.0", + }) + + const plugins = await getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "./plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + expect(plugins).toEqual([ + { + resolve: path.join(fs.basePath, "./plugins/dummy/build"), + name: "my-dummy-plugin", + id: "my-dummy-plugin", + options: { apiKey: "asecret" }, + version: "1.0.0", + modules: [], + }, + ]) + }) + + test("scan plugin modules", async () => { + await fs.createJson("plugins/dummy/package.json", { + name: "my-dummy-plugin", + version: "1.0.0", + }) + await fs.create("plugins/dummy/build/modules/blog/index.js", ``) + + const plugins = await getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "./plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + expect(plugins).toEqual([ + { + resolve: path.join(fs.basePath, "./plugins/dummy/build"), + name: "my-dummy-plugin", + id: "my-dummy-plugin", + options: { apiKey: "asecret" }, + version: "1.0.0", + modules: [ + { + options: { + apiKey: "asecret", + }, + resolve: "./plugins/dummy/build/modules/blog", + }, + ], + }, + ]) + }) + + test("throw error when package.json file is missing", async () => { + const resolvePlugins = async () => + getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "./plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + await expect(resolvePlugins()).rejects.toThrow( + `Unable to resolve plugin "./plugins/dummy". Make sure the plugin directory has a package.json file` + ) + }) +}) + +describe("getResolvedPlugins | package reference", () => { + test("resolve configured plugins", async () => { + await fs.createJson("package.json", {}) + await fs.createJson("node_modules/@plugins/dummy/package.json", { + name: "my-dummy-plugin", + version: "1.0.0", + }) + + const plugins = await getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "@plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + expect(plugins).toEqual([ + { + resolve: path.join(fs.basePath, "node_modules/@plugins/dummy/build"), + name: "my-dummy-plugin", + id: "my-dummy-plugin", + options: { apiKey: "asecret" }, + version: "1.0.0", + modules: [], + }, + ]) + }) + + test("scan plugin modules", async () => { + await fs.createJson("package.json", {}) + await fs.createJson("node_modules/@plugins/dummy/package.json", { + name: "my-dummy-plugin", + version: "1.0.0", + }) + await fs.create( + "node_modules/@plugins/dummy/build/modules/blog/index.js", + `` + ) + + const plugins = await getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "@plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + expect(plugins).toEqual([ + { + resolve: path.join(fs.basePath, "node_modules/@plugins/dummy/build"), + name: "my-dummy-plugin", + id: "my-dummy-plugin", + options: { apiKey: "asecret" }, + version: "1.0.0", + modules: [ + { + options: { + apiKey: "asecret", + }, + resolve: "@plugins/dummy/build/modules/blog", + }, + ], + }, + ]) + }) + + test("throw error when package.json file is missing", async () => { + const resolvePlugins = async () => + getResolvedPlugins( + fs.basePath, + defineConfig({ + plugins: [ + { + resolve: "@plugins/dummy", + options: { + apiKey: "asecret", + }, + }, + ], + }), + false + ) + + await expect(resolvePlugins()).rejects.toThrow( + `Unable to resolve plugin "@plugins/dummy". Make sure the plugin directory has a package.json file` + ) + }) +}) diff --git a/packages/medusa/src/loaders/helpers/resolve-plugins.ts b/packages/medusa/src/loaders/helpers/resolve-plugins.ts index 0d599e5dbb2e4..529c9b9ac5655 100644 --- a/packages/medusa/src/loaders/helpers/resolve-plugins.ts +++ b/packages/medusa/src/loaders/helpers/resolve-plugins.ts @@ -1,148 +1,103 @@ +import path from "path" +import fs from "fs/promises" +import { isString, readDir } from "@medusajs/framework/utils" import { ConfigModule, PluginDetails } from "@medusajs/framework/types" -import { isString } from "@medusajs/framework/utils" -import fs from "fs" -import { sync as existsSync } from "fs-exists-cached" -import path, { isAbsolute } from "path" +const MEDUSA_APP_SOURCE_PATH = "src" export const MEDUSA_PROJECT_NAME = "project-plugin" + function createPluginId(name: string): string { return name } -function createFileContentHash(path, files): string { +function createFileContentHash(path: string, files: string): string { return path + files } -function getExtensionDirectoryPath() { - return "src" -} - /** - * Load plugin details from a path. Return undefined if does not contains a package.json - * @param pluginName - * @param path - * @param includeExtensionDirectoryPath should include src | dist for the resolved details + * Returns the absolute path to the package.json file for a + * given plugin identifier. */ -function loadPluginDetails({ - pluginName, - resolvedPath, - includeExtensionDirectoryPath, -}: { - pluginName: string - resolvedPath: string - includeExtensionDirectoryPath?: boolean -}) { - if (existsSync(`${resolvedPath}/package.json`)) { - const packageJSON = JSON.parse( - fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`) +async function resolvePluginPkgFile( + rootDirectory: string, + pluginPath: string +): Promise<{ path: string; contents: any }> { + try { + const pkgJSONPath = require.resolve(path.join(pluginPath, "package.json"), { + paths: [rootDirectory], + }) + const packageJSONContents = JSON.parse( + await fs.readFile(pkgJSONPath, "utf-8") ) - const name = packageJSON.name || pluginName - - const extensionDirectoryPath = getExtensionDirectoryPath() - const resolve = includeExtensionDirectoryPath - ? path.join(resolvedPath, extensionDirectoryPath) - : resolvedPath - - return { - resolve, - name, - id: createPluginId(name), - options: {}, - version: packageJSON.version || createFileContentHash(path, `**`), + return { path: pkgJSONPath, contents: packageJSONContents } + } catch (error) { + if (error.code === "MODULE_NOT_FOUND" || error.code === "ENOENT") { + throw new Error( + `Unable to resolve plugin "${pluginPath}". Make sure the plugin directory has a package.json file` + ) } + throw error } - - // Make package.json a requirement for local plugins too - throw new Error(`Plugin ${pluginName} requires a package.json file`) } /** * Finds the correct path for the plugin. If it is a local plugin it will be * found in the plugins folder. Otherwise we will look for the plugin in the * installed npm packages. - * @param {string} pluginName - the name of the plugin to find. Should match + * @param {string} pluginPath - the name of the plugin to find. Should match * the name of the folder where the plugin is contained. * @return {object} the plugin details */ -function resolvePlugin(pluginName: string): { - resolve: string - id: string - name: string - options: Record - version: string -} { - if (!isAbsolute(pluginName)) { - let resolvedPath = path.resolve(`./plugins/${pluginName}`) - const doesExistsInPlugin = existsSync(resolvedPath) - - if (doesExistsInPlugin) { - return loadPluginDetails({ - pluginName, - resolvedPath, - }) - } - - // Find the plugin in the file system - resolvedPath = path.resolve(pluginName) - const doesExistsInFileSystem = existsSync(resolvedPath) - - if (doesExistsInFileSystem) { - return loadPluginDetails({ - pluginName, - resolvedPath, - includeExtensionDirectoryPath: true, - }) - } - - throw new Error(`Unable to find the plugin "${pluginName}".`) - } - - try { - // If the path is absolute, resolve the directory of the internal plugin, - // otherwise resolve the directory containing the package.json - const resolvedPath = require.resolve(pluginName, { - paths: [process.cwd()], - }) - - const packageJSON = JSON.parse( - fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`) - ) - - const computedResolvedPath = path.join(resolvedPath, "dist") - - return { - resolve: computedResolvedPath, - id: createPluginId(packageJSON.name), - name: packageJSON.name, - options: {}, - version: packageJSON.version, - } - } catch (err) { - throw new Error( - `Unable to find plugin "${pluginName}". Perhaps you need to install its package?` - ) +async function resolvePlugin( + rootDirectory: string, + pluginPath: string, + options?: any +): Promise { + const pkgJSON = await resolvePluginPkgFile(rootDirectory, pluginPath) + const resolvedPath = path.dirname(pkgJSON.path) + + const name = pkgJSON.contents.name || pluginPath + const srcDir = pkgJSON.contents.main + ? path.dirname(pkgJSON.contents.main) + : "build" + + const resolve = path.join(resolvedPath, srcDir) + const modules = await readDir(path.join(resolve, "modules"), { + ignoreMissing: true, + }) + const pluginOptions = options ?? {} + + return { + resolve, + name, + id: createPluginId(name), + options: pluginOptions, + version: pkgJSON.contents.version || "0.0.0", + modules: modules.map((mod) => { + return { + resolve: `${pluginPath}/${srcDir}/modules/${mod.name}`, + options: pluginOptions, + } + }), } } -export function getResolvedPlugins( +export async function getResolvedPlugins( rootDirectory: string, configModule: ConfigModule, isMedusaProject = false -): undefined | PluginDetails[] { - const resolved = configModule?.plugins?.map((plugin) => { - if (isString(plugin)) { - return resolvePlugin(plugin) - } - - const details = resolvePlugin(plugin.resolve) - details.options = plugin.options - - return details - }) +): Promise { + const resolved = await Promise.all( + (configModule?.plugins || []).map(async (plugin) => { + if (isString(plugin)) { + return resolvePlugin(rootDirectory, plugin) + } + return resolvePlugin(rootDirectory, plugin.resolve, plugin.options) + }) + ) if (isMedusaProject) { - const extensionDirectoryPath = getExtensionDirectoryPath() - const extensionDirectory = path.join(rootDirectory, extensionDirectoryPath) + const extensionDirectory = path.join(rootDirectory, MEDUSA_APP_SOURCE_PATH) resolved.push({ resolve: extensionDirectory, name: MEDUSA_PROJECT_NAME, diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index bd33ce8b1bd0d..e8ea0b2afa594 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -146,7 +146,7 @@ export default async ({ ContainerRegistrationKeys.CONFIG_MODULE ) - const plugins = getResolvedPlugins(rootDirectory, configModule, true) || [] + const plugins = await getResolvedPlugins(rootDirectory, configModule, true) const linksSourcePaths = plugins.map((plugin) => join(plugin.resolve, "links") ) From 3ee15f3b85dd5335efdeda4b29ab02a18b63de52 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 9 Jan 2025 11:59:52 +0200 Subject: [PATCH 4/8] docs: add inventory kit conceptual guide (#10891) --- .../inventory/inventory-kit/page.mdx | 415 ++++++++++++++++++ .../app/commerce-modules/inventory/page.mdx | 1 + .../app/commerce-modules/product/page.mdx | 1 + www/apps/resources/generated/edit-dates.mjs | 7 +- www/apps/resources/generated/files-map.mjs | 4 + www/apps/resources/generated/sidebar.mjs | 94 +++- www/apps/resources/sidebars/inventory.mjs | 5 + www/apps/resources/sidebars/product.mjs | 2 + www/packages/tags/src/tags/auth.ts | 2 +- www/packages/tags/src/tags/cart.ts | 4 + www/packages/tags/src/tags/concept.ts | 6 + www/packages/tags/src/tags/customer.ts | 2 +- www/packages/tags/src/tags/event-bus.ts | 4 + www/packages/tags/src/tags/index.ts | 41 +- www/packages/tags/src/tags/inventory.ts | 8 + .../tags/src/tags/{remote-link.ts => link.ts} | 6 +- www/packages/tags/src/tags/locking.ts | 4 + www/packages/tags/src/tags/logger.ts | 12 + www/packages/tags/src/tags/order.ts | 8 + www/packages/tags/src/tags/payment.ts | 12 + www/packages/tags/src/tags/product.ts | 4 + www/packages/tags/src/tags/query.ts | 12 + www/packages/tags/src/tags/remote-query.ts | 8 +- www/packages/tags/src/tags/step.ts | 16 + www/packages/tags/src/tags/stock-location.ts | 4 + www/packages/tags/src/tags/storefront.ts | 2 +- www/packages/tags/src/tags/tax.ts | 8 + www/packages/tags/src/tags/workflow.ts | 4 + 28 files changed, 663 insertions(+), 33 deletions(-) create mode 100644 www/apps/resources/app/commerce-modules/inventory/inventory-kit/page.mdx create mode 100644 www/packages/tags/src/tags/concept.ts rename www/packages/tags/src/tags/{remote-link.ts => link.ts} (97%) diff --git a/www/apps/resources/app/commerce-modules/inventory/inventory-kit/page.mdx b/www/apps/resources/app/commerce-modules/inventory/inventory-kit/page.mdx new file mode 100644 index 0000000000000..ff7c657d3ee26 --- /dev/null +++ b/www/apps/resources/app/commerce-modules/inventory/inventory-kit/page.mdx @@ -0,0 +1,415 @@ +--- +tags: + - inventory + - product + - stock location + - concept +--- + +export const metadata = { + title: `Inventory Kits`, +} + +# {metadata.title} + +In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. + +## What is an Inventory Kit? + +An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. + +The Medusa application links inventory items from the [Inventory Module](../page.mdx) to product variants in the [Product Module](../../product/page.mdx). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. + +Using inventory kits, you can implement use cases like: + +- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. +- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. + +--- + +## Multi-Part Products + +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. + +To implement this in Medusa, you can: + +- Create inventory items for each of the different parts. +- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. + +Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. + +![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) + +### Create Multi-Part Product + +Using the Medusa Admin, you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). + +Using [workflows](!docs!/learn/fundamentals/workflows), you can implement this by first creating the inventory items: + +export const multiPartsHighlights1 = [ + ["11", "stockLocations", "Retrieve the stock locations to create the inventory items in."], + ["19", "inventoryItems", "Create the inventory items."] +] + +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // Alternatively, you can create a stock location + const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: ["*"], + filters: { + name: "European Warehouse" + } + }) + + const inventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: [ + { + sku: "FRAME", + title: "Frame", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id + } + ] + }, + { + sku: "WHEEL", + title: "Wheel", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id + } + ] + }, + { + sku: "SEAT", + title: "Seat", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id + } + ] + } + ] + } + }) + + // TODO create the product + } +) +``` + +You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](/references/medusa-workflows/createStockLocationsWorkflow). + +Then, you create the inventory items that the product variant consists of. + +Next, create the product and pass the inventory item's IDs to the product's variant: + +export const multiPartHighlights2 = [ + ["15", "inventoryItemIds", "Create an array of inventory items to pass to the variant."], + ["26", "products", "Create the product with the inventory items."], + ["43", "inventoryItemIds", "Pass the inventory item IDs as the product variant's inventory items."] +] + +```ts highlights={multiPartHighlights2} +import { + // ... + transform +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow +} from "@medusajs/medusa/core-flows" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... + + const inventoryItemIds = transform({ + inventoryItems + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bicycle", + variants: [ + { + title: "Bicycle - Small", + prices: [ + { + amount: 100, + currency_code: "usd" + } + ], + options: { + "Default Option": "Default Variant" + }, + inventory_items: inventoryItemIds + } + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"] + } + ] + } + ] + } + }) + } +) +``` + +You prepare the inventory item IDs to pass to the variant using [transform](!docs!/learn/fundamentals/workflows/variable-manipulation) from the Workflows SDK, then pass these IDs to the created product's variant. + +You can now [execute the workflow](!docs!/learn/fundamentals/workflows#3-execute-the-workflow) in [API routes](!docs!/learn/fundamentals/api-routes), [scheduled jobs](!docs!/learn/fundamentals/scheduled-jobs), or [subscribers](!docs!/learn/fundamentals/events-and-subscribers). + +--- + +## Bundled Products + +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. + +![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) + +You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. + +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. + +![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) + +### Create Bundled Product + +You can create a bundled product in the Medusa Admin by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. + +Using [workflows](!docs!/learn/fundamentals/workflows), you can implement this by first creating the products part of the bundle: + +export const bundledHighlights1 = [ + ["11", "products", "Create the products part of the bundle."], + ["28", "manage_inventory", "Enabling this without specifying inventory items creates a default inventory item."] +] + +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + variants: [ + { + title: "Shirt", + prices: [ + { + amount: 10, + currency_code: "usd" + } + ], + options: { + "Default Option": "Default Variant" + }, + manage_inventory: true + } + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"] + } + ] + }, + { + title: "Pants", + variants: [ + { + title: "Pants", + prices: [ + { + amount: 10, + currency_code: "usd" + } + ], + options: { + "Default Option": "Default Variant" + }, + manage_inventory: true + } + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"] + } + ] + }, + { + title: "Shoes", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd" + } + ], + options: { + "Default Option": "Default Variant" + }, + manage_inventory: true + } + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"] + } + ] + } + ] + } + }) + + // TODO re-retrieve with inventory + } +) +``` + +You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). + +Next, retrieve the products again but with variant information: + +export const bundledHighlights2 = [ + ["13", "productIds", "Prepare the product IDs to retrieve them with Query."], + ["18", "productsWithInventory", "Re-retrieve the products with the inventory items of the product's variant."], + ["29", "inventoryItemIds", "Prepare the inventory items to pass to the bundled product's variant."] +] + +```ts highlights={bundledHighlights2} +import { + // ... + transform +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products + }, (data) => data.products.map((product) => product.id)) + + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*" + ], + filters: { + id: productIds + } + }) + + const inventoryItemIds = transform({ + productsWithInventory + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id + } + }) + }) + + // create bundled product + } +) +``` + +Using [Query](!docs!/learn/fundamentals/module-links/query), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. + +Finally, create the bundled product: + +export const bundledProductHighlights3 = [ + ["5", "bundledProduct", "Create the bundled product."], + ["22", "inventory_items", "Pass the inventory items of the products part of the bundle."] +] + +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + variants: [ + { + title: "Bundle", + prices: [ + { + amount: 30, + currency_code: "usd" + } + ], + options: { + "Default Option": "Default Variant" + }, + inventory_items: inventoryItemIds, + } + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"] + } + ] + } + ] + } + }).config({ name: "create-bundled-product" }) + } +) +``` + +The bundled product has the same inventory items as those of the products part of the bundle. + +You can now [execute the workflow](!docs!/learn/fundamentals/workflows#3-execute-the-workflow) in [API routes](!docs!/learn/fundamentals/api-routes), [scheduled jobs](!docs!/learn/fundamentals/scheduled-jobs), or [subscribers](!docs!/learn/fundamentals/events-and-subscribers). diff --git a/www/apps/resources/app/commerce-modules/inventory/page.mdx b/www/apps/resources/app/commerce-modules/inventory/page.mdx index 9abb351be9454..a6b4a8c4a776c 100644 --- a/www/apps/resources/app/commerce-modules/inventory/page.mdx +++ b/www/apps/resources/app/commerce-modules/inventory/page.mdx @@ -22,6 +22,7 @@ Learn more about why modules are isolated in [this documentation](!docs!/learn/f - [Inventory Across Locations](./concepts/page.mdx#inventorylevel): Manage inventory levels across different locations, such as warehouses. - [Reservation Management](./concepts/page.mdx#reservationitem): Reserve quantities of inventory items at specific locations for orders or other purposes. - [Check Inventory Availability](/references/inventory-next/confirmInventory): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](./inventory-kit/page.mdx): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. --- diff --git a/www/apps/resources/app/commerce-modules/product/page.mdx b/www/apps/resources/app/commerce-modules/product/page.mdx index 45777f4580cd0..442c46e4e5f35 100644 --- a/www/apps/resources/app/commerce-modules/product/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/page.mdx @@ -20,6 +20,7 @@ Learn more about why modules are isolated in [this documentation](!docs!/learn/f - [Products Management](/references/product/models/Product): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. - [Product Organization](/references/product/models): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. +- [Bundled and Multi-Part Products](../inventory/inventory-kit/page.mdx): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. --- diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 44d81fa1f213f..1564dbe9e28a3 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -30,7 +30,7 @@ export const generatedEditDates = { "app/commerce-modules/inventory/_events/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/inventory/concepts/page.mdx": "2024-10-08T15:11:27.634Z", "app/commerce-modules/inventory/inventory-in-flows/page.mdx": "2025-01-08T12:21:12.157Z", - "app/commerce-modules/inventory/page.mdx": "2024-12-25T15:55:02.850Z", + "app/commerce-modules/inventory/page.mdx": "2025-01-09T09:28:33.889Z", "app/commerce-modules/order/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/order/_events/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/order/claim/page.mdx": "2024-10-09T10:11:12.090Z", @@ -64,7 +64,7 @@ export const generatedEditDates = { "app/commerce-modules/product/_events/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/product/guides/price/page.mdx": "2024-12-25T15:10:37.730Z", "app/commerce-modules/product/guides/price-with-taxes/page.mdx": "2024-12-25T15:10:40.879Z", - "app/commerce-modules/product/page.mdx": "2024-12-25T15:55:02.850Z", + "app/commerce-modules/product/page.mdx": "2025-01-09T09:29:05.898Z", "app/commerce-modules/promotion/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/promotion/_events/page.mdx": "2024-07-03T19:27:13+03:00", "app/commerce-modules/promotion/actions/page.mdx": "2024-10-09T14:49:01.645Z", @@ -5787,5 +5787,6 @@ export const generatedEditDates = { "references/types/HttpTypes/interfaces/types.HttpTypes.StoreProductPricingContext/page.mdx": "2025-01-07T12:54:22.026Z", "references/types/StockLocationTypes/interfaces/types.StockLocationTypes.FilterableStockLocationAddressProps/page.mdx": "2025-01-07T12:54:23.060Z", "references/types/StockLocationTypes/types/types.StockLocationTypes.UpdateStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.057Z", - "references/types/StockLocationTypes/types/types.StockLocationTypes.UpsertStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.058Z" + "references/types/StockLocationTypes/types/types.StockLocationTypes.UpsertStockLocationAddressInput/page.mdx": "2025-01-07T12:54:23.058Z", + "app/commerce-modules/inventory/inventory-kit/page.mdx": "2025-01-09T09:39:50.221Z" } \ No newline at end of file diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index 042dbb7cdfb98..0c5448175ffdd 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -295,6 +295,10 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/page.mdx", "pathname": "/commerce-modules/inventory/inventory-in-flows" }, + { + "filePath": "/www/apps/resources/app/commerce-modules/inventory/inventory-kit/page.mdx", + "pathname": "/commerce-modules/inventory/inventory-kit" + }, { "filePath": "/www/apps/resources/app/commerce-modules/inventory/links-to-other-modules/page.mdx", "pathname": "/commerce-modules/inventory/links-to-other-modules" diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index 61b4c3f11a60a..f6eea7f5fb5c8 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -644,7 +644,7 @@ export const generatedSidebar = [ "loaded": true, "isPathHref": true, "type": "ref", - "title": "Retrieve Customer in Storefront", + "title": "Retrieve Logged-In Customer in Storefront", "path": "/storefront-development/customers/retrieve", "children": [] }, @@ -1237,6 +1237,14 @@ export const generatedSidebar = [ "path": "/references/medusa-workflows/addToCartWorkflow", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -2258,7 +2266,7 @@ export const generatedSidebar = [ "loaded": true, "isPathHref": true, "type": "ref", - "title": "Retrieve Customer in Storefront", + "title": "Retrieve Logged-In Customer in Storefront", "path": "/storefront-development/customers/retrieve", "children": [] }, @@ -4261,6 +4269,14 @@ export const generatedSidebar = [ "title": "Inventory in Flows", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "path": "/commerce-modules/inventory/inventory-kit", + "title": "Inventory Kit", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -4308,6 +4324,14 @@ export const generatedSidebar = [ "path": "/references/medusa-workflows/addToCartWorkflow", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -5225,6 +5249,14 @@ export const generatedSidebar = [ "initialOpen": false, "autogenerate_as_ref": true, "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -5873,6 +5905,14 @@ export const generatedSidebar = [ "path": "/references/medusa-workflows/refundPaymentWorkflow", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -7773,6 +7813,14 @@ export const generatedSidebar = [ "initialOpen": false, "autogenerate_as_ref": true, "children": [ + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -7845,6 +7893,14 @@ export const generatedSidebar = [ "path": "/references/medusa-workflows/refundPaymentWorkflow", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -7928,6 +7984,14 @@ export const generatedSidebar = [ "path": "/references/medusa-workflows/steps/refundPaymentStep", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "refundPaymentsStep", + "path": "/references/medusa-workflows/steps/refundPaymentsStep", + "children": [] + }, { "loaded": true, "isPathHref": true, @@ -9407,6 +9471,8 @@ export const generatedSidebar = [ "type": "category", "title": "Concepts", "initialOpen": false, + "autogenerate_tags": "concept+product", + "autogenerate_as_ref": true, "children": [ { "loaded": true, @@ -9415,6 +9481,14 @@ export const generatedSidebar = [ "path": "/commerce-modules/product/links-to-other-modules", "title": "Links to Other Modules", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Inventory Kits", + "path": "/commerce-modules/inventory/inventory-kit", + "children": [] } ] }, @@ -13347,6 +13421,14 @@ export const generatedSidebar = [ "title": "updateTaxRatesWorkflow", "path": "/references/medusa-workflows/updateTaxRatesWorkflow", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "updateTaxRegionsWorkflow", + "path": "/references/medusa-workflows/updateTaxRegionsWorkflow", + "children": [] } ] }, @@ -13430,6 +13512,14 @@ export const generatedSidebar = [ "title": "updateTaxRatesStep", "path": "/references/medusa-workflows/steps/updateTaxRatesStep", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "updateTaxRegionsStep", + "path": "/references/medusa-workflows/steps/updateTaxRegionsStep", + "children": [] } ] }, diff --git a/www/apps/resources/sidebars/inventory.mjs b/www/apps/resources/sidebars/inventory.mjs index 343aa15719814..caa869177f772 100644 --- a/www/apps/resources/sidebars/inventory.mjs +++ b/www/apps/resources/sidebars/inventory.mjs @@ -28,6 +28,11 @@ export const inventorySidebar = [ path: "/commerce-modules/inventory/inventory-in-flows", title: "Inventory in Flows", }, + { + type: "link", + path: "/commerce-modules/inventory/inventory-kit", + title: "Inventory Kit", + }, { type: "link", path: "/commerce-modules/inventory/links-to-other-modules", diff --git a/www/apps/resources/sidebars/product.mjs b/www/apps/resources/sidebars/product.mjs index d6c93a82c8a5d..8bd3064f7b56f 100644 --- a/www/apps/resources/sidebars/product.mjs +++ b/www/apps/resources/sidebars/product.mjs @@ -17,6 +17,8 @@ export const productSidebar = [ type: "category", title: "Concepts", initialOpen: false, + autogenerate_tags: "concept+product", + autogenerate_as_ref: true, children: [ { type: "link", diff --git a/www/packages/tags/src/tags/auth.ts b/www/packages/tags/src/tags/auth.ts index 3dec141dd29e1..f8023565f7336 100644 --- a/www/packages/tags/src/tags/auth.ts +++ b/www/packages/tags/src/tags/auth.ts @@ -16,7 +16,7 @@ export const auth = [ "path": "/storefront-development/customers/reset-password" }, { - "title": "Retrieve Customer in Storefront", + "title": "Retrieve Logged-In Customer in Storefront", "path": "/storefront-development/customers/retrieve" }, { diff --git a/www/packages/tags/src/tags/cart.ts b/www/packages/tags/src/tags/cart.ts index 99f58cdcde292..e060c29e009fc 100644 --- a/www/packages/tags/src/tags/cart.ts +++ b/www/packages/tags/src/tags/cart.ts @@ -99,6 +99,10 @@ export const cart = [ "title": "addToCartWorkflow", "path": "/references/medusa-workflows/addToCartWorkflow" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "createCartWorkflow", "path": "/references/medusa-workflows/createCartWorkflow" diff --git a/www/packages/tags/src/tags/concept.ts b/www/packages/tags/src/tags/concept.ts new file mode 100644 index 0000000000000..da7bc2c3f1398 --- /dev/null +++ b/www/packages/tags/src/tags/concept.ts @@ -0,0 +1,6 @@ +export const concept = [ + { + "title": "Inventory Kits", + "path": "/commerce-modules/inventory/inventory-kit" + } +] \ No newline at end of file diff --git a/www/packages/tags/src/tags/customer.ts b/www/packages/tags/src/tags/customer.ts index 4b044093710de..c42988f0ce751 100644 --- a/www/packages/tags/src/tags/customer.ts +++ b/www/packages/tags/src/tags/customer.ts @@ -28,7 +28,7 @@ export const customer = [ "path": "/storefront-development/customers/reset-password" }, { - "title": "Retrieve Customer in Storefront", + "title": "Retrieve Logged-In Customer in Storefront", "path": "/storefront-development/customers/retrieve" }, { diff --git a/www/packages/tags/src/tags/event-bus.ts b/www/packages/tags/src/tags/event-bus.ts index d5e17698f4337..44ee98d243f22 100644 --- a/www/packages/tags/src/tags/event-bus.ts +++ b/www/packages/tags/src/tags/event-bus.ts @@ -11,6 +11,10 @@ export const eventBus = [ "title": "addToCartWorkflow", "path": "/references/medusa-workflows/addToCartWorkflow" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "createCartWorkflow", "path": "/references/medusa-workflows/createCartWorkflow" diff --git a/www/packages/tags/src/tags/index.ts b/www/packages/tags/src/tags/index.ts index 374a5310832c4..4d5668befd156 100644 --- a/www/packages/tags/src/tags/index.ts +++ b/www/packages/tags/src/tags/index.ts @@ -1,36 +1,37 @@ -export * from "./product.js" +export * from "./inventory.js" +export * from "./query.js" export * from "./pricing.js" +export * from "./server.js" +export * from "./tax.js" export * from "./storefront.js" export * from "./payment.js" -export * from "./order.js" export * from "./cart.js" export * from "./stripe.js" -export * from "./server.js" -export * from "./product-category.js" -export * from "./auth.js" -export * from "./tax.js" -export * from "./inventory.js" +export * from "./fulfillment.js" +export * from "./order.js" export * from "./customer.js" export * from "./product-collection.js" -export * from "./fulfillment.js" -export * from "./region.js" +export * from "./product-category.js" +export * from "./auth.js" export * from "./api-key.js" -export * from "./query.js" -export * from "./sales-channel.js" -export * from "./remote-link.js" export * from "./publishable-api-key.js" +export * from "./stock-location.js" +export * from "./concept.js" +export * from "./sales-channel.js" +export * from "./product.js" export * from "./step.js" +export * from "./link.js" +export * from "./remote-query.js" +export * from "./region.js" export * from "./workflow.js" -export * from "./file.js" -export * from "./locking.js" -export * from "./stock-location.js" export * from "./store.js" -export * from "./user.js" +export * from "./promotion.js" +export * from "./currency.js" export * from "./event-bus.js" export * from "./logger.js" +export * from "./user.js" +export * from "./notification.js" +export * from "./file.js" export * from "./js-sdk.js" export * from "./admin.js" -export * from "./promotion.js" -export * from "./remote-query.js" -export * from "./notification.js" -export * from "./currency.js" +export * from "./locking.js" diff --git a/www/packages/tags/src/tags/inventory.ts b/www/packages/tags/src/tags/inventory.ts index e145a282f46f3..c2ebaf094065b 100644 --- a/www/packages/tags/src/tags/inventory.ts +++ b/www/packages/tags/src/tags/inventory.ts @@ -1,4 +1,8 @@ export const inventory = [ + { + "title": "Inventory Kits", + "path": "/commerce-modules/inventory/inventory-kit" + }, { "title": "Retrieve Product Variant's Inventory in Storefront", "path": "/storefront-development/products/inventory" @@ -15,6 +19,10 @@ export const inventory = [ "title": "addToCartWorkflow", "path": "/references/medusa-workflows/addToCartWorkflow" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "confirmVariantInventoryWorkflow", "path": "/references/medusa-workflows/confirmVariantInventoryWorkflow" diff --git a/www/packages/tags/src/tags/remote-link.ts b/www/packages/tags/src/tags/link.ts similarity index 97% rename from www/packages/tags/src/tags/remote-link.ts rename to www/packages/tags/src/tags/link.ts index 413efb2737eaf..3c43179eb9cc1 100644 --- a/www/packages/tags/src/tags/remote-link.ts +++ b/www/packages/tags/src/tags/link.ts @@ -1,4 +1,4 @@ -export const remoteLink = [ +export const link = [ { "title": "linkSalesChannelsToApiKeyStep", "path": "/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep" @@ -15,6 +15,10 @@ export const remoteLink = [ "title": "updateCartPromotionsStep", "path": "/references/medusa-workflows/steps/updateCartPromotionsStep" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "createCartWorkflow", "path": "/references/medusa-workflows/createCartWorkflow" diff --git a/www/packages/tags/src/tags/locking.ts b/www/packages/tags/src/tags/locking.ts index 3974662914d65..7bfee689fe88a 100644 --- a/www/packages/tags/src/tags/locking.ts +++ b/www/packages/tags/src/tags/locking.ts @@ -3,6 +3,10 @@ export const locking = [ "title": "reserveInventoryStep", "path": "/references/medusa-workflows/steps/reserveInventoryStep" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "confirmClaimRequestWorkflow", "path": "/references/medusa-workflows/confirmClaimRequestWorkflow" diff --git a/www/packages/tags/src/tags/logger.ts b/www/packages/tags/src/tags/logger.ts index ff33575388944..ee7240eab2bda 100644 --- a/www/packages/tags/src/tags/logger.ts +++ b/www/packages/tags/src/tags/logger.ts @@ -1,4 +1,8 @@ export const logger = [ + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "createCartWorkflow", "path": "/references/medusa-workflows/createCartWorkflow" @@ -23,10 +27,18 @@ export const logger = [ "title": "cancelPaymentStep", "path": "/references/medusa-workflows/steps/cancelPaymentStep" }, + { + "title": "refundPaymentsStep", + "path": "/references/medusa-workflows/steps/refundPaymentsStep" + }, { "title": "processPaymentWorkflow", "path": "/references/medusa-workflows/processPaymentWorkflow" }, + { + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow" + }, { "title": "deletePaymentSessionsStep", "path": "/references/medusa-workflows/steps/deletePaymentSessionsStep" diff --git a/www/packages/tags/src/tags/order.ts b/www/packages/tags/src/tags/order.ts index 780526122bac3..74f43befde1ae 100644 --- a/www/packages/tags/src/tags/order.ts +++ b/www/packages/tags/src/tags/order.ts @@ -3,6 +3,10 @@ export const order = [ "title": "Checkout Step 5: Complete Cart", "path": "/storefront-development/checkout/complete-cart" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "addOrderTransactionStep", "path": "/references/medusa-workflows/steps/addOrderTransactionStep" @@ -467,6 +471,10 @@ export const order = [ "title": "refundPaymentWorkflow", "path": "/references/medusa-workflows/refundPaymentWorkflow" }, + { + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow" + }, { "title": "createReturnReasonsStep", "path": "/references/medusa-workflows/steps/createReturnReasonsStep" diff --git a/www/packages/tags/src/tags/payment.ts b/www/packages/tags/src/tags/payment.ts index 0fd6be9a28e2c..7fb8ee005510e 100644 --- a/www/packages/tags/src/tags/payment.ts +++ b/www/packages/tags/src/tags/payment.ts @@ -19,6 +19,10 @@ export const payment = [ "title": "createPaymentCollectionsStep", "path": "/references/medusa-workflows/steps/createPaymentCollectionsStep" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "createCartWorkflow", "path": "/references/medusa-workflows/createCartWorkflow" @@ -59,6 +63,10 @@ export const payment = [ "title": "refundPaymentStep", "path": "/references/medusa-workflows/steps/refundPaymentStep" }, + { + "title": "refundPaymentsStep", + "path": "/references/medusa-workflows/steps/refundPaymentsStep" + }, { "title": "capturePaymentWorkflow", "path": "/references/medusa-workflows/capturePaymentWorkflow" @@ -71,6 +79,10 @@ export const payment = [ "title": "refundPaymentWorkflow", "path": "/references/medusa-workflows/refundPaymentWorkflow" }, + { + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow" + }, { "title": "createPaymentSessionStep", "path": "/references/medusa-workflows/steps/createPaymentSessionStep" diff --git a/www/packages/tags/src/tags/product.ts b/www/packages/tags/src/tags/product.ts index 4325a790b3d7e..b40705be65760 100644 --- a/www/packages/tags/src/tags/product.ts +++ b/www/packages/tags/src/tags/product.ts @@ -1,4 +1,8 @@ export const product = [ + { + "title": "Inventory Kits", + "path": "/commerce-modules/inventory/inventory-kit" + }, { "title": "Get Variant Prices", "path": "/commerce-modules/product/guides/price" diff --git a/www/packages/tags/src/tags/query.ts b/www/packages/tags/src/tags/query.ts index c41850749a7f6..ec85a61b11e84 100644 --- a/www/packages/tags/src/tags/query.ts +++ b/www/packages/tags/src/tags/query.ts @@ -59,6 +59,10 @@ export const query = [ "title": "cancelOrderTransferRequestWorkflow", "path": "/references/medusa-workflows/cancelOrderTransferRequestWorkflow" }, + { + "title": "cancelOrderWorkflow", + "path": "/references/medusa-workflows/cancelOrderWorkflow" + }, { "title": "declineOrderTransferRequestWorkflow", "path": "/references/medusa-workflows/declineOrderTransferRequestWorkflow" @@ -71,6 +75,10 @@ export const query = [ "title": "processPaymentWorkflow", "path": "/references/medusa-workflows/processPaymentWorkflow" }, + { + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow" + }, { "title": "batchProductVariantsWorkflow", "path": "/references/medusa-workflows/batchProductVariantsWorkflow" @@ -86,5 +94,9 @@ export const query = [ { "title": "deleteProductsWorkflow", "path": "/references/medusa-workflows/deleteProductsWorkflow" + }, + { + "title": "updateStockLocationsWorkflow", + "path": "/references/medusa-workflows/updateStockLocationsWorkflow" } ] \ No newline at end of file diff --git a/www/packages/tags/src/tags/remote-query.ts b/www/packages/tags/src/tags/remote-query.ts index f4a5a6eeb9943..95bab8e7d1104 100644 --- a/www/packages/tags/src/tags/remote-query.ts +++ b/www/packages/tags/src/tags/remote-query.ts @@ -15,6 +15,10 @@ export const remoteQuery = [ "title": "addToCartWorkflow", "path": "/references/medusa-workflows/addToCartWorkflow" }, + { + "title": "completeCartWorkflow", + "path": "/references/medusa-workflows/completeCartWorkflow" + }, { "title": "createCartWorkflow", "path": "/references/medusa-workflows/createCartWorkflow" @@ -147,10 +151,6 @@ export const remoteQuery = [ "title": "cancelOrderFulfillmentWorkflow", "path": "/references/medusa-workflows/cancelOrderFulfillmentWorkflow" }, - { - "title": "cancelOrderWorkflow", - "path": "/references/medusa-workflows/cancelOrderWorkflow" - }, { "title": "cancelReturnReceiveWorkflow", "path": "/references/medusa-workflows/cancelReturnReceiveWorkflow" diff --git a/www/packages/tags/src/tags/step.ts b/www/packages/tags/src/tags/step.ts index 2250612031d9b..5016b9f8ddeea 100644 --- a/www/packages/tags/src/tags/step.ts +++ b/www/packages/tags/src/tags/step.ts @@ -115,6 +115,10 @@ export const step = [ "title": "updateLineItemsStep", "path": "/references/medusa-workflows/steps/updateLineItemsStep" }, + { + "title": "validateCartPaymentsStep", + "path": "/references/medusa-workflows/steps/validateCartPaymentsStep" + }, { "title": "validateCartShippingOptionsStep", "path": "/references/medusa-workflows/steps/validateCartShippingOptionsStep" @@ -795,6 +799,14 @@ export const step = [ "title": "refundPaymentStep", "path": "/references/medusa-workflows/steps/refundPaymentStep" }, + { + "title": "refundPaymentsStep", + "path": "/references/medusa-workflows/steps/refundPaymentsStep" + }, + { + "title": "validatePaymentsRefundStep", + "path": "/references/medusa-workflows/validatePaymentsRefundStep" + }, { "title": "validateRefundStep", "path": "/references/medusa-workflows/validateRefundStep" @@ -1179,6 +1191,10 @@ export const step = [ "title": "updateTaxRatesStep", "path": "/references/medusa-workflows/steps/updateTaxRatesStep" }, + { + "title": "updateTaxRegionsStep", + "path": "/references/medusa-workflows/steps/updateTaxRegionsStep" + }, { "title": "createUsersStep", "path": "/references/medusa-workflows/steps/createUsersStep" diff --git a/www/packages/tags/src/tags/stock-location.ts b/www/packages/tags/src/tags/stock-location.ts index a1577de4b5db6..379dd13417dc7 100644 --- a/www/packages/tags/src/tags/stock-location.ts +++ b/www/packages/tags/src/tags/stock-location.ts @@ -1,4 +1,8 @@ export const stockLocation = [ + { + "title": "Inventory Kits", + "path": "/commerce-modules/inventory/inventory-kit" + }, { "title": "createStockLocations", "path": "/references/medusa-workflows/steps/createStockLocations" diff --git a/www/packages/tags/src/tags/storefront.ts b/www/packages/tags/src/tags/storefront.ts index 4b9993a06a1c0..80770477ace5f 100644 --- a/www/packages/tags/src/tags/storefront.ts +++ b/www/packages/tags/src/tags/storefront.ts @@ -76,7 +76,7 @@ export const storefront = [ "path": "/storefront-development/customers/reset-password" }, { - "title": "Retrieve Customer in Storefront", + "title": "Retrieve Logged-In Customer in Storefront", "path": "/storefront-development/customers/retrieve" }, { diff --git a/www/packages/tags/src/tags/tax.ts b/www/packages/tags/src/tags/tax.ts index 5c2f7b2cb9a52..8aa03245497f4 100644 --- a/www/packages/tags/src/tags/tax.ts +++ b/www/packages/tags/src/tags/tax.ts @@ -87,6 +87,10 @@ export const tax = [ "title": "updateTaxRatesStep", "path": "/references/medusa-workflows/steps/updateTaxRatesStep" }, + { + "title": "updateTaxRegionsStep", + "path": "/references/medusa-workflows/steps/updateTaxRegionsStep" + }, { "title": "createTaxRateRulesWorkflow", "path": "/references/medusa-workflows/createTaxRateRulesWorkflow" @@ -119,6 +123,10 @@ export const tax = [ "title": "updateTaxRatesWorkflow", "path": "/references/medusa-workflows/updateTaxRatesWorkflow" }, + { + "title": "updateTaxRegionsWorkflow", + "path": "/references/medusa-workflows/updateTaxRegionsWorkflow" + }, { "title": "taxRate", "path": "/references/js-sdk/admin/taxRate" diff --git a/www/packages/tags/src/tags/workflow.ts b/www/packages/tags/src/tags/workflow.ts index 4dd4d5222c8ac..0cc3ed14bbb07 100644 --- a/www/packages/tags/src/tags/workflow.ts +++ b/www/packages/tags/src/tags/workflow.ts @@ -627,6 +627,10 @@ export const workflow = [ "title": "refundPaymentWorkflow", "path": "/references/medusa-workflows/refundPaymentWorkflow" }, + { + "title": "refundPaymentsWorkflow", + "path": "/references/medusa-workflows/refundPaymentsWorkflow" + }, { "title": "createPaymentSessionsWorkflow", "path": "/references/medusa-workflows/createPaymentSessionsWorkflow" From a625bce7b022c28a256f81777b7ebab15ab1d930 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:07:15 +0100 Subject: [PATCH 5/8] fix(pricing): PriceLists of type Sale should not override lower prices (#10882) Resolves CMRC-840 --- .changeset/quick-buttons-raise.md | 5 + .../__tests__/product/store/product.spec.ts | 166 ++++++++++++++++++ .../pricing-module/calculate-price.spec.ts | 123 +++++++++++++ .../pricing/src/services/pricing-module.ts | 30 +++- 4 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 .changeset/quick-buttons-raise.md diff --git a/.changeset/quick-buttons-raise.md b/.changeset/quick-buttons-raise.md new file mode 100644 index 0000000000000..8cb128226fb2a --- /dev/null +++ b/.changeset/quick-buttons-raise.md @@ -0,0 +1,5 @@ +--- +"@medusajs/pricing": patch +--- + +fix(pricing): PriceLists of type Sale no longer override default prices when the price list price is higher than the default price. diff --git a/integration-tests/http/__tests__/product/store/product.spec.ts b/integration-tests/http/__tests__/product/store/product.spec.ts index 503adfb9961d9..248ab71826099 100644 --- a/integration-tests/http/__tests__/product/store/product.spec.ts +++ b/integration-tests/http/__tests__/product/store/product.spec.ts @@ -1172,6 +1172,89 @@ medusaIntegrationTestRunner({ expect(response.data.products).toEqual(expectation) }) + it("should list products with prices with a default price when the price list price is higher and the price list is of type SALE", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 3500, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { "customer.groups.id": [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: false, + is_calculated_price_tax_inclusive: false, + calculated_amount: 3000, + raw_calculated_amount: { + value: "3000", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 3000, + raw_original_amount: { + value: "3000", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }), + ]) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.products).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual(expectation) + }) + it("should list products with prices with a override price list price", async () => { const priceList = ( await api.post( @@ -1254,6 +1337,89 @@ medusaIntegrationTestRunner({ expect(response.status).toEqual(200) expect(response.data.products).toEqual(expectation) }) + + it("should list products with prices with a override price list price even if the price list price is higher than the default price", async () => { + const priceList = ( + await api.post( + `/admin/price-lists`, + { + title: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 35000, + currency_code: "usd", + variant_id: product.variants[0].id, + }, + ], + rules: { "customer.groups.id": [customerGroup.id] }, + }, + adminHeaders + ) + ).data.price_list + + let response = await api.get( + `/store/products?fields=*variants.calculated_price®ion_id=${region.id}`, + storeHeadersWithCustomer + ) + + const expectation = expect.arrayContaining([ + expect.objectContaining({ + id: product.id, + variants: [ + expect.objectContaining({ + calculated_price: { + id: expect.any(String), + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 35000, + raw_calculated_amount: { + value: "35000", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 35000, + raw_original_amount: { + value: "35000", + precision: 20, + }, + currency_code: "usd", + calculated_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: priceList.id, + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + }), + ], + }), + ]) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(3) + expect(response.data.products).toEqual(expectation) + + // with only region_id + response = await api.get( + `/store/products?region_id=${region.id}`, + storeHeadersWithCustomer + ) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual(expectation) + }) }) describe("with inventory items", () => { diff --git a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts index c2e6c4269991a..998f3a04dc64f 100644 --- a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts +++ b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -1167,6 +1167,129 @@ moduleIntegrationTestRunner({ ]) }) + it("should return default prices when the price list price is higher than the default price when the price list is of type SALE", async () => { + await createPriceLists(service, undefined, undefined, [ + { + amount: 2500, + currency_code: "PLN", + price_set_id: "price-set-PLN", + }, + { + amount: 2500, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ]) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: false, + is_calculated_price_tax_inclusive: false, + calculated_amount: 1000, + raw_calculated_amount: { + value: "1000", + precision: 20, + }, + is_original_price_price_list: false, + is_original_price_tax_inclusive: false, + original_amount: 1000, + raw_original_amount: { + value: "1000", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 10, + }, + original_price: { + id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 10, + }, + }, + ]) + }) + + it("should return price list prices even if the price list price is higher than the default price when the price list is of type OVERRIDE", async () => { + await createPriceLists( + service, + { type: PriceListType.OVERRIDE }, + {}, + [ + { + amount: 2500, + currency_code: "PLN", + price_set_id: "price-set-PLN", + }, + { + amount: 2500, + currency_code: "EUR", + price_set_id: "price-set-EUR", + }, + ] + ) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + is_calculated_price_price_list: true, + is_calculated_price_tax_inclusive: false, + calculated_amount: 2500, + raw_calculated_amount: { + value: "2500", + precision: 20, + }, + is_original_price_price_list: true, + is_original_price_tax_inclusive: false, + original_amount: 2500, + raw_original_amount: { + value: "2500", + precision: 20, + }, + currency_code: "PLN", + calculated_price: { + id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + original_price: { + id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "override", + min_quantity: null, + max_quantity: null, + }, + }, + ]) + }) + it("should return price list prices when price list conditions match for override", async () => { await createPriceLists(service, { type: PriceListType.OVERRIDE }) diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index cf0d3eabab05d..0b5d7ef3c68df 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -31,6 +31,7 @@ import { InjectTransactionManager, isPresent, isString, + MathBN, MedusaContext, MedusaError, ModulesSdkUtils, @@ -50,10 +51,10 @@ import { PriceSet, } from "@models" +import { Collection } from "@mikro-orm/core" import { ServiceTypes } from "@types" import { eventBuilders, validatePriceListDates } from "@utils" import { joinerConfig } from "../joiner-config" -import { Collection } from "@mikro-orm/core" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -288,11 +289,32 @@ export default class PricingModuleService let originalPrice: PricingTypes.CalculatedPriceSetDTO | undefined = defaultPrice + /** + * When deciding which price to use we follow the following logic: + * - If the price list is of type OVERRIDE, we always use the price list price. + * - If the price list is of type SALE, we use the lowest price between the price list price and the default price + */ if (priceListPrice) { - calculatedPrice = priceListPrice + switch (priceListPrice.price_list_type) { + case PriceListType.OVERRIDE: + calculatedPrice = priceListPrice + originalPrice = priceListPrice + break + case PriceListType.SALE: { + let lowestPrice = priceListPrice + + if (defaultPrice?.amount && priceListPrice.amount) { + lowestPrice = MathBN.lte( + priceListPrice.amount, + defaultPrice.amount + ) + ? priceListPrice + : defaultPrice + } - if (priceListPrice.price_list_type === PriceListType.OVERRIDE) { - originalPrice = priceListPrice + calculatedPrice = lowestPrice + break + } } } From 1ade80c3751d166deff46dcb8fd8ede0689da4a1 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 9 Jan 2025 13:38:24 +0200 Subject: [PATCH 6/8] docs: fix colon in api key header (#10893) --- www/apps/api-reference/markdown/admin.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/apps/api-reference/markdown/admin.mdx b/www/apps/api-reference/markdown/admin.mdx index 10b670564cdb3..2dae3f5b067ad 100644 --- a/www/apps/api-reference/markdown/admin.mdx +++ b/www/apps/api-reference/markdown/admin.mdx @@ -200,7 +200,7 @@ When using the JS SDK, you only need to specify the API key token in the [config ```js fetch(`{backend_url}/admin/products`, { headers: { - Authorization: `Basic ${window.btoa(`:${api_key_token}`)}`, + Authorization: `Basic ${window.btoa(`${api_key_token}:`)}`, }, }) ``` @@ -212,7 +212,7 @@ fetch(`{backend_url}/admin/products`, { fetch(`{backend_url}/admin/products`, { headers: { Authorization: `Basic ${ - Buffer.from(`:${api_key_token}`).toString("base64") + Buffer.from(`${api_key_token}:`).toString("base64") }`, }, }) From 635d026f7f6376299b52145257c20822075ee33b Mon Sep 17 00:00:00 2001 From: gharbi-mohamed-dev <133573945+gharbi-mohamed-dev@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:05:33 +0100 Subject: [PATCH 7/8] docs: fix typo (#10894) --- .../book/app/learn/fundamentals/workflows/conditions/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/apps/book/app/learn/fundamentals/workflows/conditions/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/conditions/page.mdx index dced1e384d1c7..2398d441cb7b9 100644 --- a/www/apps/book/app/learn/fundamentals/workflows/conditions/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/conditions/page.mdx @@ -12,7 +12,7 @@ Medusa creates an internal representation of the workflow definition you pass to 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 when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisified. +Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. --- From 6747a1564ba9743d66687c3f647b9dc0baf1859f Mon Sep 17 00:00:00 2001 From: Sze-Chi Wang Date: Thu, 9 Jan 2025 14:32:15 +0100 Subject: [PATCH 8/8] fix: event-bus-redis processor execute event before subscriber are loaded (#10823) * fix(worker): run worker after application start * test(event-bus-redis): worker should initiate with autorun:false --------- Co-authored-by: Suki Wang Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../event-bus-redis/src/services/__tests__/event-bus.ts | 1 + .../modules/event-bus-redis/src/services/event-bus-redis.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/modules/event-bus-redis/src/services/__tests__/event-bus.ts b/packages/modules/event-bus-redis/src/services/__tests__/event-bus.ts index bf65eaf0a7c5f..a504b9d84b649 100644 --- a/packages/modules/event-bus-redis/src/services/__tests__/event-bus.ts +++ b/packages/modules/event-bus-redis/src/services/__tests__/event-bus.ts @@ -63,6 +63,7 @@ describe("RedisEventBusService", () => { { connection: expect.any(Object), prefix: "RedisEventBusService", + autorun: false } ) }) diff --git a/packages/modules/event-bus-redis/src/services/event-bus-redis.ts b/packages/modules/event-bus-redis/src/services/event-bus-redis.ts index 52c80139143b0..ba27a3d7576a0 100644 --- a/packages/modules/event-bus-redis/src/services/event-bus-redis.ts +++ b/packages/modules/event-bus-redis/src/services/event-bus-redis.ts @@ -68,12 +68,16 @@ export default class RedisEventBusService extends AbstractEventBusModuleService prefix: `${this.constructor.name}`, ...(moduleOptions.workerOptions ?? {}), connection: eventBusRedisConnection, + autorun: false, } ) } } __hooks = { + onApplicationStart: async () => { + await this.bullWorker_?.run() + }, onApplicationShutdown: async () => { await this.queue_.close() // eslint-disable-next-line max-len