@nestarc/feature-flag 0.3.0 → 0.4.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.
Files changed (43) hide show
  1. package/README.md +114 -6
  2. package/dist/admin/feature-flag-admin.controller.d.ts +3 -1
  3. package/dist/admin/feature-flag-admin.controller.js +12 -0
  4. package/dist/admin/feature-flag-admin.controller.js.map +1 -1
  5. package/dist/admin/feature-flag-admin.dto.d.ts +8 -0
  6. package/dist/admin/feature-flag-admin.dto.js +24 -1
  7. package/dist/admin/feature-flag-admin.dto.js.map +1 -1
  8. package/dist/events/feature-flag.events.d.ts +35 -2
  9. package/dist/events/feature-flag.events.js +1 -0
  10. package/dist/events/feature-flag.events.js.map +1 -1
  11. package/dist/flag-registry.d.ts +24 -0
  12. package/dist/flag-registry.js +62 -0
  13. package/dist/flag-registry.js.map +1 -0
  14. package/dist/guards/feature-flag.guard.js +5 -1
  15. package/dist/guards/feature-flag.guard.js.map +1 -1
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.js +7 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/interfaces/evaluation-context.interface.d.ts +2 -0
  20. package/dist/interfaces/evaluation-details.interface.d.ts +29 -0
  21. package/dist/interfaces/evaluation-details.interface.js +3 -0
  22. package/dist/interfaces/evaluation-details.interface.js.map +1 -0
  23. package/dist/interfaces/feature-flag-options.interface.d.ts +3 -0
  24. package/dist/interfaces/feature-flag.interface.d.ts +9 -0
  25. package/dist/interfaces/flag-registry.interface.d.ts +17 -0
  26. package/dist/interfaces/flag-registry.interface.js +3 -0
  27. package/dist/interfaces/flag-registry.interface.js.map +1 -0
  28. package/dist/openfeature.d.ts +26 -0
  29. package/dist/openfeature.js +96 -0
  30. package/dist/openfeature.js.map +1 -0
  31. package/dist/services/feature-flag.service.d.ts +14 -7
  32. package/dist/services/feature-flag.service.js +103 -24
  33. package/dist/services/feature-flag.service.js.map +1 -1
  34. package/dist/services/flag-evaluator.service.d.ts +7 -8
  35. package/dist/services/flag-evaluator.service.js +62 -10
  36. package/dist/services/flag-evaluator.service.js.map +1 -1
  37. package/dist/testing/index.d.ts +1 -1
  38. package/dist/testing/index.js +2 -1
  39. package/dist/testing/index.js.map +1 -1
  40. package/dist/testing/test-feature-flag.module.d.ts +21 -0
  41. package/dist/testing/test-feature-flag.module.js +78 -19
  42. package/dist/testing/test-feature-flag.module.js.map +1 -1
  43. package/package.json +13 -1
package/README.md CHANGED
@@ -12,15 +12,17 @@ DB-backed feature flags for NestJS + Prisma + PostgreSQL -- attribute-targeted o
12
12
 
13
13
  - **Database-backed** -- flags stored in PostgreSQL via Prisma, no external service required
14
14
  - **Attribute-targeted overrides** -- exact-match targeting for tenants, users, environments, plans, regions, or custom dimensions
15
- - **Percentage rollouts** -- deterministic hashing (murmurhash3) for consistent per-user bucketing
15
+ - **Percentage rollouts** -- deterministic hashing (murmurhash3) with explicit `targetingKey` / `bucketBy`
16
16
  - **Guard decorator** -- `@FeatureFlag()` automatically gates routes and controllers
17
17
  - **Bypass decorator** -- `@BypassFeatureFlag()` exempts health checks and public endpoints
18
- - **Programmatic evaluation** -- `isEnabled()` and `evaluateAll()` for service-layer logic
18
+ - **Programmatic evaluation** -- `isEnabled()`, `evaluateBoolean()`, and `evaluateAll()` for service-layer logic
19
+ - **Type-safe registry helpers** -- define flag keys, defaults, rollout bucket keys, exposure tracking, and lifecycle metadata in code
19
20
  - **Built-in caching** -- configurable TTL with manual invalidation; Redis Pub/Sub for multi-instance
20
21
  - **Pluggable persistence** -- `FeatureFlagRepository` interface for custom backends (Prisma default)
21
22
  - **Pluggable tenancy** -- `TenantContextProvider` interface for custom tenant resolution
22
23
  - **Admin REST API** -- opt-in `FeatureFlagAdminModule` with guard injection and proper error responses
23
24
  - **Event system** -- optional integration with `@nestjs/event-emitter` for audit and observability
25
+ - **OpenFeature adapter** -- optional boolean-only provider at `@nestarc/feature-flag/openfeature`
24
26
  - **Testing utilities** -- drop-in `TestFeatureFlagModule` for unit and integration tests
25
27
 
26
28
  ## Installation
@@ -43,6 +45,9 @@ npm install @nestjs/event-emitter
43
45
 
44
46
  # Required only if you use RedisCacheAdapter
45
47
  npm install ioredis
48
+
49
+ # Required only if you use the OpenFeature adapter with the SDK
50
+ npm install @openfeature/server-sdk
46
51
  ```
47
52
 
48
53
  ## Redis Cache (Multi-Instance)
@@ -279,6 +284,14 @@ getPremiumContent() { ... }
279
284
 
280
285
  When the flag is disabled, the guard responds with the given `statusCode` (default `403`) and optional `fallback` body.
281
286
 
287
+ Use `defaultValue` when a route should choose an invocation-specific fallback if a flag is missing or evaluation fails:
288
+
289
+ ```typescript
290
+ @FeatureFlag('OPTIONAL_PREVIEW', { defaultValue: true })
291
+ @Get('preview')
292
+ getPreview() { ... }
293
+ ```
294
+
282
295
  ### Bypassing the guard
283
296
 
284
297
  Use `@BypassFeatureFlag()` on methods that should always be accessible, even when a class-level flag is applied:
@@ -348,6 +361,78 @@ Passing `null` explicitly clears that dimension, suppressing any ambient value f
348
361
  const globalResult = await this.flags.isEnabled('MY_FLAG', { userId: null });
349
362
  ```
350
363
 
