@terreno/feature-flags 0.18.0 → 0.20.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.
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ var __extends = (this && this.__extends) || (function () {
3
+ var extendStatics = function (d, b) {
4
+ extendStatics = Object.setPrototypeOf ||
5
+ ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6
+ function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7
+ return extendStatics(d, b);
8
+ };
9
+ return function (d, b) {
10
+ if (typeof b !== "function" && b !== null)
11
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
12
+ extendStatics(d, b);
13
+ function __() { this.constructor = d; }
14
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
15
+ };
16
+ })();
17
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19
+ return new (P || (P = Promise))(function (resolve, reject) {
20
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
24
+ });
25
+ };
26
+ var __generator = (this && this.__generator) || function (thisArg, body) {
27
+ 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);
28
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
29
+ function verb(n) { return function (v) { return step([n, v]); }; }
30
+ function step(op) {
31
+ if (f) throw new TypeError("Generator is already executing.");
32
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
33
+ 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;
34
+ if (y = 0, t) op = [op[0] & 2, t.value];
35
+ switch (op[0]) {
36
+ case 0: case 1: t = op; break;
37
+ case 4: _.label++; return { value: op[1], done: false };
38
+ case 5: _.label++; y = op[1]; op = [0]; continue;
39
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
40
+ default:
41
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
42
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
43
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
44
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
45
+ if (t[2]) _.ops.pop();
46
+ _.trys.pop(); continue;
47
+ }
48
+ op = body.call(thisArg, _);
49
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
50
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
51
+ }
52
+ };
53
+ var __read = (this && this.__read) || function (o, n) {
54
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
55
+ if (!m) return o;
56
+ var i = m.call(o), r, ar = [], e;
57
+ try {
58
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
59
+ }
60
+ catch (error) { e = { error: error }; }
61
+ finally {
62
+ try {
63
+ if (r && !r.done && (m = i["return"])) m.call(i);
64
+ }
65
+ finally { if (e) throw e.error; }
66
+ }
67
+ return ar;
68
+ };
69
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
70
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
71
+ if (ar || !(i in from)) {
72
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
73
+ ar[i] = from[i];
74
+ }
75
+ }
76
+ return to.concat(ar || Array.prototype.slice.call(from));
77
+ };
78
+ var __importDefault = (this && this.__importDefault) || function (mod) {
79
+ return (mod && mod.__esModule) ? mod : { "default": mod };
80
+ };
81
+ Object.defineProperty(exports, "__esModule", { value: true });
82
+ var bun_test_1 = require("bun:test");
83
+ var node_events_1 = __importDefault(require("node:events"));
84
+ var server_sdk_1 = require("@openfeature/server-sdk");
85
+ var api_1 = require("@terreno/api");
86
+ var tests_1 = require("@terreno/api/src/tests");
87
+ var featureFlagModel_1 = require("../featureFlagModel");
88
+ var featureFlagsApp_1 = require("../featureFlagsApp");
89
+ var FakeChangeStream = /** @class */ (function (_super) {
90
+ __extends(FakeChangeStream, _super);
91
+ function FakeChangeStream() {
92
+ var _this = _super.apply(this, __spreadArray([], __read(arguments), false)) || this;
93
+ _this.close = (0, bun_test_1.mock)(function () { return Promise.resolve(); });
94
+ return _this;
95
+ }
96
+ return FakeChangeStream;
97
+ }(node_events_1.default));
98
+ var buildAppWithLive = function (options) {
99
+ var app = (0, tests_1.getBaseServer)();
100
+ (0, api_1.setupAuth)(app, tests_1.UserModel);
101
+ (0, api_1.addAuthRoutes)(app, tests_1.UserModel);
102
+ var plugin = new featureFlagsApp_1.FeatureFlagsApp({
103
+ liveUpdates: {
104
+ eventName: options.eventName,
105
+ socketIoServer: function () { return options.io; },
106
+ },
107
+ });
108
+ plugin.register(app);
109
+ app.use(api_1.apiUnauthorizedMiddleware);
110
+ app.use(api_1.apiErrorMiddleware);
111
+ return app;
112
+ };
113
+ (0, bun_test_1.describe)("FeatureFlagsApp live updates", function () {
114
+ var watchSpy;
115
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
116
+ return __generator(this, function (_a) {
117
+ switch (_a.label) {
118
+ case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
119
+ case 1:
120
+ _a.sent();
121
+ return [4 /*yield*/, featureFlagModel_1.FeatureFlag.deleteMany({})];
122
+ case 2:
123
+ _a.sent();
124
+ return [2 /*return*/];
125
+ }
126
+ });
127
+ }); });
128
+ (0, bun_test_1.afterEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
129
+ return __generator(this, function (_a) {
130
+ switch (_a.label) {
131
+ case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.deleteMany({})];
132
+ case 1:
133
+ _a.sent();
134
+ watchSpy === null || watchSpy === void 0 ? void 0 : watchSpy.mockRestore();
135
+ return [4 /*yield*/, server_sdk_1.OpenFeature.clearProviders()];
136
+ case 2:
137
+ _a.sent();
138
+ return [2 /*return*/];
139
+ }
140
+ });
141
+ }); });
142
+ (0, bun_test_1.it)("emits the default socket event and payload on change stream events", function () { return __awaiter(void 0, void 0, void 0, function () {
143
+ var stream, io;
144
+ return __generator(this, function (_a) {
145
+ stream = new FakeChangeStream();
146
+ watchSpy = (0, bun_test_1.spyOn)(featureFlagModel_1.FeatureFlag, "watch").mockReturnValue(stream);
147
+ io = { emit: (0, bun_test_1.mock)(function () { }) };
148
+ buildAppWithLive({ io: io });
149
+ stream.emit("change", { fullDocument: { key: "todo-summary-card" } });
150
+ (0, bun_test_1.expect)(io.emit).toHaveBeenCalledWith("featureFlagsChanged", { key: "todo-summary-card" });
151
+ return [2 /*return*/];
152
+ });
153
+ }); });
154
+ (0, bun_test_1.it)("respects a custom liveUpdates.eventName", function () { return __awaiter(void 0, void 0, void 0, function () {
155
+ var stream, io;
156
+ return __generator(this, function (_a) {
157
+ stream = new FakeChangeStream();
158
+ watchSpy = (0, bun_test_1.spyOn)(featureFlagModel_1.FeatureFlag, "watch").mockReturnValue(stream);
159
+ io = { emit: (0, bun_test_1.mock)(function () { }) };
160
+ buildAppWithLive({ eventName: "flagsUpdated", io: io });
161
+ stream.emit("change", { fullDocument: { key: "k" } });
162
+ (0, bun_test_1.expect)(io.emit).toHaveBeenCalledWith("flagsUpdated", { key: "k" });
163
+ return [2 /*return*/];
164
+ });
165
+ }); });
166
+ (0, bun_test_1.it)("logs a warn on stream error, retries once, then disables after a second error", function () { return __awaiter(void 0, void 0, void 0, function () {
167
+ var warnSpy, stream1, stream2, call, io, disabledMsg;
168
+ return __generator(this, function (_a) {
169
+ switch (_a.label) {
170
+ case 0:
171
+ warnSpy = (0, bun_test_1.spyOn)(api_1.logger, "warn").mockImplementation(function () { });
172
+ stream1 = new FakeChangeStream();
173
+ stream2 = new FakeChangeStream();
174
+ call = 0;
175
+ watchSpy = (0, bun_test_1.spyOn)(featureFlagModel_1.FeatureFlag, "watch").mockImplementation(function () {
176
+ call += 1;
177
+ if (call === 1) {
178
+ return stream1;
179
+ }
180
+ return stream2;
181
+ });
182
+ io = { emit: (0, bun_test_1.mock)(function () { }) };
183
+ buildAppWithLive({ io: io });
184
+ stream1.emit("error", new Error("first"));
185
+ return [4 /*yield*/, new Promise(function (resolve) {
186
+ setTimeout(resolve, 0);
187
+ })];
188
+ case 1:
189
+ _a.sent();
190
+ stream2.emit("error", new Error("second"));
191
+ return [4 /*yield*/, new Promise(function (resolve) {
192
+ setTimeout(resolve, 0);
193
+ })];
194
+ case 2:
195
+ _a.sent();
196
+ disabledMsg = warnSpy.mock.calls.find(function (c) { return typeof c[0] === "string" && c[0].includes("change stream disabled"); });
197
+ (0, bun_test_1.expect)(disabledMsg).toBeDefined();
198
+ warnSpy.mockRestore();
199
+ return [2 /*return*/];
200
+ }
201
+ });
202
+ }); });
203
+ });
@@ -40,6 +40,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  var bun_test_1 = require("bun:test");
43
+ var server_sdk_1 = require("@openfeature/server-sdk");
43
44
  var api_1 = require("@terreno/api");
