From 45134e4d11cfcdc08dbd10aae687bfbe9e848ab9 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:37:26 -0300 Subject: [PATCH] chore: local workflow proxying methods to pass context (#6263) What: - When calling a module's method inside a Local Workflow the MedusaContext is passed as the last argument to the method if not provided - Add `requestId` to req - A couple of fixes on Remote Joiner and the data fetcher for internal services Why: - The context used to initialize the workflow has to be shared with all modules. properties like transactionId will be used to emit events and requestId to trace logs for example. --- .changeset/loud-kiwis-promise.md | 15 +++ .../promotion/admin/create-campaign.spec.ts | 94 ++++++++++++++++++ .../workflows/product/create-product.ts | 10 +- packages/auth/src/services/auth-module.ts | 33 +++---- .../inventory/src/services/inventory-item.ts | 10 +- .../inventory/src/services/inventory-level.ts | 14 +-- packages/inventory/src/services/inventory.ts | 22 ++--- .../src/services/reservation-item.ts | 8 +- .../src/services/link-module-service.ts | 8 +- .../src/api-v2/admin/campaigns/route.ts | 3 + packages/medusa/src/loaders/index.ts | 5 +- packages/medusa/src/types/global.ts | 1 + packages/medusa/src/types/routing.ts | 1 + .../src/utils/remote-query-fetch-data.ts | 53 ++++++++-- .../src/loaders/utils/load-internal.ts | 4 +- packages/modules-sdk/src/remote-query.ts | 15 +-- .../src/__tests__/joiner/helpers.ts | 75 ++++++++++++++ .../orchestration/src/joiner/graphql-ast.ts | 6 +- packages/orchestration/src/joiner/helpers.ts | 98 ++++++++++++------- .../orchestration/src/joiner/remote-joiner.ts | 23 +++-- .../src/workflow/global-workflow.ts | 4 +- .../src/workflow/local-workflow.ts | 62 +++++++++++- .../src/workflow/workflow-manager.ts | 2 +- .../pricing/src/services/pricing-module.ts | 14 ++- .../src/services/product-module-service.ts | 16 +-- .../src/services/promotion-module.ts | 8 +- .../src/services/stock-location.ts | 6 +- packages/types/src/shared-context.ts | 6 ++ .../src/dal/mikro-orm/mikro-orm-repository.ts | 7 +- packages/utils/src/dal/repository.ts | 2 +- packages/utils/src/decorators/index.ts | 1 - .../src/decorators/inject-entity-manager.ts | 4 +- packages/utils/src/index.ts | 6 +- .../modules-sdk/abstract-service-factory.ts | 2 +- .../decorators/context-parameter.ts | 9 ++ .../utils/src/modules-sdk/decorators/index.ts | 1 + .../modules-sdk/decorators/inject-manager.ts | 26 ++++- .../decorators/inject-shared-context.ts | 5 +- .../decorators/inject-transaction-manager.ts | 29 +++++- .../src/helper/workflow-export.ts | 38 ++++++- 40 files changed, 576 insertions(+), 170 deletions(-) create mode 100644 .changeset/loud-kiwis-promise.md rename packages/utils/src/{ => modules-sdk}/decorators/context-parameter.ts (54%) diff --git a/.changeset/loud-kiwis-promise.md b/.changeset/loud-kiwis-promise.md new file mode 100644 index 0000000000..063ae242b5 --- /dev/null +++ b/.changeset/loud-kiwis-promise.md @@ -0,0 +1,15 @@ +--- +"@medusajs/stock-location": patch +"@medusajs/orchestration": patch +"@medusajs/workflows-sdk": patch +"@medusajs/link-modules": patch +"@medusajs/modules-sdk": patch +"@medusajs/inventory": patch +"@medusajs/pricing": patch +"@medusajs/product": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +Workflows passing MedusaContext as argument diff --git a/integration-tests/plugins/__tests__/promotion/admin/create-campaign.spec.ts b/integration-tests/plugins/__tests__/promotion/admin/create-campaign.spec.ts index 81175707e9..023a6001a8 100644 --- a/integration-tests/plugins/__tests__/promotion/admin/create-campaign.spec.ts +++ b/integration-tests/plugins/__tests__/promotion/admin/create-campaign.spec.ts @@ -100,4 +100,98 @@ describe("POST /admin/campaigns", () => { }) ) }) + + it("should create 3 campaigns in parallel and have the context passed as argument when calling createCampaigns with different transactionId", async () => { + const parallelPromotion = await promotionModuleService.create({ + code: "PARALLEL", + type: "standard", + }) + + const spyCreateCampaigns = jest.spyOn( + promotionModuleService.constructor.prototype, + "createCampaigns" + ) + + const api = useApi() as any + + const a = async () => { + return await api.post( + `/admin/campaigns`, + { + name: "camp_1", + campaign_identifier: "camp_1", + starts_at: new Date("01/01/2024").toISOString(), + ends_at: new Date("01/02/2024").toISOString(), + promotions: [{ id: parallelPromotion.id }], + budget: { + limit: 1000, + type: "usage", + }, + }, + adminHeaders + ) + } + + const b = async () => { + return await api.post( + `/admin/campaigns`, + { + name: "camp_2", + campaign_identifier: "camp_2", + starts_at: new Date("01/02/2024").toISOString(), + ends_at: new Date("01/03/2029").toISOString(), + promotions: [{ id: parallelPromotion.id }], + budget: { + limit: 500, + type: "usage", + }, + }, + adminHeaders + ) + } + + const c = async () => { + return await api.post( + `/admin/campaigns`, + { + name: "camp_3", + campaign_identifier: "camp_3", + starts_at: new Date("01/03/2024").toISOString(), + ends_at: new Date("01/04/2029").toISOString(), + promotions: [{ id: parallelPromotion.id }], + budget: { + limit: 250, + type: "usage", + }, + }, + { + headers: { + ...adminHeaders.headers, + "x-request-id": "my-custom-request-id", + }, + } + ) + } + + await Promise.all([a(), b(), c()]) + + expect(spyCreateCampaigns).toHaveBeenCalledTimes(3) + expect(spyCreateCampaigns.mock.calls[0][1].__type).toBe("MedusaContext") + + const distinctTransactionId = [ + ...new Set( + spyCreateCampaigns.mock.calls.map((call) => call[1].transactionId) + ), + ] + expect(distinctTransactionId).toHaveLength(3) + + const distinctRequestId = [ + ...new Set( + spyCreateCampaigns.mock.calls.map((call) => call[1].requestId) + ), + ] + + expect(distinctRequestId).toHaveLength(3) + expect(distinctRequestId).toContain("my-custom-request-id") + }) }) diff --git a/integration-tests/plugins/__tests__/workflows/product/create-product.ts b/integration-tests/plugins/__tests__/workflows/product/create-product.ts index 0ecc3866fb..5d218d108e 100644 --- a/integration-tests/plugins/__tests__/workflows/product/create-product.ts +++ b/integration-tests/plugins/__tests__/workflows/product/create-product.ts @@ -1,15 +1,15 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { IProductModuleService, WorkflowTypes } from "@medusajs/types" import { - createProducts, CreateProductsActions, Handlers, + createProducts, } from "@medusajs/core-flows" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IProductModuleService, WorkflowTypes } from "@medusajs/types" +import { pipe } from "@medusajs/workflows-sdk" import path from "path" import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" -import { pipe } from "@medusajs/workflows-sdk" jest.setTimeout(30000) @@ -25,8 +25,6 @@ describe("CreateProduct workflow", function () { }) afterAll(async () => { - console.log("GLOABL GC()", typeof global) - const db = useDb() await db.shutdown() await shutdownServer() diff --git a/packages/auth/src/services/auth-module.ts b/packages/auth/src/services/auth-module.ts index 5d6be8fbcb..3310c28c1f 100644 --- a/packages/auth/src/services/auth-module.ts +++ b/packages/auth/src/services/auth-module.ts @@ -1,23 +1,24 @@ import jwt from "jsonwebtoken" import { + AuthProviderDTO, + AuthTypes, + AuthUserDTO, AuthenticationInput, AuthenticationResponse, - AuthTypes, Context, + CreateAuthProviderDTO, + CreateAuthUserDTO, DAL, + FilterableAuthProviderProps, + FilterableAuthUserProps, FindConfig, InternalModuleDeclaration, + JWTGenerationOptions, MedusaContainer, ModuleJoinerConfig, - JWTGenerationOptions, + UpdateAuthUserDTO, } from "@medusajs/types" - -import { AuthProvider, AuthUser } from "@models" - -import { joinerConfig } from "../joiner-config" -import { AuthProviderService, AuthUserService } from "@services" - import { AbstractAuthModuleProvider, InjectManager, @@ -25,16 +26,10 @@ import { MedusaContext, MedusaError, } from "@medusajs/utils" -import { - AuthProviderDTO, - AuthUserDTO, - CreateAuthProviderDTO, - CreateAuthUserDTO, - FilterableAuthProviderProps, - FilterableAuthUserProps, - UpdateAuthUserDTO, -} from "@medusajs/types" +import { AuthProvider, AuthUser } from "@models" +import { AuthProviderService, AuthUserService } from "@services" import { ServiceTypes } from "@types" +import { joinerConfig } from "../joiner-config" type AuthModuleOptions = { jwt_secret: string @@ -90,7 +85,7 @@ export default class AuthModuleService< async retrieveAuthProvider( provider: string, config: FindConfig = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise { const authProvider = await this.authProviderService_.retrieve( provider, @@ -107,7 +102,7 @@ export default class AuthModuleService< async listAuthProviders( filters: FilterableAuthProviderProps = {}, config: FindConfig = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise { const authProviders = await this.authProviderService_.list( filters, diff --git a/packages/inventory/src/services/inventory-item.ts b/packages/inventory/src/services/inventory-item.ts index 7cc3ecba67..207a8c6624 100644 --- a/packages/inventory/src/services/inventory-item.ts +++ b/packages/inventory/src/services/inventory-item.ts @@ -8,14 +8,14 @@ import { } from "@medusajs/types" import { InjectEntityManager, - isDefined, MedusaContext, MedusaError, + isDefined, } from "@medusajs/utils" import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm" import { InventoryItem } from "../models" -import { getListQuery } from "../utils/query" import { buildQuery } from "../utils/build-query" +import { getListQuery } from "../utils/query" type InjectedDependencies = { eventBusService: IEventBusService @@ -47,7 +47,7 @@ export default class InventoryItemService { async list( selector: FilterableInventoryItemProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const queryBuilder = getListQuery( context.transactionManager ?? this.manager_, @@ -68,7 +68,7 @@ export default class InventoryItemService { async retrieve( inventoryItemId: string, config: FindConfig = {}, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!isDefined(inventoryItemId)) { throw new MedusaError( @@ -102,7 +102,7 @@ export default class InventoryItemService { async listAndCount( selector: FilterableInventoryItemProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[InventoryItemDTO[], number]> { const queryBuilder = getListQuery( context.transactionManager ?? this.manager_, diff --git a/packages/inventory/src/services/inventory-level.ts b/packages/inventory/src/services/inventory-level.ts index c3c03fa326..38ddff8462 100644 --- a/packages/inventory/src/services/inventory-level.ts +++ b/packages/inventory/src/services/inventory-level.ts @@ -7,9 +7,9 @@ import { } from "@medusajs/types" import { InjectEntityManager, - isDefined, MedusaContext, MedusaError, + isDefined, } from "@medusajs/utils" import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm" import { InventoryLevel } from "../models" @@ -46,7 +46,7 @@ export default class InventoryLevelService { async list( selector: FilterableInventoryLevelProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const manager = context.transactionManager ?? this.manager_ const levelRepository = manager.getRepository(InventoryLevel) @@ -65,7 +65,7 @@ export default class InventoryLevelService { async listAndCount( selector: FilterableInventoryLevelProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[InventoryLevel[], number]> { const manager = context.transactionManager ?? this.manager_ const levelRepository = manager.getRepository(InventoryLevel) @@ -85,7 +85,7 @@ export default class InventoryLevelService { async retrieve( inventoryLevelId: string, config: FindConfig = {}, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!isDefined(inventoryLevelId)) { throw new MedusaError( @@ -319,7 +319,7 @@ export default class InventoryLevelService { async getStockedQuantity( inventoryItemId: string, locationIds: string[] | string, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!Array.isArray(locationIds)) { locationIds = [locationIds] @@ -348,7 +348,7 @@ export default class InventoryLevelService { async getAvailableQuantity( inventoryItemId: string, locationIds: string[] | string, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!Array.isArray(locationIds)) { locationIds = [locationIds] @@ -377,7 +377,7 @@ export default class InventoryLevelService { async getReservedQuantity( inventoryItemId: string, locationIds: string[] | string, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!Array.isArray(locationIds)) { locationIds = [locationIds] diff --git a/packages/inventory/src/services/inventory.ts b/packages/inventory/src/services/inventory.ts index c63ef02a6c..2294c21f4e 100644 --- a/packages/inventory/src/services/inventory.ts +++ b/packages/inventory/src/services/inventory.ts @@ -74,7 +74,7 @@ export default class InventoryService implements IInventoryService { async listInventoryItems( selector: FilterableInventoryItemProps, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[InventoryItemDTO[], number]> { return await this.inventoryItemService_.listAndCount( selector, @@ -85,7 +85,7 @@ export default class InventoryService implements IInventoryService { async list( selector: FilterableInventoryItemProps, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { return await this.inventoryItemService_.list(selector, config, context) } @@ -104,7 +104,7 @@ export default class InventoryService implements IInventoryService { skip: 0, take: 10, }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[InventoryLevelDTO[], number]> { return await this.inventoryLevelService_.listAndCount( selector, @@ -127,7 +127,7 @@ export default class InventoryService implements IInventoryService { skip: 0, take: 10, }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[ReservationItemDTO[], number]> { return await this.reservationItemService_.listAndCount( selector, @@ -146,7 +146,7 @@ export default class InventoryService implements IInventoryService { async retrieveInventoryItem( inventoryItemId: string, config?: FindConfig, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const inventoryItem = await this.inventoryItemService_.retrieve( inventoryItemId, @@ -166,7 +166,7 @@ export default class InventoryService implements IInventoryService { async retrieveInventoryLevel( inventoryItemId: string, locationId: string, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const [inventoryLevel] = await this.inventoryLevelService_.list( { inventory_item_id: inventoryItemId, location_id: locationId }, @@ -191,7 +191,7 @@ export default class InventoryService implements IInventoryService { */ async retrieveReservationItem( reservationId: string, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { return await this.reservationItemService_.retrieve( reservationId, @@ -202,7 +202,7 @@ export default class InventoryService implements IInventoryService { private async ensureInventoryLevels( data: { location_id: string; inventory_item_id: string }[], - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const inventoryLevels = await this.inventoryLevelService_.list( { @@ -629,7 +629,7 @@ export default class InventoryService implements IInventoryService { async retrieveAvailableQuantity( inventoryItemId: string, locationIds: string[], - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { // Throws if item does not exist await this.inventoryItemService_.retrieve( @@ -665,7 +665,7 @@ export default class InventoryService implements IInventoryService { async retrieveStockedQuantity( inventoryItemId: string, locationIds: string[], - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { // Throws if item does not exist await this.inventoryItemService_.retrieve( @@ -701,7 +701,7 @@ export default class InventoryService implements IInventoryService { async retrieveReservedQuantity( inventoryItemId: string, locationIds: string[], - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { // Throws if item does not exist await this.inventoryItemService_.retrieve( diff --git a/packages/inventory/src/services/reservation-item.ts b/packages/inventory/src/services/reservation-item.ts index 8b913e41ba..a45675c28f 100644 --- a/packages/inventory/src/services/reservation-item.ts +++ b/packages/inventory/src/services/reservation-item.ts @@ -8,9 +8,9 @@ import { } from "@medusajs/types" import { InjectEntityManager, - isDefined, MedusaContext, MedusaError, + isDefined, promiseAll, } from "@medusajs/utils" import { EntityManager, FindManyOptions, In } from "typeorm" @@ -55,7 +55,7 @@ export default class ReservationItemService { async list( selector: FilterableReservationItemProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const manager = context.transactionManager ?? this.manager_ const itemRepository = manager.getRepository(ReservationItem) @@ -75,7 +75,7 @@ export default class ReservationItemService { async listAndCount( selector: FilterableReservationItemProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[ReservationItem[], number]> { const manager = context.transactionManager ?? this.manager_ const itemRepository = manager.getRepository(ReservationItem) @@ -96,7 +96,7 @@ export default class ReservationItemService { async retrieve( reservationItemId: string, config: FindConfig = {}, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!isDefined(reservationItemId)) { throw new MedusaError( diff --git a/packages/link-modules/src/services/link-module-service.ts b/packages/link-modules/src/services/link-module-service.ts index 692e7ae987..ac15ba7dea 100644 --- a/packages/link-modules/src/services/link-module-service.ts +++ b/packages/link-modules/src/services/link-module-service.ts @@ -11,12 +11,12 @@ import { import { InjectManager, InjectTransactionManager, - isDefined, - mapObjectTo, MapToConfig, MedusaContext, MedusaError, ModulesSdkUtils, + isDefined, + mapObjectTo, } from "@medusajs/utils" import { LinkService } from "@services" import { shouldForceTransaction } from "../utils" @@ -229,7 +229,7 @@ export default class LinkModuleService implements ILinkModule { async softDelete( data: any, { returnLinkableKeys }: SoftDeleteReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise | void> { this.validateFields(data) @@ -270,7 +270,7 @@ export default class LinkModuleService implements ILinkModule { async restore( data: any, { returnLinkableKeys }: RestoreReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise | void> { this.validateFields(data) diff --git a/packages/medusa/src/api-v2/admin/campaigns/route.ts b/packages/medusa/src/api-v2/admin/campaigns/route.ts index 9267425599..e3c4159ab0 100644 --- a/packages/medusa/src/api-v2/admin/campaigns/route.ts +++ b/packages/medusa/src/api-v2/admin/campaigns/route.ts @@ -30,6 +30,9 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { const { result, errors } = await createCampaigns.run({ input: { campaignsData }, throwOnError: false, + context: { + requestId: req.requestId, + }, }) if (Array.isArray(errors) && errors[0]) { diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 99ec9ff7bf..f3310f8a8e 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -13,9 +13,9 @@ import { asValue } from "awilix" import { createMedusaContainer } from "medusa-core-utils" import { track } from "medusa-telemetry" import { EOL } from "os" -import path from "path" import requestIp from "request-ip" import { Connection } from "typeorm" +import { v4 } from "uuid" import { MedusaContainer } from "../types/global" import apiLoader from "./api" import loadConfig from "./config" @@ -192,7 +192,8 @@ export default async ({ // Add the registered services to the request scope expressApp.use((req: Request, res: Response, next: NextFunction) => { container.register({ manager: asValue(dataSource.manager) }) - ;(req as any).scope = container.createScope() + req.scope = container.createScope() as MedusaContainer + req.requestId = (req.headers["x-request-id"] as string) ?? v4() next() }) diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 9c4f9e0541..dad58aa74a 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -18,6 +18,7 @@ declare global { allowedProperties: string[] includes?: Record errors: string[] + requestId?: string } } } diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index a524e8a7c1..8a17b7eee0 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -6,6 +6,7 @@ import type { MedusaContainer } from "./global" export interface MedusaRequest extends Request { user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer + requestId?: string auth_user?: { id: string; app_metadata: Record; scope: string } } diff --git a/packages/medusa/src/utils/remote-query-fetch-data.ts b/packages/medusa/src/utils/remote-query-fetch-data.ts index 5ef138f7db..5bf63c65ee 100644 --- a/packages/medusa/src/utils/remote-query-fetch-data.ts +++ b/packages/medusa/src/utils/remote-query-fetch-data.ts @@ -1,6 +1,23 @@ import { MedusaModule, RemoteQuery } from "@medusajs/modules-sdk" import { MedusaContainer } from "@medusajs/types" +function hasPagination(options: { [attr: string]: unknown }): boolean { + if (!options) { + return false + } + + const attrs = ["skip"] + return Object.keys(options).some((key) => attrs.includes(key)) +} + +function buildPagination(options, count) { + return { + skip: options.skip, + take: options.take, + count, + } +} + export function remoteQueryFetchData(container: MedusaContainer) { return async (expand, keyField, ids, relationship) => { const serviceConfig = expand.serviceConfig @@ -12,11 +29,35 @@ export function remoteQueryFetchData(container: MedusaContainer) { return } - const filters = {} + let filters = {} const options = { ...RemoteQuery.getAllFieldsAndRelations(expand), } + const availableOptions = [ + "skip", + "take", + "limit", + "offset", + "order", + "sort", + "withDeleted", + ] + const availableOptionsAlias = new Map([ + ["limit", "take"], + ["offset", "skip"], + ["sort", "order"], + ]) + for (const arg of expand.args || []) { + if (arg.name === "filters" && arg.value) { + filters = { ...arg.value } + } else if (availableOptions.includes(arg.name)) { + const argName = availableOptionsAlias.has(arg.name) + ? availableOptionsAlias.get(arg.name) + : arg.name + options[argName] = arg.value + } + } const expandRelations = Object.keys(expand.expands ?? {}) // filter out links from relations because TypeORM will throw if the relation doesn't exist @@ -33,11 +74,9 @@ export function remoteQueryFetchData(container: MedusaContainer) { filters[keyField] = ids } - const hasPagination = Object.keys(options).some((key) => - ["skip"].includes(key) - ) + const hasPagination_ = hasPagination(options) - let methodName = hasPagination ? "listAndCount" : "list" + let methodName = hasPagination_ ? "listAndCount" : "list" if (relationship?.args?.methodSuffix) { methodName += relationship.args.methodSuffix @@ -47,12 +86,12 @@ export function remoteQueryFetchData(container: MedusaContainer) { const result = await service[methodName](filters, options) - if (hasPagination) { + if (hasPagination_) { const [data, count] = result return { data: { rows: data, - metadata: {}, + metadata: buildPagination(options, count), }, path: "rows", } diff --git a/packages/modules-sdk/src/loaders/utils/load-internal.ts b/packages/modules-sdk/src/loaders/utils/load-internal.ts index 08a2074743..778efe0119 100644 --- a/packages/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/modules-sdk/src/loaders/utils/load-internal.ts @@ -9,6 +9,7 @@ import { import { ContainerRegistrationKeys, createMedusaContainer, + MedusaModuleType, } from "@medusajs/utils" import { asFunction, asValue } from "awilix" @@ -19,7 +20,7 @@ export async function loadInternalModule( ): Promise<{ error?: Error } | void> { const registrationName = resolution.definition.registrationName - const { scope, resources } = + const { resources } = resolution.moduleDeclaration as InternalModuleDeclaration let loadedModule: ModuleExports @@ -111,6 +112,7 @@ export async function loadInternalModule( const moduleService = loadedModule.service container.register({ [registrationName]: asFunction((cradle) => { + ;(moduleService as any).__type = MedusaModuleType return new moduleService( localContainer.cradle, resolution.options, diff --git a/packages/modules-sdk/src/remote-query.ts b/packages/modules-sdk/src/remote-query.ts index afe8228885..e630c252c6 100644 --- a/packages/modules-sdk/src/remote-query.ts +++ b/packages/modules-sdk/src/remote-query.ts @@ -1,3 +1,8 @@ +import { + RemoteFetchDataCallback, + RemoteJoiner, + toRemoteJoinerQuery, +} from "@medusajs/orchestration" import { JoinerRelationship, JoinerServiceConfig, @@ -6,11 +11,6 @@ import { RemoteExpandProperty, RemoteJoinerQuery, } from "@medusajs/types" -import { - RemoteFetchDataCallback, - RemoteJoiner, - toRemoteJoinerQuery, -} from "@medusajs/orchestration" import { isString, toPascalCase } from "@medusajs/utils" import { MedusaModule } from "./medusa-module" @@ -30,7 +30,7 @@ export class RemoteQuery { servicesConfig?: ModuleJoinerConfig[] }) { const servicesConfig_ = [...servicesConfig] - + if (!modulesLoaded?.length) { modulesLoaded = MedusaModule.getLoadedModules().map( (mod) => Object.values(mod)[0] @@ -164,6 +164,7 @@ export class RemoteQuery { "offset", "cursor", "sort", + "withDeleted", ] const availableOptionsAlias = new Map([ ["limit", "take"], @@ -228,7 +229,7 @@ export class RemoteQuery { if (isString(query)) { finalQuery = RemoteJoiner.parseQuery(query, variables) } else if (!isString(finalQuery?.service) && !isString(finalQuery?.alias)) { - finalQuery = toRemoteJoinerQuery(query) + finalQuery = toRemoteJoinerQuery(query, variables) } return await this.remoteJoiner.query(finalQuery) diff --git a/packages/orchestration/src/__tests__/joiner/helpers.ts b/packages/orchestration/src/__tests__/joiner/helpers.ts index 2b5a3e037e..5b1536ec44 100644 --- a/packages/orchestration/src/__tests__/joiner/helpers.ts +++ b/packages/orchestration/src/__tests__/joiner/helpers.ts @@ -144,4 +144,79 @@ describe("toRemoteJoinerQuery", () => { ], }) }) + + it("should transform a nested object with arguments and directives to a Remote Joiner Query format only using variables", async () => { + const obj = { + product: { + fields: ["id", "title", "handle"], + variants: { + fields: ["sku"], + __directives: { + directiveName: "value", + }, + shipping_profiles: { + profile: { + fields: ["id", "name"], + }, + }, + }, + }, + } + + const rjQuery = toRemoteJoinerQuery(obj, { + product: { + limit: 10, + offset: 0, + }, + "product.variants.shipping_profiles.profile": { + context: { + customer_group: "cg_123", + region_id: "US", + }, + }, + }) + + expect(rjQuery).toEqual({ + alias: "product", + fields: ["id", "title", "handle"], + expands: [ + { + property: "variants", + directives: [ + { + name: "directiveName", + value: "value", + }, + ], + fields: ["sku"], + }, + { + property: "variants.shipping_profiles", + }, + { + property: "variants.shipping_profiles.profile", + args: [ + { + name: "context", + value: { + customer_group: "cg_123", + region_id: "US", + }, + }, + ], + fields: ["id", "name"], + }, + ], + args: [ + { + name: "limit", + value: 10, + }, + { + name: "offset", + value: 0, + }, + ], + }) + }) }) diff --git a/packages/orchestration/src/joiner/graphql-ast.ts b/packages/orchestration/src/joiner/graphql-ast.ts index e172c54da0..8f32238ced 100644 --- a/packages/orchestration/src/joiner/graphql-ast.ts +++ b/packages/orchestration/src/joiner/graphql-ast.ts @@ -37,10 +37,11 @@ class GraphQLParser { } private parseValueNode(valueNode: ValueNode): unknown { + const obj = {} + switch (valueNode.kind) { case Kind.VARIABLE: - const variableName = valueNode.name.value - return this.variables ? this.variables[variableName] : undefined + return this.variables ? this.variables[valueNode.name.value] : undefined case Kind.INT: return parseInt(valueNode.value, 10) case Kind.FLOAT: @@ -55,7 +56,6 @@ class GraphQLParser { case Kind.LIST: return valueNode.values.map((v) => this.parseValueNode(v)) case Kind.OBJECT: - let obj = {} for (const field of valueNode.fields) { obj[field.name.value] = this.parseValueNode(field.value) } diff --git a/packages/orchestration/src/joiner/helpers.ts b/packages/orchestration/src/joiner/helpers.ts index ba8f288d32..32ef4a091a 100644 --- a/packages/orchestration/src/joiner/helpers.ts +++ b/packages/orchestration/src/joiner/helpers.ts @@ -1,53 +1,81 @@ import { RemoteJoinerQuery } from "@medusajs/types" -export function toRemoteJoinerQuery(obj: any): RemoteJoinerQuery { +export function toRemoteJoinerQuery( + obj: any, + variables: Record = {} +): RemoteJoinerQuery { const remoteJoinerQuery: RemoteJoinerQuery = { alias: "", fields: [], expands: [], } - function extractRecursive(obj, parentName = "", isEntryPoint = true) { - for (const key in obj) { + let entryPoint = "" + function extractRecursive(obj: any, parentName = "", isEntryPoint = true) { + for (const key of Object.keys(obj ?? {})) { const value = obj[key] const canExpand = typeof value === "object" && !["fields", "__args", "__directives"].includes(key) - if (canExpand) { - const entityName = parentName ? `${parentName}.${key}` : key - const expandObj: any = { - property: entityName, - } - - const reference = isEntryPoint ? remoteJoinerQuery : expandObj - - if (value.__args) { - reference.args = Object.entries(value.__args).map( - ([name, value]) => ({ - name, - value, - }) - ) - } - - if (value.__directives) { - reference.directives = Object.entries(value.__directives).map( - ([name, value]) => ({ name, value }) - ) - } - - reference.fields = value.fields - - if (isEntryPoint) { - remoteJoinerQuery.alias = key - } else { - remoteJoinerQuery.expands!.push(expandObj) - } - - extractRecursive(value, isEntryPoint ? "" : entityName, false) + if (!canExpand) { + continue } + + const entityName = parentName ? `${parentName}.${key}` : key + const variablesPath = !isEntryPoint + ? `${entryPoint}${parentName ? "." + parentName : parentName}.${key}` + : key + + if (isEntryPoint) { + entryPoint = key + } + + const currentVariables = variables[variablesPath] + + const expandObj: any = { + property: entityName, + } + + const reference = isEntryPoint ? remoteJoinerQuery : expandObj + + if (currentVariables) { + reference.args = Object.entries(currentVariables).map( + ([name, value]) => ({ + name, + value, + }) + ) + } + + if (value.__args) { + reference.args = [ + ...(reference.__args || []), + ...Object.entries(value.__args).map(([name, value]) => ({ + name, + value, + })), + ] + } + + if (value.__directives) { + reference.directives = Object.entries(value.__directives).map( + ([name, value]) => ({ name, value }) + ) + } + + if (value.fields) { + reference.fields = value.fields + } + + if (isEntryPoint) { + remoteJoinerQuery.alias = key + } else { + remoteJoinerQuery.expands!.push(expandObj) + } + + extractRecursive(value, isEntryPoint ? "" : entityName, false) } return remoteJoinerQuery diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts index bac1558fdc..6bac740a7f 100644 --- a/packages/orchestration/src/joiner/remote-joiner.ts +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -53,7 +53,7 @@ export class RemoteJoiner { }, {}) if (expands) { - for (const key in expands) { + for (const key of Object.keys(expands ?? {})) { const expand = expands[key] if (expand) { if (Array.isArray(data[key])) { @@ -146,12 +146,14 @@ export class RemoteJoiner { const isReadOnlyDefinition = service.serviceName === undefined || service.isReadOnlyLink if (!isReadOnlyDefinition) { - if (!service.alias) { - service.alias = [{ name: service.serviceName!.toLowerCase() }] - } else if (!Array.isArray(service.alias)) { + service.alias ??= [] + + if (!Array.isArray(service.alias)) { service.alias = [service.alias] } + service.alias.push({ name: service.serviceName! }) + // handle alias.name as array for (let idx = 0; idx < service.alias.length; idx++) { const alias = service.alias[idx] @@ -173,6 +175,11 @@ export class RemoteJoiner { for (const alias of service.alias) { if (this.serviceConfigCache.has(`alias_${alias.name}}`)) { const defined = this.serviceConfigCache.get(`alias_${alias.name}}`) + + if (service.serviceName === defined?.serviceName) { + continue + } + throw new Error( `Cannot add alias "${alias.name}" for "${service.serviceName}". It is already defined for Service "${defined?.serviceName}".` ) @@ -223,7 +230,9 @@ export class RemoteJoiner { (rel) => rel.isInternalService === true ) - if (isInternalServicePresent) continue + if (isInternalServicePresent) { + continue + } throw new Error(`Service "${serviceName}" was not found`) } @@ -338,7 +347,9 @@ export class RemoteJoiner { relationship ) const isObj = isDefined(response.path) - const resData = isObj ? response.data[response.path!] : response.data + let resData = isObj ? response.data[response.path!] : response.data + + resData = Array.isArray(resData) ? resData : [resData] const filteredDataArray = resData.map((data: any) => RemoteJoiner.filterFields(data, expand.fields, expand.expands) diff --git a/packages/orchestration/src/workflow/global-workflow.ts b/packages/orchestration/src/workflow/global-workflow.ts index 4997506098..dd20ad8376 100644 --- a/packages/orchestration/src/workflow/global-workflow.ts +++ b/packages/orchestration/src/workflow/global-workflow.ts @@ -1,5 +1,5 @@ import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" -import { createContainerLike, createMedusaContainer } from "@medusajs/utils" +import { createMedusaContainer } from "@medusajs/utils" import { asValue } from "awilix" import { @@ -25,7 +25,7 @@ export class GlobalWorkflow extends WorkflowManager { if (!Array.isArray(modulesLoaded) && modulesLoaded) { if (!("cradle" in modulesLoaded)) { - container = createContainerLike(modulesLoaded) + container = createMedusaContainer(modulesLoaded) } else { container = modulesLoaded } diff --git a/packages/orchestration/src/workflow/local-workflow.ts b/packages/orchestration/src/workflow/local-workflow.ts index eb787e8f56..9c929da4ce 100644 --- a/packages/orchestration/src/workflow/local-workflow.ts +++ b/packages/orchestration/src/workflow/local-workflow.ts @@ -1,5 +1,10 @@ import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" -import { createContainerLike, createMedusaContainer } from "@medusajs/utils" +import { + MedusaContext, + MedusaContextType, + MedusaModuleType, + createMedusaContainer, +} from "@medusajs/utils" import { asValue } from "awilix" import { DistributedTransaction, @@ -28,6 +33,7 @@ export class LocalWorkflow { protected customOptions: Partial = {} protected workflow: WorkflowDefinition protected handlers: Map + protected medusaContext?: Context constructor( workflowId: string, @@ -47,9 +53,9 @@ export class LocalWorkflow { if (!Array.isArray(modulesLoaded) && modulesLoaded) { if (!("cradle" in modulesLoaded)) { - container = createContainerLike(modulesLoaded) + container = createMedusaContainer(modulesLoaded) } else { - container = modulesLoaded + container = createMedusaContainer({}, modulesLoaded) // copy container } } else if (Array.isArray(modulesLoaded) && modulesLoaded.length) { container = createMedusaContainer() @@ -60,7 +66,49 @@ export class LocalWorkflow { } } - this.container = container + this.container = this.contextualizedMedusaModules(container) + } + + private contextualizedMedusaModules(container) { + if (!container) { + return createMedusaContainer() + } + + // eslint-disable-next-line + const this_ = this + const originalResolver = container.resolve + container.resolve = function (registrationName, opts) { + const resolved = originalResolver(registrationName, opts) + if (resolved?.constructor?.__type !== MedusaModuleType) { + return resolved + } + + return new Proxy(resolved, { + get: function (target, prop) { + if (typeof target[prop] !== "function") { + return target[prop] + } + + return async (...args) => { + const ctxIndex = + MedusaContext.getIndex(target, prop as string) ?? args.length - 1 + + const hasContext = args[ctxIndex]?.__type === MedusaContextType + if (!hasContext) { + const context = this_.medusaContext + if (context?.__type === MedusaContextType) { + delete context?.manager + delete context?.transactionManager + + args[ctxIndex] = context + } + } + return await target[prop].apply(target, [...args]) + } + }, + }) + } + return container } protected commit() { @@ -265,7 +313,7 @@ export class LocalWorkflow { if (this.flow.hasChanges) { this.commit() } - + this.medusaContext = context const { handler, orchestrator } = this.workflow const transaction = await orchestrator.beginTransaction( @@ -288,6 +336,7 @@ export class LocalWorkflow { } async getRunningTransaction(uniqueTransactionId: string, context?: Context) { + this.medusaContext = context const { handler, orchestrator } = this.workflow const transaction = await orchestrator.retrieveExistingTransaction( @@ -303,6 +352,7 @@ export class LocalWorkflow { context?: Context, subscribe?: DistributedTransactionEvents ) { + this.medusaContext = context const { orchestrator } = this.workflow const transaction = await this.getRunningTransaction( @@ -329,6 +379,7 @@ export class LocalWorkflow { context?: Context, subscribe?: DistributedTransactionEvents ): Promise { + this.medusaContext = context const { handler, orchestrator } = this.workflow const { cleanUpEventListeners } = this.registerEventCallbacks({ @@ -355,6 +406,7 @@ export class LocalWorkflow { context?: Context, subscribe?: DistributedTransactionEvents ): Promise { + this.medusaContext = context const { handler, orchestrator } = this.workflow const { cleanUpEventListeners } = this.registerEventCallbacks({ diff --git a/packages/orchestration/src/workflow/workflow-manager.ts b/packages/orchestration/src/workflow/workflow-manager.ts index 62c648b523..d5822a0e82 100644 --- a/packages/orchestration/src/workflow/workflow-manager.ts +++ b/packages/orchestration/src/workflow/workflow-manager.ts @@ -81,7 +81,7 @@ export class WorkflowManager { const finalFlow = flow instanceof OrchestratorBuilder ? flow.build() : flow if (WorkflowManager.workflows.has(workflowId)) { - function excludeStepUuid(key, value) { + const excludeStepUuid = (key, value) => { return key === "uuid" ? undefined : value } diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 68d00fbc47..c96c6e639e 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -7,7 +7,6 @@ import { FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, - MoneyAmountDTO, PriceSetDTO, PricingContext, PricingFilters, @@ -56,14 +55,14 @@ import { PriceSetService, RuleTypeService, } from "@services" +import { ServiceTypes } from "@types" +import { validatePriceListDates } from "@utils" +import { CreatePriceListRuleValueDTO } from "src/types/services" import { LinkableKeys, entityNameToLinkableKeysMap, joinerConfig, } from "../joiner-config" -import { validatePriceListDates } from "@utils" -import { ServiceTypes } from "@types" -import { CreatePriceListRuleValueDTO } from "src/types/services" type InjectedDependencies = { baseRepository: DAL.RepositoryService pricingRepository: PricingRepositoryService @@ -639,8 +638,8 @@ export default class PricingModuleService< // Price set money amounts let maCursor = 0 - const priceSetMoneyAmountsBulkData: unknown[] = - input.flatMap(({ priceSetId, prices }) => + const priceSetMoneyAmountsBulkData: unknown[] = input.flatMap( + ({ priceSetId, prices }) => prices.map(() => { const ma = createdMoneyAmounts[maCursor] const numberOfRules = Object.entries( @@ -654,7 +653,7 @@ export default class PricingModuleService< rules_count: numberOfRules, } }) - ) + ) const createdPriceSetMoneyAmounts = await this.priceSetMoneyAmountService_.create( priceSetMoneyAmountsBulkData as ServiceTypes.CreatePriceSetMoneyAmountDTO[], @@ -1638,7 +1637,6 @@ export default class PricingModuleService< validatePriceListDates(updatePriceListData) - if (typeof rules === "object") { updatePriceListData.rules_count = Object.keys(rules).length } diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 76f55e6194..6285fd4773 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -54,6 +54,11 @@ import { MedusaError, promiseAll, } from "@medusajs/utils" +import { ProductEventData, ProductEvents } from "../types/services/product" +import { + ProductCategoryEventData, + ProductCategoryEvents, +} from "../types/services/product-category" import { CreateProductOptionValueDTO, UpdateProductOptionValueDTO, @@ -63,11 +68,6 @@ import { joinerConfig, LinkableKeys, } from "./../joiner-config" -import { - ProductCategoryEventData, - ProductCategoryEvents, -} from "../types/services/product-category" -import { ProductEventData, ProductEvents } from "../types/services/product" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -897,7 +897,7 @@ export default class ProductModuleService< @InjectManager("baseRepository_") async create( data: ProductTypes.CreateProductDTO[], - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const products = await this.create_(data, sharedContext) const createdProducts = await this.baseRepository_.serialize< @@ -1319,7 +1319,7 @@ export default class ProductModuleService< >( productIds: string[], { returnLinkableKeys }: SoftDeleteReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise, string[]> | void> { const [products, cascadedEntitiesMap] = await this.softDelete_( productIds, @@ -1369,7 +1369,7 @@ export default class ProductModuleService< >( productIds: string[], { returnLinkableKeys }: RestoreReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise, string[]> | void> { const [_, cascadedEntitiesMap] = await this.restore_( productIds, diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index dfa0510242..158e8201c6 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -939,7 +939,7 @@ export default class PromotionModuleService< >( ids: string | string[], { returnLinkableKeys }: SoftDeleteReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise, string[]> | void> { const idsToDelete = Array.isArray(ids) ? ids : [ids] let [_, cascadedEntitiesMap] = await this.softDelete_( @@ -975,7 +975,7 @@ export default class PromotionModuleService< >( ids: string | string[], { returnLinkableKeys }: RestoreReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise, string[]> | void> { const idsToRestore = Array.isArray(ids) ? ids : [ids] const [_, cascadedEntitiesMap] = await this.restore_( @@ -1376,7 +1376,7 @@ export default class PromotionModuleService< async softDeleteCampaigns( ids: string | string[], { returnLinkableKeys }: SoftDeleteReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise, string[]> | void> { const idsToDelete = Array.isArray(ids) ? ids : [ids] let [_, cascadedEntitiesMap] = await this.softDeleteCampaigns_( @@ -1412,7 +1412,7 @@ export default class PromotionModuleService< >( ids: string | string[], { returnLinkableKeys }: RestoreReturn = {}, - sharedContext: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise, string[]> | void> { const idsToRestore = Array.isArray(ids) ? ids : [ids] const [_, cascadedEntitiesMap] = await this.restoreCampaigns_( diff --git a/packages/stock-location/src/services/stock-location.ts b/packages/stock-location/src/services/stock-location.ts index c0045a185b..38fcb67f85 100644 --- a/packages/stock-location/src/services/stock-location.ts +++ b/packages/stock-location/src/services/stock-location.ts @@ -64,7 +64,7 @@ export default class StockLocationService { async list( selector: FilterableStockLocationProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { const [locations] = await this.listAndCount(selector, config, context) return locations @@ -80,7 +80,7 @@ export default class StockLocationService { async listAndCount( selector: FilterableStockLocationProps = {}, config: FindConfig = { relations: [], skip: 0, take: 10 }, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise<[StockLocation[], number]> { const manager = context.transactionManager ?? this.manager_ const locationRepo = manager.getRepository(StockLocation) @@ -119,7 +119,7 @@ export default class StockLocationService { async retrieve( stockLocationId: string, config: FindConfig = {}, - context: SharedContext = {} + @MedusaContext() context: SharedContext = {} ): Promise { if (!isDefined(stockLocationId)) { throw new MedusaError( diff --git a/packages/types/src/shared-context.ts b/packages/types/src/shared-context.ts index 0779729e25..67170989a1 100644 --- a/packages/types/src/shared-context.ts +++ b/packages/types/src/shared-context.ts @@ -22,6 +22,7 @@ export type SharedContext = { * A context used to share resources, such as transaction manager, between the application and the module. */ export type Context = { + __type?: "MedusaContext" /** * An instance of a transaction manager of type `TManager`, which is a typed parameter passed to the context to specify the type of the `transactionManager`. */ @@ -42,4 +43,9 @@ export type Context = { * A string indicating the ID of the current transaction. */ transactionId?: string + + /** + * A string indicating the ID of the current request. + */ + requestId?: string } diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 714e943f44..ea4d185af6 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -18,8 +18,11 @@ import { FilterQuery as MikroFilterQuery, } from "@mikro-orm/core/typings" import { MedusaError, isString } from "../../common" -import { MedusaContext } from "../../decorators" -import { InjectTransactionManager, buildQuery } from "../../modules-sdk" +import { + InjectTransactionManager, + MedusaContext, + buildQuery, +} from "../../modules-sdk" import { getSoftDeletedCascadedEntitiesIdsMappedBy, transactionWrapper, diff --git a/packages/utils/src/dal/repository.ts b/packages/utils/src/dal/repository.ts index 0958676662..f2f3da05a3 100644 --- a/packages/utils/src/dal/repository.ts +++ b/packages/utils/src/dal/repository.ts @@ -1,5 +1,5 @@ import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types" -import { MedusaContext } from "../decorators" +import { MedusaContext } from "../modules-sdk" import { transactionWrapper } from "./utils" class AbstractBase { diff --git a/packages/utils/src/decorators/index.ts b/packages/utils/src/decorators/index.ts index 4ffe523014..dd4e261098 100644 --- a/packages/utils/src/decorators/index.ts +++ b/packages/utils/src/decorators/index.ts @@ -1,2 +1 @@ -export * from "./context-parameter" export * from "./inject-entity-manager" diff --git a/packages/utils/src/decorators/inject-entity-manager.ts b/packages/utils/src/decorators/inject-entity-manager.ts index b94eb52b92..783b0fe0ec 100644 --- a/packages/utils/src/decorators/inject-entity-manager.ts +++ b/packages/utils/src/decorators/inject-entity-manager.ts @@ -1,5 +1,7 @@ import { Context, SharedContext } from "@medusajs/types" +import { MedusaContextType } from "../modules-sdk/decorators" +// @deprecated Use InjectManager instead export function InjectEntityManager( shouldForceTransaction: (target: any) => boolean = () => false, managerProperty: string | false = "manager_" @@ -31,7 +33,7 @@ export function InjectEntityManager( : this[managerProperty] ).transaction( async (transactionManager) => { - args[argIndex] = args[argIndex] ?? {} + args[argIndex] = args[argIndex] ?? { __type: MedusaContextType } args[argIndex].transactionManager = transactionManager return await originalMethod.apply(this, args) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index df223c851e..2d0c33da13 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,13 +4,15 @@ export * from "./common" export * from "./dal" export * from "./decorators" export * from "./event-bus" +export * from "./exceptions" export * from "./feature-flags" export * from "./modules-sdk" +export * from "./orchestration" export * from "./payment" export * from "./pricing" export * from "./product" export * from "./promotion" export * from "./search" export * from "./shipping" -export * from "./orchestration" -export * from "./exceptions" + +export const MedusaModuleType = Symbol.for("MedusaModule") diff --git a/packages/utils/src/modules-sdk/abstract-service-factory.ts b/packages/utils/src/modules-sdk/abstract-service-factory.ts index 47afc4c7eb..64a6531bb3 100644 --- a/packages/utils/src/modules-sdk/abstract-service-factory.ts +++ b/packages/utils/src/modules-sdk/abstract-service-factory.ts @@ -14,7 +14,7 @@ import { shouldForceTransaction, upperCaseFirst, } from "../common" -import { MedusaContext } from "../decorators" +import { MedusaContext } from "../modules-sdk" import { buildQuery } from "./build-query" import { InjectManager, InjectTransactionManager } from "./decorators" diff --git a/packages/utils/src/decorators/context-parameter.ts b/packages/utils/src/modules-sdk/decorators/context-parameter.ts similarity index 54% rename from packages/utils/src/decorators/context-parameter.ts rename to packages/utils/src/modules-sdk/decorators/context-parameter.ts index ffa3afdd99..119243b3d3 100644 --- a/packages/utils/src/decorators/context-parameter.ts +++ b/packages/utils/src/modules-sdk/decorators/context-parameter.ts @@ -8,3 +8,12 @@ export function MedusaContext() { target.MedusaContextIndex_[propertyKey] = parameterIndex } } + +MedusaContext.getIndex = function ( + target: any, + propertyKey: string +): number | undefined { + return target.MedusaContextIndex_?.[propertyKey] +} + +export const MedusaContextType = "MedusaContext" diff --git a/packages/utils/src/modules-sdk/decorators/index.ts b/packages/utils/src/modules-sdk/decorators/index.ts index fbea009476..f3928a0752 100644 --- a/packages/utils/src/modules-sdk/decorators/index.ts +++ b/packages/utils/src/modules-sdk/decorators/index.ts @@ -1,3 +1,4 @@ +export * from "./context-parameter" export * from "./inject-manager" export * from "./inject-shared-context" export * from "./inject-transaction-manager" diff --git a/packages/utils/src/modules-sdk/decorators/inject-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-manager.ts index 8650a61395..0b40acfd1d 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-manager.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-manager.ts @@ -1,4 +1,4 @@ -import { Context, SharedContext } from "@medusajs/types" +import { Context } from "@medusajs/types" export function InjectManager(managerProperty?: string): MethodDecorator { return function ( @@ -16,13 +16,31 @@ export function InjectManager(managerProperty?: string): MethodDecorator { const argIndex = target.MedusaContextIndex_[propertyKey] descriptor.value = function (...args: any[]) { - const context: SharedContext | Context = { ...(args[argIndex] ?? {}) } + const originalContext = args[argIndex] ?? {} + const copiedContext = {} as Context + for (const key in originalContext) { + if (key === "manager" || key === "transactionManager") { + continue + } + + Object.defineProperty(copiedContext, key, { + get: function () { + return originalContext[key] + }, + set: function (value) { + originalContext[key] = value + }, + }) + } + const resourceWithManager = !managerProperty ? this : this[managerProperty] - context.manager = context.manager ?? resourceWithManager.getFreshManager() - args[argIndex] = context + copiedContext.manager ??= resourceWithManager.getFreshManager() + copiedContext.transactionManager ??= originalContext?.transactionManager + + args[argIndex] = copiedContext return originalMethod.apply(this, args) } diff --git a/packages/utils/src/modules-sdk/decorators/inject-shared-context.ts b/packages/utils/src/modules-sdk/decorators/inject-shared-context.ts index 3996e106d3..f1c69ecfd6 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-shared-context.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-shared-context.ts @@ -1,4 +1,5 @@ import { Context, SharedContext } from "@medusajs/types" +import { MedusaContextType } from "./context-parameter" export function InjectSharedContext(): MethodDecorator { return function ( @@ -16,7 +17,9 @@ export function InjectSharedContext(): MethodDecorator { const argIndex = target.MedusaContextIndex_[propertyKey] descriptor.value = function (...args: any[]) { - const context: SharedContext | Context = { ...(args[argIndex] ?? {}) } + const context: SharedContext | Context = { + ...(args[argIndex] ?? { __type: MedusaContextType }), + } args[argIndex] = context return originalMethod.apply(this, args) diff --git a/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts index 30d9ca8550..4387cfb982 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts @@ -1,5 +1,6 @@ -import { Context, SharedContext } from "@medusajs/types" +import { Context } from "@medusajs/types" import { isString } from "../../common" +import { MedusaContextType } from "./context-parameter" export function InjectTransactionManager( shouldForceTransactionOrManagerProperty: @@ -31,7 +32,8 @@ export function InjectTransactionManager( const argIndex = target.MedusaContextIndex_[propertyKey] descriptor.value = async function (...args: any[]) { const shouldForceTransactionRes = shouldForceTransaction(target) - const context: SharedContext | Context = args[argIndex] ?? {} + const context: Context = args[argIndex] ?? {} + const originalContext = args[argIndex] ?? {} if (!shouldForceTransactionRes && context?.transactionManager) { return await originalMethod.apply(this, args) @@ -42,8 +44,27 @@ export function InjectTransactionManager( : this[managerProperty] ).transaction( async (transactionManager) => { - args[argIndex] = { ...(args[argIndex] ?? {}) } - args[argIndex].transactionManager = transactionManager + const copiedContext = {} as Context + for (const key in originalContext) { + if (key === "manager" || key === "transactionManager") { + continue + } + + Object.defineProperty(copiedContext, key, { + get: function () { + return originalContext[key] + }, + set: function (value) { + originalContext[key] = value + }, + }) + } + + copiedContext.transactionManager ??= transactionManager + copiedContext.manager ??= originalContext?.manager + copiedContext.__type ??= MedusaContextType + + args[argIndex] = copiedContext return await originalMethod.apply(this, args) }, diff --git a/packages/workflows-sdk/src/helper/workflow-export.ts b/packages/workflows-sdk/src/helper/workflow-export.ts index 04797fd3fa..5a104a2827 100644 --- a/packages/workflows-sdk/src/helper/workflow-export.ts +++ b/packages/workflows-sdk/src/helper/workflow-export.ts @@ -9,6 +9,7 @@ import { import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" import { MedusaModule } from "@medusajs/modules-sdk" +import { MedusaContextType } from "@medusajs/utils" import { EOL } from "os" import { ulid } from "ulid" import { MedusaWorkflow } from "../medusa-workflow" @@ -159,7 +160,7 @@ function createContextualWorkflowRunner< ) } - const flow = new LocalWorkflow(workflowId, container) + const flow = new LocalWorkflow(workflowId, container!) const originalRun = flow.run.bind(flow) const originalRegisterStepSuccess = flow.registerStepSuccess.bind(flow) @@ -197,7 +198,13 @@ function createContextualWorkflowRunner< } const newRun = async ( - { input, context, throwOnError, resultFrom, events }: FlowRunOptions = { + { + input, + context: outerContext, + throwOnError, + resultFrom, + events, + }: FlowRunOptions = { throwOnError: true, resultFrom: defaultResult, } @@ -205,6 +212,13 @@ function createContextualWorkflowRunner< resultFrom ??= defaultResult throwOnError ??= true + const context = { + ...outerContext, + __type: MedusaContextType, + } + + context.transactionId ??= ulid() + if (typeof dataPreparation === "function") { try { const copyInput = input ? JSON.parse(JSON.stringify(input)) : input @@ -224,7 +238,7 @@ function createContextualWorkflowRunner< return await originalExecution( originalRun, { throwOnError, resultFrom }, - context?.transactionId ?? ulid(), + context.transactionId, input, context, events @@ -236,7 +250,7 @@ function createContextualWorkflowRunner< { response, idempotencyKey, - context, + context: outerContext, throwOnError, resultFrom, events, @@ -249,6 +263,13 @@ function createContextualWorkflowRunner< resultFrom ??= defaultResult throwOnError ??= true + const [, transactionId] = idempotencyKey.split(":") + const context = { + ...outerContext, + transactionId, + __type: MedusaContextType, + } + return await originalExecution( originalRegisterStepSuccess, { throwOnError, resultFrom }, @@ -264,7 +285,7 @@ function createContextualWorkflowRunner< { response, idempotencyKey, - context, + context: outerContext, throwOnError, resultFrom, events, @@ -277,6 +298,13 @@ function createContextualWorkflowRunner< resultFrom ??= defaultResult throwOnError ??= true + const [, transactionId] = idempotencyKey.split(":") + const context = { + ...outerContext, + transactionId, + __type: MedusaContextType, + } + return await originalExecution( originalRegisterStepFailure, { throwOnError, resultFrom },