@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.
- package/dist/featureFlagModel.d.ts +23 -0
- package/dist/featureFlagModel.js +24 -1
- package/dist/featureFlagsApp.js +7 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/tests/featureFlagModel.test.js +15 -0
- package/dist/types.d.ts +14 -1
- package/package.json +2 -2
- package/src/featureFlagModel.ts +24 -0
- package/src/featureFlagsApp.ts +6 -3
- package/src/index.ts +1 -1
- package/src/tests/featureFlagModel.test.ts +17 -1
- package/src/types.ts +14 -1
|
@@ -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
|
+
};
|
package/dist/featureFlagModel.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/featureFlagsApp.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
54
|
+
"version": "0.12.2"
|
|
55
55
|
}
|
package/src/featureFlagModel.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/featureFlagsApp.ts
CHANGED
|
@@ -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
|
|
87
|
-
|
|
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>;
|