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:
1 parent
8a5751c115
commit
9b6de8c02d
21 files changed
+364
-174
No files matched your search
@@ -1,124 +0,0 @@
|
||||
import { FileSystem } from "@medusajs/utils"
|
||||
import { join } from "path"
|
||||
|
||||
import loadFeatureFlags from "../feature-flags"
|
||||
const filesystem = new FileSystem(join(__dirname, "__ff-test__"))
|
||||
|
||||
const buildFeatureFlag = (
|
||||
key: string,
|
||||
defaultVal: string | boolean
|
||||
): string => {
|
||||
const snakeCaseKey = key.replace(/-/g, "_")
|
||||
|
||||
return `
|
||||
export default {
|
||||
description: "${key} descr",
|
||||
key: "${snakeCaseKey}",
|
||||
env_key: "MEDUSA_FF_${snakeCaseKey.toUpperCase()}",
|
||||
default_val: ${defaultVal},
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
describe("feature flags", () => {
|
||||
const OLD_ENV = { ...process.env }
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules()
|
||||
jest.clearAllMocks()
|
||||
|
||||
process.env = { ...OLD_ENV }
|
||||
await filesystem.cleanup()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
process.env = OLD_ENV
|
||||
await filesystem.cleanup()
|
||||
})
|
||||
|
||||
it("should load the flag from project", async () => {
|
||||
await filesystem.create("flags/flag-1.js", buildFeatureFlag("flag-1", true))
|
||||
|
||||
const flags = loadFeatureFlags(
|
||||
{ featureFlags: { flag_1: false } },
|
||||
undefined,
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||
})
|
||||
|
||||
it("should load a nested + simple flag from project", async () => {
|
||||
await filesystem.create("flags/test.js", buildFeatureFlag("test", false))
|
||||
await filesystem.create(
|
||||
"flags/simpletest.js",
|
||||
buildFeatureFlag("simpletest", false)
|
||||
)
|
||||
|
||||
const flags = loadFeatureFlags(
|
||||
{ featureFlags: { test: { nested: true }, simpletest: true } },
|
||||
undefined,
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled({ test: "nested" })).toEqual(true)
|
||||
expect(flags.isFeatureEnabled("simpletest")).toEqual(true)
|
||||
})
|
||||
|
||||
it("should load the default feature flags", async () => {
|
||||
await filesystem.create(
|
||||
"flags/flag-1.js",
|
||||
buildFeatureFlag("flag-1", false)
|
||||
)
|
||||
|
||||
const flags = loadFeatureFlags(
|
||||
{},
|
||||
undefined,
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||
})
|
||||
|
||||
it("should load the flag from env", async () => {
|
||||
process.env.MEDUSA_FF_FLAG_1 = "false"
|
||||
|
||||
await filesystem.create(
|
||||
"flags/flag-1.js",
|
||||
buildFeatureFlag("flag-1", false)
|
||||
)
|
||||
const flags = loadFeatureFlags(
|
||||
{},
|
||||
undefined,
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||
})
|
||||
|
||||
it("should load mix of flags", async () => {
|
||||
process.env.MEDUSA_FF_FLAG_3 = "true"
|
||||
await filesystem.create(
|
||||
"flags/flag-1.js",
|
||||
buildFeatureFlag("flag-1", false)
|
||||
)
|
||||
await filesystem.create(
|
||||
"flags/flag-2.js",
|
||||
buildFeatureFlag("flag-2", false)
|
||||
)
|
||||
await filesystem.create(
|
||||
"flags/flag-3.js",
|
||||
buildFeatureFlag("flag-3", false)
|
||||
)
|
||||
|
||||
const flags = loadFeatureFlags(
|
||||
{ featureFlags: { flag_2: false } },
|
||||
undefined,
|
||||
join(filesystem.basePath, "flags")
|
||||
)
|
||||
|
||||
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
|
||||
expect(flags.isFeatureEnabled("flag_2")).toEqual(false)
|
||||
expect(flags.isFeatureEnabled("flag_3")).toEqual(true)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
createMedusaContainer,
|
||||
generateJwtToken,
|
||||
} from "@medusajs/utils"
|
||||
configManager,
|
||||
ConfigModule,
|
||||
container,
|
||||
featureFlagsLoader,
|
||||
logger,
|
||||
} from "@medusajs/framework"
|
||||
import { ContainerRegistrationKeys, generateJwtToken } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import express from "express"
|
||||
import querystring from "querystring"
|
||||
import supertest from "supertest"
|
||||
import apiLoader from "../../../../api"
|
||||
import { getResolvedPlugins } from "../../../../helpers/resolve-plugins"
|
||||
import featureFlagLoader, { featureFlagRouter } from "../../../../feature-flags"
|
||||
|
||||
import RoutesLoader from "../.."
|
||||
import { config } from "../mocks"
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
|
||||
function asArray(resolvers) {
|
||||
return {
|
||||
@@ -44,15 +47,18 @@ export const createServer = async (rootDir) => {
|
||||
)[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"
|
||||
|
||||
if (this.registrations[storeKey] === undefined) {
|
||||
this.register(storeKey, asValue([]))
|
||||
}
|
||||
const store = this.resolve(storeKey)
|
||||
const store = this.resolve(storeKey) as Array<any>
|
||||
|
||||
if (this.registrations[name] === undefined) {
|
||||
this.register(name, asArray(store))
|
||||
@@ -63,7 +69,6 @@ export const createServer = async (rootDir) => {
|
||||
}.bind(container)
|
||||
|
||||
container.register(ContainerRegistrationKeys.PG_CONNECTION, asValue({}))
|
||||
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
||||
container.register("configModule", asValue(config))
|
||||
container.register({
|
||||
logger: asValue({
|
||||
@@ -87,11 +92,11 @@ export const createServer = async (rootDir) => {
|
||||
|
||||
const plugins = getResolvedPlugins(rootDir, config) || []
|
||||
|
||||
featureFlagLoader(config)
|
||||
await moduleLoader({ container, moduleResolutions })
|
||||
await featureFlagsLoader()
|
||||
await moduleLoader({ container, moduleResolutions, logger })
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.scope = container.createScope()
|
||||
req.scope = container.createScope() as MedusaContainer
|
||||
next()
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ConfigModule, MedusaContainer, PluginDetails } from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys, promiseAll } from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import { Express, NextFunction, Request, Response } from "express"
|
||||
import path from "path"
|
||||
import path, { join } from "path"
|
||||
import requestIp from "request-ip"
|
||||
import { v4 } from "uuid"
|
||||
import adminLoader from "./admin"
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
configLoader,
|
||||
container,
|
||||
expressLoader,
|
||||
featureFlagsLoader,
|
||||
logger,
|
||||
pgConnectionLoader,
|
||||
} from "@medusajs/framework"
|
||||
import featureFlagsLoader from "./feature-flags"
|
||||
import { registerJobs } from "./helpers/register-jobs"
|
||||
import { registerWorkflows } from "./helpers/register-workflows"
|
||||
import { getResolvedPlugins } from "./helpers/resolve-plugins"
|
||||
@@ -120,9 +120,13 @@ async function loadEntrypoints(
|
||||
return shutdown
|
||||
}
|
||||
|
||||
export function initializeContainer(rootDirectory: string): MedusaContainer {
|
||||
export async function initializeContainer(
|
||||
rootDirectory: string
|
||||
): Promise<MedusaContainer> {
|
||||
const configModule = configLoader(rootDirectory, "medusa-config.js")
|
||||
const featureFlagRouter = featureFlagsLoader(configModule, logger)
|
||||
const featureFlagRouter = await featureFlagsLoader(
|
||||
join(__dirname, "feature-flags")
|
||||
)
|
||||
|
||||
container.register({
|
||||
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
||||
@@ -143,7 +147,7 @@ export default async ({
|
||||
app: Express
|
||||
shutdown: () => Promise<void>
|
||||
}> => {
|
||||
const container = initializeContainer(rootDirectory)
|
||||
const container = await initializeContainer(rootDirectory)
|
||||
const configModule = container.resolve(
|
||||
ContainerRegistrationKeys.CONFIG_MODULE
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user