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:
Oli Juhl
2023-08-07 09:38:25 +00:00
committed by GitHub
parent 03fb0479c0
commit 5c60aad177
17 changed files with 478 additions and 115 deletions
+59 -8
View File
@@ -1,21 +1,72 @@
import { FeatureFlagsResponse, IFlagRouter } from "../types/feature-flags"
import { FeatureFlagTypes } from "@medusajs/types"
import { isObject, isString } from "@medusajs/utils"
export class FlagRouter implements IFlagRouter {
private readonly flags: Record<string, boolean> = {}
export class FlagRouter implements FeatureFlagTypes.IFlagRouter {
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
}
public listFlags(): FeatureFlagsResponse {
public listFlags(): FeatureFlagTypes.FeatureFlagsResponse {
return Object.entries(this.flags || {}).map(([key, value]) => ({
key,
value,