@terreno/feature-flags 57.4.0 → 57.6.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 +3 -3
- package/dist/evaluate.js +51 -128
- package/dist/featureFlagModel.js +10 -11
- package/dist/featureFlagsApp.js +96 -184
- package/dist/flagConfiguration.js +12 -35
- package/dist/openFeatureProvider.js +81 -143
- package/package.json +4 -4
- package/tsconfig.json +6 -21
package/README.md
CHANGED
|
@@ -39,10 +39,10 @@ Pass `liveUpdates: {socketIoServer: io}` to broadcast flag-key changes over Sock
|
|
|
39
39
|
|
|
40
40
|
## Documentation
|
|
41
41
|
|
|
42
|
-
Full API reference: [docs/reference/feature-flags.md](https://github.com/
|
|
42
|
+
Full API reference: [docs/reference/feature-flags.md](https://github.com/TerrenoLabs/terreno/blob/master/docs/reference/feature-flags.md)
|
|
43
43
|
|
|
44
|
-
How-to: [Add feature flags](https://github.com/
|
|
44
|
+
How-to: [Add feature flags](https://github.com/TerrenoLabs/terreno/blob/master/docs/how-to/add-feature-flags.md)
|
|
45
45
|
|
|
46
46
|
## License and Contributing
|
|
47
47
|
|
|
48
|
-
Licensed under the [MIT License](https://github.com/
|
|
48
|
+
Licensed under the [MIT License](https://github.com/TerrenoLabs/terreno/blob/master/LICENSE). See [CONTRIBUTING.md](https://github.com/TerrenoLabs/terreno/blob/master/CONTRIBUTING.md) for contribution guidelines.
|
package/dist/evaluate.js
CHANGED
|
@@ -1,79 +1,32 @@
|
|
|
1
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
|
-
var __values = (this && this.__values) || function(o) {
|
|
39
|
-
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
40
|
-
if (m) return m.call(o);
|
|
41
|
-
if (o && typeof o.length === "number") return {
|
|
42
|
-
next: function () {
|
|
43
|
-
if (o && i >= o.length) o = void 0;
|
|
44
|
-
return { value: o && o[i++], done: !o };
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
48
|
-
};
|
|
49
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
4
|
};
|
|
52
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
6
|
exports.evaluateAllFlags = exports.evaluateFlag = exports.deterministicHash = void 0;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
7
|
+
const api_1 = require("@terreno/api");
|
|
8
|
+
const get_1 = __importDefault(require("lodash/get"));
|
|
9
|
+
const isDebugEnabled = () => process.env.FEATURE_FLAGS_DEBUG !== "false";
|
|
57
10
|
/**
|
|
58
11
|
* Deterministic hash of a string to a number between 0 and 99.
|
|
59
12
|
* Uses a simple but effective hash (djb2) to ensure the same user+flag
|
|
60
13
|
* combination always produces the same result.
|
|
61
14
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for (
|
|
15
|
+
const deterministicHash = (input) => {
|
|
16
|
+
let hash = 5381;
|
|
17
|
+
for (let i = 0; i < input.length; i++) {
|
|
65
18
|
hash = (hash * 33) ^ input.charCodeAt(i);
|
|
66
19
|
}
|
|
67
20
|
return Math.abs(hash) % 100;
|
|
68
21
|
};
|
|
69
22
|
exports.deterministicHash = deterministicHash;
|
|
70
|
-
|
|
23
|
+
const matchesFieldRule = (user, rule) => {
|
|
71
24
|
if (!rule.field || !rule.operator) {
|
|
72
25
|
return false;
|
|
73
26
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
27
|
+
const fieldValue = (0, get_1.default)(user, rule.field);
|
|
28
|
+
const ruleValue = rule.value;
|
|
29
|
+
let result = false;
|
|
77
30
|
switch (rule.operator) {
|
|
78
31
|
case "eq":
|
|
79
32
|
result = ruleValue !== undefined && fieldValue === ruleValue;
|
|
@@ -105,26 +58,26 @@ var matchesFieldRule = function (user, rule) {
|
|
|
105
58
|
break;
|
|
106
59
|
}
|
|
107
60
|
if (isDebugEnabled()) {
|
|
108
|
-
api_1.logger.info(
|
|
61
|
+
api_1.logger.info(`[feature-flags] field rule: user.${rule.field}=${JSON.stringify(fieldValue)} ${rule.operator} ${JSON.stringify(ruleValue)} → ${result}`);
|
|
109
62
|
}
|
|
110
63
|
return result;
|
|
111
64
|
};
|
|
112
|
-
|
|
65
|
+
const matchesRule = (user, rule, segments) => {
|
|
113
66
|
if (rule.segment) {
|
|
114
|
-
|
|
67
|
+
const segmentFn = segments[rule.segment];
|
|
115
68
|
if (!segmentFn) {
|
|
116
|
-
api_1.logger.warn(
|
|
69
|
+
api_1.logger.warn(`Segment function not found: ${rule.segment}`);
|
|
117
70
|
return false;
|
|
118
71
|
}
|
|
119
72
|
try {
|
|
120
|
-
|
|
73
|
+
const result = segmentFn(user);
|
|
121
74
|
if (isDebugEnabled()) {
|
|
122
|
-
api_1.logger.info(
|
|
75
|
+
api_1.logger.info(`[feature-flags] segment rule: "${rule.segment}" → ${result}`);
|
|
123
76
|
}
|
|
124
77
|
return result;
|
|
125
78
|
}
|
|
126
79
|
catch (err) {
|
|
127
|
-
api_1.logger.warn(
|
|
80
|
+
api_1.logger.warn(`Segment function "${rule.segment}" threw: ${err}`);
|
|
128
81
|
return false;
|
|
129
82
|
}
|
|
130
83
|
}
|
|
@@ -134,62 +87,50 @@ var matchesRule = function (user, rule, segments) {
|
|
|
134
87
|
* Evaluate a single feature flag for a user.
|
|
135
88
|
* Returns boolean for boolean flags, string key for variant flags, or null for disabled variant flags.
|
|
136
89
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
var _b, _c;
|
|
140
|
-
var debug = isDebugEnabled();
|
|
90
|
+
const evaluateFlag = (flag, userId, user, segments) => {
|
|
91
|
+
const debug = isDebugEnabled();
|
|
141
92
|
if (!flag.enabled) {
|
|
142
93
|
if (debug) {
|
|
143
|
-
api_1.logger.info(
|
|
94
|
+
api_1.logger.info(`[feature-flags] "${flag.key}" is disabled → ${flag.type === "variant" ? "null" : "false"}`);
|
|
144
95
|
}
|
|
145
96
|
return flag.type === "variant" ? null : false;
|
|
146
97
|
}
|
|
147
98
|
// Check rules in order — first match wins
|
|
148
|
-
for (
|
|
149
|
-
|
|
150
|
-
|
|
99
|
+
for (let i = 0; i < flag.rules.length; i++) {
|
|
100
|
+
const rule = flag.rules[i];
|
|
101
|
+
const matched = matchesRule(user, rule, segments);
|
|
151
102
|
if (matched) {
|
|
152
|
-
|
|
103
|
+
const result = flag.type === "variant" ? (rule.variant ?? null) : (rule.enabled ?? false);
|
|
153
104
|
if (debug) {
|
|
154
|
-
api_1.logger.info(
|
|
105
|
+
api_1.logger.info(`[feature-flags] "${flag.key}" matched rule ${i} → ${JSON.stringify(result)}`);
|
|
155
106
|
}
|
|
156
107
|
return result;
|
|
157
108
|
}
|
|
158
109
|
}
|
|
159
110
|
// No rules matched — use deterministic hashing
|
|
160
|
-
|
|
111
|
+
const hash = (0, exports.deterministicHash)(`${userId}:${flag.key}`);
|
|
161
112
|
if (flag.type === "boolean") {
|
|
162
|
-
|
|
113
|
+
const result = hash < flag.rolloutPercentage;
|
|
163
114
|
if (debug) {
|
|
164
|
-
api_1.logger.info(
|
|
115
|
+
api_1.logger.info(`[feature-flags] "${flag.key}" no rules matched, hash=${hash} rollout=${flag.rolloutPercentage}% → ${result}`);
|
|
165
116
|
}
|
|
166
117
|
return result;
|
|
167
118
|
}
|
|
168
119
|
// Variant assignment based on cumulative weights
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (debug) {
|
|
176
|
-
api_1.logger.info("[feature-flags] \"".concat(flag.key, "\" no rules matched, hash=").concat(hash, " \u2192 variant \"").concat(variant.key, "\""));
|
|
177
|
-
}
|
|
178
|
-
return variant.key;
|
|
120
|
+
let cumulativeWeight = 0;
|
|
121
|
+
for (const variant of flag.variants) {
|
|
122
|
+
cumulativeWeight += variant.weight;
|
|
123
|
+
if (hash < cumulativeWeight) {
|
|
124
|
+
if (debug) {
|
|
125
|
+
api_1.logger.info(`[feature-flags] "${flag.key}" no rules matched, hash=${hash} → variant "${variant.key}"`);
|
|
179
126
|
}
|
|
127
|
+
return variant.key;
|
|
180
128
|
}
|
|
181
129
|
}
|
|
182
|
-
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
183
|
-
finally {
|
|
184
|
-
try {
|
|
185
|
-
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
186
|
-
}
|
|
187
|
-
finally { if (e_1) throw e_1.error; }
|
|
188
|
-
}
|
|
189
130
|
// Fallback (shouldn't happen if weights sum to 100)
|
|
190
|
-
|
|
131
|
+
const fallback = flag.variants.length > 0 ? flag.variants[flag.variants.length - 1].key : null;
|
|
191
132
|
if (debug) {
|
|
192
|
-
api_1.logger.info(
|
|
133
|
+
api_1.logger.info(`[feature-flags] "${flag.key}" variant fallback → ${JSON.stringify(fallback)}`);
|
|
193
134
|
}
|
|
194
135
|
return fallback;
|
|
195
136
|
};
|
|
@@ -198,36 +139,18 @@ exports.evaluateFlag = evaluateFlag;
|
|
|
198
139
|
* Evaluate all enabled, non-archived feature flags for a user.
|
|
199
140
|
* Returns a map of flag key to evaluation result.
|
|
200
141
|
*/
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
flag = flags_1_1.value;
|
|
216
|
-
results[flag.key] = (0, exports.evaluateFlag)(flag, userId, user, segments);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
220
|
-
finally {
|
|
221
|
-
try {
|
|
222
|
-
if (flags_1_1 && !flags_1_1.done && (_a = flags_1.return)) _a.call(flags_1);
|
|
223
|
-
}
|
|
224
|
-
finally { if (e_2) throw e_2.error; }
|
|
225
|
-
}
|
|
226
|
-
if (isDebugEnabled()) {
|
|
227
|
-
api_1.logger.info("[feature-flags] results: ".concat(JSON.stringify(results)));
|
|
228
|
-
}
|
|
229
|
-
return [2 /*return*/, results];
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
}); };
|
|
142
|
+
const evaluateAllFlags = async (flagModel, userId, user, segments) => {
|
|
143
|
+
const flags = await flagModel.find({ archived: { $ne: true }, enabled: true });
|
|
144
|
+
const results = {};
|
|
145
|
+
if (isDebugEnabled()) {
|
|
146
|
+
api_1.logger.info(`[feature-flags] evaluating ${flags.length} flags for user ${userId}`);
|
|
147
|
+
}
|
|
148
|
+
for (const flag of flags) {
|
|
149
|
+
results[flag.key] = (0, exports.evaluateFlag)(flag, userId, user, segments);
|
|
150
|
+
}
|
|
151
|
+
if (isDebugEnabled()) {
|
|
152
|
+
api_1.logger.info(`[feature-flags] results: ${JSON.stringify(results)}`);
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
};
|
|
233
156
|
exports.evaluateAllFlags = evaluateAllFlags;
|
package/dist/featureFlagModel.js
CHANGED
|
@@ -4,9 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.featureFlagAdminConfig = exports.FeatureFlag = void 0;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const api_1 = require("@terreno/api");
|
|
8
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
9
|
+
const featureFlagSchema = new mongoose_1.default.Schema({
|
|
10
10
|
archived: {
|
|
11
11
|
default: false,
|
|
12
12
|
description: "Archived flags are excluded from evaluation. Use this instead of deleting flags to prevent bloat as new features are added.",
|
|
@@ -106,7 +106,6 @@ featureFlagSchema.plugin(api_1.findExactlyOne);
|
|
|
106
106
|
featureFlagSchema.plugin(api_1.findOneOrNone);
|
|
107
107
|
featureFlagSchema.index({ archived: 1, enabled: 1 });
|
|
108
108
|
featureFlagSchema.pre("save", function () {
|
|
109
|
-
var _a;
|
|
110
109
|
if (this.type === "variant") {
|
|
111
110
|
if (!this.variants || this.variants.length === 0) {
|
|
112
111
|
throw new api_1.APIError({
|
|
@@ -114,10 +113,10 @@ featureFlagSchema.pre("save", function () {
|
|
|
114
113
|
title: "Variant flags must have at least one variant",
|
|
115
114
|
});
|
|
116
115
|
}
|
|
117
|
-
|
|
116
|
+
const totalWeight = this.variants.reduce((sum, v) => sum + v.weight, 0);
|
|
118
117
|
if (totalWeight !== 100) {
|
|
119
118
|
throw new api_1.APIError({
|
|
120
|
-
detail:
|
|
119
|
+
detail: `Total weight is ${totalWeight}, expected 100`,
|
|
121
120
|
status: 400,
|
|
122
121
|
title: "Variant weights must sum to 100",
|
|
123
122
|
});
|
|
@@ -129,7 +128,7 @@ featureFlagSchema.pre("save", function () {
|
|
|
129
128
|
if (this.type === "boolean") {
|
|
130
129
|
this.set("defaultVariant", "off");
|
|
131
130
|
}
|
|
132
|
-
else if (this.type === "variant" &&
|
|
131
|
+
else if (this.type === "variant" && this.variants?.length) {
|
|
133
132
|
this.set("defaultVariant", this.variants[0].key);
|
|
134
133
|
}
|
|
135
134
|
}
|
|
@@ -143,7 +142,7 @@ featureFlagSchema.pre("save", function () {
|
|
|
143
142
|
}
|
|
144
143
|
}
|
|
145
144
|
else if (this.type === "variant") {
|
|
146
|
-
|
|
145
|
+
const keys = new Set(this.variants.map((v) => v.key));
|
|
147
146
|
if (!keys.has(this.defaultVariant)) {
|
|
148
147
|
throw new api_1.APIError({
|
|
149
148
|
status: 400,
|
|
@@ -171,15 +170,15 @@ exports.FeatureFlag = mongoose_1.default.model("FeatureFlag", featureFlagSchema)
|
|
|
171
170
|
* .start();
|
|
172
171
|
* ```
|
|
173
172
|
*/
|
|
174
|
-
|
|
173
|
+
const featureFlagAdminConfigValue = {
|
|
175
174
|
displayName: "Feature Flags",
|
|
176
175
|
listFields: ["key", "name", "type", "enabled", "archived", "defaultVariant", "created"],
|
|
177
176
|
model: exports.FeatureFlag,
|
|
178
177
|
routePath: "/feature-flags",
|
|
179
178
|
};
|
|
180
|
-
|
|
179
|
+
const warnedLegacyAdminConfigs = new WeakSet();
|
|
181
180
|
exports.featureFlagAdminConfig = new Proxy(featureFlagAdminConfigValue, {
|
|
182
|
-
get
|
|
181
|
+
get(target, property, receiver) {
|
|
183
182
|
if (!warnedLegacyAdminConfigs.has(target)) {
|
|
184
183
|
warnedLegacyAdminConfigs.add(target);
|
|
185
184
|
api_1.logger.warn("[feature-flags] featureFlagAdminConfig is deprecated. Use new FeatureFlagsApp() and register it with TerrenoApp — see docs/how-to/admin-import-prebuilt.md");
|
package/dist/featureFlagsApp.js
CHANGED
|
@@ -1,77 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __assign = (this && this.__assign) || function () {
|
|
3
|
-
__assign = Object.assign || function(t) {
|
|
4
|
-
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
-
s = arguments[i];
|
|
6
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
-
t[p] = s[p];
|
|
8
|
-
}
|
|
9
|
-
return t;
|
|
10
|
-
};
|
|
11
|
-
return __assign.apply(this, arguments);
|
|
12
|
-
};
|
|
13
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
-
});
|
|
21
|
-
};
|
|
22
|
-
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
-
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);
|
|
24
|
-
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
-
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
-
function step(op) {
|
|
27
|
-
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
-
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
-
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;
|
|
30
|
-
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
-
switch (op[0]) {
|
|
32
|
-
case 0: case 1: t = op; break;
|
|
33
|
-
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
-
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
-
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
-
default:
|
|
37
|
-
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
-
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
-
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
-
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
-
if (t[2]) _.ops.pop();
|
|
42
|
-
_.trys.pop(); continue;
|
|
43
|
-
}
|
|
44
|
-
op = body.call(thisArg, _);
|
|
45
|
-
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
-
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
-
}
|
|
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
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
61
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
62
4
|
};
|
|
63
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
64
6
|
exports.FeatureFlagsApp = void 0;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
7
|
+
const server_sdk_1 = require("@openfeature/server-sdk");
|
|
8
|
+
const api_1 = require("@terreno/api");
|
|
9
|
+
const mongoose_1 = __importDefault(require("mongoose"));
|
|
10
|
+
const evaluate_1 = require("./evaluate");
|
|
11
|
+
const featureFlagModel_1 = require("./featureFlagModel");
|
|
12
|
+
const flagConfiguration_1 = require("./flagConfiguration");
|
|
13
|
+
const openFeatureProvider_1 = require("./openFeatureProvider");
|
|
14
|
+
let evaluateDeprecationWarned = false;
|
|
15
|
+
const resolveSocketServer = (live) => {
|
|
16
|
+
const server = live.socketIoServer;
|
|
75
17
|
if (typeof server === "function") {
|
|
76
18
|
return server();
|
|
77
19
|
}
|
|
@@ -95,13 +37,15 @@ var resolveSocketServer = function (live) {
|
|
|
95
37
|
* .start();
|
|
96
38
|
* ```
|
|
97
39
|
*/
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
40
|
+
class FeatureFlagsApp {
|
|
41
|
+
options;
|
|
42
|
+
segments;
|
|
43
|
+
mongoProvider;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options ?? {};
|
|
46
|
+
this.segments = this.options.segments ?? {};
|
|
103
47
|
}
|
|
104
|
-
|
|
48
|
+
adminContribution() {
|
|
105
49
|
return {
|
|
106
50
|
homeWidgets: [{ displayName: "Flag Overrides", icon: "flag", id: "feature-flags-overrides" }],
|
|
107
51
|
models: [
|
|
@@ -118,146 +62,114 @@ var FeatureFlagsApp = /** @class */ (function () {
|
|
|
118
62
|
},
|
|
119
63
|
],
|
|
120
64
|
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
65
|
+
}
|
|
66
|
+
register(app, openApi) {
|
|
67
|
+
const basePath = this.options.basePath ?? "/feature-flags";
|
|
68
|
+
const openFeatureDomain = this.options.openFeatureDomain ?? "feature-flags";
|
|
69
|
+
const routerOptions = {
|
|
70
|
+
...(openApi ? { openApi: openApi } : {}),
|
|
71
|
+
permissions: this.options.permissions ?? {
|
|
128
72
|
create: [api_1.Permissions.IsAdmin],
|
|
129
73
|
delete: [api_1.Permissions.IsAdmin],
|
|
130
74
|
list: [api_1.Permissions.IsAdmin],
|
|
131
75
|
read: [api_1.Permissions.IsAdmin],
|
|
132
76
|
update: [api_1.Permissions.IsAdmin],
|
|
133
|
-
},
|
|
77
|
+
},
|
|
78
|
+
sort: "-created",
|
|
79
|
+
};
|
|
134
80
|
this.mongoProvider = new openFeatureProvider_1.MongoFeatureFlagProvider({
|
|
135
81
|
flagModel: featureFlagModel_1.FeatureFlag,
|
|
136
82
|
segments: this.segments,
|
|
137
83
|
});
|
|
138
84
|
server_sdk_1.OpenFeature.setProvider(openFeatureDomain, this.mongoProvider);
|
|
139
|
-
app.use(
|
|
140
|
-
app.get(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
evaluateDeprecationWarned = true;
|
|
182
|
-
api_1.logger.warn("[feature-flags] GET /evaluate is deprecated; migrate clients to GET /flagConfiguration or use @terreno/rtk useFeatureFlags / useTerrenoFeatureFlags.");
|
|
183
|
-
}
|
|
184
|
-
sunset = new Date(Date.now() + 90 * 86400000).toUTCString();
|
|
185
|
-
res.setHeader("Deprecation", "true");
|
|
186
|
-
res.setHeader("Sunset", sunset);
|
|
187
|
-
user = req.user;
|
|
188
|
-
if (!user) {
|
|
189
|
-
throw new api_1.APIError({ status: 401, title: "Authentication required" });
|
|
190
|
-
}
|
|
191
|
-
userId = String((_a = user._id) !== null && _a !== void 0 ? _a : user.id);
|
|
192
|
-
return [4 /*yield*/, (0, evaluate_1.evaluateAllFlags)(featureFlagModel_1.FeatureFlag, userId, user, this.segments)];
|
|
193
|
-
case 1:
|
|
194
|
-
results = _b.sent();
|
|
195
|
-
return [2 /*return*/, res.json({ data: results })];
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
}); }));
|
|
199
|
-
app.get("".concat(basePath, "/segments"), (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
200
|
-
var user, allowed;
|
|
201
|
-
return __generator(this, function (_a) {
|
|
202
|
-
user = req.user;
|
|
203
|
-
allowed = this.options.segmentsPermission
|
|
204
|
-
? this.options.segmentsPermission(user)
|
|
205
|
-
: Boolean(user === null || user === void 0 ? void 0 : user.admin);
|
|
206
|
-
if (!allowed) {
|
|
207
|
-
throw new api_1.APIError({ status: 403, title: "Only admins can view segments" });
|
|
208
|
-
}
|
|
209
|
-
return [2 /*return*/, res.json({ data: Object.keys(this.segments) })];
|
|
210
|
-
});
|
|
211
|
-
}); }));
|
|
212
|
-
var live = this.options.liveUpdates;
|
|
85
|
+
app.use(`${basePath}/flags`, (0, api_1.modelRouter)(featureFlagModel_1.FeatureFlag, routerOptions));
|
|
86
|
+
app.get(`${basePath}/flagConfiguration`, (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(async (req, res) => {
|
|
87
|
+
const user = req.user;
|
|
88
|
+
if (!user) {
|
|
89
|
+
throw new api_1.APIError({ status: 401, title: "Authentication required" });
|
|
90
|
+
}
|
|
91
|
+
const targetingKey = String(user._id ?? user.id);
|
|
92
|
+
const flags = await featureFlagModel_1.FeatureFlag.find({ archived: { $ne: true }, enabled: true });
|
|
93
|
+
const config = {};
|
|
94
|
+
for (const flag of flags) {
|
|
95
|
+
config[flag.key] = (0, flagConfiguration_1.buildFlagDefinition)(flag, targetingKey, user, this.segments);
|
|
96
|
+
}
|
|
97
|
+
const body = { data: config };
|
|
98
|
+
return res.json(body);
|
|
99
|
+
}));
|
|
100
|
+
app.get(`${basePath}/evaluate`, (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(async (req, res) => {
|
|
101
|
+
if (!evaluateDeprecationWarned) {
|
|
102
|
+
evaluateDeprecationWarned = true;
|
|
103
|
+
api_1.logger.warn("[feature-flags] GET /evaluate is deprecated; migrate clients to GET /flagConfiguration or use @terreno/rtk useFeatureFlags / useTerrenoFeatureFlags.");
|
|
104
|
+
}
|
|
105
|
+
const sunset = new Date(Date.now() + 90 * 86400000).toUTCString();
|
|
106
|
+
res.setHeader("Deprecation", "true");
|
|
107
|
+
res.setHeader("Sunset", sunset);
|
|
108
|
+
const user = req.user;
|
|
109
|
+
if (!user) {
|
|
110
|
+
throw new api_1.APIError({ status: 401, title: "Authentication required" });
|
|
111
|
+
}
|
|
112
|
+
const userId = String(user._id ?? user.id);
|
|
113
|
+
const results = await (0, evaluate_1.evaluateAllFlags)(featureFlagModel_1.FeatureFlag, userId, user, this.segments);
|
|
114
|
+
return res.json({ data: results });
|
|
115
|
+
}));
|
|
116
|
+
app.get(`${basePath}/segments`, (0, api_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(async (req, res) => {
|
|
117
|
+
const user = req.user;
|
|
118
|
+
const allowed = this.options.segmentsPermission
|
|
119
|
+
? this.options.segmentsPermission(user)
|
|
120
|
+
: Boolean(user?.admin);
|
|
121
|
+
if (!allowed) {
|
|
122
|
+
throw new api_1.APIError({ status: 403, title: "Only admins can view segments" });
|
|
123
|
+
}
|
|
124
|
+
return res.json({ data: Object.keys(this.segments) });
|
|
125
|
+
}));
|
|
126
|
+
const live = this.options.liveUpdates;
|
|
213
127
|
if (live && this.mongoProvider) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
128
|
+
const eventName = live.eventName ?? "featureFlagsChanged";
|
|
129
|
+
let stream = null;
|
|
130
|
+
let didRetry = false;
|
|
131
|
+
const cleanupStream = () => {
|
|
218
132
|
try {
|
|
219
|
-
|
|
133
|
+
stream?.close();
|
|
220
134
|
}
|
|
221
|
-
catch
|
|
135
|
+
catch {
|
|
222
136
|
/* ignore close errors */
|
|
223
137
|
}
|
|
224
|
-
|
|
138
|
+
stream = null;
|
|
225
139
|
};
|
|
226
|
-
|
|
140
|
+
const bindStream = () => {
|
|
227
141
|
try {
|
|
228
|
-
|
|
142
|
+
stream = featureFlagModel_1.FeatureFlag.watch([], { fullDocument: "updateLookup" });
|
|
229
143
|
}
|
|
230
144
|
catch (err) {
|
|
231
|
-
api_1.logger.warn(
|
|
145
|
+
api_1.logger.warn(`[feature-flags] FeatureFlag.watch() failed — live updates require MongoDB as a replica set (even single-node). ${String(err)}`);
|
|
232
146
|
return;
|
|
233
147
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
var ioResolved = resolveSocketServer(live);
|
|
148
|
+
stream.on("change", (change) => {
|
|
149
|
+
const key = change.fullDocument?.key;
|
|
150
|
+
const ioResolved = resolveSocketServer(live);
|
|
238
151
|
if (ioResolved) {
|
|
239
|
-
ioResolved.emit(
|
|
152
|
+
ioResolved.emit(eventName, { key });
|
|
240
153
|
}
|
|
241
|
-
|
|
154
|
+
this.mongoProvider?.emitConfigurationChanged();
|
|
242
155
|
});
|
|
243
|
-
|
|
244
|
-
api_1.logger.warn(
|
|
245
|
-
|
|
246
|
-
if (!
|
|
247
|
-
|
|
248
|
-
|
|
156
|
+
stream.on("error", (err) => {
|
|
157
|
+
api_1.logger.warn(`[feature-flags] change stream error: ${String(err)}`);
|
|
158
|
+
cleanupStream();
|
|
159
|
+
if (!didRetry) {
|
|
160
|
+
didRetry = true;
|
|
161
|
+
bindStream();
|
|
249
162
|
return;
|
|
250
163
|
}
|
|
251
164
|
api_1.logger.warn("[feature-flags] change stream disabled after repeated errors");
|
|
252
165
|
});
|
|
253
166
|
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
167
|
+
bindStream();
|
|
168
|
+
const onConnectionClose = () => {
|
|
169
|
+
cleanupStream();
|
|
257
170
|
};
|
|
258
171
|
mongoose_1.default.connection.on("close", onConnectionClose);
|
|
259
172
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
}());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
263
175
|
exports.FeatureFlagsApp = FeatureFlagsApp;
|
|
@@ -1,74 +1,51 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
3
|
exports.buildFlagDefinition = exports.effectiveDefaultVariantForFlag = void 0;
|
|
15
|
-
|
|
4
|
+
const evaluate_1 = require("./evaluate");
|
|
16
5
|
/**
|
|
17
6
|
* Resolves the OpenFeature defaultVariant for a flag document, including legacy
|
|
18
7
|
* documents that omit the field.
|
|
19
8
|
*/
|
|
20
|
-
|
|
21
|
-
var _a, _b;
|
|
9
|
+
const effectiveDefaultVariantForFlag = (flag) => {
|
|
22
10
|
if (flag.defaultVariant) {
|
|
23
11
|
return flag.defaultVariant;
|
|
24
12
|
}
|
|
25
13
|
if (flag.type === "boolean") {
|
|
26
14
|
return "off";
|
|
27
15
|
}
|
|
28
|
-
return
|
|
16
|
+
return flag.variants[0]?.key ?? "off";
|
|
29
17
|
};
|
|
30
18
|
exports.effectiveDefaultVariantForFlag = effectiveDefaultVariantForFlag;
|
|
31
19
|
/**
|
|
32
20
|
* Builds a {@link FlagDefinition} for the `/flagConfiguration` wire shape from a
|
|
33
21
|
* persisted flag and the resolved evaluation for one user.
|
|
34
22
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
var evaluated = (0, evaluate_1.evaluateFlag)(flag, targetingKey, user, segments);
|
|
23
|
+
const buildFlagDefinition = (flag, targetingKey, user, segments) => {
|
|
24
|
+
const evaluated = (0, evaluate_1.evaluateFlag)(flag, targetingKey, user, segments);
|
|
38
25
|
if (flag.type === "boolean") {
|
|
39
|
-
|
|
26
|
+
const active = evaluated === true;
|
|
40
27
|
return {
|
|
41
28
|
defaultVariant: active ? "on" : "off",
|
|
42
29
|
disabled: false,
|
|
43
30
|
variants: { off: false, on: true },
|
|
44
31
|
};
|
|
45
32
|
}
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
const variantKeys = flag.variants.map((v) => v.key);
|
|
34
|
+
let resolved;
|
|
48
35
|
if (typeof evaluated === "string" && variantKeys.includes(evaluated)) {
|
|
49
36
|
resolved = evaluated;
|
|
50
37
|
}
|
|
51
38
|
else {
|
|
52
39
|
resolved = (0, exports.effectiveDefaultVariantForFlag)(flag);
|
|
53
40
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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; }
|
|
41
|
+
const variants = {};
|
|
42
|
+
for (const k of variantKeys) {
|
|
43
|
+
variants[k] = k;
|
|
67
44
|
}
|
|
68
45
|
return {
|
|
69
46
|
defaultVariant: resolved,
|
|
70
47
|
disabled: false,
|
|
71
|
-
variants
|
|
48
|
+
variants,
|
|
72
49
|
};
|
|
73
50
|
};
|
|
74
51
|
exports.buildFlagDefinition = buildFlagDefinition;
|
|
@@ -1,150 +1,88 @@
|
|
|
1
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
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
3
|
exports.MongoFeatureFlagProvider = void 0;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
4
|
+
const server_sdk_1 = require("@openfeature/server-sdk");
|
|
5
|
+
const evaluate_1 = require("./evaluate");
|
|
6
|
+
class MongoFeatureFlagProvider {
|
|
7
|
+
metadata = { name: "MongoFeatureFlagProvider" };
|
|
8
|
+
runsOn = "server";
|
|
9
|
+
events = new server_sdk_1.OpenFeatureEventEmitter();
|
|
10
|
+
flagModel;
|
|
11
|
+
segments;
|
|
12
|
+
constructor(options) {
|
|
48
13
|
this.flagModel = options.flagModel;
|
|
49
|
-
this.segments =
|
|
14
|
+
this.segments = options.segments ?? {};
|
|
15
|
+
}
|
|
16
|
+
async resolveBooleanEvaluation(flagKey, defaultValue, context, _logger) {
|
|
17
|
+
return this.resolve(flagKey, defaultValue, context, "boolean", _logger);
|
|
18
|
+
}
|
|
19
|
+
async resolveStringEvaluation(flagKey, defaultValue, context, _logger) {
|
|
20
|
+
return this.resolve(flagKey, defaultValue, context, "variant", _logger);
|
|
21
|
+
}
|
|
22
|
+
async resolveNumberEvaluation(_flagKey, defaultValue, _context, _logger) {
|
|
23
|
+
return {
|
|
24
|
+
errorCode: server_sdk_1.ErrorCode.FLAG_NOT_FOUND,
|
|
25
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
26
|
+
value: defaultValue,
|
|
27
|
+
};
|
|
50
28
|
}
|
|
51
|
-
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
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 () {
|
|
29
|
+
async resolveObjectEvaluation(_flagKey, defaultValue, _context, _logger) {
|
|
30
|
+
return {
|
|
31
|
+
errorCode: server_sdk_1.ErrorCode.FLAG_NOT_FOUND,
|
|
32
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
33
|
+
value: defaultValue,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
emitConfigurationChanged() {
|
|
88
37
|
this.events.emit(server_sdk_1.ServerProviderEvents.ConfigurationChanged, { flagsChanged: [] });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
}());
|
|
38
|
+
}
|
|
39
|
+
async resolve(flagKey, defaultValue, context, expectedType, _logger) {
|
|
40
|
+
const flag = await this.flagModel.findOneOrNone({ archived: { $ne: true }, key: flagKey });
|
|
41
|
+
if (!flag) {
|
|
42
|
+
return {
|
|
43
|
+
errorCode: server_sdk_1.ErrorCode.FLAG_NOT_FOUND,
|
|
44
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
45
|
+
value: defaultValue,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (flag.type !== expectedType) {
|
|
49
|
+
return {
|
|
50
|
+
errorCode: server_sdk_1.ErrorCode.TYPE_MISMATCH,
|
|
51
|
+
reason: server_sdk_1.StandardResolutionReasons.ERROR,
|
|
52
|
+
value: defaultValue,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (!flag.enabled) {
|
|
56
|
+
return {
|
|
57
|
+
reason: server_sdk_1.StandardResolutionReasons.DISABLED,
|
|
58
|
+
value: defaultValue,
|
|
59
|
+
variant: flag.defaultVariant,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const targetingKey = context.targetingKey ?? "";
|
|
63
|
+
const userFromContext = context.user;
|
|
64
|
+
const user = userFromContext !== undefined ? userFromContext : context;
|
|
65
|
+
const result = (0, evaluate_1.evaluateFlag)(flag, targetingKey, user, this.segments);
|
|
66
|
+
if (expectedType === "boolean") {
|
|
67
|
+
const value = result;
|
|
68
|
+
return {
|
|
69
|
+
reason: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH,
|
|
70
|
+
value,
|
|
71
|
+
variant: value ? "on" : "off",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (result === null) {
|
|
75
|
+
return {
|
|
76
|
+
reason: server_sdk_1.StandardResolutionReasons.DISABLED,
|
|
77
|
+
value: defaultValue,
|
|
78
|
+
variant: flag.defaultVariant,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
reason: server_sdk_1.StandardResolutionReasons.TARGETING_MATCH,
|
|
83
|
+
value: result,
|
|
84
|
+
variant: result,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
150
88
|
exports.MongoFeatureFlagProvider = MongoFeatureFlagProvider;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Josh Gachnang",
|
|
3
3
|
"bugs": {
|
|
4
|
-
"url": "https://github.com/
|
|
4
|
+
"url": "https://github.com/TerrenoLabs/terreno/issues"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@openfeature/server-sdk": "^1.22.0",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"supertest": "^7.2.2",
|
|
21
21
|
"typescript": "~6.0.3"
|
|
22
22
|
},
|
|
23
|
-
"homepage": "https://github.com/
|
|
23
|
+
"homepage": "https://github.com/TerrenoLabs/terreno#readme",
|
|
24
24
|
"keywords": [
|
|
25
25
|
"feature-flags",
|
|
26
26
|
"ab-testing",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"repository": {
|
|
41
41
|
"type": "git",
|
|
42
|
-
"url": "git+ssh://git@github.com/
|
|
42
|
+
"url": "git+ssh://git@github.com/TerrenoLabs/terreno.git"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"compile": "bun tsc",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"test:coverage": "bun run ../scripts/check-coverage.ts"
|
|
54
54
|
},
|
|
55
55
|
"types": "dist/index.d.ts",
|
|
56
|
-
"version": "57.
|
|
56
|
+
"version": "57.6.0"
|
|
57
57
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"allowJs": true,
|
|
4
|
-
"allowSyntheticDefaultImports": true,
|
|
5
|
-
"baseUrl": "./",
|
|
6
3
|
"declaration": true,
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"ignoreDeprecations": "6.0",
|
|
11
|
-
"jsx": "react",
|
|
12
|
-
"lib": ["es2017", "dom", "es2017.object"],
|
|
13
|
-
"module": "commonjs",
|
|
14
|
-
"moduleResolution": "node",
|
|
15
|
-
"noEmit": false,
|
|
4
|
+
"isolatedModules": true,
|
|
5
|
+
"lib": ["es2023"],
|
|
6
|
+
"module": "nodenext",
|
|
16
7
|
"noImplicitAny": false,
|
|
17
8
|
"noImplicitReturns": true,
|
|
18
|
-
"noImplicitThis": true,
|
|
19
|
-
"noUnusedLocals": false,
|
|
20
|
-
"noUnusedParameters": false,
|
|
21
9
|
"outDir": "./dist",
|
|
22
10
|
"rootDir": "./src",
|
|
23
|
-
"preserveConstEnums": true,
|
|
24
|
-
"removeComments": false,
|
|
25
11
|
"skipLibCheck": true,
|
|
26
12
|
"strict": true,
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"target": "es5"
|
|
13
|
+
"target": "es2023",
|
|
14
|
+
"types": ["node"]
|
|
30
15
|
},
|
|
31
16
|
"exclude": ["node_modules", "src/tests", "**/*.test.ts"],
|
|
32
|
-
"include": ["src
|
|
17
|
+
"include": ["src"]
|
|
33
18
|
}
|