44
45
  var tests_1 = require("@terreno/api/src/tests");
45
46
  var supertest_1 = __importDefault(require("supertest"));
@@ -77,6 +78,9 @@ var buildApp = function (options) {
77
78
  switch (_a.label) {
78
79
  case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.deleteMany({})];
79
80
  case 1:
81
+ _a.sent();
82
+ return [4 /*yield*/, server_sdk_1.OpenFeature.clearProviders()];
83
+ case 2:
80
84
  _a.sent();
81
85
  return [2 /*return*/];
82
86
  }
@@ -331,6 +335,151 @@ var buildApp = function (options) {
331
335
  }
332
336
  });
333
337
  }); });
338
+ (0, bun_test_1.it)("includes Deprecation and Sunset headers on GET /evaluate", function () { return __awaiter(void 0, void 0, void 0, function () {
339
+ var res;
340
+ return __generator(this, function (_a) {
341
+ switch (_a.label) {
342
+ case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
343
+ enabled: true,
344
+ key: "hdr-flag",
345
+ name: "Hdr",
346
+ rolloutPercentage: 100,
347
+ })];
348
+ case 1:
349
+ _a.sent();
350
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
351
+ case 2:
352
+ adminAgent = _a.sent();
353
+ return [4 /*yield*/, adminAgent.get("/feature-flags/evaluate").expect(200)];
354
+ case 3:
355
+ res = _a.sent();
356
+ (0, bun_test_1.expect)(res.headers.deprecation).toBe("true");
357
+ (0, bun_test_1.expect)(res.headers.sunset).toBeDefined();
358
+ (0, bun_test_1.expect)(res.body.data["hdr-flag"]).toBe(true);
359
+ return [2 /*return*/];
360
+ }
361
+ });
362
+ }); });
363
+ });
364
+ (0, bun_test_1.describe)("GET /feature-flags/flagConfiguration", function () {
365
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
366
+ var segments;
367
+ return __generator(this, function (_a) {
368
+ segments = {
369
+ admins: function (user) { return user.admin === true; },
370
+ };
371
+ app = buildApp({ segments: segments });
372
+ return [2 /*return*/];
373
+ });
374
+ }); });
375
+ (0, bun_test_1.it)("returns 401 when unauthenticated", function () { return __awaiter(void 0, void 0, void 0, function () {
376
+ var unauth;
377
+ return __generator(this, function (_a) {
378
+ switch (_a.label) {
379
+ case 0:
380
+ unauth = (0, supertest_1.default)(app);
381
+ return [4 /*yield*/, unauth.get("/feature-flags/flagConfiguration").expect(401)];
382
+ case 1:
383
+ _a.sent();
384
+ return [2 /*return*/];
385
+ }
386
+ });
387
+ }); });
388
+ (0, bun_test_1.it)("returns OpenFeature-shaped entries for enabled flags and omits disabled and archived", function () { return __awaiter(void 0, void 0, void 0, function () {
389
+ var res, data;
390
+ var _a, _b, _c, _d;
391
+ return __generator(this, function (_e) {
392
+ switch (_e.label) {
393
+ case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
394
+ enabled: true,
395
+ key: "bool-active",
396
+ name: "Bool",
397
+ rolloutPercentage: 100,
398
+ type: "boolean",
399
+ })];
400
+ case 1:
401
+ _e.sent();
402
+ return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
403
+ defaultVariant: "compact",
404
+ enabled: true,
405
+ key: "profile",
406
+ name: "Profile",
407
+ rules: [],
408
+ type: "variant",
409
+ variants: [
410
+ { key: "compact", weight: 50 },
411
+ { key: "detailed", weight: 50 },
412
+ ],
413
+ })];
414
+ case 2:
415
+ _e.sent();
416
+ return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
417
+ enabled: false,
418
+ key: "disabled-one",
419
+ name: "Off",
420
+ rolloutPercentage: 100,
421
+ type: "boolean",
422
+ })];
423
+ case 3:
424
+ _e.sent();
425
+ return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
426
+ archived: true,
427
+ enabled: true,
428
+ key: "archived-one",
429
+ name: "Archived",
430
+ rolloutPercentage: 100,
431
+ type: "boolean",
432
+ })];
433
+ case 4:
434
+ _e.sent();
435
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
436
+ case 5:
437
+ adminAgent = _e.sent();
438
+ return [4 /*yield*/, adminAgent.get("/feature-flags/flagConfiguration").expect(200)];
439
+ case 6:
440
+ res = _e.sent();
441
+ data = res.body.data;
442
+ (0, bun_test_1.expect)(data["bool-active"]).toEqual({
443
+ defaultVariant: "on",
444
+ disabled: false,
445
+ variants: { off: false, on: true },
446
+ });
447
+ (0, bun_test_1.expect)((_a = data.profile) === null || _a === void 0 ? void 0 : _a.disabled).toBe(false);
448
+ (0, bun_test_1.expect)(["compact", "detailed"].includes((_c = (_b = data.profile) === null || _b === void 0 ? void 0 : _b.defaultVariant) !== null && _c !== void 0 ? _c : "")).toBe(true);
449
+ (0, bun_test_1.expect)((_d = data.profile) === null || _d === void 0 ? void 0 : _d.variants.compact).toBe("compact");
450
+ (0, bun_test_1.expect)(data["disabled-one"]).toBeUndefined();
451
+ (0, bun_test_1.expect)(data["archived-one"]).toBeUndefined();
452
+ return [2 /*return*/];
453
+ }
454
+ });
455
+ }); });
456
+ (0, bun_test_1.it)("uses resolved boolean variant as defaultVariant (not the persisted schema default alone)", function () { return __awaiter(void 0, void 0, void 0, function () {
457
+ var res;
458
+ return __generator(this, function (_a) {
459
+ switch (_a.label) {
460
+ case 0: return [4 /*yield*/, featureFlagModel_1.FeatureFlag.create({
461
+ defaultVariant: "off",
462
+ enabled: true,
463
+ key: "resolved-bool",
464
+ name: "Resolved",
465
+ rolloutPercentage: 100,
466
+ rules: [],
467
+ type: "boolean",
468
+ })];
469
+ case 1:
470
+ _a.sent();
471
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
472
+ case 2:
473
+ adminAgent = _a.sent();
474
+ return [4 /*yield*/, adminAgent.get("/feature-flags/flagConfiguration").expect(200)];
475
+ case 3:
476
+ res = _a.sent();
477
+ (0, bun_test_1.expect)(res.body.data["resolved-bool"].defaultVariant).toBe("on");
478
+ (0, bun_test_1.expect)(res.body.data["resolved-bool"].variants.on).toBe(true);
479
+ return [2 /*return*/];
480
+ }
481
+ });
482
+ }); });
334
483
  });
335
484
  (0, bun_test_1.describe)("GET /feature-flags/segments", function () {
336
485
  (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -0,0 +1 @@
1
+ export {};