feat(medusa): Module Resolution API (#2597)
This commit is contained in:
committed by
GitHub
parent
e09f6e8a1e
commit
d7997ef256
5
.changeset/seven-pianos-camp.md
Normal file
5
.changeset/seven-pianos-camp.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
feat(medusa): Expose Module Resolution API
|
||||
@@ -131,6 +131,7 @@ Object {
|
||||
"id": Any<String>,
|
||||
"invite_link_template": null,
|
||||
"metadata": null,
|
||||
"modules": Any<Array>,
|
||||
"name": "Medusa Store",
|
||||
"payment_link_template": null,
|
||||
"payment_providers": Array [
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 ?? [],
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { ModuleDefinition } from "../../types/global"
|
||||
|
||||
export const MODULE_DEFINITIONS: ModuleDefinition[] = []
|
||||
|
||||
export default MODULE_DEFINITIONS
|
||||
26
packages/medusa/src/loaders/module-definitions/index.ts
Normal file
26
packages/medusa/src/loaders/module-definitions/index.ts
Normal file
@@ -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<string, ModuleResolution>
|
||||
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
|
||||
}
|
||||
62
packages/medusa/src/loaders/module.ts
Normal file
62
packages/medusa/src/loaders/module.ts
Normal file
@@ -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<void> => {
|
||||
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),
|
||||
})
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, boolean | string>
|
||||
modules?: Record<string, string>
|
||||
moduleResolutions?: Record<string, ModuleResolution>
|
||||
plugins: (
|
||||
| {
|
||||
resolve: string
|
||||
|
||||
4
packages/medusa/src/types/modules.ts
Normal file
4
packages/medusa/src/types/modules.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type ModulesResponse = {
|
||||
module: string
|
||||
resolution: string
|
||||
}[]
|
||||
17
packages/medusa/src/utils/module-helper.ts
Normal file
17
packages/medusa/src/utils/module-helper.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ModuleResolution } from "../types/global"
|
||||
import { ModulesResponse } from "../types/modules"
|
||||
|
||||
export class ModulesHelper {
|
||||
private modules_: Record<string, ModuleResolution> = {}
|
||||
|
||||
setModules(modules: Record<string, ModuleResolution>) {
|
||||
this.modules_ = modules
|
||||
}
|
||||
|
||||
get modules(): ModulesResponse {
|
||||
return Object.values(this.modules_ || {}).map((value) => ({
|
||||
module: value.definition.key,
|
||||
resolution: value.resolutionPath,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"id":"https://github.com/medusajs/medusa/releases/tag/v1.6.2","content":"v1.6.2 is out","isCloseable":true}
|
||||
{"id":"https://github.com/medusajs/medusa/releases/tag/v1.6.3","content":"v1.6.3 is out","isCloseable":true}
|
||||
Reference in New Issue
Block a user