@terreno/feature-flags 0.7.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/biome.jsonc +20 -0
- package/bunfig.toml +8 -0
- package/dist/evaluate.d.ts +17 -0
- package/dist/evaluate.js +233 -0
- package/dist/featureFlagModel.d.ts +2 -0
- package/dist/featureFlagModel.js +119 -0
- package/dist/featureFlagsApp.d.ts +27 -0
- package/dist/featureFlagsApp.js +124 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +35 -0
- package/dist/tests/evaluate.test.d.ts +1 -0
- package/dist/tests/evaluate.test.js +594 -0
- package/dist/tests/featureFlagModel.test.d.ts +1 -0
- package/dist/tests/featureFlagModel.test.js +342 -0
- package/dist/tests/featureFlagsApp.test.d.ts +1 -0
- package/dist/tests/featureFlagsApp.test.js +391 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +2 -0
- package/package.json +55 -0
- package/src/evaluate.ts +195 -0
- package/src/featureFlagModel.ts +138 -0
- package/src/featureFlagsApp.ts +95 -0
- package/src/index.ts +13 -0
- package/src/tests/bunSetup.ts +36 -0
- package/src/tests/evaluate.test.ts +603 -0
- package/src/tests/featureFlagModel.test.ts +205 -0
- package/src/tests/featureFlagsApp.test.ts +221 -0
- package/src/types.ts +46 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,342 @@
|
|
|
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
|
+
var bun_test_1 = require("bun:test");
|
|
40
|
+
var featureFlagModel_1 = require("../featureFlagModel");
|
|
41
|
+
(0, bun_test_1.describe)("FeatureFlag model", function () {
|
|
42
|
+
(0, bun_test_1.afterEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
43
|
+
return __generator(this, function (_a) {
|
|
44
|
+
switch (_a.label) {
|
|
45
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.deleteMany({})];
|
|
46
|
+
case 1:
|
|
47
|
+
_a.sent();
|
|
48
|
+
return [2 /*return*/];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}); });
|
|
52
|
+
(0, bun_test_1.it)("creates a boolean flag with defaults", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
53
|
+
var flag;
|
|
54
|
+
return __generator(this, function (_a) {
|
|
55
|
+
switch (_a.label) {
|
|
56
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
57
|
+
key: "new-checkout-flow",
|
|
58
|
+
name: "New Checkout Flow",
|
|
59
|
+
})];
|
|
60
|
+
case 1:
|
|
61
|
+
flag = _a.sent();
|
|
62
|
+
(0, bun_test_1.expect)(flag.key).toBe("new-checkout-flow");
|
|
63
|
+
(0, bun_test_1.expect)(flag.name).toBe("New Checkout Flow");
|
|
64
|
+
(0, bun_test_1.expect)(flag.enabled).toBe(false);
|
|
65
|
+
(0, bun_test_1.expect)(flag.archived).toBe(false);
|
|
66
|
+
(0, bun_test_1.expect)(flag.type).toBe("boolean");
|
|
67
|
+
(0, bun_test_1.expect)(flag.rolloutPercentage).toBe(100);
|
|
68
|
+
(0, bun_test_1.expect)(flag.description).toBe("");
|
|
69
|
+
(0, bun_test_1.expect)(flag.rules).toEqual([]);
|
|
70
|
+
(0, bun_test_1.expect)(flag.variants).toEqual([]);
|
|
71
|
+
return [2 /*return*/];
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}); });
|
|
75
|
+
(0, bun_test_1.it)("rejects unknown fields (strict: 'throw')", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
76
|
+
return __generator(this, function (_a) {
|
|
77
|
+
switch (_a.label) {
|
|
78
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
79
|
+
key: "strict-test",
|
|
80
|
+
name: "Strict Test",
|
|
81
|
+
unknownField: true,
|
|
82
|
+
})).rejects.toBeDefined()];
|
|
83
|
+
case 1:
|
|
84
|
+
_a.sent();
|
|
85
|
+
return [2 /*return*/];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}); });
|
|
89
|
+
(0, bun_test_1.it)("requires a key and name", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
90
|
+
return __generator(this, function (_a) {
|
|
91
|
+
switch (_a.label) {
|
|
92
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
93
|
+
name: "Missing key",
|
|
94
|
+
})).rejects.toBeDefined()];
|
|
95
|
+
case 1:
|
|
96
|
+
_a.sent();
|
|
97
|
+
return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
98
|
+
key: "missing-name",
|
|
99
|
+
})).rejects.toBeDefined()];
|
|
100
|
+
case 2:
|
|
101
|
+
_a.sent();
|
|
102
|
+
return [2 /*return*/];
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}); });
|
|
106
|
+
(0, bun_test_1.it)("enforces unique keys", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
107
|
+
return __generator(this, function (_a) {
|
|
108
|
+
switch (_a.label) {
|
|
109
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.syncIndexes()];
|
|
110
|
+
case 1:
|
|
111
|
+
_a.sent();
|
|
112
|
+
return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({ key: "dup-key", name: "First" })];
|
|
113
|
+
case 2:
|
|
114
|
+
_a.sent();
|
|
115
|
+
return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({ key: "dup-key", name: "Second" })).rejects.toBeDefined()];
|
|
116
|
+
case 3:
|
|
117
|
+
_a.sent();
|
|
118
|
+
return [2 /*return*/];
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}); });
|
|
122
|
+
(0, bun_test_1.it)("clamps rolloutPercentage to the 0-100 range", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
123
|
+
return __generator(this, function (_a) {
|
|
124
|
+
switch (_a.label) {
|
|
125
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
126
|
+
key: "over-rollout",
|
|
127
|
+
name: "Over",
|
|
128
|
+
rolloutPercentage: 150,
|
|
129
|
+
})).rejects.toBeDefined()];
|
|
130
|
+
case 1:
|
|
131
|
+
_a.sent();
|
|
132
|
+
return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
133
|
+
key: "under-rollout",
|
|
134
|
+
name: "Under",
|
|
135
|
+
rolloutPercentage: -5,
|
|
136
|
+
})).rejects.toBeDefined()];
|
|
137
|
+
case 2:
|
|
138
|
+
_a.sent();
|
|
139
|
+
return [2 /*return*/];
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}); });
|
|
143
|
+
(0, bun_test_1.it)("accepts variant flags with variants that sum to 100", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
144
|
+
var flag;
|
|
145
|
+
return __generator(this, function (_a) {
|
|
146
|
+
switch (_a.label) {
|
|
147
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
148
|
+
key: "variant-flag",
|
|
149
|
+
name: "Variant Flag",
|
|
150
|
+
type: "variant",
|
|
151
|
+
variants: [
|
|
152
|
+
{ key: "control", weight: 40 },
|
|
153
|
+
{ key: "variant-a", weight: 60 },
|
|
154
|
+
],
|
|
155
|
+
})];
|
|
156
|
+
case 1:
|
|
157
|
+
flag = _a.sent();
|
|
158
|
+
(0, bun_test_1.expect)(flag.type).toBe("variant");
|
|
159
|
+
(0, bun_test_1.expect)(flag.variants.length).toBe(2);
|
|
160
|
+
return [2 /*return*/];
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}); });
|
|
164
|
+
(0, bun_test_1.it)("rejects variant flags with no variants", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
165
|
+
return __generator(this, function (_a) {
|
|
166
|
+
switch (_a.label) {
|
|
167
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
168
|
+
key: "variant-no-variants",
|
|
169
|
+
name: "No Variants",
|
|
170
|
+
type: "variant",
|
|
171
|
+
variants: [],
|
|
172
|
+
})).rejects.toMatchObject({ title: "Variant flags must have at least one variant" })];
|
|
173
|
+
case 1:
|
|
174
|
+
_a.sent();
|
|
175
|
+
return [2 /*return*/];
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}); });
|
|
179
|
+
(0, bun_test_1.it)("rejects variant flags whose variant weights do not sum to 100", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
180
|
+
return __generator(this, function (_a) {
|
|
181
|
+
switch (_a.label) {
|
|
182
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
183
|
+
key: "variant-bad-weights",
|
|
184
|
+
name: "Bad Weights",
|
|
185
|
+
type: "variant",
|
|
186
|
+
variants: [
|
|
187
|
+
{ key: "control", weight: 10 },
|
|
188
|
+
{ key: "variant-a", weight: 10 },
|
|
189
|
+
],
|
|
190
|
+
})).rejects.toMatchObject({ title: "Variant weights must sum to 100" })];
|
|
191
|
+
case 1:
|
|
192
|
+
_a.sent();
|
|
193
|
+
return [2 /*return*/];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}); });
|
|
197
|
+
(0, bun_test_1.it)("allows boolean flags without variants", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
198
|
+
var flag;
|
|
199
|
+
return __generator(this, function (_a) {
|
|
200
|
+
switch (_a.label) {
|
|
201
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
202
|
+
key: "boolean-no-variants",
|
|
203
|
+
name: "Boolean",
|
|
204
|
+
type: "boolean",
|
|
205
|
+
})];
|
|
206
|
+
case 1:
|
|
207
|
+
flag = _a.sent();
|
|
208
|
+
(0, bun_test_1.expect)(flag.variants).toEqual([]);
|
|
209
|
+
return [2 /*return*/];
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}); });
|
|
213
|
+
(0, bun_test_1.it)("trims whitespace on keys", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
214
|
+
var flag;
|
|
215
|
+
return __generator(this, function (_a) {
|
|
216
|
+
switch (_a.label) {
|
|
217
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
218
|
+
key: " padded-key ",
|
|
219
|
+
name: "Padded",
|
|
220
|
+
})];
|
|
221
|
+
case 1:
|
|
222
|
+
flag = _a.sent();
|
|
223
|
+
(0, bun_test_1.expect)(flag.key).toBe("padded-key");
|
|
224
|
+
return [2 /*return*/];
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}); });
|
|
228
|
+
(0, bun_test_1.it)("stores complex rules including segments and field rules", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
229
|
+
var flag;
|
|
230
|
+
return __generator(this, function (_a) {
|
|
231
|
+
switch (_a.label) {
|
|
232
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
233
|
+
key: "complex-rules",
|
|
234
|
+
name: "Complex",
|
|
235
|
+
rules: [
|
|
236
|
+
{ enabled: true, segment: "beta" },
|
|
237
|
+
{ enabled: true, field: "plan", operator: "in", value: ["pro", "enterprise"] },
|
|
238
|
+
{ enabled: false, field: "email", operator: "contains", value: "@example.com" },
|
|
239
|
+
],
|
|
240
|
+
})];
|
|
241
|
+
case 1:
|
|
242
|
+
flag = _a.sent();
|
|
243
|
+
(0, bun_test_1.expect)(flag.rules.length).toBe(3);
|
|
244
|
+
(0, bun_test_1.expect)(flag.rules[0].segment).toBe("beta");
|
|
245
|
+
(0, bun_test_1.expect)(flag.rules[1].operator).toBe("in");
|
|
246
|
+
(0, bun_test_1.expect)(flag.rules[1].value).toEqual(["pro", "enterprise"]);
|
|
247
|
+
(0, bun_test_1.expect)(flag.rules[2].enabled).toBe(false);
|
|
248
|
+
return [2 /*return*/];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}); });
|
|
252
|
+
(0, bun_test_1.it)("rejects invalid rule operators via the enum constraint", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
253
|
+
return __generator(this, function (_a) {
|
|
254
|
+
switch (_a.label) {
|
|
255
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
256
|
+
key: "bad-op",
|
|
257
|
+
name: "Bad Operator",
|
|
258
|
+
rules: [{ enabled: true, field: "plan", operator: "bogus", value: "pro" }],
|
|
259
|
+
})).rejects.toBeDefined()];
|
|
260
|
+
case 1:
|
|
261
|
+
_a.sent();
|
|
262
|
+
return [2 /*return*/];
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}); });
|
|
266
|
+
(0, bun_test_1.it)("rejects invalid type values via the enum constraint", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
267
|
+
return __generator(this, function (_a) {
|
|
268
|
+
switch (_a.label) {
|
|
269
|
+
case 0: return [4 /*yield*/, (0, bun_test_1.expect)(featureFlagModel_1.FeatureFlag.create({
|
|
270
|
+
key: "bad-type",
|
|
271
|
+
name: "Bad Type",
|
|
272
|
+
type: "nonsense",
|
|
273
|
+
})).rejects.toBeDefined()];
|
|
274
|
+
case 1:
|
|
275
|
+
_a.sent();
|
|
276
|
+
return [2 /*return*/];
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}); });
|
|
280
|
+
(0, bun_test_1.it)("supports the isDeleted soft-delete plugin", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
281
|
+
var flag, found, foundWithDeleted;
|
|
282
|
+
return __generator(this, function (_a) {
|
|
283
|
+
switch (_a.label) {
|
|
284
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
285
|
+
key: "soft-delete",
|
|
286
|
+
name: "Soft",
|
|
287
|
+
})];
|
|
288
|
+
case 1:
|
|
289
|
+
flag = _a.sent();
|
|
290
|
+
(0, bun_test_1.expect)(flag.deleted).toBe(false);
|
|
291
|
+
flag.deleted = true;
|
|
292
|
+
return [4 /*yield*/, flag.save()];
|
|
293
|
+
case 2:
|
|
294
|
+
_a.sent();
|
|
295
|
+
return [4 /*yield*/, featureFlagModel_1.FeatureFlag.findOne({ key: "soft-delete" })];
|
|
296
|
+
case 3:
|
|
297
|
+
found = _a.sent();
|
|
298
|
+
(0, bun_test_1.expect)(found).toBeNull();
|
|
299
|
+
return [4 /*yield*/, featureFlagModel_1.FeatureFlag.findOne({ deleted: true, key: "soft-delete" })];
|
|
300
|
+
case 4:
|
|
301
|
+
foundWithDeleted = _a.sent();
|
|
302
|
+
(0, bun_test_1.expect)(foundWithDeleted === null || foundWithDeleted === void 0 ? void 0 : foundWithDeleted._id.toString()).toBe(flag._id.toString());
|
|
303
|
+
return [2 /*return*/];
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}); });
|
|
307
|
+
(0, bun_test_1.it)("sets created/updated timestamps on save", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
308
|
+
var flag;
|
|
309
|
+
return __generator(this, function (_a) {
|
|
310
|
+
switch (_a.label) {
|
|
311
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
|
|
312
|
+
key: "timestamps",
|
|
313
|
+
name: "Timestamps",
|
|
314
|
+
})];
|
|
315
|
+
case 1:
|
|
316
|
+
flag = _a.sent();
|
|
317
|
+
(0, bun_test_1.expect)(flag.created).toBeInstanceOf(Date);
|
|
318
|
+
(0, bun_test_1.expect)(flag.updated).toBeInstanceOf(Date);
|
|
319
|
+
return [2 /*return*/];
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}); });
|
|
323
|
+
(0, bun_test_1.it)("supports findExactlyOne and findOneOrNone plugins", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
324
|
+
var exact, maybe;
|
|
325
|
+
return __generator(this, function (_a) {
|
|
326
|
+
switch (_a.label) {
|
|
327
|
+
case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({ key: "existing-flag", name: "Exists" })];
|
|
328
|
+
case 1:
|
|
329
|
+
_a.sent();
|
|
330
|
+
return [4 /*yield*/, featureFlagModel_1.FeatureFlag.findExactlyOne({ key: "existing-flag" })];
|
|
331
|
+
case 2:
|
|
332
|
+
exact = _a.sent();
|
|
333
|
+
(0, bun_test_1.expect)(exact.name).toBe("Exists");
|
|
334
|
+
return [4 /*yield*/, featureFlagModel_1.FeatureFlag.findOneOrNone({ key: "missing-flag" })];
|
|
335
|
+
case 3:
|
|
336
|
+
maybe = _a.sent();
|
|
337
|
+
(0, bun_test_1.expect)(maybe).toBeNull();
|
|
338
|
+
return [2 /*return*/];
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}); });
|
|
342
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|