@nestarc/feature-flag 0.3.0 → 0.5.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 +164 -13
- 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 +25 -6
- package/prisma/schema.prisma +3 -2
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
|
|
@@ -32,9 +34,13 @@ npm install @nestarc/feature-flag
|
|
|
32
34
|
### Peer dependencies
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
|
-
npm install @nestjs/common @nestjs/core @prisma/client class-transformer class-validator rxjs reflect-metadata
|
|
37
|
+
npm install @nestjs/common @nestjs/core @prisma/client @prisma/adapter-pg pg class-transformer class-validator rxjs reflect-metadata
|
|
38
|
+
npm install --save-dev prisma
|
|
36
39
|
```
|
|
37
40
|
|
|
41
|
+
Prisma 7 requires Node.js 20.19+, 22.12+, or 24+. This package follows the
|
|
42
|
+
same runtime requirement.
|
|
43
|
+
|
|
38
44
|
### Optional
|
|
39
45
|
|
|
40
46
|
```bash
|
|
@@ -43,6 +49,9 @@ npm install @nestjs/event-emitter
|
|
|
43
49
|
|
|
44
50
|
# Required only if you use RedisCacheAdapter
|
|
45
51
|
npm install ioredis
|
|
52
|
+
|
|
53
|
+
# Required only if you use the OpenFeature adapter with the SDK
|
|
54
|
+
npm install @openfeature/server-sdk
|
|
46
55
|
```
|
|
47
56
|
|
|
48
57
|
## Redis Cache (Multi-Instance)
|
|
@@ -71,6 +80,45 @@ When a flag is updated on any instance, all other instances are notified via Pub
|
|
|
71
80
|
|
|
72
81
|
## Prisma Schema
|
|
73
82
|
|
|
83
|
+
Prisma 7 keeps connection URLs in `prisma.config.ts` and requires a database
|
|
84
|
+
driver adapter at runtime. A minimal PostgreSQL setup is:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// prisma.config.ts
|
|
88
|
+
import 'dotenv/config';
|
|
89
|
+
import { defineConfig, env } from 'prisma/config';
|
|
90
|
+
|
|
91
|
+
export default defineConfig({
|
|
92
|
+
schema: 'prisma/schema.prisma',
|
|
93
|
+
migrations: { path: 'prisma/migrations' },
|
|
94
|
+
datasource: { url: env('DATABASE_URL') },
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```prisma
|
|
99
|
+
generator client {
|
|
100
|
+
provider = "prisma-client"
|
|
101
|
+
output = "../src/generated/prisma"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
datasource db {
|
|
105
|
+
provider = "postgresql"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Create the client with `@prisma/adapter-pg`, then pass that instance to
|
|
110
|
+
`FeatureFlagModule`:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { PrismaPg } from '@prisma/adapter-pg';
|
|
114
|
+
import { PrismaClient } from './generated/prisma/client';
|
|
115
|
+
|
|
116
|
+
const adapter = new PrismaPg({
|
|
117
|
+
connectionString: process.env.DATABASE_URL,
|
|
118
|
+
});
|
|
119
|
+
const prisma = new PrismaClient({ adapter });
|
|
120
|
+
```
|
|
121
|
+
|
|
74
122
|
Add the following models to your `schema.prisma`:
|
|
75
123
|
|
|
76
124
|
```prisma
|
|
@@ -279,6 +327,14 @@ getPremiumContent() { ... }
|
|
|
279
327
|
|
|
280
328
|
When the flag is disabled, the guard responds with the given `statusCode` (default `403`) and optional `fallback` body.
|
|
281
329
|
|
|
330
|
+
Use `defaultValue` when a route should choose an invocation-specific fallback if a flag is missing or evaluation fails:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
@FeatureFlag('OPTIONAL_PREVIEW', { defaultValue: true })
|
|
334
|
+
@Get('preview')
|
|
335
|
+
getPreview() { ... }
|
|
336
|
+
```
|
|
337
|
+
|
|
282
338
|
### Bypassing the guard
|
|
283
339
|
|
|
284
340
|
Use `@BypassFeatureFlag()` on methods that should always be accessible, even when a class-level flag is applied:
|
|
@@ -348,6 +404,78 @@ Passing `null` explicitly clears that dimension, suppressing any ambient value f
|
|
|
348
404
|
const globalResult = await this.flags.isEnabled('MY_FLAG', { userId: null });
|
|
349
405
|
```
|
|
350
406
|
|
|
407
|
+
### Detailed boolean evaluation
|
|
408
|
+
|
|
409
|
+
Use `evaluateBoolean()` when you need to explain why a flag resolved to a value:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
const details = await this.flags.evaluateBoolean(
|
|
413
|
+
'NEW_CHECKOUT',
|
|
414
|
+
{ targetingKey: 'tenant-1', tenantId: 'tenant-1' },
|
|
415
|
+
{ defaultValue: false, trackExposure: true },
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
console.log(details);
|
|
419
|
+
// {
|
|
420
|
+
// flagKey: 'NEW_CHECKOUT',
|
|
421
|
+
// value: true,
|
|
422
|
+
// result: true,
|
|
423
|
+
// source: 'percentage',
|
|
424
|
+
// reason: 'PERCENTAGE_MATCH',
|
|
425
|
+
// defaultUsed: false,
|
|
426
|
+
// bucket: 17,
|
|
427
|
+
// targetingKey: 'tenant-1',
|
|
428
|
+
// evaluationTimeMs: 1
|
|
429
|
+
// }
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Missing flags and evaluation errors return the selected default instead of throwing. Default priority is:
|
|
433
|
+
|
|
434
|
+
1. Invocation `defaultValue`
|
|
435
|
+
2. Registry `defaultValue`
|
|
436
|
+
3. Module `defaultOnMissing`
|
|
437
|
+
4. `false`
|
|
438
|
+
|
|
439
|
+
### Type-safe flag registry
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
import { defineFlags, createFeatureFlagClient } from '@nestarc/feature-flag';
|
|
443
|
+
|
|
444
|
+
export const flags = defineFlags({
|
|
445
|
+
NEW_CHECKOUT: {
|
|
446
|
+
defaultValue: false,
|
|
447
|
+
bucketBy: 'tenantId',
|
|
448
|
+
trackExposure: true,
|
|
449
|
+
owner: 'payments',
|
|
450
|
+
tags: ['checkout'],
|
|
451
|
+
staleAt: '2026-09-01',
|
|
452
|
+
expiresAt: '2026-12-01',
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const flagClient = createFeatureFlagClient(featureFlagService, flags);
|
|
457
|
+
const enabled = await flagClient.isEnabled('NEW_CHECKOUT', { tenantId: 'tenant-1' });
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
You can also pass the registry to `FeatureFlagModule.forRoot({ flags })` so service-level fallback and `bucketBy` defaults apply to direct `FeatureFlagService` calls.
|
|
461
|
+
|
|
462
|
+
### OpenFeature boolean adapter
|
|
463
|
+
|
|
464
|
+
The optional adapter lives on a separate subpath and delegates boolean resolution to `FeatureFlagService`:
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { createOpenFeatureBooleanProvider } from '@nestarc/feature-flag/openfeature';
|
|
468
|
+
|
|
469
|
+
const provider = createOpenFeatureBooleanProvider(featureFlagService);
|
|
470
|
+
const result = await provider.resolveBooleanEvaluation(
|
|
471
|
+
'NEW_CHECKOUT',
|
|
472
|
+
false,
|
|
473
|
+
{ targetingKey: 'tenant-1', tenantId: 'tenant-1', plan: 'pro' },
|
|
474
|
+
);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Only boolean evaluation is supported in v0.4.0. Variant flags and string/number/json remote config remain out of scope.
|
|
478
|
+
|
|
351
479
|
## Attribute Targeting
|
|
352
480
|
|
|
353
481
|
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 +560,7 @@ export class AppModule {}
|
|
|
432
560
|
| Event constant | Event string | Payload type |
|
|
433
561
|
| ---------------------------------------- | ---------------------------------- | -------------------- |
|
|
434
562
|
| `FeatureFlagEvents.EVALUATED` | `feature-flag.evaluated` | `FlagEvaluatedEvent` |
|
|
563
|
+
| `FeatureFlagEvents.EXPOSED` | `feature-flag.exposed` | `FlagExposedEvent` |
|
|
435
564
|
| `FeatureFlagEvents.CREATED` | `feature-flag.created` | `FlagMutationEvent` |
|
|
436
565
|
| `FeatureFlagEvents.UPDATED` | `feature-flag.updated` | `FlagMutationEvent` |
|
|
437
566
|
| `FeatureFlagEvents.ARCHIVED` | `feature-flag.archived` | `FlagMutationEvent` |
|
|
@@ -449,11 +578,13 @@ import { FeatureFlagEvents, FlagEvaluatedEvent } from '@nestarc/feature-flag';
|
|
|
449
578
|
export class FlagAuditListener {
|
|
450
579
|
@OnEvent(FeatureFlagEvents.EVALUATED)
|
|
451
580
|
handleEvaluation(event: FlagEvaluatedEvent) {
|
|
452
|
-
console.log(`Flag ${event.flagKey} = ${event.result} (
|
|
581
|
+
console.log(`Flag ${event.flagKey} = ${event.result} (${event.reason})`);
|
|
453
582
|
}
|
|
454
583
|
}
|
|
455
584
|
```
|
|
456
585
|
|
|
586
|
+
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.
|
|
587
|
+
|
|
457
588
|
## Testing
|
|
458
589
|
|
|
459
590
|
Import `TestFeatureFlagModule` from the `/testing` subpath to stub flag values in tests without a database connection:
|
|
@@ -489,11 +620,29 @@ describe('DashboardController', () => {
|
|
|
489
620
|
|
|
490
621
|
`TestFeatureFlagModule.register()` provides a global mock of `FeatureFlagService`:
|
|
491
622
|
- `isEnabled(key)` returns the boolean you specified (defaulting to `false` for unregistered keys)
|
|
623
|
+
- `evaluateBoolean(key)` returns `BooleanEvaluationDetails`
|
|
492
624
|
- `evaluateAll()` returns the full flag map
|
|
493
625
|
- `create()`, `update()`, `archive()`, `findByKey()`, `findAll()` return full `FeatureFlagWithOverrides` stub objects
|
|
494
626
|
- `findByKey()` throws `NotFoundException` for unknown keys
|
|
495
627
|
|
|
496
|
-
|
|
628
|
+
For registry-based tests, use `registerRegistry()` and the injected controller:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
import {
|
|
632
|
+
TestFeatureFlagController,
|
|
633
|
+
TestFeatureFlagModule,
|
|
634
|
+
} from '@nestarc/feature-flag/testing';
|
|
635
|
+
|
|
636
|
+
const module = await Test.createTestingModule({
|
|
637
|
+
imports: [TestFeatureFlagModule.registerRegistry(flags)],
|
|
638
|
+
}).compile();
|
|
639
|
+
|
|
640
|
+
const testFlags = module.get(TestFeatureFlagController);
|
|
641
|
+
testFlags.set('NEW_CHECKOUT', true);
|
|
642
|
+
testFlags.reset();
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
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
646
|
|
|
498
647
|
## Evaluation Priority
|
|
499
648
|
|
|
@@ -503,10 +652,10 @@ When `isEnabled()` is called, flags are evaluated through the current cascade. T
|
|
|
503
652
|
| -------- | ---------------------- | ------------------------------------------------------------------ |
|
|
504
653
|
| 1 | **Archived** | If the flag has `archivedAt` set, evaluation always returns `false` |
|
|
505
654
|
| 2 | **Attribute override** | Best override whose attributes are all present in the evaluation context |
|
|
506
|
-
| 3 | **Percentage rollout** | Deterministic hash of `flagKey +
|
|
655
|
+
| 3 | **Percentage rollout** | Deterministic hash of `flagKey + targetingKey` mod 100 |
|
|
507
656
|
| 4 | **Global default** | The flag's `enabled` field |
|
|
508
657
|
|
|
509
|
-
Percentage rollout uses murmurhash3 for deterministic bucketing
|
|
658
|
+
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
659
|
|
|
511
660
|
## Configuration Reference
|
|
512
661
|
|
|
@@ -520,6 +669,7 @@ Percentage rollout uses murmurhash3 for deterministic bucketing: the same user a
|
|
|
520
669
|
| `defaultOnMissing` | `boolean` | `false` | Value returned when a flag key does not exist in the database |
|
|
521
670
|
| `emitEvents` | `boolean` | `false` | Emit lifecycle events via `@nestjs/event-emitter` |
|
|
522
671
|
| `cacheAdapter` | `CacheAdapter` | `MemoryCacheAdapter` | Pluggable cache backend (e.g. `RedisCacheAdapter`) |
|
|
672
|
+
| `flags` | `FlagRegistry` | `undefined` | Optional typed registry for defaults, `bucketBy`, and exposure settings |
|
|
523
673
|
|
|
524
674
|
### FeatureFlagModuleRootOptions
|
|
525
675
|
|
|
@@ -587,6 +737,7 @@ export class AppModule {}
|
|
|
587
737
|
| GET | `/feature-flags/:key` | Get a single flag | 404 not found |
|
|
588
738
|
| PATCH | `/feature-flags/:key` | Update a flag | 404 not found, 400 invalid percentage |
|
|
589
739
|
| DELETE | `/feature-flags/:key` | Archive a flag | 404 not found |
|
|
740
|
+
| POST | `/feature-flags/:key/evaluate` | Evaluate a flag without mutating it | |
|
|
590
741
|
| POST | `/feature-flags/:key/overrides` | Set an override | 404 flag not found |
|
|
591
742
|
| DELETE | `/feature-flags/:key/overrides` | Remove an override | 404 flag not found |
|
|
592
743
|
|
|
@@ -656,16 +807,16 @@ export class AppModule {}
|
|
|
656
807
|
|
|
657
808
|
## Performance
|
|
658
809
|
|
|
659
|
-
Measured with PostgreSQL 16, Prisma
|
|
810
|
+
Measured with PostgreSQL 16, Prisma 7.9.1, 500 iterations on Apple Silicon:
|
|
660
811
|
|
|
661
812
|
| Scenario | Avg | P50 | P95 | P99 |
|
|
662
813
|
|----------|-----|-----|-----|-----|
|
|
663
|
-
| **isEnabled() — cache hit** | **0.04ms** | **0.
|
|
664
|
-
| isEnabled() — cache miss (DB lookup) | 1.
|
|
665
|
-
| isEnabled() — override cascade (cold) |
|
|
666
|
-
| **evaluateAll() — 50 flags (mixed)** | **0.19ms** | **0.04ms** | **1.
|
|
814
|
+
| **isEnabled() — cache hit** | **0.04ms** | **0.04ms** | **0.05ms** | **0.12ms** |
|
|
815
|
+
| isEnabled() — cache miss (DB lookup) | 1.17ms | 1.10ms | 1.62ms | 2.04ms |
|
|
816
|
+
| isEnabled() — override cascade (cold) | 0.95ms | 0.89ms | 1.36ms | 1.87ms |
|
|
817
|
+
| **evaluateAll() — 50 flags (mixed)** | **0.19ms** | **0.04ms** | **1.47ms** | **1.78ms** |
|
|
667
818
|
|
|
668
|
-
Cache speedup: **
|
|
819
|
+
Cache speedup: **29.2x** (hit vs miss). Keep the default 30s cache TTL for optimal performance.
|
|
669
820
|
|
|
670
821
|
> Reproduce: `docker compose up -d && dotenv -e .env.test -- npx ts-node benchmarks/evaluation-overhead.ts`
|
|
671
822
|
|
|
@@ -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"}
|