@brandtg/flapjack 1.0.0 → 1.2.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/dist/cache.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { FeatureFlagModel } from "./model.js";
2
2
  export interface Cache<T> {
3
- get(key: string): T | undefined;
4
- set(key: string, value: T, ttl?: number): void;
5
- delete(key: string): void;
3
+ get(key: string): Promise<T | undefined>;
4
+ set(key: string, value: T, ttl?: number): Promise<void>;
5
+ delete(key: string): Promise<void>;
6
6
  }
7
7
  /**
8
8
  * Simple in-memory cache with TTL support.
9
9
  */
10
10
  export declare class InMemoryCache<T> implements Cache<T> {
11
11
  private cache;
12
- get(key: string): T | undefined;
13
- set(key: string, value: T, ttl?: number): void;
14
- delete(key: string): void;
12
+ get(key: string): Promise<T | undefined>;
13
+ set(key: string, value: T, ttl?: number): Promise<void>;
14
+ delete(key: string): Promise<void>;
15
15
  /**
16
16
  * Clear all expired entries from the cache.
17
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AASD;;GAEG;AACH,qBAAa,aAAa,CAAC,CAAC,CAAE,YAAW,KAAK,CAAC,CAAC,CAAC;IAC/C,OAAO,CAAC,KAAK,CAAoC;IAEjD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAgB/B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAK9C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB;;OAEG;IACH,YAAY,IAAI,IAAI;IASpB;;OAEG;IACH,IAAI,IAAI,MAAM;IAId;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,GAAG,CAAS;gBAER,EACV,KAAK,EACL,KAAK,EACL,GAAiB,GAClB,EAAE;QACD,KAAK,EAAE,gBAAgB,CAAC;QACxB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACtB,GAAG,CAAC,EAAE,MAAM,CAAC;KACd;IAMD;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAyBlB,eAAe,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAM,GACP,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,GAAG,OAAO,CAAC,OAAO,CAAC;CAuBrB"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AASD;;GAEG;AACH,qBAAa,aAAa,CAAC,CAAC,CAAE,YAAW,KAAK,CAAC,CAAC,CAAC;IAC/C,OAAO,CAAC,KAAK,CAAoC;IAE3C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IAgBxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;OAEG;IACH,YAAY,IAAI,IAAI;IASpB;;OAEG;IACH,IAAI,IAAI,MAAM;IAId;;OAEG;IACH,KAAK,IAAI,IAAI;CAGd;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,GAAG,CAAS;gBAER,EACV,KAAK,EACL,KAAK,EACL,GAAiB,GAClB,EAAE;QACD,KAAK,EAAE,gBAAgB,CAAC;QACxB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACtB,GAAG,CAAC,EAAE,MAAM,CAAC;KACd;IAMD;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAyBlB,eAAe,CAAC,EACpB,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,MAAM,GACP,EAAE;QACD,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACnB,GAAG,OAAO,CAAC,OAAO,CAAC;CAuBrB"}
package/dist/cache.js CHANGED
@@ -5,7 +5,7 @@ const DEFAULT_TTL = 60 * 5; // 5 minutes
5
5
  */
6
6
  export class InMemoryCache {
7
7
  cache = new Map();
8
- get(key) {
8
+ async get(key) {
9
9
  const entry = this.cache.get(key);
10
10
  if (!entry) {
11
11
  return undefined;
@@ -18,11 +18,11 @@ export class InMemoryCache {
18
18
  }
19
19
  return entry.value;
20
20
  }
21
- set(key, value, ttl) {
21
+ async set(key, value, ttl) {
22
22
  const expiresAt = ttl !== undefined ? Date.now() + ttl * 1000 : undefined;
23
23
  this.cache.set(key, { value, expiresAt });
24
24
  }
25
- delete(key) {
25
+ async delete(key) {
26
26
  this.cache.delete(key);
27
27
  }
28
28
  /**
@@ -78,7 +78,7 @@ export class FeatureFlagCache {
78
78
  // Generate cache key
79
79
  const cacheKey = this.generateCacheKey({ name, user, roles, groups });
80
80
  // Try to get from cache first
81
- const cachedResult = this.cache.get(cacheKey);
81
+ const cachedResult = await this.cache.get(cacheKey);
82
82
  if (cachedResult !== undefined) {
83
83
  return cachedResult;
84
84
  }
package/dist/model.d.ts CHANGED
@@ -89,6 +89,19 @@ export declare class FeatureFlagModel {
89
89
  * ```
90
90
  */
91
91
  getByName(name: string): Promise<FeatureFlag | null>;
92
+ /**
93
+ * Retrieves multiple feature flags by their names.
94
+ *
95
+ * @param names - Array of feature flag names to retrieve
96
+ * @returns Array of feature flags found (may be shorter than input if some names don't exist)
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const flags = await model.getManyByName(["feature1", "feature2", "feature3"]);
101
+ * console.log(`Found ${flags.length} flags`);
102
+ * ```
103
+ */
104
+ getManyByName(names: string[]): Promise<FeatureFlag[]>;
92
105
  /**
93
106
  * Retrieves multiple feature flags by their IDs.
94
107
  *
@@ -161,6 +174,15 @@ export declare class FeatureFlagModel {
161
174
  * Checks if a user belongs to any of the specified groups
162
175
  */
163
176
  private isActiveForGroups;
177
+ /**
178
+ * Evaluates a feature flag for a user based on flag configuration.
179
+ * This is a stateless helper that performs the actual flag evaluation logic.
180
+ */
181
+ evaluateFlagForUser(flag: FeatureFlag, { user, roles, groups, }: {
182
+ user?: string;
183
+ roles?: string[];
184
+ groups?: string[];
185
+ }): Promise<boolean>;
164
186
  /**
165
187
  * Computes the hash value for a user ID using MurmurHash3.
166
188
  *
@@ -223,6 +245,59 @@ export declare class FeatureFlagModel {
223
245
  roles?: string[];
224
246
  groups?: string[];
225
247
  }): Promise<boolean>;
248
+ /**
249
+ * Checks if multiple feature flags are active for a user based on configured rules.
250
+ *
251
+ * @param params - Parameters for flag evaluation
252
+ * @param params.names - Optional array of feature flag names to check. If not provided, checks all flags.
253
+ * @param params.user - Optional user ID
254
+ * @param params.roles - Optional list of roles the user has
255
+ * @param params.groups - Optional list of groups the user belongs to
256
+ * @returns Record mapping flag names to their active status (true/false)
257
+ *
258
+ * @remarks
259
+ * This method fetches all flags in a single query for efficiency.
260
+ * If names is provided, only those flags are checked. Non-existent flags are marked as false.
261
+ * If names is not provided, all flags in the database are checked.
262
+ * Evaluation order for each flag (first match wins):
263
+ * 1. Everyone override (if set to true/false, returns immediately)
264
+ * 2. User ID is in the users list
265
+ * 3. User belongs to any group in the groups list
266
+ * 4. User has any role in the roles list
267
+ * 5. User falls within the percentage rollout (based on consistent hashing)
268
+ * 6. Default: returns false
269
+ *
270
+ * @example
271
+ * ```typescript
272
+ * // Check specific flags
273
+ * const results = await model.areActiveForUser({
274
+ * names: ["feature1", "feature2", "feature3"],
275
+ * user: "user_123",
276
+ * roles: ["admin"],
277
+ * groups: ["beta_testers"],
278
+ * });
279
+ *
280
+ * // Check all flags
281
+ * const allResults = await model.areActiveForUser({
282
+ * user: "user_123",
283
+ * roles: ["admin"],
284
+ * groups: ["beta_testers"],
285
+ * });
286
+ *
287
+ * if (results["feature1"]) {
288
+ * // Show feature1
289
+ * }
290
+ * if (results["feature2"]) {
291
+ * // Show feature2
292
+ * }
293
+ * ```
294
+ */
295
+ areActiveForUser({ names, user, roles, groups, }: {
296
+ names?: string[];
297
+ user?: string;
298
+ roles?: string[];
299
+ groups?: string[];
300
+ }): Promise<Record<string, boolean>>;
226
301
  }
227
302
  type CreateGroupInput = Omit<FeatureFlagGroup, "id" | "created" | "modified">;
228
303
  type UpdateGroupChanges = Partial<Omit<FeatureFlagGroup, "id" | "created" | "modified">>;
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
package/dist/model.js CHANGED
@@ -166,6 +166,25 @@ export class FeatureFlagModel {
166
166
  return null;
167
167
  return mapRow(res.rows[0]);
168
168
  }
169
+ /**
170
+ * Retrieves multiple feature flags by their names.
171
+ *
172
+ * @param names - Array of feature flag names to retrieve
173
+ * @returns Array of feature flags found (may be shorter than input if some names don't exist)
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * const flags = await model.getManyByName(["feature1", "feature2", "feature3"]);
178
+ * console.log(`Found ${flags.length} flags`);
179
+ * ```
180
+ */
181
+ async getManyByName(names) {
182
+ if (names.length === 0)
183
+ return [];
184
+ const sql = `SELECT ${COLUMNS.join(", ")} FROM ${TABLE} WHERE name = ANY($1)`;
185
+ const res = await this.db.query(sql, [names]);
186
+ return res.rows.map(mapRow);
187
+ }
169
188
  /**
170
189
  * Retrieves multiple feature flags by their IDs.
171
190
  *
@@ -300,6 +319,42 @@ export class FeatureFlagModel {
300
319
  return false;
301
320
  return flagGroups.some((group) => userGroups.includes(group));
302
321
  }
322
+ /**
323
+ * Evaluates a feature flag for a user based on flag configuration.
324
+ * This is a stateless helper that performs the actual flag evaluation logic.
325
+ */
326
+ async evaluateFlagForUser(flag, { user, roles, groups, }) {
327
+ // Everyone Override: If everyone is true or false, return that value immediately
328
+ if (flag.everyone !== undefined && flag.everyone !== null) {
329
+ return flag.everyone;
330
+ }
331
+ // User-Specific Check: If user ID is in the users array, return true
332
+ if (user && flag.users && flag.users.includes(user)) {
333
+ return true;
334
+ }
335
+ // Group Check: If any of the user's groups match any group in the groups array, return true
336
+ if (groups && this.isActiveForGroups(groups, flag.groups)) {
337
+ return true;
338
+ }
339
+ // Role Check: If any of the user's roles match any role in the roles array, return true
340
+ if (flag.roles && roles) {
341
+ for (const role of roles) {
342
+ if (flag.roles.includes(role)) {
343
+ return true;
344
+ }
345
+ }
346
+ }
347
+ // Percentage Check: If percentage rollout applies to this user, return rollout result
348
+ if (user && flag.percent && flag.percent > 0) {
349
+ const userHash = await this.hashUserId(user);
350
+ const bucket = userHash % 100;
351
+ if (bucket < flag.percent) {
352
+ return true;
353
+ }
354
+ }
355
+ // Default: Return false
356
+ return false;
357
+ }
303
358
  /**
304
359
  * Computes the hash value for a user ID using MurmurHash3.
305
360
  *
@@ -374,36 +429,93 @@ export class FeatureFlagModel {
374
429
  return expiredResult;
375
430
  }
376
431
  }
377
- // Everyone Override: If everyone is true or false, return that value immediately
378
- if (flag.everyone !== undefined && flag.everyone !== null) {
379
- return flag.everyone;
380
- }
381
- // User-Specific Check: If user ID is in the users array, return true
382
- if (user && flag.users && flag.users.includes(user)) {
383
- return true;
384
- }
385
- // Group Check: If any of the user's groups match any group in the groups array, return true
386
- if (groups && this.isActiveForGroups(groups, flag.groups)) {
387
- return true;
388
- }
389
- // Role Check: If any of the user's roles match any role in the roles array, return true
390
- if (flag.roles && roles) {
391
- for (const role of roles) {
392
- if (flag.roles.includes(role)) {
393
- return true;
394
- }
432
+ return this.evaluateFlagForUser(flag, { user, roles, groups });
433
+ }
434
+ /**
435
+ * Checks if multiple feature flags are active for a user based on configured rules.
436
+ *
437
+ * @param params - Parameters for flag evaluation
438
+ * @param params.names - Optional array of feature flag names to check. If not provided, checks all flags.
439
+ * @param params.user - Optional user ID
440
+ * @param params.roles - Optional list of roles the user has
441
+ * @param params.groups - Optional list of groups the user belongs to
442
+ * @returns Record mapping flag names to their active status (true/false)
443
+ *
444
+ * @remarks
445
+ * This method fetches all flags in a single query for efficiency.
446
+ * If names is provided, only those flags are checked. Non-existent flags are marked as false.
447
+ * If names is not provided, all flags in the database are checked.
448
+ * Evaluation order for each flag (first match wins):
449
+ * 1. Everyone override (if set to true/false, returns immediately)
450
+ * 2. User ID is in the users list
451
+ * 3. User belongs to any group in the groups list
452
+ * 4. User has any role in the roles list
453
+ * 5. User falls within the percentage rollout (based on consistent hashing)
454
+ * 6. Default: returns false
455
+ *
456
+ * @example
457
+ * ```typescript
458
+ * // Check specific flags
459
+ * const results = await model.areActiveForUser({
460
+ * names: ["feature1", "feature2", "feature3"],
461
+ * user: "user_123",
462
+ * roles: ["admin"],
463
+ * groups: ["beta_testers"],
464
+ * });
465
+ *
466
+ * // Check all flags
467
+ * const allResults = await model.areActiveForUser({
468
+ * user: "user_123",
469
+ * roles: ["admin"],
470
+ * groups: ["beta_testers"],
471
+ * });
472
+ *
473
+ * if (results["feature1"]) {
474
+ * // Show feature1
475
+ * }
476
+ * if (results["feature2"]) {
477
+ * // Show feature2
478
+ * }
479
+ * ```
480
+ */
481
+ async areActiveForUser({ names, user, roles, groups, }) {
482
+ let flags;
483
+ let requestedNames;
484
+ if (names === undefined) {
485
+ // If no names provided, get all flags
486
+ flags = await this.list();
487
+ requestedNames = flags.map((flag) => flag.name);
488
+ }
489
+ else {
490
+ // If names provided, fetch only those flags
491
+ flags = await this.getManyByName(names);
492
+ requestedNames = names;
493
+ }
494
+ const flagMap = new Map(flags.map((flag) => [flag.name, flag]));
495
+ const result = {};
496
+ for (const name of requestedNames) {
497
+ const flag = flagMap.get(name);
498
+ if (!flag) {
499
+ result[name] = false;
500
+ continue;
395
501
  }
396
- }
397
- // Percentage Check: If percentage rollout applies to this user, return rollout result
398
- if (user && flag.percent && flag.percent > 0) {
399
- const userHash = await this.hashUserId(user);
400
- const bucket = userHash % 100;
401
- if (bucket < flag.percent) {
402
- return true;
502
+ // If the flag has expired, trigger event
503
+ if (this.eventHandlers?.onExpired &&
504
+ flag.expires &&
505
+ flag.expires <= new Date()) {
506
+ const expiredResult = await this.eventHandlers.onExpired({ flag });
507
+ if (expiredResult !== undefined) {
508
+ result[name] = expiredResult;
509
+ continue;
510
+ }
403
511
  }
512
+ result[name] = await this.evaluateFlagForUser(flag, {
513
+ user,
514
+ roles,
515
+ groups,
516
+ });
404
517
  }
405
- // Default: Return false
406
- return false;
518
+ return result;
407
519
  }
408
520
  }
409
521
  const GROUP_TABLE = "flapjack.feature_flag_group";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandtg/flapjack",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A simple feature flags library with PostgreSQL integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",