@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.
- package/README.md +114 -6
- package/dist/admin/feature-flag-admin.controller.d.ts +3 -1
- package/dist/admin/feature-flag-admin.controller.js +12 -0
- package/dist/admin/feature-flag-admin.controller.js.map +1 -1
- package/dist/admin/feature-flag-admin.dto.d.ts +8 -0
- package/dist/admin/feature-flag-admin.dto.js +24 -1
- package/dist/admin/feature-flag-admin.dto.js.map +1 -1
- package/dist/events/feature-flag.events.d.ts +35 -2
- package/dist/events/feature-flag.events.js +1 -0
- package/dist/events/feature-flag.events.js.map +1 -1
- package/dist/flag-registry.d.ts +24 -0
- package/dist/flag-registry.js +62 -0
- package/dist/flag-registry.js.map +1 -0
- package/dist/guards/feature-flag.guard.js +5 -1
- package/dist/guards/feature-flag.guard.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/evaluation-context.interface.d.ts +2 -0
- package/dist/interfaces/evaluation-details.interface.d.ts +29 -0
- package/dist/interfaces/evaluation-details.interface.js +3 -0
- package/dist/interfaces/evaluation-details.interface.js.map +1 -0
- package/dist/interfaces/feature-flag-options.interface.d.ts +3 -0
- package/dist/interfaces/feature-flag.interface.d.ts +9 -0
- package/dist/interfaces/flag-registry.interface.d.ts +17 -0
- package/dist/interfaces/flag-registry.interface.js +3 -0
- package/dist/interfaces/flag-registry.interface.js.map +1 -0
- package/dist/openfeature.d.ts +26 -0
- package/dist/openfeature.js +96 -0
- package/dist/openfeature.js.map +1 -0
- package/dist/services/feature-flag.service.d.ts +14 -7
- package/dist/services/feature-flag.service.js +103 -24
- package/dist/services/feature-flag.service.js.map +1 -1
- package/dist/services/flag-evaluator.service.d.ts +7 -8
- package/dist/services/flag-evaluator.service.js +62 -10
- package/dist/services/flag-evaluator.service.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/test-feature-flag.module.d.ts +21 -0
- package/dist/testing/test-feature-flag.module.js +78 -19
- package/dist/testing/test-feature-flag.module.js.map +1 -1
- 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)
|
|
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} (
|
|
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
|
-
|
|
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 +
|
|
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
|
|
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 {
|
|
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;
|
|
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;
|
|
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
|
-
|
|
15
|
-
|
|
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":";;;
|
|
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 =
|
|
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,
|
|
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 {
|
|
10
|
-
export {
|
|
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;
|
|
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 @@
|
|
|
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 & {
|