@terreno/feature-flags 0.19.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.
@@ -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
  }
@@ -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 routerOptions = __assign(__assign({}, (openApi ? { openApi: openApi } : {})), { permissions: (_b = this.options.permissions) !== null && _b !== void 0 ? _b : {
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
- // Admin CRUD routes for flags
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
- // GET /feature-flags/evaluate — evaluate all flags for current user
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 type { EvaluationResult, FeatureFlagDocument, FeatureFlagModel, FeatureFlagRule, FeatureFlagsOptions, FeatureFlagType, FeatureFlagVariant, SegmentFunction, } from "./types";
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 {};