364
+ ### Detailed boolean evaluation
365
+
366
+ Use `evaluateBoolean()` when you need to explain why a flag resolved to a value:
367
+
368
+ ```typescript
369
+ const details = await this.flags.evaluateBoolean(
370
+ 'NEW_CHECKOUT',
371
+ { targetingKey: 'tenant-1', tenantId: 'tenant-1' },
372
+ { defaultValue: false, trackExposure: true },
373
+ );
374
+
375
+ console.log(details);
376
+ // {
377
+ // flagKey: 'NEW_CHECKOUT',
378
+ // value: true,
379
+ // result: true,
380
+ // source: 'percentage',
381
+ // reason: 'PERCENTAGE_MATCH',
382
+ // defaultUsed: false,
383
+ // bucket: 17,
384
+ // targetingKey: 'tenant-1',
385
+ // evaluationTimeMs: 1
386
+ // }
387
+ ```
388
+
389
+ Missing flags and evaluation errors return the selected default instead of throwing. Default priority is:
390
+
391
+ 1. Invocation `defaultValue`
392
+ 2. Registry `defaultValue`
393
+ 3. Module `defaultOnMissing`
394
+ 4. `false`
395
+
396
+ ### Type-safe flag registry
397
+
398
+ ```typescript
399
+ import { defineFlags, createFeatureFlagClient } from '@nestarc/feature-flag';
400
+
401
+ export const flags = defineFlags({
402
+ NEW_CHECKOUT: {
403
+ defaultValue: false,
404
+ bucketBy: 'tenantId',
405
+ trackExposure: true,
406
+ owner: 'payments',
407
+ tags: ['checkout'],
408
+ staleAt: '2026-09-01',
409
+ expiresAt: '2026-12-01',
410
+ },
411
+ });
412
+
413
+ const flagClient = createFeatureFlagClient(featureFlagService, flags);
414
+ const enabled = await flagClient.isEnabled('NEW_CHECKOUT', { tenantId: 'tenant-1' });
415
+ ```
416
+
417
+ You can also pass the registry to `FeatureFlagModule.forRoot({ flags })` so service-level fallback and `bucketBy` defaults apply to direct `FeatureFlagService` calls.
418
+
419
+ ### OpenFeature boolean adapter
420
+
421
+ The optional adapter lives on a separate subpath and delegates boolean resolution to `FeatureFlagService`:
422
+
423
+ ```typescript
424
+ import { createOpenFeatureBooleanProvider } from '@nestarc/feature-flag/openfeature';
425
+
426
+ const provider = createOpenFeatureBooleanProvider(featureFlagService);
427
+ const result = await provider.resolveBooleanEvaluation(
428
+ 'NEW_CHECKOUT',
429
+ false,
430
+ { targetingKey: 'tenant-1', tenantId: 'tenant-1', plan: 'pro' },
431
+ );
432
+ ```
433
+
434
+ Only boolean evaluation is supported in v0.4.0. Variant flags and string/number/json remote config remain out of scope.
435
+
351
436
  ## Attribute Targeting
352
437
 
353
438
  Overrides match exact attributes. Every key/value in an override's `attributes` object must exist in the evaluation context attributes for the override to apply.
@@ -432,6 +517,7 @@ export class AppModule {}
432
517
  | Event constant | Event string | Payload type |
433
518
  | ---------------------------------------- | ---------------------------------- | -------------------- |
434
519
  | `FeatureFlagEvents.EVALUATED` | `feature-flag.evaluated` | `FlagEvaluatedEvent` |
520
+ | `FeatureFlagEvents.EXPOSED` | `feature-flag.exposed` | `FlagExposedEvent` |
435
521
  | `FeatureFlagEvents.CREATED` | `feature-flag.created` | `FlagMutationEvent` |
436
522
  | `FeatureFlagEvents.UPDATED` | `feature-flag.updated` | `FlagMutationEvent` |
437
523
  | `FeatureFlagEvents.ARCHIVED` | `feature-flag.archived` | `FlagMutationEvent` |
@@ -449,11 +535,13 @@ import { FeatureFlagEvents, FlagEvaluatedEvent } from '@nestarc/feature-flag';
449
535
  export class FlagAuditListener {
450
536
  @OnEvent(FeatureFlagEvents.EVALUATED)
451
537
  handleEvaluation(event: FlagEvaluatedEvent) {
452
- console.log(`Flag ${event.flagKey} = ${event.result} (source: ${event.source})`);
538
+ console.log(`Flag ${event.flagKey} = ${event.result} (${event.reason})`);
453
539
  }
454
540
  }
