feat(medusa): Module Resolution API (#2597)

This commit is contained in:
Oliver Windall Juhl
2022-11-20 22:01:46 +01:00
committed by GitHub
parent e09f6e8a1e
commit d7997ef256
20 changed files with 209 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): Expose Module Resolution API

View File

@@ -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 [

View File

@@ -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),

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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()

View File

@@ -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({

View File

@@ -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 ?? [],
}

View File

@@ -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") {

View File

@@ -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 }
}

View File

@@ -0,0 +1,5 @@
import { ModuleDefinition } from "../../types/global"
export const MODULE_DEFINITIONS: ModuleDefinition[] = []
export default MODULE_DEFINITIONS

View 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
}

View 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),
})
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,4 @@
export type ModulesResponse = {
module: string
resolution: string
}[]

View 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,
}))
}
}

View File

@@ -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}