diff --git a/.changeset/young-teachers-sit.md b/.changeset/young-teachers-sit.md new file mode 100644 index 0000000000..3850f081a4 --- /dev/null +++ b/.changeset/young-teachers-sit.md @@ -0,0 +1,8 @@ +--- +"@medusajs/orchestration": minor +"@medusajs/link-modules": minor +"@medusajs/modules-sdk": minor +"@medusajs/medusa": minor +--- + +Use MedusaApp on core and initial JoinerConfig for internal services diff --git a/integration-tests/development/server.js b/integration-tests/development/server.js index a4dc7e494e..9c03ab8b8e 100644 --- a/integration-tests/development/server.js +++ b/integration-tests/development/server.js @@ -2,6 +2,7 @@ const path = require("path") const express = require("express") const importFrom = require("import-from") const chokidar = require("chokidar") +const { WorkflowManager } = require("@medusajs/orchestration") process.env.DEV_MODE = !!process[Symbol.for("ts-node.register.instance")] process.env.NODE_ENV = process.env.DEV_MODE && "development" @@ -107,6 +108,8 @@ const watchFiles = () => { } } + WorkflowManager.unregisterAll() + await bootstrapApp() IS_RELOADING = false diff --git a/packages/link-modules/src/definitions/product-shipping-profile.ts b/packages/link-modules/src/definitions/product-shipping-profile.ts new file mode 100644 index 0000000000..62ad744d31 --- /dev/null +++ b/packages/link-modules/src/definitions/product-shipping-profile.ts @@ -0,0 +1,52 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" +import { LINKS } from "../links" + +export const ProductShippingProfile: ModuleJoinerConfig = { + serviceName: LINKS.ProductShippingProfile, + isLink: true, + databaseConfig: { + tableName: "product_shipping_profile", + idPrefix: "psprof", + }, + alias: [ + { + name: "product_shipping_profile", + }, + ], + primaryKeys: ["id", "product_id", "profile_id"], + relationships: [ + { + serviceName: Modules.PRODUCT, + primaryKey: "id", + foreignKey: "product_id", + alias: "product", + }, + { + serviceName: "shippingProfileService", + primaryKey: "id", + foreignKey: "profile_id", + alias: "shipping_profile", + }, + ], + extends: [ + { + serviceName: Modules.PRODUCT, + relationship: { + serviceName: LINKS.ProductShippingProfile, + primaryKey: "product_id", + foreignKey: "id", + alias: "shipping_profile", + }, + }, + { + serviceName: "shippingProfileService", + relationship: { + serviceName: LINKS.ProductShippingProfile, + primaryKey: "profile_id", + foreignKey: "id", + alias: "product_link", + }, + }, + ], +} diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index c1745224da..5b92e23cb9 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -14,4 +14,12 @@ export const LINKS = { Modules.PRICING, "money_amount_id" ), + + // Internal services + ProductShippingProfile: composeLinkName( + Modules.PRODUCT, + "variant_id", + "shippingProfileService", + "profile_id" + ), } diff --git a/packages/medusa/src/joiner-config.ts b/packages/medusa/src/joiner-config.ts new file mode 100644 index 0000000000..4872b8a184 --- /dev/null +++ b/packages/medusa/src/joiner-config.ts @@ -0,0 +1,77 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" + +export const joinerConfig: ModuleJoinerConfig[] = [ + { + serviceName: "cartService", + primaryKeys: ["id"], + linkableKeys: ["cart_id"], + alias: [ + { + name: "cart", + }, + ], + relationships: [ + { + serviceName: Modules.PRODUCT, + primaryKey: "id", + foreignKey: "variant_id", + alias: "variant", + args: { + methodSuffix: "Variants", + }, + }, + { + serviceName: "regionService", + primaryKey: "id", + foreignKey: "region_id", + alias: "region", + }, + { + serviceName: "customerService", + primaryKey: "id", + foreignKey: "customer_id", + alias: "customer", + }, + ], + }, + { + serviceName: "shippingProfileService", + primaryKeys: ["id"], + linkableKeys: ["profile_id"], + alias: [ + { + name: "shipping_profile", + }, + { + name: "shipping_profiles", + }, + ], + }, + { + serviceName: "regionService", + primaryKeys: ["id"], + linkableKeys: ["region_id"], + alias: [ + { + name: "region", + }, + { + name: "regions", + }, + ], + }, + { + serviceName: "customerService", + primaryKeys: ["id"], + linkableKeys: ["customer_id"], + alias: [ + { + name: "customer", + }, + { + name: "customers", + }, + ], + }, +] diff --git a/packages/medusa/src/loaders/feature-flags/isolate-product-domain.ts b/packages/medusa/src/loaders/feature-flags/isolate-product-domain.ts new file mode 100644 index 0000000000..9202017113 --- /dev/null +++ b/packages/medusa/src/loaders/feature-flags/isolate-product-domain.ts @@ -0,0 +1,10 @@ +import { FlagSettings } from "../../types/feature-flags" + +const IsolateProductDomainFeatureFlag: FlagSettings = { + key: "isolate_product_domain", + default_val: false, + env_key: "MEDUSA_FF_ISOLATE_PRODUCT_DOMAIN", + description: "[WIP] Isolate product domain dependencies from the core", +} + +export default IsolateProductDomainFeatureFlag diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 80723ca3b5..95205dbefe 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -1,20 +1,28 @@ +import { MedusaApp, moduleLoader, registerModules } from "@medusajs/modules-sdk" +import { ContainerRegistrationKeys } from "@medusajs/utils" import { asValue } from "awilix" import { Express, NextFunction, Request, Response } from "express" +import { createMedusaContainer } from "medusa-core-utils" import { track } from "medusa-telemetry" import { EOL } from "os" import "reflect-metadata" import requestIp from "request-ip" import { Connection } from "typeorm" +import { joinerConfig } from "../joiner-config" +import modulesConfig from "../modules-config" import { MedusaContainer } from "../types/global" +import { remoteQueryFetchData } from "../utils" import apiLoader from "./api" import loadConfig from "./config" import databaseLoader, { dataSource } from "./database" import defaultsLoader from "./defaults" import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" +import IsolateProductDomainFeatureFlag from "./feature-flags/isolate-product-domain" import Logger from "./logger" import modelsLoader from "./models" import passportLoader from "./passport" +import pgConnectionLoader from "./pg-connection" import pluginsLoader, { registerPluginModels } from "./plugins" import redisLoader from "./redis" import repositoriesLoader from "./repositories" @@ -23,11 +31,6 @@ import servicesLoader from "./services" import strategiesLoader from "./strategies" import subscribersLoader from "./subscribers" -import { moduleLoader, registerModules } from "@medusajs/modules-sdk" -import { createMedusaContainer } from "medusa-core-utils" -import pgConnectionLoader from "./pg-connection" -import { ContainerRegistrationKeys } from "@medusajs/utils" - type Options = { directory: string expressApp: Express @@ -182,5 +185,20 @@ export default async ({ Logger.success(searchActivity, "Indexing event emitted") || {} track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) + if (featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)) { + const { query } = await MedusaApp({ + modulesConfig, + servicesConfig: joinerConfig, + remoteFetchData: remoteQueryFetchData(container), + injectedDependencies: { + [ContainerRegistrationKeys.PG_CONNECTION]: container.resolve( + ContainerRegistrationKeys.PG_CONNECTION + ), + }, + }) + + container.register("remoteQuery", asValue(query)) + } + return { container, dbConnection, app: expressApp } } diff --git a/packages/medusa/src/modules-config.ts b/packages/medusa/src/modules-config.ts new file mode 100644 index 0000000000..b9ca369ee5 --- /dev/null +++ b/packages/medusa/src/modules-config.ts @@ -0,0 +1,9 @@ +import { MedusaModuleConfig, Modules } from "@medusajs/modules-sdk" + +const modules: MedusaModuleConfig = [ + { + module: Modules.PRODUCT, + }, +] + +export default modules diff --git a/packages/medusa/src/utils/index.ts b/packages/medusa/src/utils/index.ts index dab6fa8d32..8a305a854f 100644 --- a/packages/medusa/src/utils/index.ts +++ b/packages/medusa/src/utils/index.ts @@ -11,8 +11,9 @@ export * from "./is-object" export * from "./is-string" export * from "./omit-deep" export * from "./product-category" +export * from "./remote-query-fetch-data" export * from "./remove-undefined-properties" export * from "./set-metadata" export * from "./validate-id" -export * from "./validators/is-type" export { registerOverriddenValidators, validator } from "./validator" +export * from "./validators/is-type" diff --git a/packages/medusa/src/utils/remote-query-fetch-data.ts b/packages/medusa/src/utils/remote-query-fetch-data.ts new file mode 100644 index 0000000000..166738f82d --- /dev/null +++ b/packages/medusa/src/utils/remote-query-fetch-data.ts @@ -0,0 +1,53 @@ +import { MedusaModule, RemoteQuery } from "@medusajs/modules-sdk" +import { MedusaContainer } from "@medusajs/types" + +export function remoteQueryFetchData(container: MedusaContainer) { + return async (expand, keyField, ids, relationship) => { + const serviceConfig = expand.serviceConfig + const service = container.resolve(serviceConfig.serviceName, { + allowUnregistered: true, + }) + + if (MedusaModule.isInstalled(serviceConfig.serviceName)) { + return + } + + const filters = {} + const options = { + ...RemoteQuery.getAllFieldsAndRelations(expand), + } + + if (ids) { + filters[keyField] = ids + } + + const hasPagination = Object.keys(options).some((key) => + ["skip"].includes(key) + ) + + let methodName = hasPagination ? "listAndCount" : "list" + + if (relationship?.args?.methodSuffix) { + methodName += relationship.args.methodSuffix + } else if (serviceConfig?.args?.methodSuffix) { + methodName += serviceConfig.args.methodSuffix + } + + const result = await service[methodName](filters, options) + + if (hasPagination) { + const [data, count] = result + return { + data: { + rows: data, + metadata: {}, + }, + path: "rows", + } + } + + return { + data: result, + } as any + } +} diff --git a/packages/modules-sdk/src/medusa-app.ts b/packages/modules-sdk/src/medusa-app.ts index ccbde5a4fd..1d636ed910 100644 --- a/packages/modules-sdk/src/medusa-app.ts +++ b/packages/modules-sdk/src/medusa-app.ts @@ -36,20 +36,30 @@ export type SharedResources = { } } -export async function MedusaApp({ - sharedResourcesConfig, - modulesConfigPath, - modulesConfig, - linkModules, - remoteFetchData, -}: { - sharedResourcesConfig?: SharedResources - loadedModules?: LoadedModule[] - modulesConfigPath?: string - modulesConfig?: MedusaModuleConfig - linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[] - remoteFetchData?: RemoteFetchDataCallback -} = {}): Promise<{ +export async function MedusaApp( + { + sharedResourcesConfig, + servicesConfig, + modulesConfigPath, + modulesConfigFileName, + modulesConfig, + linkModules, + remoteFetchData, + injectedDependencies, + }: { + sharedResourcesConfig?: SharedResources + loadedModules?: LoadedModule[] + servicesConfig?: ModuleJoinerConfig[] + modulesConfigPath?: string + modulesConfigFileName?: string + modulesConfig?: MedusaModuleConfig + linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[] + remoteFetchData?: RemoteFetchDataCallback + injectedDependencies?: any + } = { + injectedDependencies: {}, + } +): Promise<{ modules: Record link: RemoteLink | undefined query: ( @@ -59,10 +69,12 @@ export async function MedusaApp({ }> { const modules: MedusaModuleConfig = modulesConfig ?? - (await import(process.cwd() + (modulesConfigPath ?? "/modules-config"))) - .default - - const injectedDependencies: any = {} + ( + await import( + modulesConfigPath ?? + process.cwd() + (modulesConfigFileName ?? "/modules-config") + ) + ).default const dbData = ModulesSdkUtils.loadDatabaseConfig( "medusa", @@ -167,7 +179,10 @@ export async function MedusaApp({ console.warn("Error initializing link modules.", err) } - const remoteQuery = new RemoteQuery(undefined, remoteFetchData) + const remoteQuery = new RemoteQuery({ + servicesConfig, + customRemoteFetchData: remoteFetchData, + }) query = async ( query: string | RemoteJoinerQuery, variables?: Record diff --git a/packages/modules-sdk/src/remote-query.ts b/packages/modules-sdk/src/remote-query.ts index 43f48879be..cf1ef948db 100644 --- a/packages/modules-sdk/src/remote-query.ts +++ b/packages/modules-sdk/src/remote-query.ts @@ -17,8 +17,17 @@ export class RemoteQuery { private customRemoteFetchData?: RemoteFetchDataCallback constructor( - modulesLoaded?: LoadedModule[], - customRemoteFetchData?: RemoteFetchDataCallback + { + modulesLoaded, + customRemoteFetchData, + servicesConfig, + }: { + modulesLoaded?: LoadedModule[] + customRemoteFetchData?: RemoteFetchDataCallback + servicesConfig?: ModuleJoinerConfig[] + } = { + servicesConfig: [], + } ) { if (!modulesLoaded?.length) { modulesLoaded = MedusaModule.getLoadedModules().map( @@ -26,7 +35,6 @@ export class RemoteQuery { ) } - const servicesConfig: ModuleJoinerConfig[] = [] for (const mod of modulesLoaded) { if (!mod.__definition.isQueryable) { continue @@ -41,7 +49,7 @@ export class RemoteQuery { } this.modulesMap.set(serviceName, mod) - servicesConfig.push(mod.__joinerConfig) + servicesConfig!.push(mod.__joinerConfig) } this.customRemoteFetchData = customRemoteFetchData @@ -65,7 +73,7 @@ export class RemoteQuery { this.remoteJoiner.setFetchDataCallback(remoteFetchData) } - private static getAllFieldsAndRelations( + public static getAllFieldsAndRelations( data: any, prefix = "", args: Record = {} diff --git a/packages/orchestration/src/__tests__/joiner/remote-joiner.ts b/packages/orchestration/src/__tests__/joiner/remote-joiner.ts index 798a6d4b57..b36ef4cc48 100644 --- a/packages/orchestration/src/__tests__/joiner/remote-joiner.ts +++ b/packages/orchestration/src/__tests__/joiner/remote-joiner.ts @@ -317,13 +317,13 @@ describe("RemoteJoiner", () => { expect(serviceMock.productService).toHaveBeenCalledTimes(2) expect(serviceMock.productService).toHaveBeenNthCalledWith(1, { - fields: ["name", "id"], - options: { id: expect.arrayContaining([103, 102]) }, - }) - - expect(serviceMock.productService).toHaveBeenNthCalledWith(2, { fields: ["handler", "id"], options: { id: expect.arrayContaining([101, 103]) }, }) + + expect(serviceMock.productService).toHaveBeenNthCalledWith(2, { + fields: ["name", "id"], + options: { id: expect.arrayContaining([103, 102]) }, + }) }) }) diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts index 42dca68421..038e1e4781 100644 --- a/packages/orchestration/src/joiner/remote-joiner.ts +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -290,46 +290,38 @@ export class RemoteJoiner { if (!parsedExpands) { return } - - const stack: [ - any[], - Partial, - Map, - string, - Set - ][] = [[items, query, parsedExpands, BASE_PATH, new Set()]] + const resolvedPaths = new Set() + const stack: [any[], Partial, string][] = [ + [items, query, BASE_PATH], + ] while (stack.length > 0) { - const [ - currentItems, - currentQuery, - currentParsedExpands, - basePath, - resolvedPaths, - ] = stack.pop()! + const [currentItems, currentQuery, basePath] = stack.pop()! - for (const [expandedPath, expand] of currentParsedExpands.entries()) { - const isImmediateChildPath = basePath === expand.parent + for (const [expandedPath, expand] of parsedExpands.entries()) { + const isParentPath = expandedPath.startsWith(basePath) - if (!isImmediateChildPath || resolvedPaths.has(expandedPath)) { + if (!isParentPath || resolvedPaths.has(expandedPath)) { continue } resolvedPaths.add(expandedPath) - const property = expand.property || "" - const parentServiceConfig = this.getServiceConfig( - currentQuery.service, - currentQuery.alias - ) - await this.expandProperty(currentItems, parentServiceConfig!, expand) + let curItems = currentItems + const expandedPathLevels = expandedPath.split(".") + for (let idx = 1; idx < expandedPathLevels.length - 1; idx++) { + curItems = RemoteJoiner.getNestedItems( + curItems, + expandedPathLevels[idx] + ) + } + await this.expandProperty(curItems, expand.parentConfig!, expand) const nestedItems = RemoteJoiner.getNestedItems(currentItems, property) if (nestedItems.length > 0) { const relationship = expand.serviceConfig - let nextProp = currentQuery if (relationship) { const relQuery = { @@ -337,14 +329,7 @@ export class RemoteJoiner { } nextProp = relQuery } - - stack.push([ - nestedItems, - nextProp, - currentParsedExpands, - expandedPath, - new Set(), - ]) + stack.push([nestedItems, nextProp, expandedPath]) } } } @@ -538,12 +523,14 @@ export class RemoteJoiner { } if (!parsedExpands.has(fullPath)) { + const parentPath = [BASE_PATH, ...currentPath].join(".") parsedExpands.set(fullPath, { property: prop, serviceConfig: currentServiceConfig, fields, args, - parent: [BASE_PATH, ...currentPath].join("."), + parent: parentPath, + parentConfig: parsedExpands.get(parentPath).serviceConfig, }) } @@ -556,53 +543,41 @@ export class RemoteJoiner { private groupExpands( parsedExpands: Map ): Map { - const sortedParsedExpands = new Map( - Array.from(parsedExpands.entries()).sort() - ) - - const mergedExpands = new Map( - sortedParsedExpands - ) + const mergedExpands = new Map(parsedExpands) const mergedPaths = new Map() - let lastServiceName = "" - - for (const [path, expand] of sortedParsedExpands.entries()) { + for (const [path, expand] of mergedExpands.entries()) { const currentServiceName = expand.serviceConfig.serviceName - let parentPath = expand.parent - // Check if the parentPath was merged before - while (mergedPaths.has(parentPath)) { - parentPath = mergedPaths.get(parentPath)! - } - - const canMerge = currentServiceName === lastServiceName - - if (mergedExpands.has(parentPath) && canMerge) { - const parentExpand = mergedExpands.get(parentPath)! - - if (parentExpand.serviceConfig.serviceName === currentServiceName) { - const nestedKeys = path.split(".").slice(parentPath.split(".").length) - - let targetExpand: any = parentExpand - - for (let key of nestedKeys) { - if (!targetExpand.expands) { - targetExpand.expands = {} - } - if (!targetExpand.expands[key]) { - targetExpand.expands[key] = {} as any - } - targetExpand = targetExpand.expands[key] - } - - targetExpand.fields = expand.fields - targetExpand.args = expand.args - mergedPaths.set(path, parentPath) + while (parentPath) { + const parentExpand = mergedExpands.get(parentPath) + if ( + !parentExpand || + parentExpand.serviceConfig.serviceName !== currentServiceName + ) { + break } - } else { - lastServiceName = currentServiceName + + // Merge the current expand into its parent + const nestedKeys = path.split(".").slice(parentPath.split(".").length) + let targetExpand = parentExpand as Omit< + RemoteExpandProperty, + "expands" + > & { expands?: {} } + + for (const key of nestedKeys) { + targetExpand.expands ??= {} + targetExpand = targetExpand.expands[key] ??= {} + } + + targetExpand.fields = expand.fields + targetExpand.args = expand.args + + mergedExpands.delete(path) + mergedPaths.set(path, parentPath) + + parentPath = parentExpand.parent } } diff --git a/packages/types/src/joiner/index.ts b/packages/types/src/joiner/index.ts index ce505d1f89..2a8d2fe55e 100644 --- a/packages/types/src/joiner/index.ts +++ b/packages/types/src/joiner/index.ts @@ -60,6 +60,7 @@ export interface RemoteNestedExpands { export interface RemoteExpandProperty { property: string parent: string + parentConfig?: JoinerServiceConfig serviceConfig: JoinerServiceConfig fields: string[] args?: JoinerArgument[]