@terreno/feature-flags 0.12.0 → 0.12.2

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.
@@ -1,2 +1,25 @@
1
1
  import type { FeatureFlagModel } from "./types";
2
2
  export declare const FeatureFlag: FeatureFlagModel;
3
+ /**
4
+ * Pre-configured admin model config for the FeatureFlag model.
5
+ *
6
+ * Pass this directly to `AdminApp({ models: [...] })` so downstream consumers
7
+ * don't have to manually configure displayName, listFields, and routePath.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import {featureFlagAdminConfig} from "@terreno/feature-flags";
12
+ * import {AdminApp} from "@terreno/admin-backend";
13
+ *
14
+ * new TerrenoApp({userModel: User})
15
+ * .register(new FeatureFlagsApp({segments}))
16
+ * .register(new AdminApp({models: [featureFlagAdminConfig]}))
17
+ * .start();
18
+ * ```
19
+ */
20
+ export declare const featureFlagAdminConfig: {
21
+ displayName: string;
22
+ listFields: string[];
23
+ model: FeatureFlagModel;
24
+ routePath: string;
25
+ };
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FeatureFlag = void 0;
6
+ exports.featureFlagAdminConfig = exports.FeatureFlag = void 0;
7
7
  var api_1 = require("@terreno/api");
8
8
  var mongoose_1 = __importDefault(require("mongoose"));
