chore(framework): Move feature flags related resources and cleanup (#8297)
**What** cleanup and move the feature flag related resources to manage them. It also include some refactoring around loading and registering the flag as well as not relying on the `glob` package anymore plus some reorganization of the code itself FIXES FRMW-2625
This commit is contained in:
@@ -97,12 +97,15 @@ module.exports = {
|
|||||||
Object.entries(env).forEach(([k, v]) => (process.env[k] = v))
|
Object.entries(env).forEach(([k, v]) => (process.env[k] = v))
|
||||||
}
|
}
|
||||||
|
|
||||||
const { configModule } = getConfigFile(cwd, `medusa-config`)
|
const {
|
||||||
|
featureFlagsLoader,
|
||||||
|
configLoader,
|
||||||
|
container,
|
||||||
|
pgConnectionLoader,
|
||||||
|
} = require("@medusajs/framework")
|
||||||
|
|
||||||
const featureFlagsLoader =
|
const configModule = configLoader(cwd, `medusa-config`)
|
||||||
require("@medusajs/medusa/dist/loaders/feature-flags").default
|
const featureFlagRouter = await featureFlagsLoader()
|
||||||
|
|
||||||
const featureFlagRouter = featureFlagsLoader(configModule)
|
|
||||||
const modelsLoader = require("@medusajs/medusa/dist/loaders/models").default
|
const modelsLoader = require("@medusajs/medusa/dist/loaders/models").default
|
||||||
const entities = modelsLoader({}, { register: false })
|
const entities = modelsLoader({}, { register: false })
|
||||||
|
|
||||||
@@ -157,23 +160,11 @@ module.exports = {
|
|||||||
force_modules_migration ||
|
force_modules_migration ||
|
||||||
featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)
|
featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)
|
||||||
) {
|
) {
|
||||||
const { container, pgConnectionLoader } = await import(
|
|
||||||
"@medusajs/framework"
|
|
||||||
)
|
|
||||||
|
|
||||||
const featureFlagLoader =
|
|
||||||
require("@medusajs/medusa/dist/loaders/feature-flags").default
|
|
||||||
|
|
||||||
const featureFlagRouter = await featureFlagLoader(configModule)
|
|
||||||
|
|
||||||
const pgConnection = pgConnectionLoader()
|
const pgConnection = pgConnectionLoader()
|
||||||
|
|
||||||
container.register({
|
container.register({
|
||||||
[ContainerRegistrationKeys.CONFIG_MODULE]: asValue(configModule),
|
|
||||||
[ContainerRegistrationKeys.LOGGER]: asValue(console),
|
[ContainerRegistrationKeys.LOGGER]: asValue(console),
|
||||||
[ContainerRegistrationKeys.MANAGER]: asValue(dbDataSource.manager),
|
[ContainerRegistrationKeys.MANAGER]: asValue(dbDataSource.manager),
|
||||||
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(pgConnection),
|
|
||||||
featureFlagRouter: asValue(featureFlagRouter),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.setPgConnection(pgConnection)
|
instance.setPgConnection(pgConnection)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export async function configLoaderOverride(
|
|||||||
entryDirectory: string,
|
entryDirectory: string,
|
||||||
override: { clientUrl: string; debug?: boolean }
|
override: { clientUrl: string; debug?: boolean }
|
||||||
) {
|
) {
|
||||||
const { configManager } = await import("@medusajs/framework/config")
|
const { configManager } = await import("@medusajs/framework")
|
||||||
const { configModule, error } = getConfigFile<
|
const { configModule, error } = getConfigFile<
|
||||||
ReturnType<typeof configManager.loadConfig>
|
ReturnType<typeof configManager.loadConfig>
|
||||||
>(entryDirectory, "medusa-config.js")
|
>(entryDirectory, "medusa-config.js")
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ export async function initDb({ env = {} }: { env?: Record<any, any> }) {
|
|||||||
Object.entries(env).forEach(([k, v]) => (process.env[k] = v))
|
Object.entries(env).forEach(([k, v]) => (process.env[k] = v))
|
||||||
}
|
}
|
||||||
|
|
||||||
const { configManager, pgConnectionLoader, container } = await import(
|
const {
|
||||||
"@medusajs/framework"
|
configManager,
|
||||||
)
|
pgConnectionLoader,
|
||||||
|
logger,
|
||||||
|
container,
|
||||||
|
featureFlagsLoader,
|
||||||
|
} = await import("@medusajs/framework")
|
||||||
|
|
||||||
const configModule = configManager.config
|
const configModule = configManager.config
|
||||||
|
|
||||||
const pgConnection = pgConnectionLoader()
|
const pgConnection = pgConnectionLoader()
|
||||||
|
await featureFlagsLoader()
|
||||||
const featureFlagRouter =
|
|
||||||
require("@medusajs/medusa/dist/loaders/feature-flags").default(configModule)
|
|
||||||
|
|
||||||
container.register({
|
container.register({
|
||||||
[ContainerRegistrationKeys.CONFIG_MODULE]: asValue(configModule),
|
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
||||||
[ContainerRegistrationKeys.LOGGER]: asValue(console),
|
|
||||||
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(pgConnection),
|
|
||||||
[ContainerRegistrationKeys.FEATURE_FLAG_ROUTER]: asValue(featureFlagRouter),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -379,13 +379,13 @@ export type ProjectConfigOptions = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the application's worker mode.
|
* Configure the application's worker mode.
|
||||||
*
|
*
|
||||||
* Workers are processes running separately from the main application. They're useful for executing long-running or resource-heavy tasks in the background, such as importing products.
|
* Workers are processes running separately from the main application. They're useful for executing long-running or resource-heavy tasks in the background, such as importing products.
|
||||||
*
|
*
|
||||||
* With a worker, these tasks are offloaded to a separate process. So, they won't affect the performance of the main application.
|
* With a worker, these tasks are offloaded to a separate process. So, they won't affect the performance of the main application.
|
||||||
*
|
*
|
||||||
* 
|
* 
|
||||||
*
|
*
|
||||||
* Medusa has three runtime modes:
|
* Medusa has three runtime modes:
|
||||||
*
|
*
|
||||||
* - Use `shared` to run the application in a single process.
|
* - Use `shared` to run the application in a single process.
|
||||||
@@ -393,7 +393,7 @@ export type ProjectConfigOptions = {
|
|||||||
* - Use `server` to run the application server only.
|
* - Use `server` to run the application server only.
|
||||||
*
|
*
|
||||||
* In production, it's recommended to deploy two instances:
|
* In production, it's recommended to deploy two instances:
|
||||||
*
|
*
|
||||||
* 1. One having the `workerMode` configuration set to `server`.
|
* 1. One having the `workerMode` configuration set to `server`.
|
||||||
* 2. Another having the `workerMode` configuration set to `worker`.
|
* 2. Another having the `workerMode` configuration set to `worker`.
|
||||||
*
|
*
|
||||||
@@ -874,7 +874,7 @@ export type ConfigModule = {
|
|||||||
*
|
*
|
||||||
* :::
|
* :::
|
||||||
*/
|
*/
|
||||||
featureFlags: Record<string, boolean | string>
|
featureFlags: Record<string, boolean | string | Record<string, boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginDetails = {
|
export type PluginDetails = {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export * from "./get-set-difference"
|
|||||||
export * from "./graceful-shutdown-server"
|
export * from "./graceful-shutdown-server"
|
||||||
export * from "./group-by"
|
export * from "./group-by"
|
||||||
export * from "./handle-postgres-database-error"
|
export * from "./handle-postgres-database-error"
|
||||||
|
export * from "./is-truthy"
|
||||||
export * from "./is-big-number"
|
export * from "./is-big-number"
|
||||||
export * from "./is-boolean"
|
export * from "./is-boolean"
|
||||||
export * from "./is-date"
|
export * from "./is-date"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Return true if the value is truthy and otherwise false
|
||||||
|
* @param val
|
||||||
|
*/
|
||||||
|
export function isTruthy(val: string | boolean | undefined): boolean {
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return val.toLowerCase() === "true"
|
||||||
|
}
|
||||||
|
return !!val
|
||||||
|
}
|
||||||
@@ -32,6 +32,12 @@
|
|||||||
"import": "./dist/database/index.js",
|
"import": "./dist/database/index.js",
|
||||||
"require": "./dist/database/index.js",
|
"require": "./dist/database/index.js",
|
||||||
"node": "./dist/database/index.js"
|
"node": "./dist/database/index.js"
|
||||||
|
},
|
||||||
|
"./feature-flag": {
|
||||||
|
"types": "./dist/feature-flags/index.d.ts",
|
||||||
|
"import": "./dist/feature-flags/index.js",
|
||||||
|
"require": "./dist/feature-flags/index.js",
|
||||||
|
"node": "./dist/feature-flags/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -73,6 +79,7 @@
|
|||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"ioredis": "^5.2.5",
|
"ioredis": "^5.2.5",
|
||||||
"ioredis-mock": "8.4.0",
|
"ioredis-mock": "8.4.0",
|
||||||
|
"medusa-telemetry": "^0.0.18",
|
||||||
"morgan": "^1.9.1"
|
"morgan": "^1.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -856,7 +856,7 @@ export type ConfigModule = {
|
|||||||
*
|
*
|
||||||
* :::
|
* :::
|
||||||
*/
|
*/
|
||||||
featureFlags: Record<string, boolean | string>
|
featureFlags: Record<string, boolean | string | Record<string, boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginDetails = {
|
export type PluginDetails = {
|
||||||
|
|||||||
+29
-26
@@ -1,7 +1,8 @@
|
|||||||
import { FileSystem } from "@medusajs/utils"
|
import { FileSystem } from "@medusajs/utils"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
import { featureFlagsLoader } from "../feature-flag-loader"
|
||||||
|
import { configManager } from "../../config"
|
||||||
|
|
||||||
import loadFeatureFlags from "../feature-flags"
|
|
||||||
const filesystem = new FileSystem(join(__dirname, "__ff-test__"))
|
const filesystem = new FileSystem(join(__dirname, "__ff-test__"))
|
||||||
|
|
||||||
const buildFeatureFlag = (
|
const buildFeatureFlag = (
|
||||||
@@ -29,6 +30,11 @@ describe("feature flags", () => {
|
|||||||
|
|
||||||
process.env = { ...OLD_ENV }
|
process.env = { ...OLD_ENV }
|
||||||
await filesystem.cleanup()
|
await filesystem.cleanup()
|
||||||
|
|
||||||
|
configManager.loadConfig({
|
||||||
|
projectConfig: {} as any,
|
||||||
|
baseDir: filesystem.basePath,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -37,29 +43,33 @@ describe("feature flags", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should load the flag from project", async () => {
|
it("should load the flag from project", async () => {
|
||||||
|
configManager.loadConfig({
|
||||||
|
projectConfig: { featureFlags: { flag_1: false } },
|
||||||
|
baseDir: filesystem.basePath,
|
||||||
|
})
|
||||||
|
|
||||||
await filesystem.create("flags/flag-1.js", buildFeatureFlag("flag-1", true))
|
await filesystem.create("flags/flag-1.js", buildFeatureFlag("flag-1", true))
|
||||||
|
|
||||||
const flags = loadFeatureFlags(
|
const flags = await featureFlagsLoader(join(filesystem.basePath, "flags"))
|
||||||
{ featureFlags: { flag_1: false } },
|
|
||||||
undefined,
|
|
||||||
join(filesystem.basePath, "flags")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should load a nested + simple flag from project", async () => {
|
it("should load a nested + simple flag from project", async () => {
|
||||||
|
configManager.loadConfig({
|
||||||
|
projectConfig: {
|
||||||
|
featureFlags: { test: { nested: true }, simpletest: true },
|
||||||
|
},
|
||||||
|
baseDir: filesystem.basePath,
|
||||||
|
})
|
||||||
|
|
||||||
await filesystem.create("flags/test.js", buildFeatureFlag("test", false))
|
await filesystem.create("flags/test.js", buildFeatureFlag("test", false))
|
||||||
await filesystem.create(
|
await filesystem.create(
|
||||||
"flags/simpletest.js",
|
"flags/simpletest.js",
|
||||||
buildFeatureFlag("simpletest", false)
|
buildFeatureFlag("simpletest", false)
|
||||||
)
|
)
|
||||||
|
|
||||||
const flags = loadFeatureFlags(
|
const flags = await featureFlagsLoader(join(filesystem.basePath, "flags"))
|
||||||
{ featureFlags: { test: { nested: true }, simpletest: true } },
|
|
||||||
undefined,
|
|
||||||
join(filesystem.basePath, "flags")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(flags.isFeatureEnabled({ test: "nested" })).toEqual(true)
|
expect(flags.isFeatureEnabled({ test: "nested" })).toEqual(true)
|
||||||
expect(flags.isFeatureEnabled("simpletest")).toEqual(true)
|
expect(flags.isFeatureEnabled("simpletest")).toEqual(true)
|
||||||
@@ -71,11 +81,7 @@ describe("feature flags", () => {
|
|||||||
buildFeatureFlag("flag-1", false)
|
buildFeatureFlag("flag-1", false)
|
||||||
)
|
)
|
||||||
|
|
||||||
const flags = loadFeatureFlags(
|
const flags = await featureFlagsLoader(join(filesystem.basePath, "flags"))
|
||||||
{},
|
|
||||||
undefined,
|
|
||||||
join(filesystem.basePath, "flags")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||||
})
|
})
|
||||||
@@ -87,16 +93,17 @@ describe("feature flags", () => {
|
|||||||
"flags/flag-1.js",
|
"flags/flag-1.js",
|
||||||
buildFeatureFlag("flag-1", false)
|
buildFeatureFlag("flag-1", false)
|
||||||
)
|
)
|
||||||
const flags = loadFeatureFlags(
|
const flags = await featureFlagsLoader(join(filesystem.basePath, "flags"))
|
||||||
{},
|
|
||||||
undefined,
|
|
||||||
join(filesystem.basePath, "flags")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should load mix of flags", async () => {
|
it("should load mix of flags", async () => {
|
||||||
|
configManager.loadConfig({
|
||||||
|
projectConfig: { featureFlags: { flag_2: false } },
|
||||||
|
baseDir: filesystem.basePath,
|
||||||
|
})
|
||||||
|
|
||||||
process.env.MEDUSA_FF_FLAG_3 = "true"
|
process.env.MEDUSA_FF_FLAG_3 = "true"
|
||||||
await filesystem.create(
|
await filesystem.create(
|
||||||
"flags/flag-1.js",
|
"flags/flag-1.js",
|
||||||
@@ -111,11 +118,7 @@ describe("feature flags", () => {
|
|||||||
buildFeatureFlag("flag-3", false)
|
buildFeatureFlag("flag-3", false)
|
||||||
)
|
)
|
||||||
|
|
||||||
const flags = loadFeatureFlags(
|
const flags = await featureFlagsLoader(join(filesystem.basePath, "flags"))
|
||||||
{ featureFlags: { flag_2: false } },
|
|
||||||
undefined,
|
|
||||||
join(filesystem.basePath, "flags")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||||
expect(flags.isFeatureEnabled("flag_2")).toEqual(false)
|
expect(flags.isFeatureEnabled("flag_2")).toEqual(false)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
FlagRouter,
|
||||||
|
isDefined,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
isTruthy,
|
||||||
|
objectFromStringPath,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { trackFeatureFlag } from "medusa-telemetry"
|
||||||
|
import { join, normalize } from "path"
|
||||||
|
import { logger } from "../logger"
|
||||||
|
import { FlagSettings } from "./types"
|
||||||
|
import { container } from "../container"
|
||||||
|
import { asFunction } from "awilix"
|
||||||
|
import { configManager } from "../config"
|
||||||
|
import { readdir } from "fs/promises"
|
||||||
|
|
||||||
|
export const featureFlagRouter = new FlagRouter({})
|
||||||
|
|
||||||
|
container.register(
|
||||||
|
ContainerRegistrationKeys.FEATURE_FLAG_ROUTER,
|
||||||
|
asFunction(() => featureFlagRouter)
|
||||||
|
)
|
||||||
|
|
||||||
|
const excludedFiles = ["index.js", "index.ts"]
|
||||||
|
const excludedExtensions = [".d.ts", ".d.ts.map", ".js.map"]
|
||||||
|
const flagConfig: Record<string, boolean | Record<string, boolean>> = {}
|
||||||
|
|
||||||
|
function registerFlag(
|
||||||
|
flag: FlagSettings,
|
||||||
|
projectConfigFlags: Record<string, string | boolean | Record<string, boolean>>
|
||||||
|
) {
|
||||||
|
flagConfig[flag.key] = isTruthy(flag.default_val)
|
||||||
|
|
||||||
|
let from
|
||||||
|
if (isDefined(process.env[flag.env_key])) {
|
||||||
|
from = "environment"
|
||||||
|
const envVal = process.env[flag.env_key]
|
||||||
|
|
||||||
|
// MEDUSA_FF_ANALYTICS="true"
|
||||||
|
flagConfig[flag.key] = isTruthy(process.env[flag.env_key])
|
||||||
|
|
||||||
|
const parsedFromEnv = isString(envVal) ? envVal.split(",") : []
|
||||||
|
|
||||||
|
// MEDUSA_FF_WORKFLOWS=createProducts,deleteProducts
|
||||||
|
if (parsedFromEnv.length > 1) {
|
||||||
|
flagConfig[flag.key] = objectFromStringPath(parsedFromEnv)
|
||||||
|
}
|
||||||
|
} else if (isDefined(projectConfigFlags[flag.key])) {
|
||||||
|
from = "project config"
|
||||||
|
|
||||||
|
// featureFlags: { analytics: "true" | true }
|
||||||
|
flagConfig[flag.key] = isTruthy(
|
||||||
|
projectConfigFlags[flag.key] as string | boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
// featureFlags: { workflows: { createProducts: true } }
|
||||||
|
if (isObject(projectConfigFlags[flag.key])) {
|
||||||
|
flagConfig[flag.key] = projectConfigFlags[flag.key] as Record<
|
||||||
|
string,
|
||||||
|
boolean
|
||||||
|
>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger && from) {
|
||||||
|
logger.info(
|
||||||
|
`Using flag ${flag.env_key} from ${from} with value ${
|
||||||
|
flagConfig[flag.key]
|
||||||
|
}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flagConfig[flag.key]) {
|
||||||
|
trackFeatureFlag(flag.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
featureFlagRouter.setFlag(flag.key, flagConfig[flag.key])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load feature flags from a directory and from the already loaded config under the hood
|
||||||
|
* @param sourcePath
|
||||||
|
*/
|
||||||
|
export async function featureFlagsLoader(
|
||||||
|
sourcePath?: string
|
||||||
|
): Promise<FlagRouter> {
|
||||||
|
const { featureFlags: projectConfigFlags = {} } = configManager.config
|
||||||
|
|
||||||
|
if (!sourcePath) {
|
||||||
|
return featureFlagRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagDir = normalize(sourcePath)
|
||||||
|
|
||||||
|
await readdir(flagDir, { recursive: true, withFileTypes: true }).then(
|
||||||
|
async (files) => {
|
||||||
|
if (!files?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.map(async (file) => {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
return await featureFlagsLoader(join(flagDir, file.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
excludedExtensions.some((ext) => file.name.endsWith(ext)) ||
|
||||||
|
excludedFiles.includes(file.name)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileExports = await import(join(flagDir, file.name))
|
||||||
|
const featureFlag = fileExports.default
|
||||||
|
|
||||||
|
if (!featureFlag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerFlag(featureFlag, projectConfigFlags)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return featureFlagRouter
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { isObject, isString } from "@medusajs/utils"
|
||||||
|
import { FeatureFlagsResponse, IFlagRouter } from "./types"
|
||||||
|
|
||||||
|
export class FlagRouter implements IFlagRouter {
|
||||||
|
private readonly flags: Record<string, boolean | Record<string, boolean>> = {}
|
||||||
|
|
||||||
|
constructor(flags: Record<string, boolean | Record<string, boolean>>) {
|
||||||
|
this.flags = flags
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a feature flag is enabled.
|
||||||
|
* There are two ways of using this method:
|
||||||
|
* 1. `isFeatureEnabled("myFeatureFlag")`
|
||||||
|
* 2. `isFeatureEnabled({ myNestedFeatureFlag: "someNestedFlag" })`
|
||||||
|
* We use 1. for top-level feature flags and 2. for nested feature flags. Almost all flags are top-level.
|
||||||
|
* An example of a nested flag is workflows. To use it, you would do:
|
||||||
|
* `isFeatureEnabled({ workflows: Workflows.CreateCart })`
|
||||||
|
* @param flag - The flag to check
|
||||||
|
* @return {boolean} - Whether the flag is enabled or not
|
||||||
|
*/
|
||||||
|
public isFeatureEnabled(
|
||||||
|
flag: string | string[] | Record<string, string>
|
||||||
|
): boolean {
|
||||||
|
if (isObject(flag)) {
|
||||||
|
const [nestedFlag, value] = Object.entries(flag)[0]
|
||||||
|
if (typeof this.flags[nestedFlag] === "boolean") {
|
||||||
|
return this.flags[nestedFlag] as boolean
|
||||||
|
}
|
||||||
|
return !!this.flags[nestedFlag]?.[value]
|
||||||
|
}
|
||||||
|
|
||||||
|
const flags = (Array.isArray(flag) ? flag : [flag]) as string[]
|
||||||
|
return flags.every((flag_) => {
|
||||||
|
if (!isString(flag_)) {
|
||||||
|
throw Error("Flag must be a string an array of string or an object")
|
||||||
|
}
|
||||||
|
return !!this.flags[flag_]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a feature flag.
|
||||||
|
* Flags take two shapes:
|
||||||
|
* `setFlag("myFeatureFlag", true)`
|
||||||
|
* `setFlag("myFeatureFlag", { nestedFlag: true })`
|
||||||
|
* These shapes are used for top-level and nested flags respectively, as explained in isFeatureEnabled.
|
||||||
|
* @param key - The key of the flag to set.
|
||||||
|
* @param value - The value of the flag to set.
|
||||||
|
* @return {void} - void
|
||||||
|
*/
|
||||||
|
public setFlag(
|
||||||
|
key: string,
|
||||||
|
value: boolean | { [key: string]: boolean }
|
||||||
|
): void {
|
||||||
|
if (isObject(value)) {
|
||||||
|
const existing = this.flags[key]
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
this.flags[key] = value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flags[key] = { ...(this.flags[key] as object), ...value }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.flags[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
public listFlags(): FeatureFlagsResponse {
|
||||||
|
return Object.entries(this.flags || {}).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./feature-flag-loader"
|
||||||
|
export * from "./flag-router"
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export interface IFlagRouter {
|
||||||
|
isFeatureEnabled: (key: string) => boolean
|
||||||
|
listFlags: () => FeatureFlagsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @schema FeatureFlagsResponse
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - key
|
||||||
|
* - value
|
||||||
|
* properties:
|
||||||
|
* key:
|
||||||
|
* description: The key of the feature flag.
|
||||||
|
* type: string
|
||||||
|
* value:
|
||||||
|
* description: The value of the feature flag.
|
||||||
|
* type: boolean
|
||||||
|
*/
|
||||||
|
export type FeatureFlagsResponse = {
|
||||||
|
key: string
|
||||||
|
value: boolean | Record<string, boolean>
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export type FlagSettings = {
|
||||||
|
key: string
|
||||||
|
description: string
|
||||||
|
env_key: string
|
||||||
|
default_val: boolean
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export * from "./logger"
|
|||||||
export * from "./http"
|
export * from "./http"
|
||||||
export * from "./database"
|
export * from "./database"
|
||||||
export * from "./container"
|
export * from "./container"
|
||||||
|
export * from "./feature-flags"
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const main = async function ({ directory }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const container = initializeContainer(directory)
|
const container = await initializeContainer(directory)
|
||||||
|
|
||||||
const configModule = container.resolve(
|
const configModule = container.resolve(
|
||||||
ContainerRegistrationKeys.CONFIG_MODULE
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const main = async function ({ directory }) {
|
|||||||
|
|
||||||
validateInputArgs({ action, modules })
|
validateInputArgs({ action, modules })
|
||||||
|
|
||||||
const container = initializeContainer(directory)
|
const container = await initializeContainer(directory)
|
||||||
|
|
||||||
const configModule = container.resolve(
|
const configModule = container.resolve(
|
||||||
ContainerRegistrationKeys.CONFIG_MODULE
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import {
|
|
||||||
FlagRouter,
|
|
||||||
isObject,
|
|
||||||
isString,
|
|
||||||
objectFromStringPath,
|
|
||||||
} from "@medusajs/utils"
|
|
||||||
import glob from "glob"
|
|
||||||
import { isDefined } from "@medusajs/utils"
|
|
||||||
import { trackFeatureFlag } from "medusa-telemetry"
|
|
||||||
import path from "path"
|
|
||||||
import { FlagSettings } from "../../types/feature-flags"
|
|
||||||
import { Logger } from "../../types/global"
|
|
||||||
|
|
||||||
const isTruthy = (val: string | boolean | undefined): boolean => {
|
|
||||||
if (typeof val === "string") {
|
|
||||||
return val.toLowerCase() === "true"
|
|
||||||
}
|
|
||||||
return !!val
|
|
||||||
}
|
|
||||||
|
|
||||||
export const featureFlagRouter = new FlagRouter({})
|
|
||||||
|
|
||||||
export default (
|
|
||||||
configModule: {
|
|
||||||
featureFlags?: Record<string, string | boolean | Record<string, boolean>>
|
|
||||||
} = {},
|
|
||||||
logger?: Logger,
|
|
||||||
flagDirectory?: string
|
|
||||||
): FlagRouter => {
|
|
||||||
const { featureFlags: projectConfigFlags = {} } = configModule
|
|
||||||
|
|
||||||
const flagDir = path.join(flagDirectory || __dirname, "*.{j,t}s")
|
|
||||||
const supportedFlags = glob.sync(flagDir, {
|
|
||||||
ignore: ["**/index.js", "**/index.ts", "**/*.d.ts"],
|
|
||||||
})
|
|
||||||
|
|
||||||
const flagConfig: Record<string, boolean | Record<string, boolean>> = {}
|
|
||||||
for (const flag of supportedFlags) {
|
|
||||||
const flagSettings: FlagSettings = require(flag).default
|
|
||||||
if (!flagSettings) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
flagConfig[flagSettings.key] = isTruthy(flagSettings.default_val)
|
|
||||||
|
|
||||||
let from
|
|
||||||
if (isDefined(process.env[flagSettings.env_key])) {
|
|
||||||
from = "environment"
|
|
||||||
const envVal = process.env[flagSettings.env_key]
|
|
||||||
|
|
||||||
// MEDUSA_FF_ANALYTICS="true"
|
|
||||||
flagConfig[flagSettings.key] = isTruthy(process.env[flagSettings.env_key])
|
|
||||||
|
|
||||||
const parsedFromEnv = isString(envVal) ? envVal.split(",") : []
|
|
||||||
|
|
||||||
// MEDUSA_FF_WORKFLOWS=createProducts,deleteProducts
|
|
||||||
if (parsedFromEnv.length > 1) {
|
|
||||||
flagConfig[flagSettings.key] = objectFromStringPath(parsedFromEnv)
|
|
||||||
}
|
|
||||||
} else if (isDefined(projectConfigFlags[flagSettings.key])) {
|
|
||||||
from = "project config"
|
|
||||||
|
|
||||||
// featureFlags: { analytics: "true" | true }
|
|
||||||
flagConfig[flagSettings.key] = isTruthy(
|
|
||||||
projectConfigFlags[flagSettings.key] as string | boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
// featureFlags: { workflows: { createProducts: true } }
|
|
||||||
if (isObject(projectConfigFlags[flagSettings.key])) {
|
|
||||||
flagConfig[flagSettings.key] = projectConfigFlags[
|
|
||||||
flagSettings.key
|
|
||||||
] as Record<string, boolean>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger && from) {
|
|
||||||
logger.info(
|
|
||||||
`Using flag ${flagSettings.env_key} from ${from} with value ${
|
|
||||||
flagConfig[flagSettings.key]
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flagConfig[flagSettings.key]) {
|
|
||||||
trackFeatureFlag(flagSettings.key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const flag of Object.keys(flagConfig)) {
|
|
||||||
featureFlagRouter.setFlag(flag, flagConfig[flag])
|
|
||||||
}
|
|
||||||
|
|
||||||
return featureFlagRouter
|
|
||||||
}
|
|
||||||
@@ -4,20 +4,23 @@ import {
|
|||||||
registerMedusaModule,
|
registerMedusaModule,
|
||||||
} from "@medusajs/modules-sdk"
|
} from "@medusajs/modules-sdk"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
configManager,
|
||||||
createMedusaContainer,
|
ConfigModule,
|
||||||
generateJwtToken,
|
container,
|
||||||
} from "@medusajs/utils"
|
featureFlagsLoader,
|
||||||
|
logger,
|
||||||
|
} from "@medusajs/framework"
|
||||||
|
import { ContainerRegistrationKeys, generateJwtToken } from "@medusajs/utils"
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import express from "express"
|
import express from "express"
|
||||||
import querystring from "querystring"
|
import querystring from "querystring"
|
||||||
import supertest from "supertest"
|
import supertest from "supertest"
|
||||||
import apiLoader from "../../../../api"
|
import apiLoader from "../../../../api"
|
||||||
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
|
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
|
||||||
import featureFlagLoader, { featureFlagRouter } from "../../../../feature-flags"
|
|
||||||
|
|
||||||
import RoutesLoader from "../.."
|
import RoutesLoader from "../.."
|
||||||
import { config } from "../mocks"
|
import { config } from "../mocks"
|
||||||
|
import { MedusaContainer } from "@medusajs/types"
|
||||||
|
|
||||||
function asArray(resolvers) {
|
function asArray(resolvers) {
|
||||||
return {
|
return {
|
||||||
@@ -44,15 +47,18 @@ export const createServer = async (rootDir) => {
|
|||||||
)[moduleKey]
|
)[moduleKey]
|
||||||
})
|
})
|
||||||
|
|
||||||
const container = createMedusaContainer()
|
configManager.loadConfig({
|
||||||
|
projectConfig: config as ConfigModule,
|
||||||
|
baseDir: rootDir,
|
||||||
|
})
|
||||||
|
|
||||||
container.registerAdd = function (name, registration) {
|
container.registerAdd = function (this: MedusaContainer, name, registration) {
|
||||||
const storeKey = name + "_STORE"
|
const storeKey = name + "_STORE"
|
||||||
|
|
||||||
if (this.registrations[storeKey] === undefined) {
|
if (this.registrations[storeKey] === undefined) {
|
||||||
this.register(storeKey, asValue([]))
|
this.register(storeKey, asValue([]))
|
||||||
}
|
}
|
||||||
const store = this.resolve(storeKey)
|
const store = this.resolve(storeKey) as Array<any>
|
||||||
|
|
||||||
if (this.registrations[name] === undefined) {
|
if (this.registrations[name] === undefined) {
|
||||||
this.register(name, asArray(store))
|
this.register(name, asArray(store))
|
||||||
@@ -63,7 +69,6 @@ export const createServer = async (rootDir) => {
|
|||||||
}.bind(container)
|
}.bind(container)
|
||||||
|
|
||||||
container.register(ContainerRegistrationKeys.PG_CONNECTION, asValue({}))
|
container.register(ContainerRegistrationKeys.PG_CONNECTION, asValue({}))
|
||||||
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
|
||||||
container.register("configModule", asValue(config))
|
container.register("configModule", asValue(config))
|
||||||
container.register({
|
container.register({
|
||||||
logger: asValue({
|
logger: asValue({
|
||||||
@@ -87,11 +92,11 @@ export const createServer = async (rootDir) => {
|
|||||||
|
|
||||||
const plugins = getResolvedPlugins(rootDir, config) || []
|
const plugins = getResolvedPlugins(rootDir, config) || []
|
||||||
|
|
||||||
featureFlagLoader(config)
|
await featureFlagsLoader()
|
||||||
await moduleLoader({ container, moduleResolutions })
|
await moduleLoader({ container, moduleResolutions, logger })
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.scope = container.createScope()
|
req.scope = container.createScope() as MedusaContainer
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ConfigModule, MedusaContainer, PluginDetails } from "@medusajs/types"
|
|||||||
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/utils"
|
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/utils"
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import { Express, NextFunction, Request, Response } from "express"
|
import { Express, NextFunction, Request, Response } from "express"
|
||||||
import path from "path"
|
import path, { join } from "path"
|
||||||
import requestIp from "request-ip"
|
import requestIp from "request-ip"
|
||||||
import { v4 } from "uuid"
|
import { v4 } from "uuid"
|
||||||
import adminLoader from "./admin"
|
import adminLoader from "./admin"
|
||||||
@@ -12,10 +12,10 @@ import {
|
|||||||
configLoader,
|
configLoader,
|
||||||
container,
|
container,
|
||||||
expressLoader,
|
expressLoader,
|
||||||
|
featureFlagsLoader,
|
||||||
logger,
|
logger,
|
||||||
pgConnectionLoader,
|
pgConnectionLoader,
|
||||||
} from "@medusajs/framework"
|
} from "@medusajs/framework"
|
||||||
import featureFlagsLoader from "./feature-flags"
|
|
||||||
import { registerJobs } from "./helpers/register-jobs"
|
import { registerJobs } from "./helpers/register-jobs"
|
||||||
import { registerWorkflows } from "./helpers/register-workflows"
|
import { registerWorkflows } from "./helpers/register-workflows"
|
||||||
import { getResolvedPlugins } from "./helpers/resolve-plugins"
|
import { getResolvedPlugins } from "./helpers/resolve-plugins"
|
||||||
@@ -120,9 +120,13 @@ async function loadEntrypoints(
|
|||||||
return shutdown
|
return shutdown
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initializeContainer(rootDirectory: string): MedusaContainer {
|
export async function initializeContainer(
|
||||||
|
rootDirectory: string
|
||||||
|
): Promise<MedusaContainer> {
|
||||||
const configModule = configLoader(rootDirectory, "medusa-config.js")
|
const configModule = configLoader(rootDirectory, "medusa-config.js")
|
||||||
const featureFlagRouter = featureFlagsLoader(configModule, logger)
|
const featureFlagRouter = await featureFlagsLoader(
|
||||||
|
join(__dirname, "feature-flags")
|
||||||
|
)
|
||||||
|
|
||||||
container.register({
|
container.register({
|
||||||
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
||||||
@@ -143,7 +147,7 @@ export default async ({
|
|||||||
app: Express
|
app: Express
|
||||||
shutdown: () => Promise<void>
|
shutdown: () => Promise<void>
|
||||||
}> => {
|
}> => {
|
||||||
const container = initializeContainer(rootDirectory)
|
const container = await initializeContainer(rootDirectory)
|
||||||
const configModule = container.resolve(
|
const configModule = container.resolve(
|
||||||
ContainerRegistrationKeys.CONFIG_MODULE
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"@medusajs/types": "^1.11.16",
|
"@medusajs/types": "^1.11.16",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
"medusa-test-utils": "^1.1.44",
|
||||||
"pg-god": "^1.0.12",
|
"pg-god": "^1.0.12",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
|||||||
@@ -4619,6 +4619,7 @@ __metadata:
|
|||||||
express-session: ^1.17.3
|
express-session: ^1.17.3
|
||||||
ioredis: ^5.2.5
|
ioredis: ^5.2.5
|
||||||
ioredis-mock: 8.4.0
|
ioredis-mock: 8.4.0
|
||||||
|
medusa-telemetry: ^0.0.18
|
||||||
morgan: ^1.9.1
|
morgan: ^1.9.1
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
tsc-alias: ^1.8.6
|
tsc-alias: ^1.8.6
|
||||||
@@ -4758,6 +4759,7 @@ __metadata:
|
|||||||
awilix: ^8.0.0
|
awilix: ^8.0.0
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^29.7.0
|
jest: ^29.7.0
|
||||||
|
medusa-test-utils: ^1.1.44
|
||||||
pg-god: ^1.0.12
|
pg-god: ^1.0.12
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-node: ^10.9.1
|
ts-node: ^10.9.1
|
||||||
@@ -22704,6 +22706,24 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"medusa-telemetry@npm:^0.0.18":
|
||||||
|
version: 0.0.18
|
||||||
|
resolution: "medusa-telemetry@npm:0.0.18"
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime": ^7.22.10
|
||||||
|
axios: ^0.21.4
|
||||||
|
axios-retry: ^3.1.9
|
||||||
|
boxen: ^5.0.1
|
||||||
|
ci-info: ^3.2.0
|
||||||
|
configstore: 5.0.1
|
||||||
|
global: ^4.4.0
|
||||||
|
is-docker: ^2.2.1
|
||||||
|
remove-trailing-slash: ^0.1.1
|
||||||
|
uuid: ^8.3.2
|
||||||
|
checksum: 3571c3f578582667b3a48f2f3d9d27299a954fcc1e9165c2fdc441219d6faa0cd0f6be453f0edec783acea83500586c199a21d8b052e6ed3c815da85850fef7f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"medusa-test-utils@^1.1.42, medusa-test-utils@^1.1.43, medusa-test-utils@^1.1.44, medusa-test-utils@workspace:*, medusa-test-utils@workspace:^, medusa-test-utils@workspace:packages/core/medusa-test-utils":
|
"medusa-test-utils@^1.1.42, medusa-test-utils@^1.1.43, medusa-test-utils@^1.1.44, medusa-test-utils@workspace:*, medusa-test-utils@workspace:^, medusa-test-utils@workspace:packages/core/medusa-test-utils":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "medusa-test-utils@workspace:packages/core/medusa-test-utils"
|
resolution: "medusa-test-utils@workspace:packages/core/medusa-test-utils"
|
||||||
|
|||||||
Reference in New Issue
Block a user