@nestarc/feature-flag 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +509 -0
  3. package/dist/decorators/bypass-feature-flag.decorator.d.ts +1 -0
  4. package/dist/decorators/bypass-feature-flag.decorator.js +8 -0
  5. package/dist/decorators/bypass-feature-flag.decorator.js.map +1 -0
  6. package/dist/decorators/feature-flag.decorator.d.ts +2 -0
  7. package/dist/decorators/feature-flag.decorator.js +10 -0
  8. package/dist/decorators/feature-flag.decorator.js.map +1 -0
  9. package/dist/events/feature-flag.events.d.ts +29 -0
  10. package/dist/events/feature-flag.events.js +13 -0
  11. package/dist/events/feature-flag.events.js.map +1 -0
  12. package/dist/feature-flag.constants.d.ts +5 -0
  13. package/dist/feature-flag.constants.js +9 -0
  14. package/dist/feature-flag.constants.js.map +1 -0
  15. package/dist/feature-flag.module.d.ts +14 -0
  16. package/dist/feature-flag.module.js +132 -0
  17. package/dist/feature-flag.module.js.map +1 -0
  18. package/dist/guards/feature-flag.guard.d.ts +9 -0
  19. package/dist/guards/feature-flag.guard.js +53 -0
  20. package/dist/guards/feature-flag.guard.js.map +1 -0
  21. package/dist/index.d.ts +11 -0
  22. package/dist/index.js +26 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/interfaces/evaluation-context.interface.d.ts +8 -0
  25. package/dist/interfaces/evaluation-context.interface.js +3 -0
  26. package/dist/interfaces/evaluation-context.interface.js.map +1 -0
  27. package/dist/interfaces/feature-flag-options.interface.d.ts +27 -0
  28. package/dist/interfaces/feature-flag-options.interface.js +3 -0
  29. package/dist/interfaces/feature-flag-options.interface.js.map +1 -0
  30. package/dist/interfaces/feature-flag.interface.d.ts +45 -0
  31. package/dist/interfaces/feature-flag.interface.js +3 -0
  32. package/dist/interfaces/feature-flag.interface.js.map +1 -0
  33. package/dist/middleware/flag-context.middleware.d.ts +10 -0
  34. package/dist/middleware/flag-context.middleware.js +35 -0
  35. package/dist/middleware/flag-context.middleware.js.map +1 -0
  36. package/dist/services/feature-flag.service.d.ts +29 -0
  37. package/dist/services/feature-flag.service.js +208 -0
  38. package/dist/services/feature-flag.service.js.map +1 -0
  39. package/dist/services/flag-cache.service.d.ts +13 -0
  40. package/dist/services/flag-cache.service.js +72 -0
  41. package/dist/services/flag-cache.service.js.map +1 -0
  42. package/dist/services/flag-context.d.ts +9 -0
  43. package/dist/services/flag-context.js +26 -0
  44. package/dist/services/flag-context.js.map +1 -0
  45. package/dist/services/flag-evaluator.service.d.ts +12 -0
  46. package/dist/services/flag-evaluator.service.js +63 -0
  47. package/dist/services/flag-evaluator.service.js.map +1 -0
  48. package/dist/testing/index.d.ts +1 -0
  49. package/dist/testing/index.js +6 -0
  50. package/dist/testing/index.js.map +1 -0
  51. package/dist/testing/test-feature-flag.module.d.ts +4 -0
  52. package/dist/testing/test-feature-flag.module.js +41 -0
  53. package/dist/testing/test-feature-flag.module.js.map +1 -0
  54. package/dist/utils/hash.d.ts +6 -0
  55. package/dist/utils/hash.js +28 -0
  56. package/dist/utils/hash.js.map +1 -0
  57. package/package.json +88 -0
  58. package/prisma/migrations/20260405000000_init/migration.sql +76 -0
  59. package/prisma/migrations/migration_lock.toml +3 -0
  60. package/prisma/schema.prisma +41 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nestarc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,509 @@
