@terreno/feature-flags 57.2.0 → 57.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @terreno/feature-flags
2
+
3
+ Feature flags and A/B testing plugin for @terreno/api.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @terreno/feature-flags
9
+ ```
10
+
11
+ Peer dependency: `mongoose ^8.0.0 || ^9.0.0`. `@terreno/api` is required at runtime.
12
+
13
+ ## Quick start
14
+
15
+ ```typescript
16
+ import {TerrenoApp} from "@terreno/api";
17
+ import {AdminApp} from "@terreno/admin-backend";
18
+ import {FeatureFlagsApp, featureFlagAdminConfig} from "@terreno/feature-flags";
19
+ import {User} from "./models/user";
20
+
21
+ new TerrenoApp({userModel: User})
22
+ .register(new FeatureFlagsApp({}))
23
+ .register(new AdminApp({models: [featureFlagAdminConfig]}))
24
+ .start();
25
+ ```
26
+
27
+ Authenticated clients load flags from `GET /feature-flags/flagConfiguration`. Prefer that bulk map over the deprecated `GET /feature-flags/evaluate` endpoint (responses include `Deprecation: true` and a `Sunset` header).
28
+
29
+ Pass `liveUpdates: {socketIoServer: io}` to broadcast flag-key changes over Socket.io. Live updates require MongoDB running as a replica set (a single-node replica set is enough).
30
+
31
+ ## What's included
32
+
33
+ - `FeatureFlagsApp` — TerrenoPlugin for admin CRUD, `/flagConfiguration`, and `/evaluate`
34
+ - `FeatureFlag` — Mongoose model for flag documents
35
+ - `featureFlagAdminConfig` — ready-to-use `AdminApp` model config
36
+ - `evaluateFlag` / `evaluateAllFlags` — server-side evaluation helpers
37
+ - `MongoFeatureFlagProvider` — OpenFeature server provider
38
+ - Optional socket live updates on change streams
39
+
40
+ ## Documentation
41
+
42
+ Full API reference: [docs/reference/feature-flags.md](https://github.com/flourishhealth/terreno/blob/master/docs/reference/feature-flags.md)
43
+
44
+ How-to: [Add feature flags](https://github.com/flourishhealth/terreno/blob/master/docs/how-to/add-feature-flags.md)
45
+
46
+ ## License and Contributing
47
+
48
+ Licensed under the [MIT License](https://github.com/flourishhealth/terreno/blob/master/LICENSE). See [CONTRIBUTING.md](https://github.com/flourishhealth/terreno/blob/master/CONTRIBUTING.md) for contribution guidelines.
package/package.json CHANGED
@@ -5,19 +5,18 @@
5
5
  },
6
6
  "dependencies": {
7
7
  "@openfeature/server-sdk": "^1.22.0",
8
- "@terreno/api": "57.2.0",
8
+ "@terreno/api": "57.4.0",
9
9
  "express": "^5.2.1",
10
10
  "lodash": "^4.18.1"
11
11
  },
12
- "description": "Feature flags and A/B testing plugin for @terreno/api",
12
+ "description": "Feature flags and A/B testing plugin for @terreno/api — part of Terreno, the batteries-included TypeScript framework for universal apps.",
13
13
  "devDependencies": {
14
14
  "@biomejs/biome": "^2.3.6",
15
- "@terreno/test": "57.2.0",
15
+ "@terreno/test": "57.4.0",
16
16
  "@types/bun": "^1.4.0",
17
17
  "@types/express": "^5.0.6",
18
18
  "@types/lodash": "^4.17.17",
19
19
  "@types/supertest": "^7.2.0",
20
- "passport-local-mongoose": "^9.0.1",
21
20
  "supertest": "^7.2.2",
22
21
  "typescript": "~6.0.3"
23
22
  },
@@ -30,11 +29,11 @@
30
29
  "terreno"
31
30
  ],
32
31
  "license": "MIT",
32
+ "main": "dist/index.js",
33
+ "name": "@terreno/feature-flags",
33
34
  "peerDependencies": {
34
35
  "mongoose": "^8.0.0 || ^9.0.0"
35
36
  },
36
- "main": "dist/index.js",
37
- "name": "@terreno/feature-flags",
38
37
  "publishConfig": {
39
38
  "access": "public"
40
39
  },
@@ -54,5 +53,5 @@
54
53
  "test:coverage": "bun run ../scripts/check-coverage.ts"
55
54
  },
56
55
  "types": "dist/index.d.ts",
57
- "version": "57.2.0"
56
+ "version": "57.4.0"
58
57
  }
@@ -0,0 +1,80 @@
1
+ import {describe, expect, it} from "bun:test";
2
+
3
+ import {buildFlagDefinition, effectiveDefaultVariantForFlag} from "./flagConfiguration";
4
+ import type {FeatureFlagDocument, SegmentFunction} from "./types";
5
+
6
+ const booleanFlag = (overrides: Partial<FeatureFlagDocument> = {}): FeatureFlagDocument =>
7
+ ({
8
+ archived: false,
9
+ description: "d",
10
+ enabled: true,
11
+ key: "bool",
12
+ name: "Bool",
13
+ rolloutPercentage: 100,
14
+ rules: [],
15
+ type: "boolean",
16
+ variants: [],
17
+ ...overrides,
18
+ }) as unknown as FeatureFlagDocument;
19
+
20
+ const variantFlag = (overrides: Partial<FeatureFlagDocument> = {}): FeatureFlagDocument =>
21
+ ({
22
+ archived: false,
23
+ description: "d",
24
+ enabled: true,
25
+ key: "exp",
26
+ name: "Exp",
27
+ rolloutPercentage: 100,
28
+ rules: [],
29
+ type: "variant",
30
+ variants: [
31
+ {key: "control", weight: 50},
32
+ {key: "treatment", weight: 50},
33
+ ],
34
+ ...overrides,
35
+ }) as unknown as FeatureFlagDocument;
36
+
37
+ const noSegments: Record<string, SegmentFunction> = {};
38
+
39
+ describe("effectiveDefaultVariantForFlag", () => {
40
+ it("uses defaultVariant when present", () => {
41
+ expect(effectiveDefaultVariantForFlag(booleanFlag({defaultVariant: "on"}))).toBe("on");
42
+ });
43
+
44
+ it("defaults boolean flags to off", () => {
45
+ expect(effectiveDefaultVariantForFlag(booleanFlag())).toBe("off");
46
+ });
47
+
48
+ it("defaults variant flags to the first variant key", () => {
49
+ expect(effectiveDefaultVariantForFlag(variantFlag())).toBe("control");
50
+ expect(effectiveDefaultVariantForFlag(variantFlag({variants: []}))).toBe("off");
51
+ });
52
+ });
53
+
54
+ describe("buildFlagDefinition", () => {
55
+ it("maps a boolean evaluation to on/off variants", () => {
56
+ expect(buildFlagDefinition(booleanFlag(), "user-1", {}, noSegments)).toEqual({
57
+ defaultVariant: "on",
58
+ disabled: false,
59
+ variants: {off: false, on: true},
60
+ });
61
+ });
62
+
63
+ it("maps a disabled variant flag onto the default variant", () => {
64
+ const definition = buildFlagDefinition(variantFlag({enabled: false}), "user-1", {}, noSegments);
65
+ expect(definition.defaultVariant).toBe("control");
66
+ expect(definition.variants).toEqual({control: "control", treatment: "treatment"});
67
+ });
68
+
69
+ it("uses the evaluated variant key when it matches a defined variant", () => {
70
+ const definition = buildFlagDefinition(
71
+ variantFlag({
72
+ rules: [{field: "id", operator: "eq", value: "user-1", variant: "treatment"}],
73
+ }),
74
+ "user-1",
75
+ {id: "user-1"},
76
+ noSegments
77
+ );
78
+ expect(definition.defaultVariant).toBe("treatment");
79
+ });
80
+ });