diff --git a/.changeset/seven-pianos-camp.md b/.changeset/seven-pianos-camp.md new file mode 100644 index 0000000000..8071298078 --- /dev/null +++ b/.changeset/seven-pianos-camp.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Expose Module Resolution API diff --git a/integration-tests/api/__tests__/admin/__snapshots__/store.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/store.js.snap index 9ff29ca7db..6510ccfb6f 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/store.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/store.js.snap @@ -131,6 +131,7 @@ Object { "id": Any, "invite_link_template": null, "metadata": null, + "modules": Any, "name": "Medusa Store", "payment_link_template": null, "payment_providers": Array [ diff --git a/integration-tests/api/__tests__/admin/store.js b/integration-tests/api/__tests__/admin/store.js index 2b062d33d6..3b346f127f 100644 --- a/integration-tests/api/__tests__/admin/store.js +++ b/integration-tests/api/__tests__/admin/store.js @@ -52,6 +52,7 @@ describe("/admin/store", () => { code: "usd", }, ], + modules: expect.any(Array), feature_flags: expect.any(Array), default_currency_code: "usd", created_at: expect.any(String), diff --git a/packages/medusa-telemetry/src/index.js b/packages/medusa-telemetry/src/index.js index 761137885a..a7f81f3e92 100644 --- a/packages/medusa-telemetry/src/index.js +++ b/packages/medusa-telemetry/src/index.js @@ -21,4 +21,15 @@ export function trackFeatureFlag(flag) { telemeter.trackFeatureFlag(flag) } +export function trackInstallation(installation, type) { + switch (type) { + case `plugin`: + telemeter.trackPlugin(installation) + break + case `module`: + telemeter.trackModule(installation) + break + } +} + export { default as Telemeter } from "./telemeter" diff --git a/packages/medusa-telemetry/src/telemeter.js b/packages/medusa-telemetry/src/telemeter.js index 8da93594ac..32ccec4adf 100644 --- a/packages/medusa-telemetry/src/telemeter.js +++ b/packages/medusa-telemetry/src/telemeter.js @@ -26,6 +26,8 @@ class Telemeter { this.queueCount_ = this.store_.getQueueCount() this.featureFlags_ = new Set() + this.modules_ = new Set() + this.plugins_ = [] } getMachineId() { @@ -133,6 +135,8 @@ class Telemeter { medusa_version: this.getMedusaVersion(), cli_version: this.getCliVersion(), feature_flags: Array.from(this.featureFlags_), + modules: Array.from(this.modules_), + plugins: this.plugins_, } this.store_.addEvent(event) @@ -161,6 +165,18 @@ class Telemeter { this.featureFlags_.add(flag) } } + + trackModule(module) { + if (module) { + this.modules_.add(module) + } + } + + trackPlugin(plugin) { + if (plugin) { + this.plugins_.push(plugin) + } + } } export default Telemeter diff --git a/packages/medusa/src/api/middlewares/error-handler.ts b/packages/medusa/src/api/middlewares/error-handler.ts index 36ab5b639f..266c0800e9 100644 --- a/packages/medusa/src/api/middlewares/error-handler.ts +++ b/packages/medusa/src/api/middlewares/error-handler.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from "express" import { MedusaError } from "medusa-core-utils" import { Logger } from "../../types/global" -import { formatException } from "../../utils"; +import { formatException } from "../../utils" const QUERY_RUNNER_RELEASED = "QueryRunnerAlreadyReleasedError" const TRANSACTION_STARTED = "TransactionAlreadyStartedError" diff --git a/packages/medusa/src/api/routes/admin/store/get-store.ts b/packages/medusa/src/api/routes/admin/store/get-store.ts index b8139571fa..319fc72e3e 100644 --- a/packages/medusa/src/api/routes/admin/store/get-store.ts +++ b/packages/medusa/src/api/routes/admin/store/get-store.ts @@ -5,7 +5,9 @@ import { StoreService, } from "../../../../services" import { FeatureFlagsResponse } from "../../../../types/feature-flags" +import { ModulesResponse } from "../../../../types/modules" import { FlagRouter } from "../../../../utils/flag-router" +import { ModulesHelper } from "../../../../utils/module-helper" /** * @oas [get] /store @@ -60,6 +62,7 @@ export default async (req, res) => { const storeService: StoreService = req.scope.resolve("storeService") const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + const modulesHelper: ModulesHelper = req.scope.resolve("modulesHelper") const paymentProviderService: PaymentProviderService = req.scope.resolve( "paymentProviderService" @@ -78,9 +81,11 @@ export default async (req, res) => { payment_providers: PaymentProvider[] fulfillment_providers: FulfillmentProvider[] feature_flags: FeatureFlagsResponse + modules: ModulesResponse } data.feature_flags = featureFlagRouter.listFlags() + data.modules = modulesHelper.modules const paymentProviders = await paymentProviderService.list() const fulfillmentProviders = await fulfillmentProviderService.list() diff --git a/packages/medusa/src/helpers/test-request.js b/packages/medusa/src/helpers/test-request.js index fdbc5ae98d..41e9df2c37 100644 --- a/packages/medusa/src/helpers/test-request.js +++ b/packages/medusa/src/helpers/test-request.js @@ -2,15 +2,15 @@ import { asValue, createContainer } from "awilix" import express from "express" import jwt from "jsonwebtoken" import { MockManager } from "medusa-test-utils" +import querystring from "querystring" import "reflect-metadata" import supertest from "supertest" -import querystring from "querystring" import apiLoader from "../loaders/api" -import passportLoader from "../loaders/passport" import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags" +import { moduleHelper } from "../loaders/module" +import passportLoader from "../loaders/passport" import servicesLoader from "../loaders/services" import strategiesLoader from "../loaders/strategies" -import logger from "../loaders/logger" const adminSessionOpts = { cookieName: "session", @@ -38,6 +38,7 @@ const testApp = express() const container = createContainer() container.register("featureFlagRouter", asValue(featureFlagRouter)) +container.register("modulesHelper", asValue(moduleHelper)) container.register("configModule", asValue(config)) container.register({ logger: asValue({ diff --git a/packages/medusa/src/loaders/config.ts b/packages/medusa/src/loaders/config.ts index 015ebd5532..36352de6a3 100644 --- a/packages/medusa/src/loaders/config.ts +++ b/packages/medusa/src/loaders/config.ts @@ -1,6 +1,7 @@ +import { getConfigFile } from "medusa-core-utils" import { ConfigModule } from "../types/global" -import { getConfigFile } from "medusa-core-utils/dist" import logger from "./logger" +import registerModuleDefinitions from "./module-definitions" const isProduction = ["production", "prod"].includes(process.env.NODE_ENV || "") @@ -67,12 +68,16 @@ export default (rootDirectory: string): ConfigModule => { ) } + const moduleResolutions = registerModuleDefinitions(configModule) + return { projectConfig: { jwt_secret: jwt_secret ?? "supersecret", cookie_secret: cookie_secret ?? "supersecret", ...configModule?.projectConfig, }, + modules: configModule.modules ?? {}, + moduleResolutions, featureFlags: configModule?.featureFlags ?? {}, plugins: configModule?.plugins ?? [], } diff --git a/packages/medusa/src/loaders/feature-flags/index.ts b/packages/medusa/src/loaders/feature-flags/index.ts index 3ec3d9c9ce..1bb6abffda 100644 --- a/packages/medusa/src/loaders/feature-flags/index.ts +++ b/packages/medusa/src/loaders/feature-flags/index.ts @@ -4,8 +4,8 @@ import path from "path" import { trackFeatureFlag } from "medusa-telemetry" import { FlagSettings } from "../../types/feature-flags" import { Logger } from "../../types/global" -import { FlagRouter } from "../../utils/flag-router" import { isDefined } from "../../utils" +import { FlagRouter } from "../../utils/flag-router" const isTruthy = (val: string | boolean | undefined): boolean => { if (typeof val === "string") { diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index e3bac0b29d..6ae03b6431 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -8,6 +8,7 @@ import { import { ClassOrFunctionReturning } from "awilix/lib/container" import { Express, NextFunction, Request, Response } from "express" import { track } from "medusa-telemetry" +import { EOL } from "os" import "reflect-metadata" import requestIp from "request-ip" import { Connection, getManager } from "typeorm" @@ -20,6 +21,7 @@ import expressLoader from "./express" import featureFlagsLoader from "./feature-flags" import Logger from "./logger" import modelsLoader from "./models" +import moduleLoader from "./module" import passportLoader from "./passport" import pluginsLoader, { registerPluginModels } from "./plugins" import redisLoader from "./redis" @@ -91,13 +93,13 @@ export default async ({ await redisLoader({ container, configModule, logger: Logger }) - const modelsActivity = Logger.activity("Initializing models") + const modelsActivity = Logger.activity(`Initializing models${EOL}`) track("MODELS_INIT_STARTED") modelsLoader({ container }) const mAct = Logger.success(modelsActivity, "Models initialized") || {} track("MODELS_INIT_COMPLETED", { duration: mAct.duration }) - const pmActivity = Logger.activity("Initializing plugin models") + const pmActivity = Logger.activity(`Initializing plugin models${EOL}`) track("PLUGIN_MODELS_INIT_STARTED") await registerPluginModels({ rootDirectory, @@ -107,13 +109,13 @@ export default async ({ const pmAct = Logger.success(pmActivity, "Plugin models initialized") || {} track("PLUGIN_MODELS_INIT_COMPLETED", { duration: pmAct.duration }) - const repoActivity = Logger.activity("Initializing repositories") + const repoActivity = Logger.activity(`Initializing repositories${EOL}`) track("REPOSITORIES_INIT_STARTED") repositoriesLoader({ container }) const rAct = Logger.success(repoActivity, "Repositories initialized") || {} track("REPOSITORIES_INIT_COMPLETED", { duration: rAct.duration }) - const dbActivity = Logger.activity("Initializing database") + const dbActivity = Logger.activity(`Initializing database${EOL}`) track("DATABASE_INIT_STARTED") const dbConnection = await databaseLoader({ container, @@ -124,19 +126,19 @@ export default async ({ container.register({ manager: asValue(dbConnection.manager) }) - const stratActivity = Logger.activity("Initializing strategies") + const stratActivity = Logger.activity(`Initializing strategies${EOL}`) track("STRATEGIES_INIT_STARTED") strategiesLoader({ container, configModule, isTest }) const stratAct = Logger.success(stratActivity, "Strategies initialized") || {} track("STRATEGIES_INIT_COMPLETED", { duration: stratAct.duration }) - const servicesActivity = Logger.activity("Initializing services") + const servicesActivity = Logger.activity(`Initializing services${EOL}`) track("SERVICES_INIT_STARTED") servicesLoader({ container, configModule, isTest }) const servAct = Logger.success(servicesActivity, "Services initialized") || {} track("SERVICES_INIT_COMPLETED", { duration: servAct.duration }) - const expActivity = Logger.activity("Initializing express") + const expActivity = Logger.activity(`Initializing express${EOL}`) track("EXPRESS_INIT_STARTED") await expressLoader({ app: expressApp, configModule }) await passportLoader({ app: expressApp, container, configModule }) @@ -150,7 +152,7 @@ export default async ({ next() }) - const pluginsActivity = Logger.activity("Initializing plugins") + const pluginsActivity = Logger.activity(`Initializing plugins${EOL}`) track("PLUGINS_INIT_STARTED") await pluginsLoader({ container, @@ -162,31 +164,39 @@ export default async ({ const pAct = Logger.success(pluginsActivity, "Plugins intialized") || {} track("PLUGINS_INIT_COMPLETED", { duration: pAct.duration }) - const subActivity = Logger.activity("Initializing subscribers") + const subActivity = Logger.activity(`Initializing subscribers${EOL}`) track("SUBSCRIBERS_INIT_STARTED") subscribersLoader({ container }) const subAct = Logger.success(subActivity, "Subscribers initialized") || {} track("SUBSCRIBERS_INIT_COMPLETED", { duration: subAct.duration }) - const apiActivity = Logger.activity("Initializing API") + const apiActivity = Logger.activity(`Initializing API${EOL}`) track("API_INIT_STARTED") await apiLoader({ container, app: expressApp, configModule }) const apiAct = Logger.success(apiActivity, "API initialized") || {} track("API_INIT_COMPLETED", { duration: apiAct.duration }) - const defaultsActivity = Logger.activity("Initializing defaults") + const defaultsActivity = Logger.activity(`Initializing defaults${EOL}`) track("DEFAULTS_INIT_STARTED") await defaultsLoader({ container }) const dAct = Logger.success(defaultsActivity, "Defaults initialized") || {} track("DEFAULTS_INIT_COMPLETED", { duration: dAct.duration }) - const searchActivity = Logger.activity("Initializing search engine indexing") + const searchActivity = Logger.activity( + `Initializing search engine indexing${EOL}` + ) track("SEARCH_ENGINE_INDEXING_STARTED") await searchIndexLoader({ container }) const searchAct = Logger.success(searchActivity, "Indexing event emitted") || {} track("SEARCH_ENGINE_INDEXING_COMPLETED", { duration: searchAct.duration }) + const modulesActivity = Logger.activity(`Initializing modules${EOL}`) + track("MODULES_INIT_STARTED") + await moduleLoader({ container, configModule, logger: Logger }) + const modAct = Logger.success(modulesActivity, "Modules initialized") || {} + track("MODULES_INIT_COMPLETED", { duration: modAct.duration }) + return { container, dbConnection, app: expressApp } } diff --git a/packages/medusa/src/loaders/module-definitions/definitions.ts b/packages/medusa/src/loaders/module-definitions/definitions.ts new file mode 100644 index 0000000000..8962c3df03 --- /dev/null +++ b/packages/medusa/src/loaders/module-definitions/definitions.ts @@ -0,0 +1,5 @@ +import { ModuleDefinition } from "../../types/global" + +export const MODULE_DEFINITIONS: ModuleDefinition[] = [] + +export default MODULE_DEFINITIONS diff --git a/packages/medusa/src/loaders/module-definitions/index.ts b/packages/medusa/src/loaders/module-definitions/index.ts new file mode 100644 index 0000000000..49c59e96a0 --- /dev/null +++ b/packages/medusa/src/loaders/module-definitions/index.ts @@ -0,0 +1,26 @@ +import resolveCwd from "resolve-cwd" + +import { ConfigModule, ModuleResolution } from "../../types/global" +import MODULE_DEFINITIONS from "./definitions" + +export default ({ modules }: ConfigModule) => { + const moduleResolutions = {} as Record + const projectModules = modules ?? {} + + for (const definition of MODULE_DEFINITIONS) { + let resolutionPath = definition.defaultPackage + + // If user added a module and it's overridable, we resolve that instead + if (definition.canOverride && definition.key in projectModules) { + const mod = projectModules[definition.key] + resolutionPath = resolveCwd(mod) + } + + moduleResolutions[definition.key] = { + resolutionPath, + definition, + } + } + + return moduleResolutions +} diff --git a/packages/medusa/src/loaders/module.ts b/packages/medusa/src/loaders/module.ts new file mode 100644 index 0000000000..44c87fe46f --- /dev/null +++ b/packages/medusa/src/loaders/module.ts @@ -0,0 +1,62 @@ +import { asFunction, asValue } from "awilix" +import { trackInstallation } from "medusa-telemetry" +import { ConfigModule, Logger, MedusaContainer } from "../types/global" +import { ModulesHelper } from "../utils/module-helper" + +type Options = { + container: MedusaContainer + configModule: ConfigModule + logger: Logger +} + +export const moduleHelper = new ModulesHelper() + +export default async ({ + container, + configModule, + logger, +}: Options): Promise => { + const moduleResolutions = configModule?.moduleResolutions ?? {} + + for (const resolution of Object.values(moduleResolutions)) { + try { + const loadedModule = await import(resolution.resolutionPath!) + + const moduleLoaders = loadedModule?.loaders || [] + for (const loader of moduleLoaders) { + await loader({ container, configModule, logger }) + } + + const moduleServices = loadedModule?.services || [] + + for (const service of moduleServices) { + container.register({ + [resolution.definition.registrationName]: asFunction( + (cradle) => new service(cradle, configModule) + ).singleton(), + }) + } + + const installation = { + module: resolution.definition.key, + resolution: resolution.resolutionPath, + } + + trackInstallation(installation, "module") + } catch (err) { + if (resolution.definition.isRequired) { + throw new Error( + `Could not resolve required module: ${resolution.definition.label}` + ) + } + + logger.warn(`Couldn not resolve module: ${resolution.definition.label}`) + } + } + + moduleHelper.setModules(moduleResolutions) + + container.register({ + modulesHelper: asValue(moduleHelper), + }) +} diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 5f3dab2014..a031137989 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -11,6 +11,7 @@ import { FulfillmentService, OauthService, } from "medusa-interfaces" +import { trackInstallation } from "medusa-telemetry" import path from "path" import { EntitySchema } from "typeorm" import { @@ -77,6 +78,8 @@ export default async ({ await Promise.all( resolved.map(async (pluginDetails) => runLoaders(pluginDetails, container)) ) + + resolved.forEach((plugin) => trackInstallation(plugin.name, "plugin")) } function getResolvedPlugins( diff --git a/packages/medusa/src/loaders/services.ts b/packages/medusa/src/loaders/services.ts index 706e1e3a9f..67f297b26f 100644 --- a/packages/medusa/src/loaders/services.ts +++ b/packages/medusa/src/loaders/services.ts @@ -1,9 +1,9 @@ +import { asFunction } from "awilix" import glob from "glob" import path from "path" -import { asFunction } from "awilix" -import formatRegistrationName from "../utils/format-registration-name" import { ConfigModule, MedusaContainer } from "../types/global" import { isDefined } from "../utils" +import formatRegistrationName from "../utils/format-registration-name" type Options = { container: MedusaContainer diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 8e890e6e22..867173f4d1 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -37,6 +37,20 @@ export type Logger = _Logger & { warn: (msg: string) => void } +export type ModuleResolution = { + resolutionPath: string + definition: ModuleDefinition +} + +export type ModuleDefinition = { + key: string + registrationName: string + defaultPackage: string + label: string + canOverride?: boolean + isRequired?: boolean +} + export type ConfigModule = { projectConfig: { redis_url?: string @@ -56,6 +70,8 @@ export type ConfigModule = { admin_cors?: string } featureFlags: Record + modules?: Record + moduleResolutions?: Record plugins: ( | { resolve: string diff --git a/packages/medusa/src/types/modules.ts b/packages/medusa/src/types/modules.ts new file mode 100644 index 0000000000..2385f54f3b --- /dev/null +++ b/packages/medusa/src/types/modules.ts @@ -0,0 +1,4 @@ +export type ModulesResponse = { + module: string + resolution: string +}[] diff --git a/packages/medusa/src/utils/module-helper.ts b/packages/medusa/src/utils/module-helper.ts new file mode 100644 index 0000000000..2e4df1bae5 --- /dev/null +++ b/packages/medusa/src/utils/module-helper.ts @@ -0,0 +1,17 @@ +import { ModuleResolution } from "../types/global" +import { ModulesResponse } from "../types/modules" + +export class ModulesHelper { + private modules_: Record = {} + + setModules(modules: Record) { + this.modules_ = modules + } + + get modules(): ModulesResponse { + return Object.values(this.modules_ || {}).map((value) => ({ + module: value.definition.key, + resolution: value.resolutionPath, + })) + } +} diff --git a/www/docs/announcement.json b/www/docs/announcement.json index 1e12a272c1..a88ecd2ea5 100644 --- a/www/docs/announcement.json +++ b/www/docs/announcement.json @@ -1 +1 @@ -{"id":"https://github.com/medusajs/medusa/releases/tag/v1.6.2","content":"v1.6.2 is out","isCloseable":true} \ No newline at end of file +{"id":"https://github.com/medusajs/medusa/releases/tag/v1.6.3","content":"v1.6.3 is out","isCloseable":true} \ No newline at end of file