Feat(medusa): implement feature flags (#1768)

* feat: add feature flag loading in projects

* fix: make feature flag consume itself

* fix: rename container registration to featureFlagRouter

* fix: refactor

* behavioral feature flags

* add environment to server

* limit "useTemplateDb" to non feature flagged migrations

* filter migrations and entities according to those which are enabled in the environment

* run only migrations that are enabled when running 'medusa migrations run'

* add logging to the featureflag loader

* initial implementation of featureFlagEntity

* column descriptors

* initial startServerWithEnv (to be refactored)

* update commands

* final touches

* update loaders to fix unit tests

* enable all batch job tests

* update seed method

* add api test capabilities

* revert batch job test

* revert formatting changes

* pr feedback

* pr feedback

* remove unused imports

* rename feature flag decorators

* pr feedback

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
This commit is contained in:
Philip Korsholm
2022-07-04 15:39:30 +02:00
committed by GitHub
parent f0704a7e17
commit 41681b45b1
19 changed files with 527 additions and 58 deletions

View File

@@ -95,25 +95,25 @@ describe("/admin/batch-jobs", () => {
id: "job_5",
created_at: expect.any(String),
updated_at: expect.any(String),
created_by: "admin_user"
created_by: "admin_user",
},
{
id: "job_3",
created_at: expect.any(String),
updated_at: expect.any(String),
created_by: "admin_user"
created_by: "admin_user",
},
{
id: "job_2",
created_at: expect.any(String),
updated_at: expect.any(String),
created_by: "admin_user"
created_by: "admin_user",
},
{
id: "job_1",
created_at: expect.any(String),
updated_at: expect.any(String),
created_by: "admin_user"
created_by: "admin_user",
},
],
})
@@ -121,7 +121,10 @@ describe("/admin/batch-jobs", () => {
it("lists batch jobs created by the user and where completed_at is null ", async () => {
const api = useApi()
const response = await api.get("/admin/batch-jobs?completed_at=null", adminReqConfig)
const response = await api.get(
"/admin/batch-jobs?completed_at=null",
adminReqConfig
)
expect(response.status).toEqual(200)
expect(response.data.batch_jobs.length).toEqual(3)

View File

@@ -2,7 +2,7 @@ const path = require("path")
const { spawn } = require("child_process")
const { setPort } = require("./use-api")
module.exports = ({ cwd, redisUrl, uploadDir, verbose }) => {
module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => {
const serverPath = path.join(__dirname, "test-server.js")
// in order to prevent conflicts in redis, use a different db for each worker
@@ -21,6 +21,7 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose }) => {
COOKIE_SECRET: "test",
REDIS_URL: redisUrl ? redisUrlWithDatabase : undefined, // If provided, will use a real instance, otherwise a fake instance
UPLOAD_DIR: uploadDir, // If provided, will be used for the fake local file service
...env,
},
stdio: verbose
? ["inherit", "inherit", "inherit", "ipc"]

View File

@@ -0,0 +1,28 @@
const setupServer = require("./setup-server")
const { initDb } = require("./use-db")
const startServerWithEnvironment = async ({ cwd, verbose, env }) => {
if (env) {
Object.entries(env).forEach(([key, value]) => {
process.env[key] = value
})
}
const dbConnection = await initDb({
cwd,
})
Object.entries(env).forEach(([key, value]) => {
delete process.env[key]
})
const medusaProcess = await setupServer({
cwd,
verbose,
env,
})
return [medusaProcess, dbConnection]
}
export default startServerWithEnvironment

View File

@@ -79,6 +79,19 @@ const instance = DbTestUtil
module.exports = {
initDb: async function ({ cwd }) {
const configPath = path.resolve(path.join(cwd, `medusa-config.js`))
const { projectConfig, featureFlags } = require(configPath)
const featureFlagsLoader = require(path.join(
cwd,
`node_modules`,
`@medusajs`,
`medusa`,
`dist`,
`loaders`,
`feature-flags`
)).default
const featureFlagsRouter = featureFlagsLoader({ featureFlags })
const modelsLoader = require(path.join(
cwd,
@@ -89,9 +102,9 @@ module.exports = {
`loaders`,
`models`
)).default
const entities = modelsLoader({}, { register: false })
const { projectConfig } = require(configPath)
if (projectConfig.database_type === "sqlite") {
connectionType = "sqlite"
const dbConnection = await createConnection({
@@ -108,12 +121,48 @@ module.exports = {
await dbFactory.createFromTemplate(databaseName)
// get migraitons with enabled featureflags
const migrationDir = path.resolve(
path.join(
cwd,
`node_modules`,
`@medusajs`,
`medusa`,
`dist`,
`migrations`,
`*.js`
)
)
const { getEnabledMigrations } = require(path.join(
cwd,
`node_modules`,
`@medusajs`,
`medusa`,
`dist`,
`commands`,
`utils`,
`get-migrations`
))
const enabledMigrations = await getEnabledMigrations(
[migrationDir],
(flag) => featureFlagsRouter.isFeatureEnabled(flag)
)
const enabledEntities = entities.filter(
(e) => typeof e.isFeatureEnabled === "undefined" || e.isFeatureEnabled()
)
const dbConnection = await createConnection({
type: "postgres",
url: DB_URL,
entities,
entities: enabledEntities,
migrations: enabledMigrations,
})
await dbConnection.runMigrations()
instance.setDb(dbConnection)
return dbConnection
}

View File

@@ -31,10 +31,28 @@ class DatabaseFactory {
`@medusajs`,
`medusa`,
`dist`,
`migrations`
`migrations`,
`*.js`
)
)
const { getEnabledMigrations } = require(path.join(
cwd,
`node_modules`,
`@medusajs`,
`medusa`,
`dist`,
`commands`,
`utils`,
`get-migrations`
))
// filter migrations to only include those that dont have feature flags
const enabledMigrations = await getEnabledMigrations(
[migrationDir],
(flag) => false
)
await dropDatabase(
{
databaseName: this.templateDbName,
@@ -51,7 +69,7 @@ class DatabaseFactory {
type: "postgres",
name: "templateConnection",
url: `${DB_URL}/${this.templateDbName}`,
migrations: [`${migrationDir}/*.js`],
migrations: enabledMigrations,
})
await templateDbConnection.runMigrations()
@@ -92,7 +110,7 @@ class DatabaseFactory {
}
async destroy() {
let connection = await this.getMasterConnection()
const connection = await this.getMasterConnection()
await connection.query(`DROP DATABASE IF EXISTS "${this.templateDbName}";`)
await connection.close()

View File

@@ -0,0 +1,18 @@
import { NextFunction, Request, Response } from "express"
import { FlagRouter } from "../../utils/flag-router"
export function isFeatureFlagEnabled(
flagKey: string
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
const featureFlagRouter = req.scope.resolve(
"featureFlagRouter"
) as FlagRouter
if (!featureFlagRouter.isFeatureEnabled(flagKey)) {
res.sendStatus(404)
} else {
next()
}
}
}

View File

@@ -1,24 +1,27 @@
import { createConnection } from "typeorm"
import { getConfigFile } from "medusa-core-utils"
import featureFlagLoader from "../loaders/feature-flags"
import Logger from "../loaders/logger"
import getMigrations from "./utils/get-migrations"
const t = async function({ directory }) {
const t = async function ({ directory }) {
const args = process.argv
args.shift()
args.shift()
args.shift()
const { configModule } = getConfigFile(directory, `medusa-config`)
const migrationDirs = getMigrations(directory)
const featureFlagRouter = featureFlagLoader(configModule)
const enabledMigrations = await getMigrations(directory, featureFlagRouter)
const connection = await createConnection({
type: configModule.projectConfig.database_type,
url: configModule.projectConfig.database_url,
extra: configModule.projectConfig.database_extra || {},
migrations: migrationDirs,
migrations: enabledMigrations,
logging: true,
})

View File

@@ -9,9 +9,11 @@ import { track } from "medusa-telemetry"
import Logger from "../loaders/logger"
import loaders from "../loaders"
import featureFlagLoader from "../loaders/feature-flags"
import getMigrations from "./utils/get-migrations"
const t = async function({ directory, migrate, seedFile }) {
const t = async function ({ directory, migrate, seedFile }) {
track("CLI_SEED")
let resolvedPath = seedFile
@@ -28,9 +30,12 @@ const t = async function({ directory, migrate, seedFile }) {
}
const { configModule } = getConfigFile(directory, `medusa-config`)
const featureFlagRouter = featureFlagLoader(configModule)
const dbType = configModule.projectConfig.database_type
if (migrate && dbType !== "sqlite") {
const migrationDirs = getMigrations(directory)
const migrationDirs = await getMigrations(directory, featureFlagRouter)
const connection = await createConnection({
type: configModule.projectConfig.database_type,
database: configModule.projectConfig.database_database,
@@ -61,7 +66,7 @@ const t = async function({ directory, migrate, seedFile }) {
const shippingOptionService = container.resolve("shippingOptionService")
const shippingProfileService = container.resolve("shippingProfileService")
await manager.transaction(async tx => {
await manager.transaction(async (tx) => {
const { store, regions, products, shipping_options, users } = JSON.parse(
fs.readFileSync(resolvedPath, `utf-8`)
)
@@ -74,14 +79,14 @@ const t = async function({ directory, migrate, seedFile }) {
}
for (const u of users) {
let pass = u.password
const pass = u.password
if (pass) {
delete u.password
}
await userService.withTransaction(tx).create(u, pass)
}
let regionIds = {}
const regionIds = {}
for (const r of regions) {
let dummyId
if (!r.id || !r.id.startsWith("reg_")) {
@@ -126,7 +131,7 @@ const t = async function({ directory, migrate, seedFile }) {
if (variants && variants.length) {
const optionIds = p.options.map(
o => newProd.options.find(newO => newO.title === o.title).id
(o) => newProd.options.find((newO) => newO.title === o.title).id
)
for (const v of variants) {

View File

@@ -1,3 +1,4 @@
import glob from "glob"
import path from "path"
import fs from "fs"
import { isString } from "lodash"
@@ -33,7 +34,7 @@ function resolvePlugin(pluginName) {
fs.readFileSync(`${resolvedPath}/package.json`, `utf-8`)
)
const name = packageJSON.name || pluginName
//warnOnIncompatiblePeerDependency(name, packageJSON)
// warnOnIncompatiblePeerDependency(name, packageJSON)
return {
resolve: resolvedPath,
@@ -86,11 +87,11 @@ function resolvePlugin(pluginName) {
}
}
export default directory => {
export default async (directory, featureFlagRouter) => {
const { configModule } = getConfigFile(directory, `medusa-config`)
const { plugins } = configModule
const resolved = plugins.map(plugin => {
const resolved = plugins.map((plugin) => {
if (isString(plugin)) {
return resolvePlugin(plugin)
}
@@ -123,5 +124,26 @@ export default directory => {
}
}
return migrationDirs
return getEnabledMigrations(migrationDirs, (flag) =>
featureFlagRouter.isFeatureEnabled(flag)
)
}
export const getEnabledMigrations = (migrationDirs, isFlagEnabled) => {
const allMigrations = migrationDirs.flatMap((dir) => {
return glob.sync(dir)
})
return allMigrations
.map((file) => {
const loaded = require(file)
if (
typeof loaded.featureFlag === "undefined" ||
isFlagEnabled(loaded.featureFlag)
) {
return file
}
return false
})
.filter(Boolean)
}

View File

@@ -7,6 +7,7 @@ import supertest from "supertest"
import querystring from "querystring"
import apiLoader from "../loaders/api"
import passportLoader from "../loaders/passport"
import featureFlagLoader from "../loaders/feature-flags"
import servicesLoader from "../loaders/services"
import strategiesLoader from "../loaders/strategies"
@@ -24,17 +25,21 @@ const clientSessionOpts = {
const config = {
projectConfig: {
jwt_secret: 'supersecret',
cookie_secret: 'superSecret',
admin_cors: '',
store_cors: ''
}
jwt_secret: "supersecret",
cookie_secret: "superSecret",
admin_cors: "",
store_cors: "",
},
}
const testApp = express()
const container = createContainer()
container.register('configModule', asValue(config))
const featureFlagRouter = featureFlagLoader(config)
container.register("featureFlagRouter", asValue(featureFlagRouter))
container.register("configModule", asValue(config))
container.register({
logger: asValue({
error: () => {},
@@ -69,10 +74,16 @@ apiLoader({ container, app: testApp, configModule: config })
const supertestRequest = supertest(testApp)
export async function request(method, url, opts = {}) {
const { payload, query, headers = {} } = opts
const { payload, query, headers = {}, flags = [] } = opts
const queryParams = query && querystring.stringify(query);
const req = supertestRequest[method.toLowerCase()](`${url}${queryParams ? "?" + queryParams : ''}`)
flags.forEach((flag) => {
featureFlagRouter.setFlag(flag, true)
})
const queryParams = query && querystring.stringify(query)
const req = supertestRequest[method.toLowerCase()](
`${url}${queryParams ? "?" + queryParams : ""}`
)
headers.Cookie = headers.Cookie || ""
if (opts.adminSession) {
if (opts.adminSession.jwt) {

View File

@@ -0,0 +1,130 @@
import { resolve } from "path"
import { mkdirSync, rmSync, writeFileSync } from "fs"
import loadFeatureFlags from "../feature-flags"
const distTestTargetDirectorPath = resolve(__dirname, "__ff-test__")
const getFolderTestTargetDirectoryPath = (folderName: string): string => {
return resolve(distTestTargetDirectorPath, folderName)
}
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(() => {
jest.resetModules()
jest.clearAllMocks()
process.env = { ...OLD_ENV }
rmSync(distTestTargetDirectorPath, { recursive: true, force: true })
mkdirSync(getFolderTestTargetDirectoryPath("project"), {
mode: "777",
recursive: true,
})
mkdirSync(getFolderTestTargetDirectoryPath("flags"), {
mode: "777",
recursive: true,
})
})
afterAll(() => {
process.env = OLD_ENV
rmSync(distTestTargetDirectorPath, { recursive: true, force: true })
})
it("should load the flag from project", async () => {
writeFileSync(
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
buildFeatureFlag("flag-1", true)
)
const flags = await loadFeatureFlags(
{ featureFlags: { flag_1: false } },
undefined,
getFolderTestTargetDirectoryPath("flags")
)
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
})
it("should load the default feature flags", async () => {
writeFileSync(
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
buildFeatureFlag("flag-1", true)
)
const flags = await loadFeatureFlags(
{},
undefined,
getFolderTestTargetDirectoryPath("flags")
)
expect(flags.isFeatureEnabled("flag_1")).toEqual(true)
})
it("should load the flag from env", async () => {
process.env.MEDUSA_FF_FLAG_1 = "false"
writeFileSync(
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
buildFeatureFlag("flag-1", true)
)
const flags = await loadFeatureFlags(
{},
undefined,
getFolderTestTargetDirectoryPath("flags")
)
expect(flags.isFeatureEnabled("flag_1")).toEqual(false)
})
it("should load mix of flags", async () => {
process.env.MEDUSA_FF_FLAG_3 = "false"
writeFileSync(
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-1.js"),
buildFeatureFlag("flag-1", true)
)
writeFileSync(
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-2.js"),
buildFeatureFlag("flag-2", true)
)
writeFileSync(
resolve(getFolderTestTargetDirectoryPath("flags"), "flag-3.js"),
buildFeatureFlag("flag-3", true)
)
const flags = await loadFeatureFlags(
{ featureFlags: { flag_2: false } },
undefined,
getFolderTestTargetDirectoryPath("flags")
)
expect(flags.isFeatureEnabled("flag_1")).toEqual(true)
expect(flags.isFeatureEnabled("flag_2")).toEqual(false)
expect(flags.isFeatureEnabled("flag_3")).toEqual(false)
})
})

View File

@@ -56,6 +56,7 @@ export default (rootDirectory: string): ConfigModule => {
cookie_secret: cookie_secret ?? "supersecret",
...configModule?.projectConfig,
},
featureFlags: configModule?.featureFlags ?? {},
plugins: configModule?.plugins ?? [],
}
}

View File

@@ -0,0 +1,68 @@
import path from "path"
import glob from "glob"
import { FlagSettings } from "../../types/feature-flags"
import { FlagRouter } from "../../utils/flag-router"
import { Logger } from "../../types/global"
const isTruthy = (val: string | boolean | undefined): boolean => {
if (typeof val === "string") {
return val.toLowerCase() === "true"
}
return !!val
}
export default (
configModule: { featureFlags?: Record<string, string | boolean> } = {},
logger?: Logger,
flagDirectory?: string
): FlagRouter => {
const { featureFlags: projectConfigFlags = {} } = configModule
const flagDir = path.join(flagDirectory || __dirname, "*.js")
const supportedFlags = glob.sync(flagDir, {
ignore: ["**/index.js"],
})
const flagConfig: Record<string, boolean> = {}
for (const flag of supportedFlags) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const importedModule = require(flag)
if (!importedModule.default) {
continue
}
const flagSettings: FlagSettings = importedModule.default
switch (true) {
case typeof process.env[flagSettings.env_key] !== "undefined":
if (logger) {
logger.info(
`Using flag ${flagSettings.env_key} from environment with value ${
process.env[flagSettings.env_key]
}`
)
}
flagConfig[flagSettings.key] = isTruthy(
process.env[flagSettings.env_key]
)
break
case typeof projectConfigFlags[flagSettings.key] !== "undefined":
if (logger) {
logger.info(
`Using flag ${flagSettings.key} from project config with value ${
projectConfigFlags[flagSettings.key]
}`
)
}
flagConfig[flagSettings.key] = isTruthy(
projectConfigFlags[flagSettings.key]
)
break
default:
flagConfig[flagSettings.key] = flagSettings.default_val
}
}
return new FlagRouter(flagConfig)
}

View File

@@ -1,10 +1,11 @@
import loadConfig from './config'
import loadConfig from "./config"
import "reflect-metadata"
import Logger from "./logger"
import apiLoader from "./api"
import featureFlagsLoader from "./feature-flags"
import databaseLoader from "./database"
import defaultsLoader from "./defaults"
import expressLoader from "./express"
import expressLoader from "./express"
import modelsLoader from "./models"
import passportLoader from "./passport"
import pluginsLoader, { registerPluginModels } from "./plugins"
@@ -18,35 +19,50 @@ import subscribersLoader from "./subscribers"
import { ClassOrFunctionReturning } from "awilix/lib/container"
import { Connection, getManager } from "typeorm"
import { Express, NextFunction, Request, Response } from "express"
import { asFunction, asValue, AwilixContainer, createContainer, Resolver } from "awilix"
import {
asFunction,
asValue,
AwilixContainer,
createContainer,
Resolver,
} from "awilix"
import { track } from "medusa-telemetry"
import { MedusaContainer } from "../types/global"
type Options = {
directory: string;
expressApp: Express;
directory: string
expressApp: Express
isTest: boolean
}
export default async (
{
directory: rootDirectory,
expressApp,
isTest
}: Options
): Promise<{ container: MedusaContainer; dbConnection: Connection; app: Express }> => {
export default async ({
directory: rootDirectory,
expressApp,
isTest,
}: Options): Promise<{
container: MedusaContainer
dbConnection: Connection
app: Express
}> => {
const configModule = loadConfig(rootDirectory)
const container = createContainer() as MedusaContainer
container.register('configModule', asValue(configModule))
container.register("configModule", asValue(configModule))
container.registerAdd = function (this: MedusaContainer, name: string, registration: typeof asFunction | typeof asValue) {
container.registerAdd = function (
this: MedusaContainer,
name: string,
registration: typeof asFunction | typeof asValue
) {
const storeKey = name + "_STORE"
if (this.registrations[storeKey] === undefined) {
this.register(storeKey, asValue([] as Resolver<unknown>[]))
}
const store = this.resolve(storeKey) as (ClassOrFunctionReturning<unknown> | Resolver<unknown>)[]
const store = this.resolve(storeKey) as (
| ClassOrFunctionReturning<unknown>
| Resolver<unknown>
)[]
if (this.registrations[name] === undefined) {
this.register(name, asArray(store))
@@ -59,15 +75,17 @@ export default async (
// Add additional information to context of request
expressApp.use((req: Request, res: Response, next: NextFunction) => {
const ipAddress = requestIp.getClientIp(req) as string
(req as any).request_context = {
;(req as any).request_context = {
ip_address: ipAddress,
}
next()
})
const featureFlagRouter = featureFlagsLoader(configModule, Logger)
container.register({
logger: asValue(Logger)
logger: asValue(Logger),
featureFlagRouter: asValue(featureFlagRouter),
})
await redisLoader({ container, configModule, logger: Logger })
@@ -83,7 +101,7 @@ export default async (
await registerPluginModels({
rootDirectory,
container,
configModule
configModule,
})
const pmAct = Logger.success(pmActivity, "Plugin models initialized") || {}
track("PLUGIN_MODELS_INIT_COMPLETED", { duration: pmAct.duration })
@@ -100,7 +118,7 @@ export default async (
const dbAct = Logger.success(dbActivity, "Database initialized") || {}
track("DATABASE_INIT_COMPLETED", { duration: dbAct.duration })
container.register({ manager: asValue(dbConnection.manager), })
container.register({ manager: asValue(dbConnection.manager) })
const stratActivity = Logger.activity("Initializing strategies")
track("STRATEGIES_INIT_STARTED")
@@ -123,8 +141,8 @@ export default async (
// Add the registered services to the request scope
expressApp.use((req: Request, res: Response, next: NextFunction) => {
container.register({ manager: asValue(getManager()) });
(req as any).scope = container.createScope()
container.register({ manager: asValue(getManager()) })
;(req as any).scope = container.createScope()
next()
})

View File

@@ -0,0 +1,10 @@
export interface IFlagRouter {
isFeatureEnabled: (key: string) => boolean
}
export type FlagSettings = {
key: string
description: string
env_key: string
default_val: boolean
}

View File

@@ -52,6 +52,7 @@ export type ConfigModule = {
store_cors?: string
admin_cors?: string
}
featureFlags: Record<string, boolean | string>
plugins: (
| {
resolve: string

View File

@@ -1,6 +1,6 @@
import { getConfigFile } from "medusa-core-utils"
import path from "path"
import { Column, ColumnOptions, ColumnType } from "typeorm"
import path from "path"
import { getConfigFile } from "medusa-core-utils"
const pgSqliteTypeMapping: { [key: string]: ColumnType } = {
increment: "rowid",

View File

@@ -0,0 +1,66 @@
import { getConfigFile } from "medusa-core-utils"
import { Column, ColumnOptions, Entity, EntityOptions } from "typeorm"
import featureFlagsLoader from "../loaders/feature-flags"
import path from "path"
import { ConfigModule } from "../types/global"
import { FlagRouter } from "./flag-router"
export function FeatureFlagColumn(
featureFlag: string,
columnOptions: ColumnOptions
): PropertyDecorator {
const featureFlagRouter = getFeatureFlagRouter()
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
return (): void => {
// noop
}
}
return Column(columnOptions)
}
export function FeatureFlagDecorators(
featureFlag: string,
decorators: PropertyDecorator[]
): PropertyDecorator {
const featureFlagRouter = getFeatureFlagRouter()
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
return (): void => {
// noop
}
}
// eslint-disable-next-line @typescript-eslint/ban-types
return (target: Object, propertyKey: string | symbol): void => {
decorators.forEach((decorator) => {
decorator(target, propertyKey)
})
}
}
export function FeatureFlagEntity(
featureFlag: string,
name?: string,
options?: EntityOptions
): ClassDecorator {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (target: Function): void {
target["isFeatureEnabled"] = function (): boolean {
const featureFlagRouter = getFeatureFlagRouter()
// const featureFlagRouter = featureFlagsLoader(configModule)
return featureFlagRouter.isFeatureEnabled(featureFlag)
}
Entity(name, options)(target)
}
}
function getFeatureFlagRouter(): FlagRouter {
const { configModule } = getConfigFile(
path.resolve("."),
`medusa-config`
) as { configModule: ConfigModule }
return featureFlagsLoader(configModule)
}

View File

@@ -0,0 +1,17 @@
import { IFlagRouter } from "../types/feature-flags"
export class FlagRouter implements IFlagRouter {
private flags: Record<string, boolean> = {}
constructor(flags: Record<string, boolean>) {
this.flags = flags
}
public isFeatureEnabled(key: string): boolean {
return !!this.flags[key]
}
public setFlag(key: string, value = true): void {
this.flags[key] = value
}
}