9
9
  var featureFlagSchema = new mongoose_1.default.Schema({
@@ -117,3 +117,26 @@ featureFlagSchema.pre("save", function () {
117
117
  }
118
118
  });
119
119
  exports.FeatureFlag = mongoose_1.default.model("FeatureFlag", featureFlagSchema);
120
+ /**
121
+ * Pre-configured admin model config for the FeatureFlag model.
122
+ *
123
+ * Pass this directly to `AdminApp({ models: [...] })` so downstream consumers
124
+ * don't have to manually configure displayName, listFields, and routePath.
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * import {featureFlagAdminConfig} from "@terreno/feature-flags";
129
+ * import {AdminApp} from "@terreno/admin-backend";
130
+ *
131
+ * new TerrenoApp({userModel: User})
132
+ * .register(new FeatureFlagsApp({segments}))
133
+ * .register(new AdminApp({models: [featureFlagAdminConfig]}))
134
+ * .start();
135
+ * ```
136
+ */
137
+ exports.featureFlagAdminConfig = {
138
+ displayName: "Feature Flags",
139
+ listFields: ["key", "name", "type", "enabled", "archived", "created"],
140
+ model: exports.FeatureFlag,
141
+ routePath: "/feature-flags",
142
+ };
@@ -77,9 +77,9 @@ var FeatureFlagsApp = /** @class */ (function () {
77
77
  }
78
78
  FeatureFlagsApp.prototype.register = function (app, openApi) {
79
79
  var _this = this;
80
- var _a;
80
+ var _a, _b;
81
81
  var basePath = (_a = this.options.basePath) !== null && _a !== void 0 ? _a : "/feature-flags";
82
- var routerOptions = __assign(__assign({}, (openApi ? { openApi: openApi } : {})), { permissions: {
82
+ var routerOptions = __assign(__assign({}, (openApi ? { openApi: openApi } : {})), { permissions: (_b = this.options.permissions) !== null && _b !== void 0 ? _b : {
83
83
  create: [api_1.Permissions.IsAdmin],
84
84
  delete: [api_1.Permissions.IsAdmin],
85
85
  list: [api_1.Permissions.IsAdmin],
@@ -109,10 +109,13 @@ var FeatureFlagsApp = /** @class */ (function () {
109
109
  }); }));
110
110
  // GET /feature-flags/segments — list registered segment names (admin only)
111
111
  app.get("".concat(basePath, "/segments"), (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
112
- var user;
112
+ var user, allowed;
113
113
  return __generator(this, function (_a) {
114
114
  user = req.user;
115
- if (!(user === null || user === void 0 ? void 0 : user.admin)) {
115
+ allowed = this.options.segmentsPermission
116
+ ? this.options.segmentsPermission(user)
117
+ : Boolean(user === null || user === void 0 ? void 0 : user.admin);
118
+ if (!allowed) {
116
119
  throw new api_1.APIError({ status: 403, title: "Only admins can view segments" });
117
120
  }
118
121
  return [2 /*return*/, res.json({ data: Object.keys(this.segments) })];
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { deterministicHash, evaluateAllFlags, evaluateFlag } from "./evaluate";
2
- export { FeatureFlag } from "./featureFlagModel";
2
+ export { FeatureFlag, featureFlagAdminConfig } from "./featureFlagModel";
3
3
  export { FeatureFlagsApp } from "./featureFlagsApp";
4
4
  export type { EvaluationResult, FeatureFlagDocument, FeatureFlagModel, FeatureFlagRule, FeatureFlagsOptions, FeatureFlagType, FeatureFlagVariant, SegmentFunction, } from "./types";
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FeatureFlagsApp = exports.FeatureFlag = exports.evaluateFlag = exports.evaluateAllFlags = exports.deterministicHash = void 0;
3
+ exports.FeatureFlagsApp = exports.featureFlagAdminConfig = exports.FeatureFlag = exports.evaluateFlag = exports.evaluateAllFlags = exports.deterministicHash = void 0;
4
4
  var evaluate_1 = require("./evaluate");
5
5
  Object.defineProperty(exports, "deterministicHash", { enumerable: true, get: function () { return evaluate_1.deterministicHash; } });
6
6
  Object.defineProperty(exports, "evaluateAllFlags", { enumerable: true, get: function () { return evaluate_1.evaluateAllFlags; } });
7
7
  Object.defineProperty(exports, "evaluateFlag", { enumerable: true, get: function () { return evaluate_1.evaluateFlag; } });
8
8
  var featureFlagModel_1 = require("./featureFlagModel");
9
9
  Object.defineProperty(exports, "FeatureFlag", { enumerable: true, get: function () { return featureFlagModel_1.FeatureFlag; } });
10
+ Object.defineProperty(exports, "featureFlagAdminConfig", { enumerable: true, get: function () { return featureFlagModel_1.featureFlagAdminConfig; } });
10
11
  var featureFlagsApp_1 = require("./featureFlagsApp");
11
12
  Object.defineProperty(exports, "FeatureFlagsApp", { enumerable: true, get: function () { return featureFlagsApp_1.FeatureFlagsApp; } });
@@ -340,3 +340,18 @@ var featureFlagModel_1 = require("../featureFlagModel");
340
340
  });
341
341
  }); });
342
342
  });
343
+ (0, bun_test_1.describe)("featureFlagAdminConfig", function () {
344
+ (0, bun_test_1.it)("has the expected shape for AdminApp registration", function () {
345
+ (0, bun_test_1.expect)(featureFlagModel_1.featureFlagAdminConfig.model).toBe(featureFlagModel_1.FeatureFlag);
346
+ (0, bun_test_1.expect)(featureFlagModel_1.featureFlagAdminConfig.routePath).toBe("/feature-flags");
347
+ (0, bun_test_1.expect)(featureFlagModel_1.featureFlagAdminConfig.displayName).toBe("Feature Flags");
348
+ (0, bun_test_1.expect)(featureFlagModel_1.featureFlagAdminConfig.listFields).toEqual([
349
+ "key",
350
+ "name",
351
+ "type",
352
+ "enabled",
353
+ "archived",
354
+ "created",
355
+ ]);
356
+ });
357
+ });
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { FindExactlyOnePlugin, FindOneOrNonePlugin } from "@terreno/api";
1
+ import type { FindExactlyOnePlugin, FindOneOrNonePlugin, ModelRouterOptions } from "@terreno/api";
2
2
  import type mongoose from "mongoose";
3
3
  export interface FeatureFlagVariant {
4
4
  key: string;
@@ -32,5 +32,18 @@ export type SegmentFunction = (user: unknown) => boolean;
32
32
  export interface FeatureFlagsOptions {
33
33
  basePath?: string;
34
34
  segments?: Record<string, SegmentFunction>;
35
+ /**
36
+ * Permission predicates that gate the admin CRUD routes for flags. When
37
+ * omitted, defaults to requiring `Permissions.IsAdmin` (i.e. `user.admin === true`)
38
+ * on every method. Provide this when consumers need role-based gating
39
+ * other than the boolean `admin` flag.
40
+ */
41
+ permissions?: ModelRouterOptions<FeatureFlagDocument>["permissions"];
42
+ /**
43
+ * Predicate used to gate the `/segments` admin endpoint. Receives the
44
+ * authenticated user and should return true to allow the request. Defaults
45
+ * to checking `user.admin === true` when not provided.
46
+ */
47
+ segmentsPermission?: (user: unknown) => boolean;
35
48
  }
36
49
  export type EvaluationResult = Record<string, boolean | string | null>;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "url": "https://github.com/FlourishHealth/terreno/issues"
5
5
  },
6
6
  "dependencies": {
7
- "@terreno/api": "0.12.0",
7
+ "@terreno/api": "0.12.2",
8
8
  "express": "^5.2.1",
9
9
  "lodash": "^4.17.23"
10
10
  },
@@ -51,5 +51,5 @@
51
51
  "test:coverage": "bun run ../scripts/check-coverage.ts"
52
52
  },
53
53
  "types": "dist/index.d.ts",
54
- "version": "0.12.0"
54
+ "version": "0.12.2"
55
55
  }
@@ -136,3 +136,27 @@ export const FeatureFlag = mongoose.model<FeatureFlagDocument, FeatureFlagModel>
136
136
  "FeatureFlag",
137
137
  featureFlagSchema
138
138
  );