455
541
  ```
456
542
 
543
+ Exposure events are opt-in per call, registry entry, or flag metadata via `trackExposure`. They do not persist analytics; attach your own listener if you need sampling, batching, or storage.
544
+
457
545
  ## Testing
458
546
 
459
547
  Import `TestFeatureFlagModule` from the `/testing` subpath to stub flag values in tests without a database connection:
@@ -489,11 +577,29 @@ describe('DashboardController', () => {
489
577
 
490
578
  `TestFeatureFlagModule.register()` provides a global mock of `FeatureFlagService`:
491
579
  - `isEnabled(key)` returns the boolean you specified (defaulting to `false` for unregistered keys)
580
+ - `evaluateBoolean(key)` returns `BooleanEvaluationDetails`
492
581
  - `evaluateAll()` returns the full flag map
493
582
  - `create()`, `update()`, `archive()`, `findByKey()`, `findAll()` return full `FeatureFlagWithOverrides` stub objects
494
583
  - `findByKey()` throws `NotFoundException` for unknown keys
495
584
 
496
- This is a **stateless boolean stub** -- write operations do not persist state across calls. For stateful test doubles, use your own mock implementation.
585
+ For registry-based tests, use `registerRegistry()` and the injected controller:
586
+
587
+ ```typescript
588
+ import {
589
+ TestFeatureFlagController,
590
+ TestFeatureFlagModule,
591
+ } from '@nestarc/feature-flag/testing';
592
+
593
+ const module = await Test.createTestingModule({
594
+ imports: [TestFeatureFlagModule.registerRegistry(flags)],
595
+ }).compile();
596
+
597
+ const testFlags = module.get(TestFeatureFlagController);
598
+ testFlags.set('NEW_CHECKOUT', true);
599
+ testFlags.reset();
600
+ ```
601
+
602
+ The testing controller keeps state inside the compiled testing module. CRUD-style write methods on the mocked service still return stub objects and do not persist database rows.
497
603
 
498
604
  ## Evaluation Priority
499
605
 
@@ -503,10 +609,10 @@ When `isEnabled()` is called, flags are evaluated through the current cascade. T
503
609
  | -------- | ---------------------- | ------------------------------------------------------------------ |
504
610
  | 1 | **Archived** | If the flag has `archivedAt` set, evaluation always returns `false` |
505
611
  | 2 | **Attribute override** | Best override whose attributes are all present in the evaluation context |
506
- | 3 | **Percentage rollout** | Deterministic hash of `flagKey + userId` (or `tenantId`) mod 100 |
612
+ | 3 | **Percentage rollout** | Deterministic hash of `flagKey + targetingKey` mod 100 |
507
613
  | 4 | **Global default** | The flag's `enabled` field |
508
614
 
509
- Percentage rollout uses murmurhash3 for deterministic bucketing: the same user always gets the same result for a given flag, ensuring a consistent experience across requests.
615
+ Percentage rollout uses murmurhash3 for deterministic bucketing. The targeting key is resolved in this order: explicit `context.targetingKey`, registry or metadata `bucketBy`, then the legacy `userId ?? tenantId` fallback.
510
616
 
511
617
  ## Configuration Reference
512
618
 
@@ -520,6 +626,7 @@ Percentage rollout uses murmurhash3 for deterministic bucketing: the same user a
520
626
  | `defaultOnMissing` | `boolean` | `false` | Value returned when a flag key does not exist in the database |
521
627
  | `emitEvents` | `boolean` | `false` | Emit lifecycle events via `@nestjs/event-emitter` |
522
628
  | `cacheAdapter` | `CacheAdapter` | `MemoryCacheAdapter` | Pluggable cache backend (e.g. `RedisCacheAdapter`) |
629
+ | `flags` | `FlagRegistry` | `undefined` | Optional typed registry for defaults, `bucketBy`, and exposure settings |
523
630
 
524
631
  ### FeatureFlagModuleRootOptions
525
632
 
@@ -587,6 +694,7 @@ export class AppModule {}
587
694
  | GET | `/feature-flags/:key` | Get a single flag | 404 not found |
588
695
  | PATCH | `/feature-flags/:key` | Update a flag | 404 not found, 400 invalid percentage |
589
696
  | DELETE | `/feature-flags/:key` | Archive a flag | 404 not found |
697
+ | POST | `/feature-flags/:key/evaluate` | Evaluate a flag without mutating it | |
590
698
  | POST | `/feature-flags/:key/overrides` | Set an override | 404 flag not found |
591
699
  | DELETE | `/feature-flags/:key/overrides` | Remove an override | 404 flag not found |
592
700
 
@@ -1,6 +1,7 @@
1
1
  import { FeatureFlagService } from '../services/feature-flag.service';
2
2
  import { FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
3
- import { CreateFeatureFlagDto, RemoveOverrideDto, SetOverrideDto, UpdateFeatureFlagDto } from './feature-flag-admin.dto';
3
+ import { BooleanEvaluationDetails } from '../interfaces/evaluation-details.interface';
4
+ import { CreateFeatureFlagDto, EvaluateFeatureFlagDto, RemoveOverrideDto, SetOverrideDto, UpdateFeatureFlagDto } from './feature-flag-admin.dto';
4
5
  export declare class FeatureFlagAdminController {
5
6
  private readonly service;
6
7
  constructor(service: FeatureFlagService);
@@ -9,6 +10,7 @@ export declare class FeatureFlagAdminController {
9
10
  findByKey(key: string): Promise<FeatureFlagWithOverrides>;
10
11
  update(key: string, input: UpdateFeatureFlagDto): Promise<FeatureFlagWithOverrides>;
11
12
  archive(key: string): Promise<FeatureFlagWithOverrides>;
13
+ evaluate(key: string, input: EvaluateFeatureFlagDto): Promise<BooleanEvaluationDetails>;
12
14
  setOverride(key: string, input: SetOverrideDto): Promise<void>;
13
15
  removeOverride(key: string, input: RemoveOverrideDto): Promise<void>;
14
16
  }
@@ -35,6 +35,10 @@ let FeatureFlagAdminController = class FeatureFlagAdminController {
35
35
  archive(key) {
36
36
  return this.service.archive(key);
37
37
  }
38
+ evaluate(key, input) {
39
+ const { context, ...options } = input;
40
+ return this.service.evaluateBoolean(key, context, options);
41
+ }
38
42
  setOverride(key, input) {
39
43
  return this.service.setOverride(key, input);
40
44
  }
@@ -78,6 +82,14 @@ __decorate([
78
82
  __metadata("design:paramtypes", [String]),
79
83
  __metadata("design:returntype", Promise)
80
84
  ], FeatureFlagAdminController.prototype, "archive", null);
85
+ __decorate([
86
+ (0, common_1.Post)(':key/evaluate'),
87
+ __param(0, (0, common_1.Param)('key')),
88
+ __param(1, (0, common_1.Body)()),
89
+ __metadata("design:type", Function),
90
+ __metadata("design:paramtypes", [String, feature_flag_admin_dto_1.EvaluateFeatureFlagDto]),
91
+ __metadata("design:returntype", Promise)
92
+ ], FeatureFlagAdminController.prototype, "evaluate", null);
81
93
  __decorate([
82
94
  (0, common_1.Post)(':key/overrides'),
83
95
  __param(0, (0, common_1.Param)('key')),
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag-admin.controller.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,2EAAsE;AAEtE,qEAKkC;AAU3B,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAG5D,MAAM,CAAS,KAA2B;QACxC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAGD,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAGD,SAAS,CAAe,GAAW;QACjC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;IAGD,MAAM,CACU,GAAW,EACjB,KAA2B;QAEnC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAGD,OAAO,CAAe,GAAW;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAGD,WAAW,CAAe,GAAW,EAAU,KAAqB;QAClE,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAGD,cAAc,CAAe,GAAW,EAAU,KAAwB;QACxE,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAxCY,gEAA0B;AAIrC;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAQ,6CAAoB;;wDAEzC;AAGD;IADC,IAAA,YAAG,GAAE;;;;yDAGL;AAGD;IADC,IAAA,YAAG,EAAC,MAAM,CAAC;IACD,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;2DAEtB;AAGD;IADC,IAAA,cAAK,EAAC,MAAM,CAAC;IAEX,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,6CAAoB;;wDAGpC;AAGD;IADC,IAAA,eAAM,EAAC,MAAM,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;yDAEpB;AAGD;IADC,IAAA,aAAI,EAAC,gBAAgB,CAAC;IACV,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IAAe,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,uCAAc;;6DAEnE;AAGD;IADC,IAAA,eAAM,EAAC,gBAAgB,CAAC;IACT,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IAAe,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,0CAAiB;;gEAEzE;qCAvCU,0BAA0B;IARtC,IAAA,mBAAU,GAAE;IACZ,IAAA,iBAAQ,EACP,IAAI,uBAAc,CAAC;QACjB,SAAS,EAAE,IAAI;QACf,oBAAoB,EAAE,IAAI;QAC1B,SAAS,EAAE,IAAI;KAChB,CAAC,CACH;qCAEuC,yCAAkB;GAD7C,0BAA0B,CAwCtC"}
1
+ {"version":3,"file":"feature-flag-admin.controller.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,2EAAsE;AAGtE,qEAMkC;AAU3B,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAG5D,MAAM,CAAS,KAA2B;QACxC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAGD,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAGD,SAAS,CAAe,GAAW;QACjC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;IAGD,MAAM,CACU,GAAW,EACjB,KAA2B;QAEnC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAGD,OAAO,CAAe,GAAW;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAGD,QAAQ,CACQ,GAAW,EACjB,KAA6B;QAErC,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,GAAG,KAAK,CAAC;QACtC,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC7D,CAAC;IAGD,WAAW,CAAe,GAAW,EAAU,KAAqB;QAClE,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAGD,cAAc,CAAe,GAAW,EAAU,KAAwB;QACxE,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAjDY,gEAA0B;AAIrC;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAQ,6CAAoB;;wDAEzC;AAGD;IADC,IAAA,YAAG,GAAE;;;;yDAGL;AAGD;IADC,IAAA,YAAG,EAAC,MAAM,CAAC;IACD,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;2DAEtB;AAGD;IADC,IAAA,cAAK,EAAC,MAAM,CAAC;IAEX,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,6CAAoB;;wDAGpC;AAGD;IADC,IAAA,eAAM,EAAC,MAAM,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;yDAEpB;AAGD;IADC,IAAA,aAAI,EAAC,eAAe,CAAC;IAEnB,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,+CAAsB;;0DAItC;AAGD;IADC,IAAA,aAAI,EAAC,gBAAgB,CAAC;IACV,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IAAe,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,uCAAc;;6DAEnE;AAGD;IADC,IAAA,eAAM,EAAC,gBAAgB,CAAC;IACT,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IAAe,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,0CAAiB;;gEAEzE;qCAhDU,0BAA0B;IARtC,IAAA,mBAAU,GAAE;IACZ,IAAA,iBAAQ,EACP,IAAI,uBAAc,CAAC;QACjB,SAAS,EAAE,IAAI;QACf,oBAAoB,EAAE,IAAI;QAC1B,SAAS,EAAE,IAAI;KAChB,CAAC,CACH;qCAEuC,yCAAkB;GAD7C,0BAA0B,CAiDtC"}
@@ -1,3 +1,5 @@
1
+ import { EvaluationContext } from '../interfaces/evaluation-context.interface';
2
+ import { EvaluateBooleanOptions } from '../interfaces/evaluation-details.interface';
1
3
  import { TargetingAttributes } from '../interfaces/feature-flag.interface';
2
4
  export declare class CreateFeatureFlagDto {
3
5
  key: string;
@@ -20,3 +22,9 @@ export declare class SetOverrideDto {
20
22
  export declare class RemoveOverrideDto {
21
23
  attributes: TargetingAttributes;
22
24
  }
25
+ export declare class EvaluateFeatureFlagDto implements EvaluateBooleanOptions {
26
+ context?: EvaluationContext;
27
+ defaultValue?: boolean;
28
+ trackExposure?: boolean;
29
+ includeContextInEvent?: boolean;
30
+ }
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.RemoveOverrideDto = exports.SetOverrideDto = exports.UpdateFeatureFlagDto = exports.CreateFeatureFlagDto = void 0;
12
+ exports.EvaluateFeatureFlagDto = exports.RemoveOverrideDto = exports.SetOverrideDto = exports.UpdateFeatureFlagDto = exports.CreateFeatureFlagDto = void 0;
13
13
  const class_validator_1 = require("class-validator");
14
14
  const targeting_attributes_validator_1 = require("./targeting-attributes.validator");
15
15
  class CreateFeatureFlagDto {
@@ -90,4 +90,27 @@ __decorate([
90
90
  (0, targeting_attributes_validator_1.IsTargetingAttributes)(),
91
91
  __metadata("design:type", Object)
92
92
  ], RemoveOverrideDto.prototype, "attributes", void 0);
93
+ class EvaluateFeatureFlagDto {
94
+ }
95
+ exports.EvaluateFeatureFlagDto = EvaluateFeatureFlagDto;
96
+ __decorate([
97
+ (0, class_validator_1.IsOptional)(),
98
+ (0, class_validator_1.IsObject)(),
99
+ __metadata("design:type", Object)
100
+ ], EvaluateFeatureFlagDto.prototype, "context", void 0);
101
+ __decorate([
102
+ (0, class_validator_1.IsOptional)(),
103
+ (0, class_validator_1.IsBoolean)(),
104
+ __metadata("design:type", Boolean)
105
+ ], EvaluateFeatureFlagDto.prototype, "defaultValue", void 0);
106
+ __decorate([
107
+ (0, class_validator_1.IsOptional)(),
108
+ (0, class_validator_1.IsBoolean)(),
109
+ __metadata("design:type", Boolean)
110
+ ], EvaluateFeatureFlagDto.prototype, "trackExposure", void 0);
111
+ __decorate([
112
+ (0, class_validator_1.IsOptional)(),
113
+ (0, class_validator_1.IsBoolean)(),
114
+ __metadata("design:type", Boolean)
115
+ ], EvaluateFeatureFlagDto.prototype, "includeContextInEvent", void 0);
93
116
  //# sourceMappingURL=feature-flag-admin.dto.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag-admin.dto.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDASyB;AAEzB,qFAAyE;AAEzE,MAAa,oBAAoB;CAsBhC;AAtBD,oDAsBC;AAnBC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,4BAAU,GAAE;;iDACA;AAIb;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;yDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qDACM;AAMlB;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,GAAG,CAAC;;wDACW;AAIpB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sDACwB;AAGrC,MAAa,oBAAoB;CAkBhC;AAlBD,oDAkBC;AAfC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;yDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qDACM;AAMlB;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,GAAG,CAAC;;wDACW;AAIpB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sDACwB;AAGrC,MAAa,cAAc;CAU1B;AAVD,wCAUC;AARC;IADC,IAAA,sDAAqB,GAAE;;kDACS;AAGjC;IADC,IAAA,2BAAS,GAAE;;+CACM;AAIlB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;;gDACU;AAGpB,MAAa,iBAAiB;CAG7B;AAHD,8CAGC;AADC;IADC,IAAA,sDAAqB,GAAE;;qDACS"}
1
+ {"version":3,"file":"feature-flag-admin.dto.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDASyB;AAIzB,qFAAyE;AAEzE,MAAa,oBAAoB;CAsBhC;AAtBD,oDAsBC;AAnBC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,4BAAU,GAAE;;iDACA;AAIb;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;yDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qDACM;AAMlB;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,GAAG,CAAC;;wDACW;AAIpB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sDACwB;AAGrC,MAAa,oBAAoB;CAkBhC;AAlBD,oDAkBC;AAfC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;yDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qDACM;AAMlB;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,GAAG,CAAC;;wDACW;AAIpB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sDACwB;AAGrC,MAAa,cAAc;CAU1B;AAVD,wCAUC;AARC;IADC,IAAA,sDAAqB,GAAE;;kDACS;AAGjC;IADC,IAAA,2BAAS,GAAE;;+CACM;AAIlB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;;gDACU;AAGpB,MAAa,iBAAiB;CAG7B;AAHD,8CAGC;AADC;IADC,IAAA,sDAAqB,GAAE;;qDACS;AAGnC,MAAa,sBAAsB;CAgBlC;AAhBD,wDAgBC;AAbC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;uDACiB;AAI5B;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;4DACW;AAIvB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;6DACY;AAIxB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qEACoB"}
@@ -1,6 +1,8 @@
1
1
  import { EvaluationContext } from '../interfaces/evaluation-context.interface';
2
+ import { EvaluationReason, EvaluationSource } from '../interfaces/evaluation-details.interface';
2
3
  export declare const FeatureFlagEvents: {
3
4
  readonly EVALUATED: "feature-flag.evaluated";
5
+ readonly EXPOSED: "feature-flag.exposed";
4
6
  readonly CREATED: "feature-flag.created";
5
7
  readonly UPDATED: "feature-flag.updated";
6
8
  readonly ARCHIVED: "feature-flag.archived";
@@ -11,13 +13,39 @@ export declare const FeatureFlagEvents: {
11
13
  export interface FlagEvaluatedEvent {
12
14
  flagKey: string;
13
15
  result: boolean;
14
- context: EvaluationContext;
15
- source: 'override' | 'percentage' | 'global';
16
+ value?: boolean;
17
+ context?: EvaluationContext;
18
+ source: EvaluationSource;
19
+ reason?: EvaluationReason;
20
+ defaultUsed?: boolean;
21
+ errorCode?: string;
22
+ errorMessage?: string;
23
+ matchedOverrideId?: string;
24
+ bucket?: number;
25
+ targetingKey?: string;
16
26
  evaluationTimeMs: number;
17
27
  }
28
+ export interface FlagExposedEvent {
29
+ flagKey: string;
30
+ value: boolean;
31
+ result: boolean;
32
+ source: EvaluationSource;
33
+ reason: EvaluationReason;
34
+ defaultUsed: boolean;
35
+ context?: EvaluationContext;
36
+ matchedOverrideId?: string;
37
+ bucket?: number;
38
+ targetingKey?: string;
39
+ evaluationTimeMs?: number;
40
+ }
18
41
  export interface FlagMutationEvent {
19
42
  flagKey: string;
20
43
  action: 'created' | 'updated' | 'archived';
44
+ actorId?: string;
45
+ actorType?: string;
46
+ reason?: string;
47
+ requestId?: string;
48
+ correlationId?: string;
21
49
  }
22
50
  export interface FlagOverrideEvent {
23
51
  flagKey: string;
@@ -25,4 +53,9 @@ export interface FlagOverrideEvent {
25
53
  enabled?: boolean;
26
54
  priority?: number;
27
55
  action: 'set' | 'removed';
56
+ actorId?: string;
57
+ actorType?: string;
58
+ reason?: string;
59
+ requestId?: string;
60
+ correlationId?: string;
28
61
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FeatureFlagEvents = void 0;
4
4
  exports.FeatureFlagEvents = {
5
5
  EVALUATED: 'feature-flag.evaluated',
6
+ EXPOSED: 'feature-flag.exposed',
6
7
  CREATED: 'feature-flag.created',
7
8
  UPDATED: 'feature-flag.updated',
8
9
  ARCHIVED: 'feature-flag.archived',
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.events.js","sourceRoot":"","sources":["../../src/events/feature-flag.events.ts"],"names":[],"mappings":";;;AAEa,QAAA,iBAAiB,GAAG;IAC/B,SAAS,EAAE,wBAAwB;IACnC,OAAO,EAAE,sBAAsB;IAC/B,OAAO,EAAE,sBAAsB;IAC/B,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,2BAA2B;IACzC,gBAAgB,EAAE,+BAA+B;IACjD,iBAAiB,EAAE,gCAAgC;CAC3C,CAAC"}
1
+ {"version":3,"file":"feature-flag.events.js","sourceRoot":"","sources":["../../src/events/feature-flag.events.ts"],"names":[],"mappings":";;;AAMa,QAAA,iBAAiB,GAAG;IAC/B,SAAS,EAAE,wBAAwB;IACnC,OAAO,EAAE,sBAAsB;IAC/B,OAAO,EAAE,sBAAsB;IAC/B,OAAO,EAAE,sBAAsB;IAC/B,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,2BAA2B;IACzC,gBAAgB,EAAE,+BAA+B;IACjD,iBAAiB,EAAE,gCAAgC;CAC3C,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { EvaluationContext } from './interfaces/evaluation-context.interface';
2
+ import { BooleanEvaluationDetails, EvaluateBooleanOptions } from './interfaces/evaluation-details.interface';
3
+ import { FeatureFlagGuardOptions } from './interfaces/feature-flag.interface';
4
+ import { FeatureFlagLifecycleMetadata, FlagKey, FlagRegistry } from './interfaces/flag-registry.interface';
5
+ import { FeatureFlagService } from './services/feature-flag.service';
6
+ export interface TypedFeatureFlagClient<TFlags extends FlagRegistry> {
7
+ isEnabled<K extends FlagKey<TFlags>>(flagKey: K, context?: EvaluationContext, options?: EvaluateBooleanOptions): Promise<boolean>;
8
+ evaluateBoolean<K extends FlagKey<TFlags>>(flagKey: K, context?: EvaluationContext, options?: EvaluateBooleanOptions): Promise<BooleanEvaluationDetails>;
9
+ registry: TFlags;
10
+ }
11
+ export interface TypedFeatureFlagDecorators<TFlags extends FlagRegistry> {
12
+ FeatureFlag<K extends FlagKey<TFlags>>(flagKey: K, options?: FeatureFlagGuardOptions): ClassDecorator & MethodDecorator;
13
+ }
14
+ export type FlagLifecycleStatusName = 'active' | 'stale' | 'expired';
15
+ export interface FlagLifecycleStatus extends FeatureFlagLifecycleMetadata {
16
+ status: FlagLifecycleStatusName;
17
+ tags: string[];
18
+ staleAt?: Date;
19
+ expiresAt?: Date;
20
+ }
21
+ export declare function defineFlags<const TFlags extends FlagRegistry>(flags: TFlags): TFlags;
22
+ export declare function createFeatureFlagClient<TFlags extends FlagRegistry>(service: FeatureFlagService, registry: TFlags): TypedFeatureFlagClient<TFlags>;
23
+ export declare function createFeatureFlagDecorators<TFlags extends FlagRegistry>(registry: TFlags): TypedFeatureFlagDecorators<TFlags>;
24
+ export declare function getFlagLifecycleStatus(metadata: FeatureFlagLifecycleMetadata, now?: Date): FlagLifecycleStatus;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineFlags = defineFlags;
4
+ exports.createFeatureFlagClient = createFeatureFlagClient;
5
+ exports.createFeatureFlagDecorators = createFeatureFlagDecorators;
6
+ exports.getFlagLifecycleStatus = getFlagLifecycleStatus;
7
+ const feature_flag_decorator_1 = require("./decorators/feature-flag.decorator");
8
+ function defineFlags(flags) {
9
+ return flags;
10
+ }
11
+ function createFeatureFlagClient(service, registry) {
12
+ return {
13
+ registry,
14
+ isEnabled: (flagKey, context, options) => service.isEnabled(flagKey, context, mergeRegistryOptions(registry[flagKey], options)),
15
+ evaluateBoolean: (flagKey, context, options) => service.evaluateBoolean(flagKey, context, mergeRegistryOptions(registry[flagKey], options)),
16
+ };
17
+ }
18
+ function createFeatureFlagDecorators(registry) {
19
+ return {
20
+ FeatureFlag: (flagKey, options = {}) => (0, feature_flag_decorator_1.FeatureFlag)(flagKey, {
21
+ defaultValue: registry[flagKey].defaultValue,
22
+ ...options,
23
+ }),
24
+ };
25
+ }
26
+ function getFlagLifecycleStatus(metadata, now = new Date()) {
27
+ const staleAt = parseLifecycleDate(metadata.staleAt);
28
+ const expiresAt = parseLifecycleDate(metadata.expiresAt);
29
+ const status = getLifecycleStatusName(now, staleAt, expiresAt);
30
+ return {
31
+ ...metadata,
32
+ tags: [...(metadata.tags ?? [])],
33
+ staleAt,
34
+ expiresAt,
35
+ status,
36
+ };
37
+ }
38
+ function mergeRegistryOptions(definition, options = {}) {
39
+ const merged = {
40
+ defaultValue: definition.defaultValue,
41
+ };
42
+ if (definition.trackExposure !== undefined) {
43
+ merged.trackExposure = definition.trackExposure;
44
+ }
45
+ return { ...merged, ...options };
46
+ }
47
+ function getLifecycleStatusName(now, staleAt, expiresAt) {
48
+ if (expiresAt && now >= expiresAt) {
49
+ return 'expired';
50
+ }
51
+ if (staleAt && now >= staleAt) {
52
+ return 'stale';
53
+ }
54
+ return 'active';
55
+ }
56
+ function parseLifecycleDate(value) {
57
+ if (!value) {
58
+ return undefined;
59
+ }
60
+ return value instanceof Date ? value : new Date(value);
61
+ }
62
+ //# sourceMappingURL=flag-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flag-registry.js","sourceRoot":"","sources":["../src/flag-registry.ts"],"names":[],"mappings":";;AA8CA,kCAEC;AAED,0DAeC;AAED,kEAUC;AAED,wDAeC;AA9FD,gFAAkE;AA8ClE,SAAgB,WAAW,CAAoC,KAAa;IAC1E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAgB,uBAAuB,CACrC,OAA2B,EAC3B,QAAgB;IAEhB,OAAO;QACL,QAAQ;QACR,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CACvC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;QACvF,eAAe,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAC7C,OAAO,CAAC,eAAe,CACrB,OAAO,EACP,OAAO,EACP,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CACjD;KACJ,CAAC;AACJ,CAAC;AAED,SAAgB,2BAA2B,CACzC,QAAgB;IAEhB,OAAO;QACL,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CACrC,IAAA,oCAAW,EAAC,OAAO,EAAE;YACnB,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,YAAY;YAC5C,GAAG,OAAO;SACX,CAAC;KACL,CAAC;AACJ,CAAC;AAED,SAAgB,sBAAsB,CACpC,QAAsC,EACtC,MAAY,IAAI,IAAI,EAAE;IAEtB,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;IAE/D,OAAO;QACL,GAAG,QAAQ;QACX,IAAI,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAChC,OAAO;QACP,SAAS;QACT,MAAM;KACP,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAC3B,UAAgC,EAChC,UAAkC,EAAE;IAEpC,MAAM,MAAM,GAA2B;QACrC,YAAY,EAAE,UAAU,CAAC,YAAY;KACtC,CAAC;IAEF,IAAI,UAAU,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,CAAC,aAAa,GAAG,UAAU,CAAC,aAAa,CAAC;IAClD,CAAC;IAED,OAAO,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,SAAS,sBAAsB,CAC7B,GAAS,EACT,OAAc,EACd,SAAgB;IAEhB,IAAI,SAAS,IAAI,GAAG,IAAI,SAAS,EAAE,CAAC;QAClC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,IAAI,OAAO,IAAI,GAAG,IAAI,OAAO,EAAE,CAAC;QAC9B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAgC;IAC1D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC"}
@@ -35,7 +35,11 @@ let FeatureFlagGuard = class FeatureFlagGuard {
35
35
  const options = this.reflector.get(feature_flag_constants_1.FEATURE_FLAG_OPTIONS_KEY, handler) ??
36
36
  this.reflector.get(feature_flag_constants_1.FEATURE_FLAG_OPTIONS_KEY, classRef) ??
37
37
  {};
38
- const enabled = await this.featureFlagService.isEnabled(flagKey);
38
+ const enabled = options.defaultValue === undefined
39
+ ? await this.featureFlagService.isEnabled(flagKey)
40
+ : await this.featureFlagService.isEnabled(flagKey, undefined, {
41
+ defaultValue: options.defaultValue,
42
+ });
39
43
  if (!enabled) {
40
44
  const statusCode = options.statusCode ?? 403;
41
45
  const body = options.fallback ?? { message: 'Feature not available' };
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.guard.js","sourceRoot":"","sources":["../../src/guards/feature-flag.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA0F;AAC1F,uCAAyC;AACzC,2EAAsE;AACtE,sEAImC;AAI5B,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAC3B,YACmB,SAAoB,EACpB,kBAAsC;QADtC,cAAS,GAAT,SAAS,CAAW;QACpB,uBAAkB,GAAlB,kBAAkB,CAAoB;IACtD,CAAC;IAEJ,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAEpC,eAAe;QACf,MAAM,MAAM,GACV,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,gDAAuB,EAAE,OAAO,CAAC;YAC7D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,gDAAuB,EAAE,QAAQ,CAAC,CAAC;QACjE,IAAI,MAAM;YAAE,OAAO,IAAI,CAAC;QAExB,yDAAyD;QACzD,MAAM,OAAO,GACX,IAAI,CAAC,SAAS,CAAC,GAAG,CAAS,yCAAgB,EAAE,OAAO,CAAC;YACrD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAS,yCAAgB,EAAE,QAAQ,CAAC,CAAC;QACzD,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAE1B,MAAM,OAAO,GACX,IAAI,CAAC,SAAS,CAAC,GAAG,CAA0B,iDAAwB,EAAE,OAAO,CAAC;YAC9E,IAAI,CAAC,SAAS,CAAC,GAAG,CAA0B,iDAAwB,EAAE,QAAQ,CAAC;YAC/E,EAAE,CAAC;QAEL,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAEjE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;YAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC;YACtE,MAAM,IAAI,sBAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC5C,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AArCY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;qCAGmB,gBAAS;QACA,yCAAkB;GAH9C,gBAAgB,CAqC5B"}
1
+ {"version":3,"file":"feature-flag.guard.js","sourceRoot":"","sources":["../../src/guards/feature-flag.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA0F;AAC1F,uCAAyC;AACzC,2EAAsE;AACtE,sEAImC;AAI5B,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAC3B,YACmB,SAAoB,EACpB,kBAAsC;QADtC,cAAS,GAAT,SAAS,CAAW;QACpB,uBAAkB,GAAlB,kBAAkB,CAAoB;IACtD,CAAC;IAEJ,KAAK,CAAC,WAAW,CAAC,OAAyB;QACzC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;QAEpC,eAAe;QACf,MAAM,MAAM,GACV,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,gDAAuB,EAAE,OAAO,CAAC;YAC7D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAU,gDAAuB,EAAE,QAAQ,CAAC,CAAC;QACjE,IAAI,MAAM;YAAE,OAAO,IAAI,CAAC;QAExB,yDAAyD;QACzD,MAAM,OAAO,GACX,IAAI,CAAC,SAAS,CAAC,GAAG,CAAS,yCAAgB,EAAE,OAAO,CAAC;YACrD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAS,yCAAgB,EAAE,QAAQ,CAAC,CAAC;QACzD,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAE1B,MAAM,OAAO,GACX,IAAI,CAAC,SAAS,CAAC,GAAG,CAA0B,iDAAwB,EAAE,OAAO,CAAC;YAC9E,IAAI,CAAC,SAAS,CAAC,GAAG,CAA0B,iDAAwB,EAAE,QAAQ,CAAC;YAC/E,EAAE,CAAC;QAEL,MAAM,OAAO,GACX,OAAO,CAAC,YAAY,KAAK,SAAS;YAChC,CAAC,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,OAAO,CAAC;YAClD,CAAC,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE;gBAC1D,YAAY,EAAE,OAAO,CAAC,YAAY;aACnC,CAAC,CAAC;QAET,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;YAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC;YACtE,MAAM,IAAI,sBAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC5C,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AA1CY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;qCAGmB,gBAAS;QACA,yCAAkB;GAH9C,gBAAgB,CA0C5B"}
package/dist/index.d.ts CHANGED
@@ -6,8 +6,11 @@ export { FeatureFlag } from './decorators/feature-flag.decorator';
6
6
  export { BypassFeatureFlag } from './decorators/bypass-feature-flag.decorator';
7
7
  export { FeatureFlagModuleOptions, FeatureFlagModuleAsyncOptions, FeatureFlagModuleOptionsFactory, } from './interfaces/feature-flag-options.interface';
8
8
  export { EvaluationContext } from './interfaces/evaluation-context.interface';
9
- export { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, RemoveOverrideInput, TargetingAttributeValue, TargetingAttributes, FeatureFlagGuardOptions, FeatureFlagWithOverrides, FlagOverride, } from './interfaces/feature-flag.interface';
10
- export { FeatureFlagEvents, FlagEvaluatedEvent, FlagMutationEvent, FlagOverrideEvent, } from './events/feature-flag.events';
9
+ export { EvaluationSource, EvaluationReason, EvaluateBooleanOptions, BooleanEvaluationDetails, BucketBy, FlagEvaluatorOptions, } from './interfaces/evaluation-details.interface';
10
+ export { FeatureFlagLifecycleMetadata, FeatureFlagType, FlagDefinition, FlagRegistry, FlagKey, } from './interfaces/flag-registry.interface';
11
+ export { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, RemoveOverrideInput, TargetingAttributeValue, TargetingAttributes, FeatureFlagGuardOptions, FeatureFlagWithOverrides, FlagOverride, FlagMutationMetadata, } from './interfaces/feature-flag.interface';
12
+ export { defineFlags, createFeatureFlagClient, createFeatureFlagDecorators, getFlagLifecycleStatus, TypedFeatureFlagClient, TypedFeatureFlagDecorators, FlagLifecycleStatus, FlagLifecycleStatusName, } from './flag-registry';
13
+ export { FeatureFlagEvents, FlagEvaluatedEvent, FlagExposedEvent, FlagMutationEvent, FlagOverrideEvent, } from './events/feature-flag.events';
11
14
  export { FEATURE_FLAG_MODULE_OPTIONS, CACHE_ADAPTER, FEATURE_FLAG_REPOSITORY, TENANT_CONTEXT_PROVIDER, } from './feature-flag.constants';
12
15
  export type { CacheAdapter } from './interfaces/cache-adapter.interface';
13
16
  export { MemoryCacheAdapter } from './cache/memory-cache.adapter';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FeatureFlagAdminModule = exports.PrismaFeatureFlagRepository = exports.RedisCacheAdapter = exports.MemoryCacheAdapter = exports.TENANT_CONTEXT_PROVIDER = exports.FEATURE_FLAG_REPOSITORY = exports.CACHE_ADAPTER = exports.FEATURE_FLAG_MODULE_OPTIONS = exports.FeatureFlagEvents = exports.BypassFeatureFlag = exports.FeatureFlag = exports.FeatureFlagGuard = exports.FlagContext = exports.FeatureFlagService = exports.FeatureFlagModule = void 0;
3
+ exports.FeatureFlagAdminModule = exports.PrismaFeatureFlagRepository = exports.RedisCacheAdapter = exports.MemoryCacheAdapter = exports.TENANT_CONTEXT_PROVIDER = exports.FEATURE_FLAG_REPOSITORY = exports.CACHE_ADAPTER = exports.FEATURE_FLAG_MODULE_OPTIONS = exports.FeatureFlagEvents = exports.getFlagLifecycleStatus = exports.createFeatureFlagDecorators = exports.createFeatureFlagClient = exports.defineFlags = exports.BypassFeatureFlag = exports.FeatureFlag = exports.FeatureFlagGuard = exports.FlagContext = exports.FeatureFlagService = exports.FeatureFlagModule = void 0;
4
4
  // Module
5
5
  var feature_flag_module_1 = require("./feature-flag.module");
6
6
  Object.defineProperty(exports, "FeatureFlagModule", { enumerable: true, get: function () { return feature_flag_module_1.FeatureFlagModule; } });
@@ -17,6 +17,12 @@ var feature_flag_decorator_1 = require("./decorators/feature-flag.decorator");
17
17
  Object.defineProperty(exports, "FeatureFlag", { enumerable: true, get: function () { return feature_flag_decorator_1.FeatureFlag; } });
18
18
  var bypass_feature_flag_decorator_1 = require("./decorators/bypass-feature-flag.decorator");
19
19
  Object.defineProperty(exports, "BypassFeatureFlag", { enumerable: true, get: function () { return bypass_feature_flag_decorator_1.BypassFeatureFlag; } });
20
+ // Registry helpers
21
+ var flag_registry_1 = require("./flag-registry");
22
+ Object.defineProperty(exports, "defineFlags", { enumerable: true, get: function () { return flag_registry_1.defineFlags; } });
23
+ Object.defineProperty(exports, "createFeatureFlagClient", { enumerable: true, get: function () { return flag_registry_1.createFeatureFlagClient; } });
24
+ Object.defineProperty(exports, "createFeatureFlagDecorators", { enumerable: true, get: function () { return flag_registry_1.createFeatureFlagDecorators; } });
25
+ Object.defineProperty(exports, "getFlagLifecycleStatus", { enumerable: true, get: function () { return flag_registry_1.getFlagLifecycleStatus; } });
20
26
  // Events
21
27
  var feature_flag_events_1 = require("./events/feature-flag.events");
22
28
  Object.defineProperty(exports, "FeatureFlagEvents", { enumerable: true, get: function () { return feature_flag_events_1.FeatureFlagEvents; } });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,SAAS;AACT,6DAA2H;AAAlH,wHAAA,iBAAiB,OAAA;AAE1B,WAAW;AACX,wEAAqE;AAA5D,0HAAA,kBAAkB,OAAA;AAC3B,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,QAAQ;AACR,kEAA+D;AAAtD,sHAAA,gBAAgB,OAAA;AAEzB,aAAa;AACb,8EAAkE;AAAzD,qHAAA,WAAW,OAAA;AACpB,4FAA+E;AAAtE,kIAAA,iBAAiB,OAAA;AAqB1B,SAAS;AACT,oEAKsC;AAJpC,wHAAA,iBAAiB,OAAA;AAMnB,YAAY;AACZ,mEAKkC;AAJhC,qIAAA,2BAA2B,OAAA;AAC3B,uHAAA,aAAa,OAAA;AACb,iIAAA,uBAAuB,OAAA;AACvB,iIAAA,uBAAuB,OAAA;AAKzB,qEAAkE;AAAzD,0HAAA,kBAAkB,OAAA;AAC3B,mEAA+F;AAAtF,wHAAA,iBAAiB,OAAA;AAQ1B,gGAA4F;AAAnF,6IAAA,2BAA2B,OAAA;AAKpC,eAAe;AACf,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,SAAS;AACT,6DAA2H;AAAlH,wHAAA,iBAAiB,OAAA;AAE1B,WAAW;AACX,wEAAqE;AAA5D,0HAAA,kBAAkB,OAAA;AAC3B,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,QAAQ;AACR,kEAA+D;AAAtD,sHAAA,gBAAgB,OAAA;AAEzB,aAAa;AACb,8EAAkE;AAAzD,qHAAA,WAAW,OAAA;AACpB,4FAA+E;AAAtE,kIAAA,iBAAiB,OAAA;AAqC1B,mBAAmB;AACnB,iDASyB;AARvB,4GAAA,WAAW,OAAA;AACX,wHAAA,uBAAuB,OAAA;AACvB,4HAAA,2BAA2B,OAAA;AAC3B,uHAAA,sBAAsB,OAAA;AAOxB,SAAS;AACT,oEAMsC;AALpC,wHAAA,iBAAiB,OAAA;AAOnB,YAAY;AACZ,mEAKkC;AAJhC,qIAAA,2BAA2B,OAAA;AAC3B,uHAAA,aAAa,OAAA;AACb,iIAAA,uBAAuB,OAAA;AACvB,iIAAA,uBAAuB,OAAA;AAKzB,qEAAkE;AAAzD,0HAAA,kBAAkB,OAAA;AAC3B,mEAA+F;AAAtF,wHAAA,iBAAiB,OAAA;AAQ1B,gGAA4F;AAAnF,6IAAA,2BAA2B,OAAA;AAKpC,eAAe;AACf,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA"}
@@ -6,6 +6,8 @@ export interface EvaluationContext {
6
6
  tenantId?: string | null;
7
7
  /** Environment - auto-injected from module options. Can be explicitly overridden */
8
8
  environment?: string;
9
+ /** Explicit stable key for percentage rollout bucketing */
10
+ targetingKey?: string | null;
9
11
  /** Additional exact-match targeting attributes */
10
12
  attributes?: TargetingAttributes;
11
13
  }
@@ -0,0 +1,29 @@
1
+ export type EvaluationSource = 'override' | 'percentage' | 'global' | 'default';
2
+ export type EvaluationReason = 'ARCHIVED' | 'OVERRIDE_MATCH' | 'PERCENTAGE_MATCH' | 'PERCENTAGE_MISS' | 'PERCENTAGE_NO_TARGETING_KEY' | 'GLOBAL' | 'FLAG_NOT_FOUND' | 'ERROR';
3
+ export type BucketBy = 'userId' | 'tenantId' | 'environment' | 'targetingKey' | (string & {});
4
+ export interface EvaluateBooleanOptions {
5
+ /** Invocation-level default used when the flag is missing or evaluation fails. */
6
+ defaultValue?: boolean;
7
+ /** Emit an exposure event for this evaluation. */
8
+ trackExposure?: boolean;
9
+ /** Include the full resolved context in evaluation/exposure events. */
10
+ includeContextInEvent?: boolean;
11
+ }
12
+ export interface BooleanEvaluationDetails {
13
+ flagKey: string;
14
+ value: boolean;
15
+ /** Backward-compatible alias for value. */
16
+ result: boolean;
17
+ source: EvaluationSource;
18
+ reason: EvaluationReason;
19
+ defaultUsed: boolean;
20
+ errorCode?: string;
21
+ errorMessage?: string;
22
+ matchedOverrideId?: string;
23
+ bucket?: number;
24
+ targetingKey?: string;
25
+ evaluationTimeMs?: number;
26
+ }
27
+ export interface FlagEvaluatorOptions {
28
+ bucketBy?: BucketBy;
29
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=evaluation-details.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluation-details.interface.js","sourceRoot":"","sources":["../../src/interfaces/evaluation-details.interface.ts"],"names":[],"mappings":""}
@@ -1,6 +1,7 @@
1
1
  import { ModuleMetadata, Type } from '@nestjs/common';
2
2
  import { Request } from 'express';
3
3
  import { CacheAdapter } from './cache-adapter.interface';
4
+ import { FlagRegistry } from './flag-registry.interface';
4
5
  export interface FeatureFlagModuleOptions {
5
6
  /** Current environment (e.g., 'development', 'staging', 'production') */
6
7
  environment: string;
@@ -14,6 +15,8 @@ export interface FeatureFlagModuleOptions {
14
15
  emitEvents?: boolean;
15
16
  /** Custom cache adapter implementation. If not provided, an in-memory cache is used. */
16
17
  cacheAdapter?: CacheAdapter;
18
+ /** Optional type-safe flag registry used for defaults and evaluation metadata. */
19
+ flags?: FlagRegistry;
17
20
  }
18
21
  export interface FeatureFlagModuleOptionsFactory {
19
22
  createFeatureFlagOptions(): Promise<FeatureFlagModuleOptions & {