1
+ # @nestarc/feature-flag
2
+
3
+ DB-backed feature flags for NestJS + Prisma + PostgreSQL -- tenant-aware overrides, percentage rollouts, and zero external dependencies.
4
+
5
+ ## Features
6
+
7
+ - **Database-backed** -- flags stored in PostgreSQL via Prisma, no external service required
8
+ - **Tenant / user / environment overrides** -- granular control per tenant, user, or deployment environment
9
+ - **Percentage rollouts** -- deterministic hashing (murmurhash3) for consistent per-user bucketing
10
+ - **Guard decorator** -- `@FeatureFlag()` automatically gates routes and controllers
11
+ - **Bypass decorator** -- `@BypassFeatureFlag()` exempts health checks and public endpoints
12
+ - **Programmatic evaluation** -- `isEnabled()` and `evaluateAll()` for service-layer logic
13
+ - **Built-in caching** -- configurable TTL with manual invalidation
14
+ - **Event system** -- optional integration with `@nestjs/event-emitter` for audit and observability
15
+ - **Testing utilities** -- drop-in `TestFeatureFlagModule` for unit and integration tests
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @nestarc/feature-flag
21
+ ```
22
+
23
+ ### Peer dependencies
24
+
25
+ ```bash
26
+ npm install @nestjs/common @nestjs/core @prisma/client rxjs reflect-metadata
27
+ ```
28
+
29
+ ### Optional
30
+
31
+ ```bash
32
+ # Required only if you enable emitEvents
33
+ npm install @nestjs/event-emitter
34
+ ```
35
+
36
+ ## Prisma Schema
37
+
38
+ Add the following models to your `schema.prisma`:
39
+
40
+ ```prisma
41
+ model FeatureFlag {
42
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
43
+ key String @unique
44
+ description String?
45
+ enabled Boolean @default(false)
46
+ percentage Int @default(0)
47
+ metadata Json @default("{}")
48
+ archivedAt DateTime? @map("archived_at") @db.Timestamptz()
49
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
50
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
51
+
52
+ overrides FeatureFlagOverride[]
53
+
54
+ @@map("feature_flags")
55
+ }
56
+
57
+ model FeatureFlagOverride {
58
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
59
+ flagId String @map("flag_id") @db.Uuid
60
+ tenantId String? @map("tenant_id")
61
+ userId String? @map("user_id")
62
+ environment String?
63
+ enabled Boolean
64
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
65
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
66
+
67
+ flag FeatureFlag @relation(fields: [flagId], references: [id], onDelete: Cascade)
68
+
69
+ @@index([flagId], map: "idx_override_flag_id")
70
+ @@map("feature_flag_overrides")
71
+ }
72
+ ```
73
+
74
+ ### Partial unique indexes for overrides
75
+
76
+ PostgreSQL treats `NULL != NULL` in standard unique constraints, which means a simple `UNIQUE(flag_id, tenant_id, user_id, environment)` would allow duplicate rows when any nullable column is `NULL`. To enforce true uniqueness across all combinations, apply the following migration that creates one partial index per NULL/NOT-NULL pattern:
77
+
78
+ ```sql
79
+ -- Drop the old unique constraint that does not handle NULLs correctly
80
+ ALTER TABLE feature_flag_overrides
81
+ DROP CONSTRAINT IF EXISTS uq_override_context;
82
+
83
+ -- Global override (all nullable columns NULL)
84
+ CREATE UNIQUE INDEX uq_override_000
85
+ ON feature_flag_overrides (flag_id)
86
+ WHERE tenant_id IS NULL AND user_id IS NULL AND environment IS NULL;
87
+
88
+ -- Only environment is NOT NULL
89
+ CREATE UNIQUE INDEX uq_override_001
90
+ ON feature_flag_overrides (flag_id, environment)
91
+ WHERE tenant_id IS NULL AND user_id IS NULL AND environment IS NOT NULL;
92
+
93
+ -- Only user_id is NOT NULL
94
+ CREATE UNIQUE INDEX uq_override_010
95
+ ON feature_flag_overrides (flag_id, user_id)
96
+ WHERE tenant_id IS NULL AND user_id IS NOT NULL AND environment IS NULL;
97
+
98
+ -- user_id + environment
99
+ CREATE UNIQUE INDEX uq_override_011
100
+ ON feature_flag_overrides (flag_id, user_id, environment)
101
+ WHERE tenant_id IS NULL AND user_id IS NOT NULL AND environment IS NOT NULL;
102
+
103
+ -- Only tenant_id is NOT NULL
104
+ CREATE UNIQUE INDEX uq_override_100
105
+ ON feature_flag_overrides (flag_id, tenant_id)
106
+ WHERE tenant_id IS NOT NULL AND user_id IS NULL AND environment IS NULL;
107
+
108
+ -- tenant_id + environment
109
+ CREATE UNIQUE INDEX uq_override_101
110
+ ON feature_flag_overrides (flag_id, tenant_id, environment)
111
+ WHERE tenant_id IS NOT NULL AND user_id IS NULL AND environment IS NOT NULL;
112
+
113
+ -- tenant_id + user_id
114
+ CREATE UNIQUE INDEX uq_override_110
115
+ ON feature_flag_overrides (flag_id, tenant_id, user_id)
116
+ WHERE tenant_id IS NOT NULL AND user_id IS NOT NULL AND environment IS NULL;
117
+
118
+ -- All three NOT NULL
119
+ CREATE UNIQUE INDEX uq_override_111
120
+ ON feature_flag_overrides (flag_id, tenant_id, user_id, environment)
121
+ WHERE tenant_id IS NOT NULL AND user_id IS NOT NULL AND environment IS NOT NULL;
122
+ ```
123
+
124
+ This SQL is included in the initial migration at `prisma/migrations/20260405000000_init/migration.sql`.
125
+
126
+ ## Module Registration
127
+
128
+ ### forRoot (synchronous)
129
+
130
+ ```typescript
131
+ import { FeatureFlagModule } from '@nestarc/feature-flag';
132
+
133
+ @Module({
134
+ imports: [
135
+ FeatureFlagModule.forRoot({
136
+ environment: 'production',
137
+ prisma: prismaService,
138
+ userIdExtractor: (req) => req.headers['x-user-id'] as string,
139
+ emitEvents: true,
140
+ cacheTtlMs: 30_000,
141
+ }),
142
+ ],
143
+ })
144
+ export class AppModule {}
145
+ ```
146
+
147
+ ### forRootAsync (with useFactory)
148
+
149
+ ```typescript
150
+ import { FeatureFlagModule } from '@nestarc/feature-flag';
151
+
152
+ @Module({
153
+ imports: [
154
+ FeatureFlagModule.forRootAsync({
155
+ imports: [ConfigModule],
156
+ inject: [ConfigService, PrismaService],
157
+ useFactory: (config: ConfigService, prisma: PrismaService) => ({
158
+ environment: config.get('NODE_ENV'),
159
+ prisma,
160
+ userIdExtractor: (req) => req.headers['x-user-id'] as string,
161
+ }),
162
+ }),
163
+ ],
164
+ })
165
+ export class AppModule {}
166
+ ```
167
+
168
+ ### forRootAsync (with useClass)
169
+
170
+ ```typescript
171
+ @Injectable()
172
+ class FeatureFlagConfigService implements FeatureFlagModuleOptionsFactory {
173
+ constructor(
174
+ private readonly config: ConfigService,
175
+ private readonly prisma: PrismaService,
176
+ ) {}
177
+
178
+ createFeatureFlagOptions() {
179
+ return {
180
+ environment: this.config.get('NODE_ENV'),
181
+ prisma: this.prisma,
182
+ };
183
+ }
184
+ }
185
+
186
+ @Module({
187
+ imports: [
188
+ FeatureFlagModule.forRootAsync({
189
+ imports: [ConfigModule, PrismaModule],
190
+ useClass: FeatureFlagConfigService,
191
+ }),
192
+ ],
193
+ })
194
+ export class AppModule {}
195
+ ```
196
+
197
+ ### forRootAsync (with useExisting)
198
+
199
+ ```typescript
200
+ @Module({
201
+ imports: [
202
+ FeatureFlagModule.forRootAsync({
203
+ useExisting: FeatureFlagConfigService,
204
+ }),
205
+ ],
206
+ })
207
+ export class AppModule {}
208
+ ```
209
+
210
+ ## Feature Flag Guard
211
+
212
+ The `@FeatureFlag()` decorator automatically applies `UseGuards(FeatureFlagGuard)`, so you do not need to add `@UseGuards()` yourself.
213
+
214
+ ### Method-level
215
+
216
+ ```typescript
217
+ import { FeatureFlag } from '@nestarc/feature-flag';
218
+
219
+ @Controller('dashboard')
220
+ export class DashboardController {
221
+ @FeatureFlag('NEW_DASHBOARD')
222
+ @Get()
223
+ getDashboard() {
224
+ return { message: 'Welcome to the new dashboard' };
225
+ }
226
+ }
227
+ ```
228
+
229
+ ### Class-level
230
+
231
+ ```typescript
232
+ @FeatureFlag('BETA_API')
233
+ @Controller('beta')
234
+ export class BetaController {
235
+ @Get('feature-a')
236
+ featureA() { /* guarded */ }
237
+
238
+ @Get('feature-b')
239
+ featureB() { /* guarded */ }
240
+ }
241
+ ```
242
+
243
+ ### Custom status code and fallback
244
+
245
+ ```typescript
246
+ @FeatureFlag('PREMIUM_FEATURE', {
247
+ statusCode: 402,
248
+ fallback: { message: 'Upgrade required' },
249
+ })
250
+ @Get('premium')
251
+ getPremiumContent() { ... }
252
+ ```
253
+
254
+ When the flag is disabled, the guard responds with the given `statusCode` (default `403`) and optional `fallback` body.
255
+
256
+ ### Bypassing the guard
257
+
258
+ Use `@BypassFeatureFlag()` on methods that should always be accessible, even when a class-level flag is applied:
259
+
260
+ ```typescript
261
+ import { BypassFeatureFlag } from '@nestarc/feature-flag';
262
+
263
+ @FeatureFlag('BETA_API')
264
+ @Controller('beta')
265
+ export class BetaController {
266
+ @Get('docs')
267
+ betaDocs() { /* guarded by BETA_API */ }
268
+
269
+ @BypassFeatureFlag()
270
+ @Get('health')
271
+ healthCheck() {
272
+ return { status: 'ok' };
273
+ }
274
+ }
275
+ ```
276
+
277
+ ## Programmatic Evaluation
278
+
279
+ Inject `FeatureFlagService` for service-layer checks outside the HTTP request cycle:
280
+
281
+ ```typescript
282
+ import { FeatureFlagService } from '@nestarc/feature-flag';
283
+
284
+ @Injectable()
285
+ export class PaymentService {
286
+ constructor(private readonly flags: FeatureFlagService) {}
287
+
288
+ async processPayment(order: Order) {
289
+ const useNewGateway = await this.flags.isEnabled('NEW_PAYMENT_GATEWAY');
290
+
291
+ if (useNewGateway) {
292
+ return this.newGateway.process(order);
293
+ }
294
+ return this.legacyGateway.process(order);
295
+ }
296
+ }
297
+ ```
298
+
299
+ ### Evaluate all flags at once
300
+
301
+ ```typescript
302
+ const allFlags = await this.flags.evaluateAll();
303
+ // { NEW_DASHBOARD: true, PREMIUM_FEATURE: false, ... }
304
+ ```
305
+
306
+ ### Explicit evaluation context
307
+
308
+ Both `isEnabled()` and `evaluateAll()` accept an optional `EvaluationContext` to override the auto-detected context:
309
+
310
+ ```typescript
311
+ const enabled = await this.flags.isEnabled('MY_FLAG', {
312
+ userId: 'user-123',
313
+ tenantId: 'tenant-abc',
314
+ environment: 'staging',
315
+ });
316
+ ```
317
+
318
+ Passing `null` explicitly clears that dimension, suppressing any ambient value from the request context:
319
+
320
+ ```typescript
321
+ // Evaluate as if no user is present, even within a request with x-user-id
322
+ const globalResult = await this.flags.isEnabled('MY_FLAG', { userId: null });
323
+ ```
324
+
325
+ ## Overrides
326
+
327
+ Set context-specific overrides that take precedence over the global flag value:
328
+
329
+ ```typescript
330
+ // Enable for a specific tenant
331
+ await this.flags.setOverride('MY_FLAG', {
332
+ tenantId: 'tenant-1',
333
+ enabled: true,
334
+ });
335
+
336
+ // Disable for a specific user
337
+ await this.flags.setOverride('MY_FLAG', {
338
+ userId: 'user-42',
339
+ enabled: false,
340
+ });
341
+
342
+ // Enable only in staging
343
+ await this.flags.setOverride('MY_FLAG', {
344
+ environment: 'staging',
345
+ enabled: true,
346
+ });
347
+
348
+ // Combine dimensions
349
+ await this.flags.setOverride('MY_FLAG', {
350
+ tenantId: 'tenant-1',
351
+ userId: 'user-42',
352
+ environment: 'production',
353
+ enabled: true,
354
+ });
355
+ ```
356
+
357
+ ## Events
358
+
359
+ Enable event emission to observe flag lifecycle changes. Requires `@nestjs/event-emitter` as an optional peer dependency.
360
+
361
+ **Important:** You must import `EventEmitterModule.forRoot()` in your app module. The feature-flag module reuses the same `EventEmitter2` singleton that NestJS manages, so `@OnEvent()` listeners work out of the box.
362
+
363
+ ### Setup
364
+
365
+ ```typescript
366
+ import { EventEmitterModule } from '@nestjs/event-emitter';
367
+
368
+ @Module({
369
+ imports: [
370
+ EventEmitterModule.forRoot(), // must be imported
371
+ FeatureFlagModule.forRoot({
372
+ environment: 'production',
373
+ prisma: prismaService,
374
+ emitEvents: true,
375
+ }),
376
+ ],
377
+ })
378
+ export class AppModule {}
379
+ ```
380
+
381
+ ### Event types
382
+
383
+ | Event constant | Event string | Payload type |
384
+ | ---------------------------------------- | ---------------------------------- | -------------------- |
385
+ | `FeatureFlagEvents.EVALUATED` | `feature-flag.evaluated` | `FlagEvaluatedEvent` |
386
+ | `FeatureFlagEvents.CREATED` | `feature-flag.created` | `FlagMutationEvent` |
387
+ | `FeatureFlagEvents.UPDATED` | `feature-flag.updated` | `FlagMutationEvent` |
388
+ | `FeatureFlagEvents.ARCHIVED` | `feature-flag.archived` | `FlagMutationEvent` |
389
+ | `FeatureFlagEvents.OVERRIDE_SET` | `feature-flag.override.set` | `FlagOverrideEvent` |
390
+ | `FeatureFlagEvents.OVERRIDE_REMOVED` | `feature-flag.override.removed` | `FlagOverrideEvent` |
391
+ | `FeatureFlagEvents.CACHE_INVALIDATED` | `feature-flag.cache.invalidated` | `{}` |
392
+
393
+ ### Listening to events
394
+
395
+ ```typescript
396
+ import { OnEvent } from '@nestjs/event-emitter';
397
+ import { FeatureFlagEvents, FlagEvaluatedEvent } from '@nestarc/feature-flag';
398
+
399
+ @Injectable()
400
+ export class FlagAuditListener {
401
+ @OnEvent(FeatureFlagEvents.EVALUATED)
402
+ handleEvaluation(event: FlagEvaluatedEvent) {
403
+ console.log(`Flag ${event.flagKey} = ${event.result} (source: ${event.source})`);
404
+ }
405
+ }
406
+ ```
407
+
408
+ ## Testing
409
+
410
+ Import `TestFeatureFlagModule` from the `/testing` subpath to stub flag values in tests without a database connection:
411
+
412
+ ```typescript
413
+ import { TestFeatureFlagModule } from '@nestarc/feature-flag/testing';
414
+
415
+ describe('DashboardController', () => {
416
+ let app: INestApplication;
417
+
418
+ beforeEach(async () => {
419
+ const module = await Test.createTestingModule({
420
+ imports: [
421
+ TestFeatureFlagModule.register({
422
+ NEW_DASHBOARD: true,
423
+ PREMIUM_FEATURE: false,
424
+ }),
425
+ ],
426
+ controllers: [DashboardController],
427
+ }).compile();
428
+
429
+ app = module.createNestApplication();
430
+ await app.init();
431
+ });
432
+
433
+ it('should allow access when flag is enabled', () => {
434
+ return request(app.getHttpServer())
435
+ .get('/dashboard')
436
+ .expect(200);
437
+ });
438
+ });
439
+ ```
440
+
441
+ `TestFeatureFlagModule.register()` provides a global mock of `FeatureFlagService` where `isEnabled(key)` returns the boolean you specified (defaulting to `false` for unregistered keys) and `evaluateAll()` returns the full map.
442
+
443
+ ## Evaluation Priority
444
+
445
+ When `isEnabled()` is called, flags are evaluated through a 6-layer cascade. The first matching layer wins:
446
+
447
+ | Priority | Layer | Description |
448
+ | -------- | ---------------------- | ------------------------------------------------------------------ |
449
+ | 1 | **Archived** | If the flag has `archivedAt` set, evaluation always returns `false` |
450
+ | 2 | **User override** | Override matching the current `userId` (most specific) |
451
+ | 3 | **Tenant override** | Override matching the current `tenantId` |
452
+ | 4 | **Environment override**| Override matching the current `environment` |
453
+ | 5 | **Percentage rollout** | Deterministic hash of `flagKey + userId` (or `tenantId`) mod 100 |
454
+ | 6 | **Global default** | The flag's `enabled` field |
455
+
456
+ Percentage rollout uses murmurhash3 for deterministic bucketing: the same user always gets the same result for a given flag, ensuring a consistent experience across requests.
457
+
458
+ ## Configuration Reference
459
+
460
+ ### FeatureFlagModuleOptions
461
+
462
+ | Option | Type | Default | Description |
463
+ | ------------------- | --------------------------------- | --------- | --------------------------------------------------------------- |
464
+ | `environment` | `string` | *required*| Deployment environment (e.g. `'production'`, `'staging'`) |
465
+ | `cacheTtlMs` | `number` | `30000` | Cache TTL in ms. Set to `0` to disable caching |
466
+ | `userIdExtractor` | `(req: Request) => string \| null`| `undefined`| Extracts user ID from the incoming request |
467
+ | `defaultOnMissing` | `boolean` | `false` | Value returned when a flag key does not exist in the database |
468
+ | `emitEvents` | `boolean` | `false` | Emit lifecycle events via `@nestjs/event-emitter` |
469
+
470
+ ### FeatureFlagModuleRootOptions
471
+
472
+ Extends `FeatureFlagModuleOptions` with:
473
+
474
+ | Option | Type | Description |
475
+ | ------- | ----- | ------------------------------ |
476
+ | `prisma`| `any` | Prisma client instance |
477
+
478
+ ## CRUD Operations
479
+
480
+ `FeatureFlagService` also exposes methods for managing flags programmatically:
481
+
482
+ ```typescript
483
+ // Create a flag
484
+ const flag = await this.flags.create({
485
+ key: 'NEW_FEATURE',
486
+ description: 'Enables the new feature',
487
+ enabled: false,
488
+ percentage: 0,
489
+ });
490
+
491
+ // Update a flag
492
+ await this.flags.update('NEW_FEATURE', {
493
+ enabled: true,
494
+ percentage: 50,
495
+ });
496
+
497
+ // Archive a flag (soft delete -- evaluations return false)
498
+ await this.flags.archive('OLD_FEATURE');
499
+
500
+ // List all active (non-archived) flags
501
+ const allFlags = await this.flags.findAll();
502
+
503
+ // Manually invalidate the cache
504
+ this.flags.invalidateCache();
505
+ ```
506
+
507
+ ## License
508
+
509
+ MIT
@@ -0,0 +1 @@
1
+ export declare const BypassFeatureFlag: () => import("@nestjs/common").CustomDecorator<string>;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BypassFeatureFlag = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ const feature_flag_constants_1 = require("../feature-flag.constants");
6
+ const BypassFeatureFlag = () => (0, common_1.SetMetadata)(feature_flag_constants_1.BYPASS_FEATURE_FLAG_KEY, true);
7
+ exports.BypassFeatureFlag = BypassFeatureFlag;
8
+ //# sourceMappingURL=bypass-feature-flag.decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bypass-feature-flag.decorator.js","sourceRoot":"","sources":["../../src/decorators/bypass-feature-flag.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAC7C,sEAAoE;AAE7D,MAAM,iBAAiB,GAAG,GAAG,EAAE,CAAC,IAAA,oBAAW,EAAC,gDAAuB,EAAE,IAAI,CAAC,CAAC;AAArE,QAAA,iBAAiB,qBAAoD"}
@@ -0,0 +1,2 @@
1
+ import { FeatureFlagGuardOptions } from '../interfaces/feature-flag.interface';
2
+ export declare function FeatureFlag(flagKey: string, options?: FeatureFlagGuardOptions): ClassDecorator & MethodDecorator;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureFlag = FeatureFlag;
4
+ const common_1 = require("@nestjs/common");
5
+ const feature_flag_constants_1 = require("../feature-flag.constants");
6
+ const feature_flag_guard_1 = require("../guards/feature-flag.guard");
7
+ function FeatureFlag(flagKey, options = {}) {
8
+ return (0, common_1.applyDecorators)((0, common_1.SetMetadata)(feature_flag_constants_1.FEATURE_FLAG_KEY, flagKey), (0, common_1.SetMetadata)(feature_flag_constants_1.FEATURE_FLAG_OPTIONS_KEY, options), (0, common_1.UseGuards)(feature_flag_guard_1.FeatureFlagGuard));
9
+ }
10
+ //# sourceMappingURL=feature-flag.decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.decorator.js","sourceRoot":"","sources":["../../src/decorators/feature-flag.decorator.ts"],"names":[],"mappings":";;AAKA,kCASC;AAdD,2CAAyE;AACzE,sEAAuF;AAEvF,qEAAgE;AAEhE,SAAgB,WAAW,CACzB,OAAe,EACf,UAAmC,EAAE;IAErC,OAAO,IAAA,wBAAe,EACpB,IAAA,oBAAW,EAAC,yCAAgB,EAAE,OAAO,CAAC,EACtC,IAAA,oBAAW,EAAC,iDAAwB,EAAE,OAAO,CAAC,EAC9C,IAAA,kBAAS,EAAC,qCAAgB,CAAC,CAC5B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { EvaluationContext } from '../interfaces/evaluation-context.interface';
2
+ export declare const FeatureFlagEvents: {
3
+ readonly EVALUATED: "feature-flag.evaluated";
4
+ readonly CREATED: "feature-flag.created";
5
+ readonly UPDATED: "feature-flag.updated";
6
+ readonly ARCHIVED: "feature-flag.archived";
7
+ readonly OVERRIDE_SET: "feature-flag.override.set";
8
+ readonly OVERRIDE_REMOVED: "feature-flag.override.removed";
9
+ readonly CACHE_INVALIDATED: "feature-flag.cache.invalidated";
10
+ };
11
+ export interface FlagEvaluatedEvent {
12
+ flagKey: string;
13
+ result: boolean;
14
+ context: EvaluationContext;
15
+ source: 'user_override' | 'tenant_override' | 'env_override' | 'percentage' | 'global';
16
+ evaluationTimeMs: number;
17
+ }
18
+ export interface FlagMutationEvent {
19
+ flagKey: string;
20
+ action: 'created' | 'updated' | 'archived';
21
+ }
22
+ export interface FlagOverrideEvent {
23
+ flagKey: string;
24
+ tenantId?: string | null;
25
+ userId?: string | null;
26
+ environment?: string | null;
27
+ enabled: boolean;
28
+ action: 'set' | 'removed';
29
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FeatureFlagEvents = void 0;
4
+ exports.FeatureFlagEvents = {
5
+ EVALUATED: 'feature-flag.evaluated',
6
+ CREATED: 'feature-flag.created',
7
+ UPDATED: 'feature-flag.updated',
8
+ ARCHIVED: 'feature-flag.archived',
9
+ OVERRIDE_SET: 'feature-flag.override.set',
10
+ OVERRIDE_REMOVED: 'feature-flag.override.removed',
11
+ CACHE_INVALIDATED: 'feature-flag.cache.invalidated',
12
+ };
13
+ //# sourceMappingURL=feature-flag.events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.events.js","sourceRoot":"","sources":["../../src/events/feature-flag.events.ts"],"names":[],"mappings":";;;AAEa,QAAA,iBAAiB,GAAG;IAC/B,SAAS,EAAE,wBAAwB;IACnC,OAAO,EAAE,sBAAsB;IAC/B,OAAO,EAAE,sBAAsB;IAC/B,QAAQ,EAAE,uBAAuB;IACjC,YAAY,EAAE,2BAA2B;IACzC,gBAAgB,EAAE,+BAA+B;IACjD,iBAAiB,EAAE,gCAAgC;CAC3C,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare const FEATURE_FLAG_MODULE_OPTIONS: unique symbol;
2
+ export declare const DEFAULT_CACHE_TTL_MS = 30000;
3
+ export declare const FEATURE_FLAG_KEY = "FEATURE_FLAG_KEY";
4
+ export declare const FEATURE_FLAG_OPTIONS_KEY = "FEATURE_FLAG_OPTIONS_KEY";
5
+ export declare const BYPASS_FEATURE_FLAG_KEY = "BYPASS_FEATURE_FLAG_KEY";
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BYPASS_FEATURE_FLAG_KEY = exports.FEATURE_FLAG_OPTIONS_KEY = exports.FEATURE_FLAG_KEY = exports.DEFAULT_CACHE_TTL_MS = exports.FEATURE_FLAG_MODULE_OPTIONS = void 0;
4
+ exports.FEATURE_FLAG_MODULE_OPTIONS = Symbol('FEATURE_FLAG_MODULE_OPTIONS');
5
+ exports.DEFAULT_CACHE_TTL_MS = 30_000;
6
+ exports.FEATURE_FLAG_KEY = 'FEATURE_FLAG_KEY';
7
+ exports.FEATURE_FLAG_OPTIONS_KEY = 'FEATURE_FLAG_OPTIONS_KEY';
8
+ exports.BYPASS_FEATURE_FLAG_KEY = 'BYPASS_FEATURE_FLAG_KEY';
9
+ //# sourceMappingURL=feature-flag.constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag.constants.js","sourceRoot":"","sources":["../src/feature-flag.constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,2BAA2B,GAAG,MAAM,CAAC,6BAA6B,CAAC,CAAC;AAEpE,QAAA,oBAAoB,GAAG,MAAM,CAAC;AAE9B,QAAA,gBAAgB,GAAG,kBAAkB,CAAC;AACtC,QAAA,wBAAwB,GAAG,0BAA0B,CAAC;AACtD,QAAA,uBAAuB,GAAG,yBAAyB,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
2
+ import { FeatureFlagModuleAsyncOptions, FeatureFlagModuleOptions } from './interfaces/feature-flag-options.interface';
3
+ export interface FeatureFlagModuleRootOptions extends FeatureFlagModuleOptions {
4
+ prisma: any;
5
+ }
6
+ export interface FeatureFlagModuleRootAsyncOptions extends FeatureFlagModuleAsyncOptions {
7
+ useFactory?: (...args: any[]) => Promise<FeatureFlagModuleRootOptions> | FeatureFlagModuleRootOptions;
8
+ }
9
+ export declare class FeatureFlagModule implements NestModule {
10
+ configure(consumer: MiddlewareConsumer): void;
11
+ static forRoot(options: FeatureFlagModuleRootOptions): DynamicModule;
12
+ static forRootAsync(options: FeatureFlagModuleRootAsyncOptions): DynamicModule;
13
+ private static createAsyncProviders;
14
+ }