139
+
140
+ /**
141
+ * Pre-configured admin model config for the FeatureFlag model.
142
+ *
143
+ * Pass this directly to `AdminApp({ models: [...] })` so downstream consumers
144
+ * don't have to manually configure displayName, listFields, and routePath.
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * import {featureFlagAdminConfig} from "@terreno/feature-flags";
149
+ * import {AdminApp} from "@terreno/admin-backend";
150
+ *
151
+ * new TerrenoApp({userModel: User})
152
+ * .register(new FeatureFlagsApp({segments}))
153
+ * .register(new AdminApp({models: [featureFlagAdminConfig]}))
154
+ * .start();
155
+ * ```
156
+ */
157
+ export const featureFlagAdminConfig = {
158
+ displayName: "Feature Flags",
159
+ listFields: ["key", "name", "type", "enabled", "archived", "created"],
160
+ model: FeatureFlag,
161
+ routePath: "/feature-flags",
162
+ };
@@ -45,7 +45,7 @@ export class FeatureFlagsApp implements TerrenoPlugin {
45
45
  const basePath = this.options.basePath ?? "/feature-flags";
46
46
  const routerOptions: ModelRouterOptions<FeatureFlagDocument> = {
47
47
  ...(openApi ? {openApi: openApi as OpenApiMiddleware} : {}),
48
- permissions: {
48
+ permissions: this.options.permissions ?? {
49
49
  create: [Permissions.IsAdmin],
50
50
  delete: [Permissions.IsAdmin],
51
51
  list: [Permissions.IsAdmin],
@@ -83,8 +83,11 @@ export class FeatureFlagsApp implements TerrenoPlugin {
83
83
  `${basePath}/segments`,
84
84
  authenticateMiddleware(),
85
85
  asyncHandler(async (req: express.Request, res: express.Response) => {
86
- const user = req.user as {admin?: boolean} | undefined;
87
- if (!user?.admin) {
86
+ const user = req.user;
87
+ const allowed = this.options.segmentsPermission
88
+ ? this.options.segmentsPermission(user)
89
+ : Boolean((user as {admin?: boolean} | undefined)?.admin);
90
+ if (!allowed) {
88
91
  throw new APIError({status: 403, title: "Only admins can view segments"});
89
92
  }
90
93
 
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export {deterministicHash, evaluateAllFlags, evaluateFlag} from "./evaluate";
2
- export {FeatureFlag} from "./featureFlagModel";
2
+ export {FeatureFlag, featureFlagAdminConfig} from "./featureFlagModel";
3
3
  export {FeatureFlagsApp} from "./featureFlagsApp";
4
4
  export type {
5
5
  EvaluationResult,
@@ -1,5 +1,5 @@
1
1
  import {afterEach, describe, expect, it} from "bun:test";
2
- import {FeatureFlag} from "../featureFlagModel";
2
+ import {FeatureFlag, featureFlagAdminConfig} from "../featureFlagModel";
3
3
 
4
4
  describe("FeatureFlag model", () => {
5
5
  afterEach(async () => {
@@ -203,3 +203,19 @@ describe("FeatureFlag model", () => {
203
203
  expect(maybe).toBeNull();
204
204
  });
205
205
  });
206
+
207
+ describe("featureFlagAdminConfig", () => {
208
+ it("has the expected shape for AdminApp registration", () => {
209
+ expect(featureFlagAdminConfig.model).toBe(FeatureFlag);
210
+ expect(featureFlagAdminConfig.routePath).toBe("/feature-flags");
211
+ expect(featureFlagAdminConfig.displayName).toBe("Feature Flags");
212
+ expect(featureFlagAdminConfig.listFields).toEqual([
213
+ "key",
214
+ "name",
215
+ "type",
216
+ "enabled",
217
+ "archived",
218
+ "created",
219
+ ]);
220
+ });
221
+ });
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type {FindExactlyOnePlugin, FindOneOrNonePlugin} from "@terreno/api";
1
+ import type {FindExactlyOnePlugin, FindOneOrNonePlugin, ModelRouterOptions} from "@terreno/api";
2
2
  import type mongoose from "mongoose";
3
3
 
4
4
  export interface FeatureFlagVariant {
@@ -41,6 +41,19 @@ export type SegmentFunction = (user: unknown) => boolean;
41
41
  export interface FeatureFlagsOptions {
42
42
  basePath?: string;
43
43
  segments?: Record<string, SegmentFunction>;
44
+ /**
45
+ * Permission predicates that gate the admin CRUD routes for flags. When
46
+ * omitted, defaults to requiring `Permissions.IsAdmin` (i.e. `user.admin === true`)
47
+ * on every method. Provide this when consumers need role-based gating
48
+ * other than the boolean `admin` flag.
49
+ */
50
+ permissions?: ModelRouterOptions<FeatureFlagDocument>["permissions"];
51
+ /**
52
+ * Predicate used to gate the `/segments` admin endpoint. Receives the
53
+ * authenticated user and should return true to allow the request. Defaults
54
+ * to checking `user.admin === true` when not provided.
55
+ */
56
+ segmentsPermission?: (user: unknown) => boolean;
44
57
  }
45
58
 
46
59
  export type EvaluationResult = Record<string, boolean | string | null>;