feat(medusa, utils): Allow object feature flags (#4701)
Feature flags can be set as follows:
**Environment variables**
```
MEDUSA_FF_ANALYTICS=true
MEDUSA_FF_WORKFLOWS=createProducts,addShippingMethods
```
**Project config**
```
{
featureFlags: {
analytics: true,
workflows: {
createProducts: true,
addShippingMethods: true,
}
}
}
```
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
export * from "./analytics"
|
||||
export * from "./order-editing"
|
||||
export * from "./sales-channels"
|
||||
export * from "./product-categories"
|
||||
export * from "./publishable-api-keys"
|
||||
export * from "./sales-channels"
|
||||
export * from "./tax-inclusive-pricing"
|
||||
export * from "./utils"
|
||||
export * from "./workflows"
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { FlagRouter } from "../flag-router"
|
||||
|
||||
const someFlag = {
|
||||
key: "some_flag",
|
||||
default_val: false,
|
||||
env_key: "MEDUSA_FF_SOME_FLAG",
|
||||
description: "[WIP] Enable some flag",
|
||||
}
|
||||
|
||||
const workflows = {
|
||||
key: "workflows",
|
||||
default_val: {},
|
||||
env_key: "MEDUSA_FF_WORKFLOWS",
|
||||
description: "[WIP] Enable workflows",
|
||||
}
|
||||
|
||||
describe("FlagRouter", function () {
|
||||
it("should set a top-level flag", async function () {
|
||||
const flagRouter = new FlagRouter({})
|
||||
|
||||
flagRouter.setFlag(someFlag.key, true)
|
||||
|
||||
expect(flagRouter.listFlags()).toEqual([
|
||||
{
|
||||
key: someFlag.key,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should set a nested flag", async function () {
|
||||
const flagRouter = new FlagRouter({})
|
||||
|
||||
flagRouter.setFlag(workflows.key, { createCart: true })
|
||||
|
||||
expect(flagRouter.listFlags()).toEqual([
|
||||
{
|
||||
key: workflows.key,
|
||||
value: {
|
||||
createCart: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should append to a nested flag", async function () {
|
||||
const flagRouter = new FlagRouter({})
|
||||
|
||||
flagRouter.setFlag(workflows.key, { createCart: true })
|
||||
flagRouter.setFlag(workflows.key, { addShippingMethod: true })
|
||||
|
||||
expect(flagRouter.listFlags()).toEqual([
|
||||
{
|
||||
key: workflows.key,
|
||||
value: {
|
||||
createCart: true,
|
||||
addShippingMethod: true,
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should check if top-level flag is enabled", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[someFlag.key]: true,
|
||||
})
|
||||
|
||||
const isEnabled = flagRouter.isFeatureEnabled(someFlag.key)
|
||||
|
||||
expect(isEnabled).toEqual(true)
|
||||
})
|
||||
|
||||
it("should check if nested flag is enabled", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[workflows.key]: {
|
||||
createCart: true,
|
||||
},
|
||||
})
|
||||
|
||||
const isEnabled = flagRouter.isFeatureEnabled({ workflows: "createCart" })
|
||||
|
||||
expect(isEnabled).toEqual(true)
|
||||
})
|
||||
|
||||
it("should check if nested flag is enabled using top-level access", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[workflows.key]: {
|
||||
createCart: true,
|
||||
},
|
||||
})
|
||||
|
||||
const isEnabled = flagRouter.isFeatureEnabled(workflows.key)
|
||||
|
||||
expect(isEnabled).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return true if top-level is enabled using nested-level access", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[workflows.key]: true,
|
||||
})
|
||||
|
||||
const isEnabled = flagRouter.isFeatureEnabled({
|
||||
[workflows.key]: "createCart",
|
||||
})
|
||||
|
||||
expect(isEnabled).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return false if flag is disabled using top-level access", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[workflows.key]: false,
|
||||
})
|
||||
|
||||
const isEnabled = flagRouter.isFeatureEnabled(workflows.key)
|
||||
|
||||
expect(isEnabled).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return false if nested flag is disabled", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[workflows.key]: {
|
||||
createCart: false,
|
||||
},
|
||||
})
|
||||
|
||||
const isEnabled = flagRouter.isFeatureEnabled({ workflows: "createCart" })
|
||||
|
||||
expect(isEnabled).toEqual(false)
|
||||
})
|
||||
|
||||
it("should initialize with both types of flags", async function () {
|
||||
const flagRouter = new FlagRouter({
|
||||
[workflows.key]: {
|
||||
createCart: true,
|
||||
},
|
||||
[someFlag.key]: true,
|
||||
})
|
||||
|
||||
const flags = flagRouter.listFlags()
|
||||
|
||||
expect(flags).toEqual([
|
||||
{
|
||||
key: workflows.key,
|
||||
value: {
|
||||
createCart: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: someFlag.key,
|
||||
value: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,68 @@
|
||||
import { FeatureFlagTypes } from "@medusajs/types"
|
||||
import { isObject, isString } from "../../common"
|
||||
|
||||
export class FlagRouter implements FeatureFlagTypes.IFlagRouter {
|
||||
private readonly flags: Record<string, boolean> = {}
|
||||
private readonly flags: Record<string, boolean | Record<string, boolean>> = {}
|
||||
|
||||
constructor(flags: Record<string, boolean>) {
|
||||
constructor(flags: Record<string, boolean | Record<string, boolean>>) {
|
||||
this.flags = flags
|
||||
}
|
||||
|
||||
public isFeatureEnabled(key: string): boolean {
|
||||
return !!this.flags[key]
|
||||
/**
|
||||
* 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 | Record<string, string>): boolean {
|
||||
if (isString(flag)) {
|
||||
return !!this.flags[flag]
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
throw Error("Flag must be a string or an object")
|
||||
}
|
||||
|
||||
public setFlag(key: string, value = true): void {
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
|
||||
8
packages/utils/src/feature-flags/workflows.ts
Normal file
8
packages/utils/src/feature-flags/workflows.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FeatureFlagTypes } from "@medusajs/types"
|
||||
|
||||
export const WorkflowsFeatureFlag: FeatureFlagTypes.FlagSettings = {
|
||||
key: "workflows",
|
||||
default_val: false,
|
||||
env_key: "MEDUSA_FF_WORKFLOWS",
|
||||
description: "[WIP] Enable workflows",
|
||||
}
|
||||
Reference in New Issue
Block a user