@terreno/feature-flags 0.19.0 → 0.20.1
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.js +38 -1
- package/dist/featureFlagsApp.d.ts +1 -0
- package/dist/featureFlagsApp.js +124 -6
- package/dist/flagConfiguration.d.ts +11 -0
- package/dist/flagConfiguration.js +74 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/openFeatureProvider.d.ts +22 -0
- package/dist/openFeatureProvider.js +150 -0
- package/dist/tests/featureFlagModel.test.js +71 -0
- package/dist/tests/featureFlagsApp.liveUpdates.test.d.ts +1 -0
- package/dist/tests/featureFlagsApp.liveUpdates.test.js +203 -0
- package/dist/tests/featureFlagsApp.test.js +149 -0
- package/dist/tests/openFeatureProvider.test.d.ts +1 -0
- package/dist/tests/openFeatureProvider.test.js +395 -0
- package/dist/types.d.ts +31 -0
- package/package.json +3 -2
- package/src/featureFlagModel.ts +40 -1
- package/src/featureFlagsApp.ts +124 -4
- package/src/flagConfiguration.ts +57 -0
- package/src/index.ts +6 -0
- package/src/openFeatureProvider.ts +141 -0
- package/src/tests/featureFlagModel.test.ts +46 -0
- package/src/tests/featureFlagsApp.liveUpdates.test.ts +114 -0
- package/src/tests/featureFlagsApp.test.ts +101 -0
- package/src/tests/openFeatureProvider.test.ts +289 -0
- package/src/types.ts +35 -0
package/dist/featureFlagModel.js
CHANGED
|
@@ -12,6 +12,13 @@ var featureFlagSchema = new mongoose_1.default.Schema({
|
|
|
12
12
|
description: "Archived flags are excluded from evaluation. Use this instead of deleting flags to prevent bloat as new features are added.",
|
|
13
13
|
type: Boolean,
|
|
14
14
|
},
|
|
15
|
+
defaultVariant: {
|
|
16
|
+
description: "OpenFeature defaultVariant key. Returned when the flag is disabled or errors during evaluation. " +
|
|
17
|
+
"For boolean flags, must be 'on' or 'off'. For variant flags, must be one of the keys in `variants`. " +
|
|
18
|
+
"Auto-populated on save when omitted (boolean → 'off', variant → first variant key).",
|
|
19
|
+
required: false,
|
|
20
|
+
type: String,
|
|
21
|
+
},
|
|
15
22
|
description: {
|
|
16
23
|
default: "",
|
|
17
24
|
description: "Explanation of what this flag controls",
|
|
@@ -99,6 +106,7 @@ featureFlagSchema.plugin(api_1.findExactlyOne);
|
|
|
99
106
|
featureFlagSchema.plugin(api_1.findOneOrNone);
|
|
100
107
|
featureFlagSchema.index({ archived: 1, enabled: 1 });
|
|
101
108
|
featureFlagSchema.pre("save", function () {
|
|
109
|
+
var _a;
|
|
102
110
|
if (this.type === "variant") {
|
|
103
111
|
if (!this.variants || this.variants.length === 0) {
|
|
104
112
|
throw new api_1.APIError({
|
|
@@ -115,6 +123,35 @@ featureFlagSchema.pre("save", function () {
|
|
|
115
123
|
});
|
|
116
124
|
}
|
|
117
125
|
}
|
|
126
|
+
if (this.defaultVariant === undefined ||
|
|
127
|
+
this.defaultVariant === null ||
|
|
128
|
+
this.defaultVariant === "") {
|
|
129
|
+
if (this.type === "boolean") {
|
|
130
|
+
this.set("defaultVariant", "off");
|
|
131
|
+
}
|
|
132
|
+
else if (this.type === "variant" && ((_a = this.variants) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
133
|
+
this.set("defaultVariant", this.variants[0].key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (this.defaultVariant) {
|
|
137
|
+
if (this.type === "boolean") {
|
|
138
|
+
if (this.defaultVariant !== "on" && this.defaultVariant !== "off") {
|
|
139
|
+
throw new api_1.APIError({
|
|
140
|
+
status: 400,
|
|
141
|
+
title: "Boolean flags must use defaultVariant 'on' or 'off'",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (this.type === "variant") {
|
|
146
|
+
var keys = new Set(this.variants.map(function (v) { return v.key; }));
|
|
147
|
+
if (!keys.has(this.defaultVariant)) {
|
|
148
|
+
throw new api_1.APIError({
|
|
149
|
+
status: 400,
|
|
150
|
+
title: "defaultVariant must match one of the variant keys",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
118
155
|
});
|
|
119
156
|
exports.FeatureFlag = mongoose_1.default.model("FeatureFlag", featureFlagSchema);
|
|
120
157
|
/**
|
|
@@ -136,7 +173,7 @@ exports.FeatureFlag = mongoose_1.default.model("FeatureFlag", featureFlagSchema)
|
|
|
136
173
|
*/
|
|
137
174
|
exports.featureFlagAdminConfig = {
|
|
138
175
|
displayName: "Feature Flags",
|
|
139
|
-
listFields: ["key", "name", "type", "enabled", "archived", "created"],
|
|
176
|
+
listFields: ["key", "name", "type", "enabled", "archived", "defaultVariant", "created"],
|
|
140
177
|
model: exports.FeatureFlag,
|
|
141
178
|
routePath: "/feature-flags",
|
|
142
179
|
};
|
|
@@ -22,6 +22,7 @@ import type { FeatureFlagsOptions } from "./types";
|
|
|
22
22
|
export declare class FeatureFlagsApp implements TerrenoPlugin {
|
|
23
23
|
private options;
|
|
24
24
|
private segments;
|
|
25
|
+
private mongoProvider?;
|
|
25
26
|
constructor(options?: FeatureFlagsOptions);
|
|
26
27
|
register(app: express.Application, openApi?: unknown): void;
|
|
27
28
|
}
|
package/dist/featureFlagsApp.js
CHANGED
|
@@ -46,11 +46,37 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
46
46
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
var __values = (this && this.__values) || function(o) {
|
|
50
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
51
|
+
if (m) return m.call(o);
|
|
52
|
+
if (o && typeof o.length === "number") return {
|
|
53
|
+
next: function () {
|
|
54
|
+
if (o && i >= o.length) o = void 0;
|
|
55
|
+
return { value: o && o[i++], done: !o };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
59
|
+
};
|
|
60
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
61
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
62
|
+
};
|
|
49
63
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
64
|
exports.FeatureFlagsApp = void 0;
|
|
65
|
+
var server_sdk_1 = require("@openfeature/server-sdk");
|
|
51
66
|
var api_1 = require("@terreno/api");
|
|
67
|
+
var mongoose_1 = __importDefault(require("mongoose"));
|
|
52
68
|
var evaluate_1 = require("./evaluate");
|
|
53
69
|
var featureFlagModel_1 = require("./featureFlagModel");
|
|
70
|
+
var flagConfiguration_1 = require("./flagConfiguration");
|
|
71
|
+
var openFeatureProvider_1 = require("./openFeatureProvider");
|
|
72
|
+
var evaluateDeprecationWarned = false;
|
|
73
|
+
var resolveSocketServer = function (live) {
|
|
74
|
+
var server = live.socketIoServer;
|
|
75
|
+
if (typeof server === "function") {
|
|
76
|
+
return server();
|
|
77
|
+
}
|
|
78
|
+
return server;
|
|
79
|
+
};
|
|
54
80
|
/**
|
|
55
81
|
* TerrenoPlugin that provides feature flags and A/B testing.
|
|
56
82
|
*
|
|
@@ -77,24 +103,69 @@ var FeatureFlagsApp = /** @class */ (function () {
|
|
|
77
103
|
}
|
|
78
104
|
FeatureFlagsApp.prototype.register = function (app, openApi) {
|
|
79
105
|
var _this = this;
|
|
80
|
-
var _a, _b;
|
|
106
|
+
var _a, _b, _c, _d;
|
|
81
107
|
var basePath = (_a = this.options.basePath) !== null && _a !== void 0 ? _a : "/feature-flags";
|
|
82
|
-
var
|
|
108
|
+
var openFeatureDomain = (_b = this.options.openFeatureDomain) !== null && _b !== void 0 ? _b : "feature-flags";
|
|
109
|
+
var routerOptions = __assign(__assign({}, (openApi ? { openApi: openApi } : {})), { permissions: (_c = this.options.permissions) !== null && _c !== void 0 ? _c : {
|
|
83
110
|
create: [api_1.Permissions.IsAdmin],
|
|
84
111
|
delete: [api_1.Permissions.IsAdmin],
|
|
85
112
|
list: [api_1.Permissions.IsAdmin],
|
|
86
113
|
read: [api_1.Permissions.IsAdmin],
|
|
87
114
|
update: [api_1.Permissions.IsAdmin],
|
|
88
115
|
}, sort: "-created" });
|
|
89
|
-
|
|
116
|
+
this.mongoProvider = new openFeatureProvider_1.MongoFeatureFlagProvider({
|
|
117
|
+
flagModel: featureFlagModel_1.FeatureFlag,
|
|
118
|
+
segments: this.segments,
|
|
119
|
+
});
|
|
120
|
+
server_sdk_1.OpenFeature.setProvider(openFeatureDomain, this.mongoProvider);
|
|
90
121
|
app.use("".concat(basePath, "/flags"), (0, api_1.modelRouter)(featureFlagModel_1.FeatureFlag, routerOptions));
|
|
91
|
-
|
|
122
|
+
app.get("".concat(basePath, "/flagConfiguration"), (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
123
|
+
var user, targetingKey, flags, config, flags_1, flags_1_1, flag, body;
|
|
124
|
+
var e_1, _a;
|
|
125
|
+
var _b;
|
|
126
|
+
return __generator(this, function (_c) {
|
|
127
|
+
switch (_c.label) {
|
|
128
|
+
case 0:
|
|
129
|
+
user = req.user;
|
|
130
|
+
if (!user) {
|
|
131
|
+
throw new api_1.APIError({ status: 401, title: "Authentication required" });
|
|
132
|
+
}
|
|
133
|
+
targetingKey = String((_b = user._id) !== null && _b !== void 0 ? _b : user.id);
|
|
134
|
+
return [4 /*yield*/, featureFlagModel_1.FeatureFlag.find({ archived: { $ne: true }, enabled: true })];
|
|
135
|
+
case 1:
|
|
136
|
+
flags = _c.sent();
|
|
137
|
+
config = {};
|
|
138
|
+
try {
|
|
139
|
+
for (flags_1 = __values(flags), flags_1_1 = flags_1.next(); !flags_1_1.done; flags_1_1 = flags_1.next()) {
|
|
140
|
+
flag = flags_1_1.value;
|
|
141
|
+
config[flag.key] = (0, flagConfiguration_1.buildFlagDefinition)(flag, targetingKey, user, this.segments);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
145
|
+
finally {
|
|
146
|
+
try {
|
|
147
|
+
if (flags_1_1 && !flags_1_1.done && (_a = flags_1.return)) _a.call(flags_1);
|
|
148
|
+
}
|
|
149
|
+
finally { if (e_1) throw e_1.error; }
|
|
150
|
+
}
|
|
151
|
+
body = { data: config };
|
|
152
|
+
return [2 /*return*/, res.json(body)];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}); }));
|
|
92
156
|
app.get("".concat(basePath, "/evaluate"), (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
93
|
-
var user, userId, results;
|
|
157
|
+
var sunset, user, userId, results;
|
|
94
158
|
var _a;
|
|
95
159
|
return __generator(this, function (_b) {
|
|
96
160
|
switch (_b.label) {
|
|
97
161
|
case 0:
|
|
162
|
+
if (!evaluateDeprecationWarned) {
|
|
163
|
+
evaluateDeprecationWarned = true;
|
|
164
|
+
api_1.logger.warn("[feature-flags] GET /evaluate is deprecated; migrate clients to GET /flagConfiguration or use @terreno/rtk useFeatureFlags / useTerrenoFeatureFlags.");
|
|
165
|
+
}
|
|
166
|
+
sunset = new Date(Date.now() + 90 * 86400000).toUTCString();
|
|
167
|
+
res.setHeader("Deprecation", "true");
|
|
168
|
+
res.setHeader("Sunset", sunset);
|
|
98
169
|
user = req.user;
|
|
99
170
|
if (!user) {
|
|
100
171
|
throw new api_1.APIError({ status: 401, title: "Authentication required" });
|
|
@@ -107,7 +178,6 @@ var FeatureFlagsApp = /** @class */ (function () {
|
|
|
107
178
|
}
|
|
108
179
|
});
|
|
109
180
|
}); }));
|
|
110
|
-
// GET /feature-flags/segments — list registered segment names (admin only)
|
|
111
181
|
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
182
|
var user, allowed;
|
|
113
183
|
return __generator(this, function (_a) {
|
|
@@ -121,6 +191,54 @@ var FeatureFlagsApp = /** @class */ (function () {
|
|
|
121
191
|
return [2 /*return*/, res.json({ data: Object.keys(this.segments) })];
|
|
122
192
|
});
|
|
123
193
|
}); }));
|
|
194
|
+
var live = this.options.liveUpdates;
|
|
195
|
+
if (live && this.mongoProvider) {
|
|
196
|
+
var eventName_1 = (_d = live.eventName) !== null && _d !== void 0 ? _d : "featureFlagsChanged";
|
|
197
|
+
var stream_1 = null;
|
|
198
|
+
var didRetry_1 = false;
|
|
199
|
+
var cleanupStream_1 = function () {
|
|
200
|
+
try {
|
|
201
|
+
stream_1 === null || stream_1 === void 0 ? void 0 : stream_1.close();
|
|
202
|
+
}
|
|
203
|
+
catch (_a) {
|
|
204
|
+
/* ignore close errors */
|
|
205
|
+
}
|
|
206
|
+
stream_1 = null;
|
|
207
|
+
};
|
|
208
|
+
var bindStream_1 = function () {
|
|
209
|
+
try {
|
|
210
|
+
stream_1 = featureFlagModel_1.FeatureFlag.watch([], { fullDocument: "updateLookup" });
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
api_1.logger.warn("[feature-flags] FeatureFlag.watch() failed \u2014 live updates require MongoDB as a replica set (even single-node). ".concat(String(err)));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
stream_1.on("change", function (change) {
|
|
217
|
+
var _a, _b;
|
|
218
|
+
var key = (_a = change.fullDocument) === null || _a === void 0 ? void 0 : _a.key;
|
|
219
|
+
var ioResolved = resolveSocketServer(live);
|
|
220
|
+
if (ioResolved) {
|
|
221
|
+
ioResolved.emit(eventName_1, { key: key });
|
|
222
|
+
}
|
|
223
|
+
(_b = _this.mongoProvider) === null || _b === void 0 ? void 0 : _b.emitConfigurationChanged();
|
|
224
|
+
});
|
|
225
|
+
stream_1.on("error", function (err) {
|
|
226
|
+
api_1.logger.warn("[feature-flags] change stream error: ".concat(String(err)));
|
|
227
|
+
cleanupStream_1();
|
|
228
|
+
if (!didRetry_1) {
|
|
229
|
+
didRetry_1 = true;
|
|
230
|
+
bindStream_1();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
api_1.logger.warn("[feature-flags] change stream disabled after repeated errors");
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
bindStream_1();
|
|
237
|
+
var onConnectionClose = function () {
|
|
238
|
+
cleanupStream_1();
|
|
239
|
+
};
|
|
240
|
+
mongoose_1.default.connection.on("close", onConnectionClose);
|
|
241
|
+
}
|
|
124
242
|
};
|
|
125
243
|
return FeatureFlagsApp;
|
|
126
244
|
}());
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FeatureFlagDocument, FlagDefinition, SegmentFunction } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the OpenFeature defaultVariant for a flag document, including legacy
|
|
4
|
+
* documents that omit the field.
|
|
5
|
+
*/
|
|
6
|
+
export declare const effectiveDefaultVariantForFlag: (flag: FeatureFlagDocument) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Builds a {@link FlagDefinition} for the `/flagConfiguration` wire shape from a
|
|
9
|
+
* persisted flag and the resolved evaluation for one user.
|
|
10
|
+
*/
|
|
11
|
+
export declare const buildFlagDefinition: (flag: FeatureFlagDocument, targetingKey: string, user: unknown, segments: Record<string, SegmentFunction>) => FlagDefinition;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __values = (this && this.__values) || function(o) {
|
|
3
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
4
|
+
if (m) return m.call(o);
|
|
5
|
+
if (o && typeof o.length === "number") return {
|
|
6
|
+
next: function () {
|
|
7
|
+
if (o && i >= o.length) o = void 0;
|
|
8
|
+
return { value: o && o[i++], done: !o };
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.buildFlagDefinition = exports.effectiveDefaultVariantForFlag = void 0;
|
|
15
|
+
var evaluate_1 = require("./evaluate");
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the OpenFeature defaultVariant for a flag document, including legacy
|
|
18
|
+
* documents that omit the field.
|
|
19
|
+
*/
|
|
20
|
+
var effectiveDefaultVariantForFlag = function (flag) {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
if (flag.defaultVariant) {
|
|
23
|
+
return flag.defaultVariant;
|
|
24
|
+
}
|
|
25
|
+
if (flag.type === "boolean") {
|
|
26
|
+
return "off";
|
|
27
|
+
}
|
|
28
|
+
return (_b = (_a = flag.variants[0]) === null || _a === void 0 ? void 0 : _a.key) !== null && _b !== void 0 ? _b : "off";
|
|
29
|
+
};
|
|
30
|
+
exports.effectiveDefaultVariantForFlag = effectiveDefaultVariantForFlag;
|
|
31
|
+
/**
|
|
32
|
+
* Builds a {@link FlagDefinition} for the `/flagConfiguration` wire shape from a
|
|
33
|
+
* persisted flag and the resolved evaluation for one user.
|
|
34
|
+
*/
|
|
35
|
+
var buildFlagDefinition = function (flag, targetingKey, user, segments) {
|
|
36
|
+
var e_1, _a;
|
|
37
|
+
var evaluated = (0, evaluate_1.evaluateFlag)(flag, targetingKey, user, segments);
|
|
38
|
+
if (flag.type === "boolean") {
|
|
39
|
+
var active = evaluated === true;
|
|
40
|
+
return {
|
|
41
|
+
defaultVariant: active ? "on" : "off",
|
|
42
|
+
disabled: false,
|
|
43
|
+
variants: { off: false, on: true },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
var variantKeys = flag.variants.map(function (v) { return v.key; });
|
|
47
|
+
var resolved;
|
|
48
|
+
if (typeof evaluated === "string" && variantKeys.includes(evaluated)) {
|
|
49
|
+
resolved = evaluated;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
resolved = (0, exports.effectiveDefaultVariantForFlag)(flag);
|
|
53
|
+
}
|
|
54
|
+
var variants = {};
|
|
55
|
+
try {
|
|
56
|
+
for (var variantKeys_1 = __values(variantKeys), variantKeys_1_1 = variantKeys_1.next(); !variantKeys_1_1.done; variantKeys_1_1 = variantKeys_1.next()) {
|
|
57
|
+
var k = variantKeys_1_1.value;
|
|
58
|
+
variants[k] = k;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
62
|
+
finally {
|
|
63
|
+
try {
|
|
64
|
+
if (variantKeys_1_1 && !variantKeys_1_1.done && (_a = variantKeys_1.return)) _a.call(variantKeys_1);
|
|
65
|
+
}
|
|
66
|
+
finally { if (e_1) throw e_1.error; }
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
defaultVariant: resolved,
|
|
70
|
+
disabled: false,
|
|
71
|
+
variants: variants,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
exports.buildFlagDefinition = buildFlagDefinition;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { deterministicHash, evaluateAllFlags, evaluateFlag } from "./evaluate";
|
|
2
2
|
export { FeatureFlag, featureFlagAdminConfig } from "./featureFlagModel";
|
|
3
3
|
export { FeatureFlagsApp } from "./featureFlagsApp";
|
|
4
|
-
export
|
|
4
|
+
export { buildFlagDefinition, effectiveDefaultVariantForFlag } from "./flagConfiguration";
|
|
5
|
+
export { MongoFeatureFlagProvider } from "./openFeatureProvider";
|
|
6
|
+
export type { EvaluationResult, FeatureFlagDocument, FeatureFlagModel, FeatureFlagRule, FeatureFlagsLiveUpdatesOptions, FeatureFlagsOptions, FeatureFlagsSocketEmitter, FeatureFlagType, FeatureFlagVariant, FlagConfigurationResponse, FlagDefinition, SegmentFunction, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FeatureFlagsApp = exports.featureFlagAdminConfig = exports.FeatureFlag = exports.evaluateFlag = exports.evaluateAllFlags = exports.deterministicHash = void 0;
|
|
3
|
+
exports.MongoFeatureFlagProvider = exports.effectiveDefaultVariantForFlag = exports.buildFlagDefinition = 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; } });
|
|
@@ -10,3 +10,8 @@ Object.defineProperty(exports, "FeatureFlag", { enumerable: true, get: function
|
|
|
10
10
|
Object.defineProperty(exports, "featureFlagAdminConfig", { enumerable: true, get: function () { return featureFlagModel_1.featureFlagAdminConfig; } });
|
|
11
11
|
var featureFlagsApp_1 = require("./featureFlagsApp");
|
|
12
12
|
Object.defineProperty(exports, "FeatureFlagsApp", { enumerable: true, get: function () { return featureFlagsApp_1.FeatureFlagsApp; } });
|
|
13
|
+
var flagConfiguration_1 = require("./flagConfiguration");
|
|
14
|
+
Object.defineProperty(exports, "buildFlagDefinition", { enumerable: true, get: function () { return flagConfiguration_1.buildFlagDefinition; } });
|
|
15
|
+
Object.defineProperty(exports, "effectiveDefaultVariantForFlag", { enumerable: true, get: function () { return flagConfiguration_1.effectiveDefaultVariantForFlag; } });
|
|
16
|
+
var openFeatureProvider_1 = require("./openFeatureProvider");
|
|
17
|
+
Object.defineProperty(exports, "MongoFeatureFlagProvider", { enumerable: true, get: function () { return openFeatureProvider_1.MongoFeatureFlagProvider; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type EvaluationContext, type JsonValue, type Logger, OpenFeatureEventEmitter, type Provider, type ResolutionDetails } from "@openfeature/server-sdk";
|
|
2
|
+
import type { FeatureFlagModel, SegmentFunction } from "./types";
|
|
3
|
+
export interface MongoFeatureFlagProviderOptions {
|
|
4
|
+
flagModel: FeatureFlagModel;
|
|
5
|
+
segments?: Record<string, SegmentFunction>;
|
|
6
|
+
}
|
|
7
|
+
export declare class MongoFeatureFlagProvider implements Provider {
|
|
8
|
+
readonly metadata: {
|
|
9
|
+
readonly name: "MongoFeatureFlagProvider";
|
|
10
|
+
};
|
|
11
|
+
readonly runsOn: "server";
|
|
12
|
+
readonly events: OpenFeatureEventEmitter;
|
|
13
|
+
private readonly flagModel;
|
|
14
|
+
private readonly segments;
|
|
15
|
+
constructor(options: MongoFeatureFlagProviderOptions);
|
|
16
|
+
resolveBooleanEvaluation(flagKey: string, defaultValue: boolean, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<boolean>>;
|
|
17
|
+
resolveStringEvaluation(flagKey: string, defaultValue: string, context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<string>>;
|
|
18
|
+
resolveNumberEvaluation(_flagKey: string, defaultValue: number, _context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<number>>;
|
|
19
|
+
resolveObjectEvaluation<T extends JsonValue>(_flagKey: string, defaultValue: T, _context: EvaluationContext, _logger: Logger): Promise<ResolutionDetails<T>>;
|
|
20
|
+
emitConfigurationChanged(): void;
|
|
21
|
+
private resolve;
|
|
22
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MongoFeatureFlagProvider = void 0;
|
|
40
|
+
var server_sdk_1 = require("@openfeature/server-sdk");
|
|
41
|
+
var evaluate_1 = require("./evaluate");
|
|
42
|
+
var MongoFeatureFlagProvider = /** @class */ (function () {
|
|
43
|
+
function MongoFeatureFlagProvider(options) {
|
|
44
|
+
var _a;
|
|
45
|
+
this.metadata = { name: "MongoFeatureFlagProvider" };
|
|
46
|
+
this.runsOn = "server";
|
|
47
|
+
this.events = new server_sdk_1.OpenFeatureEventEmitter();
|
|
48
|
+
this.flagModel = options.flagModel;
|
|
49
|
+
this.segments = (_a = options.segments) !== null && _a !== void 0 ? _a : {};
|
|
50
|
+
}
|
|
51
|
+
MongoFeatureFlagProvider.prototype.resolveBooleanEvaluation = function (flagKey, defaultValue, context, _logger) {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
53
|
+
return __generator(this, function (_a) {
|
|
54
|
+
return [2 /*return*/, this.resolve(flagKey, defaultValue, context, "boolean", _logger)];
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
MongoFeatureFlagProvider.prototype.resolveStringEvaluation = function (flagKey, defaultValue, context, _logger) {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
60
|
+
return __generator(this, function (_a) {
|
|
61
|
+
return [2 /*return*/, this.resolve(flagKey, defaultValue, context, "variant", _logger)];
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
MongoFeatureFlagProvider.prototype.resolveNumberEvaluation = function (_flagKey, defaultValue, _context, _logger) {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
67
|
+
return __generator(this, function (_a) {
|
|
68
|
+
return [2 /*return*/, {
|
|
69
|
+
errorCode: server_sdk_1.ErrorCode.FLAG_NOT_FOUND,
|
|
70
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
71
|
+
value: defaultValue,
|
|
72
|
+
}];
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
MongoFeatureFlagProvider.prototype.resolveObjectEvaluation = function (_flagKey, defaultValue, _context, _logger) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
78
|
+
return __generator(this, function (_a) {
|
|
79
|
+
return [2 /*return*/, {
|
|
80
|
+
errorCode: server_sdk_1.ErrorCode.FLAG_NOT_FOUND,
|
|
81
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
82
|
+
value: defaultValue,
|
|
83
|
+
}];
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
MongoFeatureFlagProvider.prototype.emitConfigurationChanged = function () {
|
|
88
|
+
this.events.emit(server_sdk_1.ServerProviderEvents.ConfigurationChanged, { flagsChanged: [] });
|
|
89
|
+
};
|
|
90
|
+
MongoFeatureFlagProvider.prototype.resolve = function (flagKey, defaultValue, context, expectedType, _logger) {
|
|
91
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
92
|
+
var flag, targetingKey, userFromContext, user, result, value;
|
|
93
|
+
var _a;
|
|
94
|
+
return __generator(this, function (_b) {
|
|
95
|
+
switch (_b.label) {
|
|
96
|
+
case 0: return [4 /*yield*/, this.flagModel.findOneOrNone({ archived: { $ne: true }, key: flagKey })];
|
|
97
|
+
case 1:
|
|
98
|
+
flag = _b.sent();
|
|
99
|
+
if (!flag) {
|
|
100
|
+
return [2 /*return*/, {
|
|
101
|
+
errorCode: server_sdk_1.ErrorCode.FLAG_NOT_FOUND,
|
|
102
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
103
|
+
value: defaultValue,
|
|
104
|
+
}];
|
|
105
|
+
}
|
|
106
|
+
if (flag.type !== expectedType) {
|
|
107
|
+
return [2 /*return*/, {
|
|
108
|
+
errorCode: server_sdk_1.ErrorCode.TYPE_MISMATCH,
|
|
109
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
110
|
+
value: defaultValue,
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
113
|
+
if (!flag.enabled) {
|
|
114
|
+
return [2 /*return*/, {
|
|
115
|
+
reason: server_sdk_1.StandardResolutionReasons.DISABLED,
|
|
116
|
+
value: defaultValue,
|
|
117
|
+
variant: flag.defaultVariant,
|
|
118
|
+
}];
|
|
119
|
+
}
|
|
120
|
+
targetingKey = (_a = context.targetingKey) !== null && _a !== void 0 ? _a : "";
|
|
121
|
+
userFromContext = context.user;
|
|
122
|
+
user = userFromContext !== undefined ? userFromContext : context;
|
|
123
|
+
result = (0, evaluate_1.evaluateFlag)(flag, targetingKey, user, this.segments);
|
|
124
|
+
if (expectedType === "boolean") {
|
|
125
|
+
value = result;
|
|
126
|
+
return [2 /*return*/, {
|
|
127
|
+
reason: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH,
|
|
128
|
+
value: value,
|
|
129
|
+
variant: value ? "on" : "off",
|
|
130
|
+
}];
|
|
131
|
+
}
|
|
132
|
+
if (result === null) {
|
|
133
|
+
return [2 /*return*/, {
|
|
134
|
+
reason: server_sdk_1.StandardResolutionReasons.DISABLED,
|
|
135
|
+
value: defaultValue,
|
|
136
|
+
variant: flag.defaultVariant,
|
|
137
|
+
}];
|
|
138
|
+
}
|
|
139
|
+
return [2 /*return*/, {
|
|
140
|
+
reason: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH,
|
|
141
|
+
value: result,
|
|
142
|
+
variant: result,
|
|
143
|
+
}];
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
return MongoFeatureFlagProvider;
|
|
149
|
+
}());
|
|
150
|
+
exports.MongoFeatureFlagProvider = MongoFeatureFlagProvider;
|
|
@@ -339,6 +339,76 @@ var featureFlagModel_1 = require("../featureFlagModel");
|
|
|
339
339
|
}
|
|
340
340
|
});
|
|
341
341
|
}); });
|
|
342
|
+
(0, bun_test_1.it)("auto-fills defaultVariant to off for boolean flags when omitted", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
343
|
+
var flag;
|
|
344
|
+
return __generator(this, function (_a) {
|
|
345
|
+
switch (_a.label) {
|
|
346
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
347
|
+
key: "default-variant-bool",
|
|
348
|
+
name: "Default variant bool",
|
|
349
|
+
})];
|
|
350
|
+
case 1:
|
|
351
|
+
flag = _a.sent();
|
|
352
|
+
(0, bun_test_1.expect)(flag.defaultVariant).toBe("off");
|
|
353
|
+
return [2 /*return*/];
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}); });
|
|
357
|
+
(0, bun_test_1.it)("auto-fills defaultVariant to the first variant key when omitted on variant flags", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
358
|
+
var flag;
|
|
359
|
+
return __generator(this, function (_a) {
|
|
360
|
+
switch (_a.label) {
|
|
361
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
362
|
+
key: "default-variant-var",
|
|
363
|
+
name: "Default variant var",
|
|
364
|
+
type: "variant",
|
|
365
|
+
variants: [
|
|
366
|
+
{ key: "control", weight: 40 },
|
|
367
|
+
{ key: "treatment", weight: 60 },
|
|
368
|
+
],
|
|
369
|
+
})];
|
|
370
|
+
case 1:
|
|
371
|
+
flag = _a.sent();
|
|
372
|
+
(0, bun_test_1.expect)(flag.defaultVariant).toBe("control");
|
|
373
|
+
return [2 /*return*/];
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}); });
|
|
377
|
+
(0, bun_test_1.it)("accepts explicit defaultVariant on for boolean flags", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
378
|
+
var flag;
|
|
379
|
+
return __generator(this, function (_a) {
|
|
380
|
+
switch (_a.label) {
|
|
381
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
382
|
+
defaultVariant: "on",
|
|
383
|
+
enabled: true,
|
|
384
|
+
key: "explicit-on-bool",
|
|
385
|
+
name: "Explicit on",
|
|
386
|
+
rolloutPercentage: 100,
|
|
387
|
+
type: "boolean",
|
|
388
|
+
})];
|
|
389
|
+
case 1:
|
|
390
|
+
flag = _a.sent();
|
|
391
|
+
(0, bun_test_1.expect)(flag.defaultVariant).toBe("on");
|
|
392
|
+
return [2 /*return*/];
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}); });
|
|
396
|
+
(0, bun_test_1.it)("rejects boolean defaultVariant values other than on or off", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
397
|
+
return __generator(this, function (_a) {
|
|
398
|
+
switch (_a.label) {
|
|
399
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
400
|
+
defaultVariant: "maybe",
|
|
401
|
+
enabled: true,
|
|
402
|
+
key: "bad-default-variant",
|
|
403
|
+
name: "Bad default",
|
|
404
|
+
type: "boolean",
|
|
405
|
+
})).rejects.toMatchObject({ title: "Boolean flags must use defaultVariant 'on' or 'off'" })];
|
|
406
|
+
case 1:
|
|
407
|
+
_a.sent();
|
|
408
|
+
return [2 /*return*/];
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}); });
|
|
342
412
|
});
|
|
343
413
|
(0, bun_test_1.describe)("featureFlagAdminConfig", function () {
|
|
344
414
|
(0, bun_test_1.it)("has the expected shape for AdminApp registration", function () {
|
|
@@ -351,6 +421,7 @@ var featureFlagModel_1 = require("../featureFlagModel");
|
|
|
351
421
|
"type",
|
|
352
422
|
"enabled",
|
|
353
423
|
"archived",
|
|
424
|
+
"defaultVariant",
|
|
354
425
|
"created",
|
|
355
426
|
]);
|
|
356
427
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|