@nestarc/feature-flag 0.2.0 → 0.3.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 (33) hide show
  1. package/README.md +102 -85
  2. package/dist/admin/feature-flag-admin.controller.d.ts +6 -6
  3. package/dist/admin/feature-flag-admin.controller.js +10 -4
  4. package/dist/admin/feature-flag-admin.controller.js.map +1 -1
  5. package/dist/admin/feature-flag-admin.dto.d.ts +22 -0
  6. package/dist/admin/feature-flag-admin.dto.js +93 -0
  7. package/dist/admin/feature-flag-admin.dto.js.map +1 -0
  8. package/dist/admin/targeting-attributes.validator.d.ts +6 -0
  9. package/dist/admin/targeting-attributes.validator.js +36 -0
  10. package/dist/admin/targeting-attributes.validator.js.map +1 -0
  11. package/dist/events/feature-flag.events.d.ts +4 -5
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/interfaces/evaluation-context.interface.d.ts +6 -3
  15. package/dist/interfaces/feature-flag-repository.interface.d.ts +8 -6
  16. package/dist/interfaces/feature-flag.interface.d.ts +9 -9
  17. package/dist/repositories/prisma-feature-flag.repository.d.ts +3 -3
  18. package/dist/repositories/prisma-feature-flag.repository.js +18 -14
  19. package/dist/repositories/prisma-feature-flag.repository.js.map +1 -1
  20. package/dist/services/feature-flag.service.d.ts +1 -0
  21. package/dist/services/feature-flag.service.js +23 -14
  22. package/dist/services/feature-flag.service.js.map +1 -1
  23. package/dist/services/flag-context-resolver.js +14 -3
  24. package/dist/services/flag-context-resolver.js.map +1 -1
  25. package/dist/services/flag-evaluator.service.d.ts +1 -0
  26. package/dist/services/flag-evaluator.service.js +29 -27
  27. package/dist/services/flag-evaluator.service.js.map +1 -1
  28. package/dist/utils/targeting-attributes.d.ts +9 -0
  29. package/dist/utils/targeting-attributes.js +44 -0
  30. package/dist/utils/targeting-attributes.js.map +1 -0
  31. package/package.json +6 -2
  32. package/prisma/migrations/20260512000000_attribute_targeting/migration.sql +59 -0
  33. package/prisma/schema.prisma +7 -9
package/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Docs](https://img.shields.io/badge/docs-nestarc.dev-blue.svg)](https://nestarc.dev/packages/feature-flag/)
8
8
 
9
- DB-backed feature flags for NestJS + Prisma + PostgreSQL -- tenant-aware overrides, percentage rollouts, and zero external dependencies.
9
+ DB-backed feature flags for NestJS + Prisma + PostgreSQL -- attribute-targeted overrides, percentage rollouts, and zero external dependencies.
10
10
 
11
11
  ## Features
12
12
 
13
13
  - **Database-backed** -- flags stored in PostgreSQL via Prisma, no external service required
14
- - **Tenant / user / environment overrides** -- granular control per tenant, user, or deployment environment
14
+ - **Attribute-targeted overrides** -- exact-match targeting for tenants, users, environments, plans, regions, or custom dimensions
15
15
  - **Percentage rollouts** -- deterministic hashing (murmurhash3) for consistent per-user bucketing
16
16
  - **Guard decorator** -- `@FeatureFlag()` automatically gates routes and controllers
17
17
  - **Bypass decorator** -- `@BypassFeatureFlag()` exempts health checks and public endpoints
@@ -32,7 +32,7 @@ npm install @nestarc/feature-flag
32
32
  ### Peer dependencies
33
33
 
34
34
  ```bash
35
- npm install @nestjs/common @nestjs/core @prisma/client rxjs reflect-metadata
35
+ npm install @nestjs/common @nestjs/core @prisma/client class-transformer class-validator rxjs reflect-metadata
36
36
  ```
37
37
 
38
38
  ### Optional
@@ -91,14 +91,13 @@ model FeatureFlag {
91
91
  }
92
92
 
93
93
  model FeatureFlagOverride {
94
- id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
95
- flagId String @map("flag_id") @db.Uuid
96
- tenantId String? @map("tenant_id")
97
- userId String? @map("user_id")
98
- environment String?
99
- enabled Boolean
100
- createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
101
- updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
94
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
95
+ flagId String @map("flag_id") @db.Uuid
96
+ attributes Json
97
+ priority Int @default(0)
98
+ enabled Boolean
99
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
100
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
102
101
 
103
102
  flag FeatureFlag @relation(fields: [flagId], references: [id], onDelete: Cascade)
104
103
 
@@ -107,57 +106,48 @@ model FeatureFlagOverride {
107
106
  }
108
107
  ```
109
108
 
110
- ### Partial unique indexes for overrides
111
-
112
- 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:
109
+ The v0.3.0 migration uses an `{}` default only while backfilling legacy rows, then drops that default. It also creates a unique index on `(flag_id, attributes)` and a check constraint requiring override attributes to be a non-empty JSON object. If you copy this schema into a greenfield app instead of running the included migrations, add an equivalent raw SQL migration because Prisma schema cannot express these PostgreSQL constraints:
113
110
 
114
111
  ```sql
115
- -- Drop the old unique constraint that does not handle NULLs correctly
116
- ALTER TABLE feature_flag_overrides
117
- DROP CONSTRAINT IF EXISTS uq_override_context;
118
-
119
- -- Global override (all nullable columns NULL)
120
- CREATE UNIQUE INDEX uq_override_000
121
- ON feature_flag_overrides (flag_id)
122
- WHERE tenant_id IS NULL AND user_id IS NULL AND environment IS NULL;
123
-
124
- -- Only environment is NOT NULL
125
- CREATE UNIQUE INDEX uq_override_001
126
- ON feature_flag_overrides (flag_id, environment)
127
- WHERE tenant_id IS NULL AND user_id IS NULL AND environment IS NOT NULL;
128
-
129
- -- Only user_id is NOT NULL
130
- CREATE UNIQUE INDEX uq_override_010
131
- ON feature_flag_overrides (flag_id, user_id)
132
- WHERE tenant_id IS NULL AND user_id IS NOT NULL AND environment IS NULL;
133
-
134
- -- user_id + environment
135
- CREATE UNIQUE INDEX uq_override_011
136
- ON feature_flag_overrides (flag_id, user_id, environment)
137
- WHERE tenant_id IS NULL AND user_id IS NOT NULL AND environment IS NOT NULL;
138
-
139
- -- Only tenant_id is NOT NULL
140
- CREATE UNIQUE INDEX uq_override_100
141
- ON feature_flag_overrides (flag_id, tenant_id)
142
- WHERE tenant_id IS NOT NULL AND user_id IS NULL AND environment IS NULL;
143
-
144
- -- tenant_id + environment
145
- CREATE UNIQUE INDEX uq_override_101
146
- ON feature_flag_overrides (flag_id, tenant_id, environment)
147
- WHERE tenant_id IS NOT NULL AND user_id IS NULL AND environment IS NOT NULL;
148
-
149
- -- tenant_id + user_id
150
- CREATE UNIQUE INDEX uq_override_110
151
- ON feature_flag_overrides (flag_id, tenant_id, user_id)
152
- WHERE tenant_id IS NOT NULL AND user_id IS NOT NULL AND environment IS NULL;
153
-
154
- -- All three NOT NULL
155
- CREATE UNIQUE INDEX uq_override_111
156
- ON feature_flag_overrides (flag_id, tenant_id, user_id, environment)
157
- WHERE tenant_id IS NOT NULL AND user_id IS NOT NULL AND environment IS NOT NULL;
112
+ CREATE UNIQUE INDEX "uq_feature_flag_override_attributes"
113
+ ON "feature_flag_overrides"("flag_id", "attributes");
114
+
115
+ ALTER TABLE "feature_flag_overrides"
116
+ ADD CONSTRAINT "chk_feature_flag_override_attributes_non_empty"
117
+ CHECK (jsonb_typeof("attributes") = 'object' AND "attributes" <> '{}'::jsonb);
118
+ ```
119
+
120
+ ### Migration from 0.2.0 to 0.3.0
121
+
122
+ v0.3.0 changes override storage from fixed `tenant_id`, `user_id`, and `environment` columns to an `attributes` `jsonb` object plus `priority`.
123
+
124
+ Run your Prisma migrations during deployment:
125
+
126
+ ```bash
127
+ npx prisma migrate deploy
158
128
  ```
159
129
 
160
- This SQL is included in the initial migration at `prisma/migrations/20260405000000_init/migration.sql`.
130
+ The migration maps legacy override columns into attributes:
131
+
132
+ | v0.2.0 column | v0.3.0 attribute |
133
+ | ------------- | ---------------- |
134
+ | `tenant_id` | `attributes.tenantId` |
135
+ | `user_id` | `attributes.userId` |
136
+ | `environment` | `attributes.environment` |
137
+
138
+ Rows with all three legacy columns set to `NULL` are deleted because empty override attributes are not valid in v0.3.0. If multiple legacy rows backfill to the same `(flag_id, attributes)`, the migration keeps the row with the latest `updated_at`, then latest `created_at`, then highest `id`, and deletes the other duplicates before creating the unique index.
139
+
140
+ Legacy Admin API bodies are rejected:
141
+
142
+ ```json
143
+ { "tenantId": "tenant-1", "enabled": true }
144
+ ```
145
+
146
+ Use an `attributes` object instead:
147
+
148
+ ```json
149
+ { "attributes": { "tenantId": "tenant-1" }, "enabled": true }
150
+ ```
161
151
 
162
152
  ## Module Registration
163
153
 
@@ -358,41 +348,64 @@ Passing `null` explicitly clears that dimension, suppressing any ambient value f
358
348
  const globalResult = await this.flags.isEnabled('MY_FLAG', { userId: null });
359
349
  ```
360
350
 
361
- ## Overrides
351
+ ## Attribute Targeting
362
352
 
363
- Set context-specific overrides that take precedence over the global flag value:
353
+ 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.
364
354
 
365
355
  ```typescript
366
- // Enable for a specific tenant
367
- await this.flags.setOverride('MY_FLAG', {
356
+ const enabled = await this.flags.isEnabled('NEW_CHECKOUT', {
357
+ userId: 'user-123',
368
358
  tenantId: 'tenant-1',
369
- enabled: true,
359
+ environment: 'production',
360
+ attributes: {
361
+ plan: 'pro',
362
+ country: 'KR',
363
+ },
370
364
  });
365
+ ```
371
366
 
372
- // Disable for a specific user
373
- await this.flags.setOverride('MY_FLAG', {
374
- userId: 'user-42',
375
- enabled: false,
376
- });
367
+ Top-level `userId`, `tenantId`, and `environment` are merged into targeting attributes. If the same key also appears in `attributes`, the top-level value wins.
377
368
 
378
- // Enable only in staging
379
- await this.flags.setOverride('MY_FLAG', {
380
- environment: 'staging',
381
- enabled: true,
382
- });
369
+ When multiple overrides match, the evaluator chooses the winner by:
383
370
 
384
- // Combine dimensions
385
- await this.flags.setOverride('MY_FLAG', {
386
- tenantId: 'tenant-1',
387
- userId: 'user-42',
388
- environment: 'production',
371
+ 1. More attributes
372
+ 2. Higher `priority`
373
+ 3. Earlier `createdAt`
374
+ 4. Lower `id`
375
+
376
+ ## Overrides
377
+
378
+ Set attribute-based overrides that take precedence over the global flag value:
379
+
380
+ ```typescript
381
+ await flags.setOverride('NEW_CHECKOUT', {
382
+ attributes: {
383
+ tenantId: 'tenant-1',
384
+ plan: 'pro',
385
+ country: 'KR',
386
+ },
389
387
  enabled: true,
388
+ priority: 10,
390
389
  });
391
390
  ```
392
391
 
392
+ REST Admin API body:
393
+
394
+ ```json
395
+ {
396
+ "attributes": {
397
+ "tenantId": "tenant-1",
398
+ "plan": "pro",
399
+ "country": "KR"
400
+ },
401
+ "enabled": true,
402
+ "priority": 10
403
+ }
404
+ ```
405
+
393
406
  ## Events
394
407
 
395
- Enable event emission to observe flag lifecycle changes. Requires `@nestjs/event-emitter` as an optional peer dependency.
408
+ Enable event emission to observe flag lifecycle changes. Requires installing `@nestjs/event-emitter`.
396
409
 
397
410
  **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.
398
411
 
@@ -484,16 +497,14 @@ This is a **stateless boolean stub** -- write operations do not persist state ac
484
497
 
485
498
  ## Evaluation Priority
486
499
 
487
- When `isEnabled()` is called, flags are evaluated through a 6-layer cascade. The first matching layer wins:
500
+ When `isEnabled()` is called, flags are evaluated through the current cascade. The first matching layer wins:
488
501
 
489
502
  | Priority | Layer | Description |
490
503
  | -------- | ---------------------- | ------------------------------------------------------------------ |
491
504
  | 1 | **Archived** | If the flag has `archivedAt` set, evaluation always returns `false` |
492
- | 2 | **User override** | Override matching the current `userId` (most specific) |
493
- | 3 | **Tenant override** | Override matching the current `tenantId` |
494
- | 4 | **Environment override**| Override matching the current `environment` |
495
- | 5 | **Percentage rollout** | Deterministic hash of `flagKey + userId` (or `tenantId`) mod 100 |
496
- | 6 | **Global default** | The flag's `enabled` field |
505
+ | 2 | **Attribute override** | Best override whose attributes are all present in the evaluation context |
506
+ | 3 | **Percentage rollout** | Deterministic hash of `flagKey + userId` (or `tenantId`) mod 100 |
507
+ | 4 | **Global default** | The flag's `enabled` field |
497
508
 
498
509
  Percentage rollout uses murmurhash3 for deterministic bucketing: the same user always gets the same result for a given flag, ensuring a consistent experience across requests.
499
510
 
@@ -637,6 +648,12 @@ class MyTenantProvider implements TenantContextProvider {
637
648
  export class AppModule {}
638
649
  ```
639
650
 
651
+ ## Examples
652
+
653
+ - [examples/basic-guard](examples/basic-guard) - route gating with `@FeatureFlag()`
654
+ - [examples/multi-tenant-targeting](examples/multi-tenant-targeting) - tenant and plan targeting with attributes
655
+ - [examples/redis-events](examples/redis-events) - Redis cache invalidation and feature flag events
656
+
640
657
  ## Performance
641
658
 
642
659
  Measured with PostgreSQL 16, Prisma 6, 500 iterations on Apple Silicon:
@@ -1,14 +1,14 @@
1
1
  import { FeatureFlagService } from '../services/feature-flag.service';
2
- import { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
3
- import { RemoveOverrideInput } from '../interfaces/feature-flag.interface';
2
+ import { FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
3
+ import { CreateFeatureFlagDto, RemoveOverrideDto, SetOverrideDto, UpdateFeatureFlagDto } from './feature-flag-admin.dto';
4
4
  export declare class FeatureFlagAdminController {
5
5
  private readonly service;
6
6
  constructor(service: FeatureFlagService);
7
- create(input: CreateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
7
+ create(input: CreateFeatureFlagDto): Promise<FeatureFlagWithOverrides>;
8
8
  findAll(): Promise<FeatureFlagWithOverrides[]>;
9
9
  findByKey(key: string): Promise<FeatureFlagWithOverrides>;
10
- update(key: string, input: UpdateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
10
+ update(key: string, input: UpdateFeatureFlagDto): Promise<FeatureFlagWithOverrides>;
11
11
  archive(key: string): Promise<FeatureFlagWithOverrides>;
12
- setOverride(key: string, input: SetOverrideInput): Promise<void>;
13
- removeOverride(key: string, input: RemoveOverrideInput): Promise<void>;
12
+ setOverride(key: string, input: SetOverrideDto): Promise<void>;
13
+ removeOverride(key: string, input: RemoveOverrideDto): Promise<void>;
14
14
  }
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.FeatureFlagAdminController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const feature_flag_service_1 = require("../services/feature-flag.service");
18
+ const feature_flag_admin_dto_1 = require("./feature-flag-admin.dto");
18
19
  let FeatureFlagAdminController = class FeatureFlagAdminController {
19
20
  constructor(service) {
20
21
  this.service = service;
@@ -46,7 +47,7 @@ __decorate([
46
47
  (0, common_1.Post)(),
47
48
  __param(0, (0, common_1.Body)()),
48
49
  __metadata("design:type", Function),
49
- __metadata("design:paramtypes", [Object]),
50
+ __metadata("design:paramtypes", [feature_flag_admin_dto_1.CreateFeatureFlagDto]),
50
51
  __metadata("design:returntype", Promise)
51
52
  ], FeatureFlagAdminController.prototype, "create", null);
52
53
  __decorate([
@@ -67,7 +68,7 @@ __decorate([
67
68
  __param(0, (0, common_1.Param)('key')),
68
69
  __param(1, (0, common_1.Body)()),
69
70
  __metadata("design:type", Function),
70
- __metadata("design:paramtypes", [String, Object]),
71
+ __metadata("design:paramtypes", [String, feature_flag_admin_dto_1.UpdateFeatureFlagDto]),
71
72
  __metadata("design:returntype", Promise)
72
73
  ], FeatureFlagAdminController.prototype, "update", null);
73
74
  __decorate([
@@ -82,7 +83,7 @@ __decorate([
82
83
  __param(0, (0, common_1.Param)('key')),
83
84
  __param(1, (0, common_1.Body)()),
84
85
  __metadata("design:type", Function),
85
- __metadata("design:paramtypes", [String, Object]),
86
+ __metadata("design:paramtypes", [String, feature_flag_admin_dto_1.SetOverrideDto]),
86
87
  __metadata("design:returntype", Promise)
87
88
  ], FeatureFlagAdminController.prototype, "setOverride", null);
88
89
  __decorate([
@@ -90,11 +91,16 @@ __decorate([
90
91
  __param(0, (0, common_1.Param)('key')),
91
92
  __param(1, (0, common_1.Body)()),
92
93
  __metadata("design:type", Function),
93
- __metadata("design:paramtypes", [String, Object]),
94
+ __metadata("design:paramtypes", [String, feature_flag_admin_dto_1.RemoveOverrideDto]),
94
95
  __metadata("design:returntype", Promise)
95
96
  ], FeatureFlagAdminController.prototype, "removeOverride", null);
96
97
  exports.FeatureFlagAdminController = FeatureFlagAdminController = __decorate([
97
98
  (0, common_1.Controller)(),
99
+ (0, common_1.UsePipes)(new common_1.ValidationPipe({
100
+ whitelist: true,
101
+ forbidNonWhitelisted: true,
102
+ transform: true,
103
+ })),
98
104
  __metadata("design:paramtypes", [feature_flag_service_1.FeatureFlagService])
99
105
  ], FeatureFlagAdminController);
100
106
  //# sourceMappingURL=feature-flag-admin.controller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag-admin.controller.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAQwB;AACxB,2EAAsE;AAU/D,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAG5D,MAAM,CAAS,KAA6B;QAC1C,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,KAA6B;QAErC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAGD,OAAO,CAAe,GAAW;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAGD,WAAW,CACK,GAAW,EACjB,KAAuB;QAE/B,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAGD,cAAc,CACE,GAAW,EACjB,KAA0B;QAElC,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AA9CY,gEAA0B;AAIrC;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;;;wDAEb;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;;;;wDAGR;AAGD;IADC,IAAA,eAAM,EAAC,MAAM,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;yDAEpB;AAGD;IADC,IAAA,aAAI,EAAC,gBAAgB,CAAC;IAEpB,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;;;;6DAGR;AAGD;IADC,IAAA,eAAM,EAAC,gBAAgB,CAAC;IAEtB,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;;;;gEAGR;qCA7CU,0BAA0B;IADtC,IAAA,mBAAU,GAAE;qCAE2B,yCAAkB;GAD7C,0BAA0B,CA8CtC"}
1
+ {"version":3,"file":"feature-flag-admin.controller.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,2EAAsE;AAEtE,qEAKkC;AAU3B,IAAM,0BAA0B,GAAhC,MAAM,0BAA0B;IACrC,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAG5D,MAAM,CAAS,KAA2B;QACxC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAGD,OAAO;QACL,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAGD,SAAS,CAAe,GAAW;QACjC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;IAGD,MAAM,CACU,GAAW,EACjB,KAA2B;QAEnC,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAGD,OAAO,CAAe,GAAW;QAC/B,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAGD,WAAW,CAAe,GAAW,EAAU,KAAqB;QAClE,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IAGD,cAAc,CAAe,GAAW,EAAU,KAAwB;QACxE,OAAO,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAxCY,gEAA0B;AAIrC;IADC,IAAA,aAAI,GAAE;IACC,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAQ,6CAAoB;;wDAEzC;AAGD;IADC,IAAA,YAAG,GAAE;;;;yDAGL;AAGD;IADC,IAAA,YAAG,EAAC,MAAM,CAAC;IACD,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;2DAEtB;AAGD;IADC,IAAA,cAAK,EAAC,MAAM,CAAC;IAEX,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IACZ,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,6CAAoB;;wDAGpC;AAGD;IADC,IAAA,eAAM,EAAC,MAAM,CAAC;IACN,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;yDAEpB;AAGD;IADC,IAAA,aAAI,EAAC,gBAAgB,CAAC;IACV,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IAAe,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,uCAAc;;6DAEnE;AAGD;IADC,IAAA,eAAM,EAAC,gBAAgB,CAAC;IACT,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;IAAe,WAAA,IAAA,aAAI,GAAE,CAAA;;6CAAQ,0CAAiB;;gEAEzE;qCAvCU,0BAA0B;IARtC,IAAA,mBAAU,GAAE;IACZ,IAAA,iBAAQ,EACP,IAAI,uBAAc,CAAC;QACjB,SAAS,EAAE,IAAI;QACf,oBAAoB,EAAE,IAAI;QAC1B,SAAS,EAAE,IAAI;KAChB,CAAC,CACH;qCAEuC,yCAAkB;GAD7C,0BAA0B,CAwCtC"}
@@ -0,0 +1,22 @@
1
+ import { TargetingAttributes } from '../interfaces/feature-flag.interface';
2
+ export declare class CreateFeatureFlagDto {
3
+ key: string;
4
+ description?: string;
5
+ enabled?: boolean;
6
+ percentage?: number;
7
+ metadata?: Record<string, unknown>;
8
+ }
9
+ export declare class UpdateFeatureFlagDto {
10
+ description?: string;
11
+ enabled?: boolean;
12
+ percentage?: number;
13
+ metadata?: Record<string, unknown>;
14
+ }
15
+ export declare class SetOverrideDto {
16
+ attributes: TargetingAttributes;
17
+ enabled: boolean;
18
+ priority?: number;
19
+ }
20
+ export declare class RemoveOverrideDto {
21
+ attributes: TargetingAttributes;
22
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.RemoveOverrideDto = exports.SetOverrideDto = exports.UpdateFeatureFlagDto = exports.CreateFeatureFlagDto = void 0;
13
+ const class_validator_1 = require("class-validator");
14
+ const targeting_attributes_validator_1 = require("./targeting-attributes.validator");
15
+ class CreateFeatureFlagDto {
16
+ }
17
+ exports.CreateFeatureFlagDto = CreateFeatureFlagDto;
18
+ __decorate([
19
+ (0, class_validator_1.IsString)(),
20
+ (0, class_validator_1.IsNotEmpty)(),
21
+ __metadata("design:type", String)
22
+ ], CreateFeatureFlagDto.prototype, "key", void 0);
23
+ __decorate([
24
+ (0, class_validator_1.IsOptional)(),
25
+ (0, class_validator_1.IsString)(),
26
+ __metadata("design:type", String)
27
+ ], CreateFeatureFlagDto.prototype, "description", void 0);
28
+ __decorate([
29
+ (0, class_validator_1.IsOptional)(),
30
+ (0, class_validator_1.IsBoolean)(),
31
+ __metadata("design:type", Boolean)
32
+ ], CreateFeatureFlagDto.prototype, "enabled", void 0);
33
+ __decorate([
34
+ (0, class_validator_1.IsOptional)(),
35
+ (0, class_validator_1.IsInt)(),
36
+ (0, class_validator_1.Min)(0),
37
+ (0, class_validator_1.Max)(100),
38
+ __metadata("design:type", Number)
39
+ ], CreateFeatureFlagDto.prototype, "percentage", void 0);
40
+ __decorate([
41
+ (0, class_validator_1.IsOptional)(),
42
+ (0, class_validator_1.IsObject)(),
43
+ __metadata("design:type", Object)
44
+ ], CreateFeatureFlagDto.prototype, "metadata", void 0);
45
+ class UpdateFeatureFlagDto {
46
+ }
47
+ exports.UpdateFeatureFlagDto = UpdateFeatureFlagDto;
48
+ __decorate([
49
+ (0, class_validator_1.IsOptional)(),
50
+ (0, class_validator_1.IsString)(),
51
+ __metadata("design:type", String)
52
+ ], UpdateFeatureFlagDto.prototype, "description", void 0);
53
+ __decorate([
54
+ (0, class_validator_1.IsOptional)(),
55
+ (0, class_validator_1.IsBoolean)(),
56
+ __metadata("design:type", Boolean)
57
+ ], UpdateFeatureFlagDto.prototype, "enabled", void 0);
58
+ __decorate([
59
+ (0, class_validator_1.IsOptional)(),
60
+ (0, class_validator_1.IsInt)(),
61
+ (0, class_validator_1.Min)(0),
62
+ (0, class_validator_1.Max)(100),
63
+ __metadata("design:type", Number)
64
+ ], UpdateFeatureFlagDto.prototype, "percentage", void 0);
65
+ __decorate([
66
+ (0, class_validator_1.IsOptional)(),
67
+ (0, class_validator_1.IsObject)(),
68
+ __metadata("design:type", Object)
69
+ ], UpdateFeatureFlagDto.prototype, "metadata", void 0);
70
+ class SetOverrideDto {
71
+ }
72
+ exports.SetOverrideDto = SetOverrideDto;
73
+ __decorate([
74
+ (0, targeting_attributes_validator_1.IsTargetingAttributes)(),
75
+ __metadata("design:type", Object)
76
+ ], SetOverrideDto.prototype, "attributes", void 0);
77
+ __decorate([
78
+ (0, class_validator_1.IsBoolean)(),
79
+ __metadata("design:type", Boolean)
80
+ ], SetOverrideDto.prototype, "enabled", void 0);
81
+ __decorate([
82
+ (0, class_validator_1.IsOptional)(),
83
+ (0, class_validator_1.IsInt)(),
84
+ __metadata("design:type", Number)
85
+ ], SetOverrideDto.prototype, "priority", void 0);
86
+ class RemoveOverrideDto {
87
+ }
88
+ exports.RemoveOverrideDto = RemoveOverrideDto;
89
+ __decorate([
90
+ (0, targeting_attributes_validator_1.IsTargetingAttributes)(),
91
+ __metadata("design:type", Object)
92
+ ], RemoveOverrideDto.prototype, "attributes", void 0);
93
+ //# sourceMappingURL=feature-flag-admin.dto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flag-admin.dto.js","sourceRoot":"","sources":["../../src/admin/feature-flag-admin.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDASyB;AAEzB,qFAAyE;AAEzE,MAAa,oBAAoB;CAsBhC;AAtBD,oDAsBC;AAnBC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,4BAAU,GAAE;;iDACA;AAIb;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;yDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qDACM;AAMlB;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,GAAG,CAAC;;wDACW;AAIpB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sDACwB;AAGrC,MAAa,oBAAoB;CAkBhC;AAlBD,oDAkBC;AAfC;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;yDACU;AAIrB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,2BAAS,GAAE;;qDACM;AAMlB;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;IACP,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,GAAG,CAAC;;wDACW;AAIpB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;;sDACwB;AAGrC,MAAa,cAAc;CAU1B;AAVD,wCAUC;AARC;IADC,IAAA,sDAAqB,GAAE;;kDACS;AAGjC;IADC,IAAA,2BAAS,GAAE;;+CACM;AAIlB;IAFC,IAAA,4BAAU,GAAE;IACZ,IAAA,uBAAK,GAAE;;gDACU;AAGpB,MAAa,iBAAiB;CAG7B;AAHD,8CAGC;AADC;IADC,IAAA,sDAAqB,GAAE;;qDACS"}
@@ -0,0 +1,6 @@
1
+ import { ValidationArguments, ValidationOptions, ValidatorConstraintInterface } from 'class-validator';
2
+ export declare class IsTargetingAttributesConstraint implements ValidatorConstraintInterface {
3
+ validate(value: unknown): boolean;
4
+ defaultMessage(_args: ValidationArguments): string;
5
+ }
6
+ export declare function IsTargetingAttributes(validationOptions?: ValidationOptions): (target: object, propertyName: string) => void;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IsTargetingAttributesConstraint = void 0;
10
+ exports.IsTargetingAttributes = IsTargetingAttributes;
11
+ const class_validator_1 = require("class-validator");
12
+ const targeting_attributes_1 = require("../utils/targeting-attributes");
13
+ let IsTargetingAttributesConstraint = class IsTargetingAttributesConstraint {
14
+ validate(value) {
15
+ return (0, targeting_attributes_1.isTargetingAttributes)(value, { allowEmpty: false });
16
+ }
17
+ defaultMessage(_args) {
18
+ return 'attributes must be a non-empty object with string, number, boolean, or null values';
19
+ }
20
+ };
21
+ exports.IsTargetingAttributesConstraint = IsTargetingAttributesConstraint;
22
+ exports.IsTargetingAttributesConstraint = IsTargetingAttributesConstraint = __decorate([
23
+ (0, class_validator_1.ValidatorConstraint)({ name: 'isTargetingAttributes', async: false })
24
+ ], IsTargetingAttributesConstraint);
25
+ function IsTargetingAttributes(validationOptions) {
26
+ return function registerTargetingAttributesDecorator(target, propertyName) {
27
+ (0, class_validator_1.registerDecorator)({
28
+ target: target.constructor,
29
+ propertyName,
30
+ options: validationOptions,
31
+ constraints: [],
32
+ validator: IsTargetingAttributesConstraint,
33
+ });
34
+ };
35
+ }
36
+ //# sourceMappingURL=targeting-attributes.validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targeting-attributes.validator.js","sourceRoot":"","sources":["../../src/admin/targeting-attributes.validator.ts"],"names":[],"mappings":";;;;;;;;;AAoBA,sDAUC;AA9BD,qDAMyB;AACzB,wEAAsE;AAG/D,IAAM,+BAA+B,GAArC,MAAM,+BAA+B;IAC1C,QAAQ,CAAC,KAAc;QACrB,OAAO,IAAA,4CAAqB,EAAC,KAAK,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,cAAc,CAAC,KAA0B;QACvC,OAAO,oFAAoF,CAAC;IAC9F,CAAC;CACF,CAAA;AARY,0EAA+B;0CAA/B,+BAA+B;IAD3C,IAAA,qCAAmB,EAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;GACxD,+BAA+B,CAQ3C;AAED,SAAgB,qBAAqB,CAAC,iBAAqC;IACzE,OAAO,SAAS,oCAAoC,CAAC,MAAc,EAAE,YAAoB;QACvF,IAAA,mCAAiB,EAAC;YAChB,MAAM,EAAE,MAAM,CAAC,WAAW;YAC1B,YAAY;YACZ,OAAO,EAAE,iBAAiB;YAC1B,WAAW,EAAE,EAAE;YACf,SAAS,EAAE,+BAA+B;SAC3C,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
@@ -12,7 +12,7 @@ export interface FlagEvaluatedEvent {
12
12
  flagKey: string;
13
13
  result: boolean;
14
14
  context: EvaluationContext;
15
- source: 'user_override' | 'tenant_override' | 'env_override' | 'percentage' | 'global';
15
+ source: 'override' | 'percentage' | 'global';
16
16
  evaluationTimeMs: number;
17
17
  }
18
18
  export interface FlagMutationEvent {
@@ -21,9 +21,8 @@ export interface FlagMutationEvent {
21
21
  }
22
22
  export interface FlagOverrideEvent {
23
23
  flagKey: string;
24
- tenantId?: string | null;
25
- userId?: string | null;
26
- environment?: string | null;
27
- enabled: boolean;
24
+ attributes: Record<string, string | number | boolean | null>;
25
+ enabled?: boolean;
26
+ priority?: number;
28
27
  action: 'set' | 'removed';
29
28
  }
package/dist/index.d.ts CHANGED
@@ -6,13 +6,13 @@ export { FeatureFlag } from './decorators/feature-flag.decorator';
6
6
  export { BypassFeatureFlag } from './decorators/bypass-feature-flag.decorator';
7
7
  export { FeatureFlagModuleOptions, FeatureFlagModuleAsyncOptions, FeatureFlagModuleOptionsFactory, } from './interfaces/feature-flag-options.interface';
8
8
  export { EvaluationContext } from './interfaces/evaluation-context.interface';
9
- export { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, RemoveOverrideInput, FeatureFlagGuardOptions, FeatureFlagWithOverrides, FlagOverride, } from './interfaces/feature-flag.interface';
9
+ export { CreateFeatureFlagInput, UpdateFeatureFlagInput, SetOverrideInput, RemoveOverrideInput, TargetingAttributeValue, TargetingAttributes, FeatureFlagGuardOptions, FeatureFlagWithOverrides, FlagOverride, } from './interfaces/feature-flag.interface';
10
10
  export { FeatureFlagEvents, FlagEvaluatedEvent, FlagMutationEvent, FlagOverrideEvent, } from './events/feature-flag.events';
11
11
  export { FEATURE_FLAG_MODULE_OPTIONS, CACHE_ADAPTER, FEATURE_FLAG_REPOSITORY, TENANT_CONTEXT_PROVIDER, } from './feature-flag.constants';
12
12
  export type { CacheAdapter } from './interfaces/cache-adapter.interface';
13
13
  export { MemoryCacheAdapter } from './cache/memory-cache.adapter';
14
14
  export { RedisCacheAdapter, type RedisCacheAdapterOptions } from './cache/redis-cache.adapter';
15
- export type { FeatureFlagRepository, OverrideCriteria } from './interfaces/feature-flag-repository.interface';
15
+ export type { FeatureFlagRepository, OverrideCriteria, UpdateOverrideInput, } from './interfaces/feature-flag-repository.interface';
16
16
  export { PrismaFeatureFlagRepository } from './repositories/prisma-feature-flag.repository';
17
17
  export type { TenantContextProvider } from './interfaces/tenant-context-provider.interface';
18
18
  export { FeatureFlagAdminModule } from './admin/feature-flag-admin.module';
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;AAmB1B,SAAS;AACT,oEAKsC;AAJpC,wHAAA,iBAAiB,OAAA;AAMnB,YAAY;AACZ,mEAKkC;AAJhC,qIAAA,2BAA2B,OAAA;AAC3B,uHAAA,aAAa,OAAA;AACb,iIAAA,uBAAuB,OAAA;AACvB,iIAAA,uBAAuB,OAAA;AAKzB,qEAAkE;AAAzD,0HAAA,kBAAkB,OAAA;AAC3B,mEAA+F;AAAtF,wHAAA,iBAAiB,OAAA;AAI1B,gGAA4F;AAAnF,6IAAA,2BAA2B,OAAA;AAKpC,eAAe;AACf,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,SAAS;AACT,6DAA2H;AAAlH,wHAAA,iBAAiB,OAAA;AAE1B,WAAW;AACX,wEAAqE;AAA5D,0HAAA,kBAAkB,OAAA;AAC3B,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,QAAQ;AACR,kEAA+D;AAAtD,sHAAA,gBAAgB,OAAA;AAEzB,aAAa;AACb,8EAAkE;AAAzD,qHAAA,WAAW,OAAA;AACpB,4FAA+E;AAAtE,kIAAA,iBAAiB,OAAA;AAqB1B,SAAS;AACT,oEAKsC;AAJpC,wHAAA,iBAAiB,OAAA;AAMnB,YAAY;AACZ,mEAKkC;AAJhC,qIAAA,2BAA2B,OAAA;AAC3B,uHAAA,aAAa,OAAA;AACb,iIAAA,uBAAuB,OAAA;AACvB,iIAAA,uBAAuB,OAAA;AAKzB,qEAAkE;AAAzD,0HAAA,kBAAkB,OAAA;AAC3B,mEAA+F;AAAtF,wHAAA,iBAAiB,OAAA;AAQ1B,gGAA4F;AAAnF,6IAAA,2BAA2B,OAAA;AAKpC,eAAe;AACf,+EAA2E;AAAlE,mIAAA,sBAAsB,OAAA"}
@@ -1,8 +1,11 @@
1
+ import { TargetingAttributes } from './feature-flag.interface';
1
2
  export interface EvaluationContext {
2
- /** User ID used for user overrides and percentage hash */
3
+ /** User ID - used for user-scoped targeting and percentage hash */
3
4
  userId?: string | null;
4
- /** Tenant ID used for tenant overrides. Ignored if tenancy is not installed */
5
+ /** Tenant ID - used for tenant-scoped targeting. Ignored if tenancy is not installed */
5
6
  tenantId?: string | null;
6
- /** Environment auto-injected from module options. Can be explicitly overridden */
7
+ /** Environment - auto-injected from module options. Can be explicitly overridden */
7
8
  environment?: string;
9
+ /** Additional exact-match targeting attributes */
10
+ attributes?: TargetingAttributes;
8
11
  }
@@ -1,8 +1,10 @@
1
- import { CreateFeatureFlagInput, UpdateFeatureFlagInput, FeatureFlagWithOverrides } from './feature-flag.interface';
1
+ import { CreateFeatureFlagInput, UpdateFeatureFlagInput, FeatureFlagWithOverrides, TargetingAttributes } from './feature-flag.interface';
2
2
  export interface OverrideCriteria {
3
- tenantId: string | null;
4
- userId: string | null;
5
- environment: string | null;
3
+ attributes: TargetingAttributes;
4
+ }
5
+ export interface UpdateOverrideInput {
6
+ enabled: boolean;
7
+ priority: number;
6
8
  }
7
9
  export interface FeatureFlagRepository {
8
10
  createFlag(input: CreateFeatureFlagInput): Promise<FeatureFlagWithOverrides>;
@@ -14,7 +16,7 @@ export interface FeatureFlagRepository {
14
16
  findOverride(flagId: string, criteria: OverrideCriteria): Promise<{
15
17
  id: string;
16
18
  } | null>;
17
- createOverride(flagId: string, criteria: OverrideCriteria, enabled: boolean): Promise<void>;
18
- updateOverrideEnabled(id: string, enabled: boolean): Promise<void>;
19
+ createOverride(flagId: string, criteria: OverrideCriteria, enabled: boolean, priority: number): Promise<void>;
20
+ updateOverride(id: string, input: UpdateOverrideInput): Promise<void>;
19
21
  deleteOverride(id: string): Promise<void>;
20
22
  }
@@ -1,3 +1,5 @@
1
+ export type TargetingAttributeValue = string | number | boolean | null;
2
+ export type TargetingAttributes = Record<string, TargetingAttributeValue>;
1
3
  export interface CreateFeatureFlagInput {
2
4
  key: string;
3
5
  description?: string;
@@ -12,10 +14,9 @@ export interface UpdateFeatureFlagInput {
12
14
  metadata?: Record<string, unknown>;
13
15
  }
14
16
  export interface SetOverrideInput {
15
- tenantId?: string;
16
- userId?: string;
17
- environment?: string;
17
+ attributes: TargetingAttributes;
18
18
  enabled: boolean;
19
+ priority?: number;
19
20
  }
20
21
  export interface FeatureFlagGuardOptions {
21
22
  /** HTTP status code when flag is OFF. Default: 403 */
@@ -38,13 +39,12 @@ export interface FeatureFlagWithOverrides {
38
39
  export interface FlagOverride {
39
40
  id: string;
40
41
  flagId: string;
41
- tenantId: string | null;
42
- userId: string | null;
43
- environment: string | null;
42
+ attributes: TargetingAttributes;
43
+ priority: number;
44
44
  enabled: boolean;
45
+ createdAt: Date;
46
+ updatedAt: Date;
45
47
  }
46
48
  export interface RemoveOverrideInput {
47
- tenantId?: string;
48
- userId?: string;
49
- environment?: string;
49
+ attributes: TargetingAttributes;
50
50
  }
@@ -1,4 +1,4 @@
1
- import { FeatureFlagRepository, OverrideCriteria } from '../interfaces/feature-flag-repository.interface';
1
+ import { FeatureFlagRepository, OverrideCriteria, UpdateOverrideInput } from '../interfaces/feature-flag-repository.interface';
2
2
  import { CreateFeatureFlagInput, UpdateFeatureFlagInput, FeatureFlagWithOverrides } from '../interfaces/feature-flag.interface';
3
3
  export declare class PrismaFeatureFlagRepository implements FeatureFlagRepository {
4
4
  private readonly prisma;
@@ -12,7 +12,7 @@ export declare class PrismaFeatureFlagRepository implements FeatureFlagRepositor
12
12
  findOverride(flagId: string, criteria: OverrideCriteria): Promise<{
13
13
  id: string;
14
14
  } | null>;
15
- createOverride(flagId: string, criteria: OverrideCriteria, enabled: boolean): Promise<void>;
16
- updateOverrideEnabled(id: string, enabled: boolean): Promise<void>;
15
+ createOverride(flagId: string, criteria: OverrideCriteria, enabled: boolean, priority: number): Promise<void>;
16
+ updateOverride(id: string, input: UpdateOverrideInput): Promise<void>;
17
17
  deleteOverride(id: string): Promise<void>;
18
18
  }
@@ -102,38 +102,42 @@ let PrismaFeatureFlagRepository = class PrismaFeatureFlagRepository {
102
102
  }
103
103
  async findOverride(flagId, criteria) {
104
104
  return this.prisma.featureFlagOverride.findFirst({
105
- where: { flagId, ...criteria },
105
+ where: {
106
+ flagId,
107
+ attributes: { equals: criteria.attributes },
108
+ },
106
109
  select: { id: true },
107
110
  });
108
111
  }
109
- async createOverride(flagId, criteria, enabled) {
112
+ async createOverride(flagId, criteria, enabled, priority) {
110
113
  try {
111
114
  await this.prisma.featureFlagOverride.create({
112
- data: { flagId, ...criteria, enabled },
115
+ data: {
116
+ flagId,
117
+ attributes: criteria.attributes,
118
+ priority,
119
+ enabled,
120
+ },
113
121
  });
114
122
  }
115
123
  catch (error) {
116
124
  if (isPrismaError(error, 'P2002')) {
117
- // Concurrent insert hit the unique index — fall back to update
118
- const existing = await this.prisma.featureFlagOverride.findFirst({
119
- where: { flagId, ...criteria },
120
- select: { id: true },
121
- });
125
+ const existing = await this.findOverride(flagId, criteria);
122
126
  if (existing) {
123
- await this.prisma.featureFlagOverride.update({
124
- where: { id: existing.id },
125
- data: { enabled },
126
- });
127
+ await this.updateOverride(existing.id, { enabled, priority });
127
128
  return;
128
129
  }
129
130
  }
130
131
  throw error;
131
132
  }
132
133
  }
133
- async updateOverrideEnabled(id, enabled) {
134
+ async updateOverride(id, input) {
134
135
  await this.prisma.featureFlagOverride.update({
135
136
  where: { id },
136
- data: { enabled },
137
+ data: {
138
+ enabled: input.enabled,
139
+ priority: input.priority,
140
+ },
137
141
  });
138
142
  }
139
143
  async deleteOverride(id) {
@@ -1 +1 @@
1
- {"version":3,"file":"prisma-feature-flag.repository.js","sourceRoot":"","sources":["../../src/repositories/prisma-feature-flag.repository.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAKwB;AAWxB,SAAS,aAAa,CAAC,KAAc,EAAE,IAAY;IACjD,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACd,KAAa,CAAC,IAAI,KAAK,IAAI,CAC7B,CAAC;AACJ,CAAC;AAGM,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;IACtC,YAA6B,MAAW;QAAX,WAAM,GAAN,MAAM,CAAK;IAAG,CAAC;IAE5C,KAAK,CAAC,UAAU,CAAC,KAA6B;QAC5C,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;QACzC,IAAI,UAAU,GAAG,CAAC,IAAI,UAAU,GAAG,GAAG,EAAE,CAAC;YACvC,MAAM,IAAI,4BAAmB,CAAC,6CAA6C,UAAU,EAAE,CAAC,CAAC;QAC3F,CAAC;QAED,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC;gBAC1C,IAAI,EAAE;oBACJ,GAAG,EAAE,KAAK,CAAC,GAAG;oBACd,WAAW,EAAE,KAAK,CAAC,WAAW;oBAC9B,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK;oBAC/B,UAAU;oBACV,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,EAAE;iBAC/B;gBACD,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aAC7B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClC,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,KAAK,CAAC,GAAG,kBAAkB,CAAC,CAAC;YAC5E,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAW,EAAE,KAA6B;QACzD,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,EAAE,CAAC;YACvF,MAAM,IAAI,4BAAmB,CAAC,6CAA6C,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;QACjG,CAAC;QAED,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC;gBAC1C,KAAK,EAAE,EAAE,GAAG,EAAE;gBACd,IAAI,EAAE;oBACJ,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC;oBAC1E,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;oBAC9D,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC;oBACvE,GAAG,CAAC,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;iBAClE;gBACD,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aAC7B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClC,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,GAAG,aAAa,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW;QAC3B,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC;gBAC1C,KAAK,EAAE,EAAE,GAAG,EAAE;gBACd,IAAI,EAAE,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE;gBAChC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aAC7B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClC,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,GAAG,aAAa,CAAC,CAAC;YACjE,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,GAAW;QAC7B,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC;YACxC,KAAK,EAAE,EAAE,GAAG,EAAE;YACd,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,GAAW;QAC/B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1E,OAAO,IAAI,EAAE,EAAE,IAAI,IAAI,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC;YACtC,KAAK,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE;YAC3B,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;YAC5B,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;SAC/B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,MAAc,EAAE,QAA0B;QAC3D,OAAO,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC;YAC/C,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE;YAC9B,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;SACrB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,MAAc,EAAE,QAA0B,EAAE,OAAgB;QAC/E,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;gBAC3C,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,OAAO,EAAE;aACvC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClC,+DAA+D;gBAC/D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC;oBAC/D,KAAK,EAAE,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE;oBAC9B,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;iBACrB,CAAC,CAAC;gBACH,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;wBAC3C,KAAK,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE;wBAC1B,IAAI,EAAE,EAAE,OAAO,EAAE;qBAClB,CAAC,CAAC;oBACH,OAAO;gBACT,CAAC;YACH,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,EAAU,EAAE,OAAgB;QACtD,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;YAC3C,KAAK,EAAE,EAAE,EAAE,EAAE;YACb,IAAI,EAAE,EAAE,OAAO,EAAE;SAClB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,EAAU;QAC7B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClC,uDAAuD;gBACvD,OAAO;YACT,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;CACF,CAAA;AAxIY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,mBAAU,GAAE;;GACA,2BAA2B,CAwIvC"}
1
+ {"version":3,"file":"prisma-feature-flag.repository.js","sourceRoot":"","sources":["../../src/repositories/prisma-feature-flag.repository.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,7 @@ export declare class FeatureFlagService {
25
25
  invalidateCache(): Promise<void>;
26
26
  findByKey(key: string): Promise<FeatureFlagWithOverrides>;
27
27
  removeOverride(key: string, input: RemoveOverrideInput): Promise<void>;
28
+ private normalizeOverrideAttributes;
28
29
  /**
29
30
  * Best-effort cache invalidation for mutation paths.
30
31
  * DB write already succeeded — cache failure should not fail the caller.
@@ -19,6 +19,7 @@ const flag_evaluator_service_1 = require("./flag-evaluator.service");
19
19
  const flag_context_resolver_1 = require("./flag-context-resolver");
20
20
  const flag_event_publisher_1 = require("./flag-event-publisher");
21
21
  const feature_flag_events_1 = require("../events/feature-flag.events");
22
+ const targeting_attributes_1 = require("../utils/targeting-attributes");
22
23
  const CACHE_INVALIDATION_FAILED = 'feature-flag.cache.invalidation-failed';
23
24
  let FeatureFlagService = class FeatureFlagService {
24
25
  constructor(options, repository, cacheAdapter, evaluator, contextResolver, eventPublisher) {
@@ -78,26 +79,29 @@ let FeatureFlagService = class FeatureFlagService {
78
79
  return flag;
79
80
  }
80
81
  async setOverride(key, input) {
82
+ const attributes = this.normalizeOverrideAttributes(input.attributes);
83
+ const priority = input.priority ?? 0;
81
84
  const flagId = await this.repository.findFlagIdByKey(key);
82
85
  if (!flagId) {
83
86
  throw new common_1.NotFoundException(`Feature flag "${key}" not found`);
84
87
  }
85
- const criteria = {
86
- tenantId: input.tenantId ?? null,
87
- userId: input.userId ?? null,
88
- environment: input.environment ?? null,
89
- };
88
+ const criteria = { attributes };
90
89
  const existing = await this.repository.findOverride(flagId, criteria);
91
90
  if (existing) {
92
- await this.repository.updateOverrideEnabled(existing.id, input.enabled);
91
+ await this.repository.updateOverride(existing.id, {
92
+ enabled: input.enabled,
93
+ priority,
94
+ });
93
95
  }
94
96
  else {
95
- await this.repository.createOverride(flagId, criteria, input.enabled);
97
+ await this.repository.createOverride(flagId, criteria, input.enabled, priority);
96
98
  }
97
99
  await this.safeInvalidateCache(key);
98
100
  this.eventPublisher.emit(feature_flag_events_1.FeatureFlagEvents.OVERRIDE_SET, {
99
101
  flagKey: key,
100
- ...input,
102
+ attributes,
103
+ enabled: input.enabled,
104
+ priority,
101
105
  action: 'set',
102
106
  });
103
107
  }
@@ -116,15 +120,12 @@ let FeatureFlagService = class FeatureFlagService {
116
120
  return flag;
117
121
  }
118
122
  async removeOverride(key, input) {
123
+ const attributes = this.normalizeOverrideAttributes(input.attributes);
119
124
  const flagId = await this.repository.findFlagIdByKey(key);
120
125
  if (!flagId) {
121
126
  throw new common_1.NotFoundException(`Feature flag "${key}" not found`);
122
127
  }
123
- const criteria = {
124
- tenantId: input.tenantId ?? null,
125
- userId: input.userId ?? null,
126
- environment: input.environment ?? null,
127
- };
128
+ const criteria = { attributes };
128
129
  const existing = await this.repository.findOverride(flagId, criteria);
129
130
  if (existing) {
130
131
  await this.repository.deleteOverride(existing.id);
@@ -132,10 +133,18 @@ let FeatureFlagService = class FeatureFlagService {
132
133
  await this.safeInvalidateCache(key);
133
134
  this.eventPublisher.emit(feature_flag_events_1.FeatureFlagEvents.OVERRIDE_REMOVED, {
134
135
  flagKey: key,
135
- ...input,
136
+ attributes,
136
137
  action: 'removed',
137
138
  });
138
139
  }
140
+ normalizeOverrideAttributes(input) {
141
+ try {
142
+ return (0, targeting_attributes_1.normalizeTargetingAttributes)(input, { allowEmpty: false });
143
+ }
144
+ catch (error) {
145
+ throw new common_1.BadRequestException(String(error instanceof Error ? error.message : error));
146
+ }
147
+ }
139
148
  /**
140
149
  * Best-effort cache invalidation for mutation paths.
141
150
  * DB write already succeeded — cache failure should not fail the caller.
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.service.js","sourceRoot":"","sources":["../../src/services/feature-flag.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"feature-flag.service.js","sourceRoot":"","sources":["../../src/services/feature-flag.service.ts"],"names":[],"mappings":"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"}
@@ -23,10 +23,21 @@ let FlagContextResolver = class FlagContextResolver {
23
23
  this.tenantProvider = tenantProvider;
24
24
  }
25
25
  resolve(explicit) {
26
+ const userId = explicit?.userId !== undefined ? explicit.userId : this.flagContext.getUserId();
27
+ const tenantId = explicit?.tenantId !== undefined
28
+ ? explicit.tenantId
29
+ : this.tenantProvider.getCurrentTenantId();
30
+ const environment = explicit?.environment !== undefined ? explicit.environment : this.options.environment;
26
31
  return {
27
- userId: explicit?.userId !== undefined ? explicit.userId : this.flagContext.getUserId(),
28
- tenantId: explicit?.tenantId !== undefined ? explicit.tenantId : this.tenantProvider.getCurrentTenantId(),
29
- environment: explicit?.environment !== undefined ? explicit.environment : this.options.environment,
32
+ userId,
33
+ tenantId,
34
+ environment,
35
+ attributes: {
36
+ ...(explicit?.attributes ?? {}),
37
+ ...(userId !== undefined && { userId }),
38
+ ...(tenantId !== undefined && { tenantId }),
39
+ ...(environment !== undefined && { environment }),
40
+ },
30
41
  };
31
42
  }
32
43
  };
@@ -1 +1 @@
1
- {"version":3,"file":"flag-context-resolver.js","sourceRoot":"","sources":["../../src/services/flag-context-resolver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AACpD,sEAAiG;AAIjG,iDAA6C;AAGtC,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,YACwD,OAAiC,EACtE,WAAwB,EACS,cAAqC;QAFjC,YAAO,GAAP,OAAO,CAA0B;QACtE,gBAAW,GAAX,WAAW,CAAa;QACS,mBAAc,GAAd,cAAc,CAAuB;IACtF,CAAC;IAEJ,OAAO,CAAC,QAA4B;QAClC,OAAO;YACL,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE;YACvF,QAAQ,EAAE,QAAQ,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,kBAAkB,EAAE;YACzG,WAAW,EAAE,QAAQ,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW;SACnG,CAAC;IACJ,CAAC;CACF,CAAA;AAdY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,oDAA2B,CAAC,CAAA;IAEnC,WAAA,IAAA,eAAM,EAAC,gDAAuB,CAAC,CAAA;6CADF,0BAAW;GAHhC,mBAAmB,CAc/B"}
1
+ {"version":3,"file":"flag-context-resolver.js","sourceRoot":"","sources":["../../src/services/flag-context-resolver.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAoD;AACpD,sEAAiG;AAIjG,iDAA6C;AAGtC,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,YACwD,OAAiC,EACtE,WAAwB,EACS,cAAqC;QAFjC,YAAO,GAAP,OAAO,CAA0B;QACtE,gBAAW,GAAX,WAAW,CAAa;QACS,mBAAc,GAAd,cAAc,CAAuB;IACtF,CAAC;IAEJ,OAAO,CAAC,QAA4B;QAClC,MAAM,MAAM,GACV,QAAQ,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC;QAClF,MAAM,QAAQ,GACZ,QAAQ,EAAE,QAAQ,KAAK,SAAS;YAC9B,CAAC,CAAC,QAAQ,CAAC,QAAQ;YACnB,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,kBAAkB,EAAE,CAAC;QAC/C,MAAM,WAAW,GACf,QAAQ,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC;QAExF,OAAO;YACL,MAAM;YACN,QAAQ;YACR,WAAW;YACX,UAAU,EAAE;gBACV,GAAG,CAAC,QAAQ,EAAE,UAAU,IAAI,EAAE,CAAC;gBAC/B,GAAG,CAAC,MAAM,KAAK,SAAS,IAAI,EAAE,MAAM,EAAE,CAAC;gBACvC,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,CAAC;gBAC3C,GAAG,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,CAAC;aAClD;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AA7BY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,oDAA2B,CAAC,CAAA;IAEnC,WAAA,IAAA,eAAM,EAAC,gDAAuB,CAAC,CAAA;6CADF,0BAAW;GAHhC,mBAAmB,CA6B/B"}
@@ -8,5 +8,6 @@ export interface EvaluationResult {
8
8
  }
9
9
  export declare class FlagEvaluatorService {
10
10
  evaluate(flag: FeatureFlagWithOverrides, context: EvaluationContext): EvaluationResult;
11
+ private findMatchingOverride;
11
12
  }
12
13
  export {};
@@ -9,38 +9,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.FlagEvaluatorService = void 0;
10
10
  const common_1 = require("@nestjs/common");
11
11
  const hash_1 = require("../utils/hash");
12
+ const targeting_attributes_1 = require("../utils/targeting-attributes");
12
13
  let FlagEvaluatorService = class FlagEvaluatorService {
13
14
  evaluate(flag, context) {
14
- // 1. Archived → always false
15
15
  if (flag.archivedAt) {
16
16
  return { result: false, source: 'global' };
17
17
  }
18
- // 2. User override (most specific)
19
- if (context.userId) {
20
- const userOverride = flag.overrides.find((o) => o.userId === context.userId &&
21
- (o.tenantId === null || o.tenantId === context.tenantId) &&
22
- (o.environment === null || o.environment === context.environment));
23
- if (userOverride) {
24
- return { result: userOverride.enabled, source: 'user_override' };
25
- }
26
- }
27
- // 3. Tenant override
28
- if (context.tenantId) {
29
- const tenantOverride = flag.overrides.find((o) => o.tenantId === context.tenantId &&
30
- o.userId === null &&
31
- (o.environment === null || o.environment === context.environment));
32
- if (tenantOverride) {
33
- return { result: tenantOverride.enabled, source: 'tenant_override' };
34
- }
35
- }
36
- // 4. Environment override
37
- if (context.environment) {
38
- const envOverride = flag.overrides.find((o) => o.environment === context.environment && o.tenantId === null && o.userId === null);
39
- if (envOverride) {
40
- return { result: envOverride.enabled, source: 'env_override' };
41
- }
18
+ const override = this.findMatchingOverride(flag.overrides, context);
19
+ if (override) {
20
+ return { result: override.enabled, source: 'override' };
42
21
  }
43
- // 5. Percentage rollout
44
22
  if (flag.percentage > 0) {
45
23
  if (flag.percentage === 100) {
46
24
  return { result: true, source: 'percentage' };
@@ -52,12 +30,36 @@ let FlagEvaluatorService = class FlagEvaluatorService {
52
30
  const bucket = (0, hash_1.murmurhash3)(flag.key + hashKey) % 100;
53
31
  return { result: bucket < flag.percentage, source: 'percentage' };
54
32
  }
55
- // 6. Global default
56
33
  return { result: flag.enabled, source: 'global' };
57
34
  }
35
+ findMatchingOverride(overrides, context) {
36
+ const contextAttributes = context.attributes ?? {};
37
+ return (overrides
38
+ .filter((override) => (0, targeting_attributes_1.matchesTargetingAttributes)(override.attributes, contextAttributes))
39
+ .sort(compareOverrides)[0] ?? null);
40
+ }
58
41
  };
59
42
  exports.FlagEvaluatorService = FlagEvaluatorService;
60
43
  exports.FlagEvaluatorService = FlagEvaluatorService = __decorate([
61
44
  (0, common_1.Injectable)()
62
45
  ], FlagEvaluatorService);
46
+ function compareOverrides(a, b) {
47
+ const specificityDelta = Object.keys(b.attributes).length - Object.keys(a.attributes).length;
48
+ if (specificityDelta !== 0) {
49
+ return specificityDelta;
50
+ }
51
+ const priorityDelta = b.priority - a.priority;
52
+ if (priorityDelta !== 0) {
53
+ return priorityDelta;
54
+ }
55
+ const createdAtDelta = getOverrideCreatedAtTime(a) - getOverrideCreatedAtTime(b);
56
+ if (createdAtDelta !== 0) {
57
+ return createdAtDelta;
58
+ }
59
+ return a.id.localeCompare(b.id);
60
+ }
61
+ function getOverrideCreatedAtTime(override) {
62
+ const createdAt = override.createdAt;
63
+ return createdAt instanceof Date ? createdAt.getTime() : new Date(createdAt).getTime();
64
+ }
63
65
  //# sourceMappingURL=flag-evaluator.service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"flag-evaluator.service.js","sourceRoot":"","sources":["../../src/services/flag-evaluator.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"flag-evaluator.service.js","sourceRoot":"","sources":["../../src/services/flag-evaluator.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { TargetingAttributes, TargetingAttributeValue } from '../interfaces/feature-flag.interface';
2
+ interface AttributeValidationOptions {
3
+ allowEmpty: boolean;
4
+ }
5
+ export declare function isTargetingAttributeValue(value: unknown): value is TargetingAttributeValue;
6
+ export declare function isTargetingAttributes(value: unknown, options?: AttributeValidationOptions): value is TargetingAttributes;
7
+ export declare function normalizeTargetingAttributes(value: unknown, options?: AttributeValidationOptions): TargetingAttributes;
8
+ export declare function matchesTargetingAttributes(overrideAttributes: TargetingAttributes, contextAttributes: TargetingAttributes): boolean;
9
+ export {};
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isTargetingAttributeValue = isTargetingAttributeValue;
4
+ exports.isTargetingAttributes = isTargetingAttributes;
5
+ exports.normalizeTargetingAttributes = normalizeTargetingAttributes;
6
+ exports.matchesTargetingAttributes = matchesTargetingAttributes;
7
+ const INVALID_ATTRIBUTES_MESSAGE = 'attributes must be a non-empty object with string, number, boolean, or null values';
8
+ function isTargetingAttributeValue(value) {
9
+ return (value === null ||
10
+ typeof value === 'string' ||
11
+ (typeof value === 'number' && Number.isFinite(value)) ||
12
+ typeof value === 'boolean');
13
+ }
14
+ function isTargetingAttributes(value, options = { allowEmpty: false }) {
15
+ if (!isPlainObject(value)) {
16
+ return false;
17
+ }
18
+ const entries = Object.entries(value);
19
+ if (!options.allowEmpty && entries.length === 0) {
20
+ return false;
21
+ }
22
+ return entries.every(([, attributeValue]) => isTargetingAttributeValue(attributeValue));
23
+ }
24
+ function normalizeTargetingAttributes(value, options = { allowEmpty: false }) {
25
+ if (!isTargetingAttributes(value, options)) {
26
+ throw new Error(INVALID_ATTRIBUTES_MESSAGE);
27
+ }
28
+ return { ...value };
29
+ }
30
+ function matchesTargetingAttributes(overrideAttributes, contextAttributes) {
31
+ const entries = Object.entries(overrideAttributes);
32
+ if (entries.length === 0) {
33
+ return false;
34
+ }
35
+ return entries.every(([key, value]) => contextAttributes[key] === value);
36
+ }
37
+ function isPlainObject(value) {
38
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
39
+ return false;
40
+ }
41
+ const prototype = Object.getPrototypeOf(value);
42
+ return prototype === Object.prototype || prototype === null;
43
+ }
44
+ //# sourceMappingURL=targeting-attributes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"targeting-attributes.js","sourceRoot":"","sources":["../../src/utils/targeting-attributes.ts"],"names":[],"mappings":";;AAYA,8DAOC;AAED,sDAcC;AAED,oEASC;AAED,gEAUC;AAjDD,MAAM,0BAA0B,GAC9B,oFAAoF,CAAC;AAEvF,SAAgB,yBAAyB,CAAC,KAAc;IACtD,OAAO,CACL,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrD,OAAO,KAAK,KAAK,SAAS,CAC3B,CAAC;AACJ,CAAC;AAED,SAAgB,qBAAqB,CACnC,KAAc,EACd,UAAsC,EAAE,UAAU,EAAE,KAAK,EAAE;IAE3D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,yBAAyB,CAAC,cAAc,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED,SAAgB,4BAA4B,CAC1C,KAAc,EACd,UAAsC,EAAE,UAAU,EAAE,KAAK,EAAE;IAE3D,IAAI,CAAC,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;AACtB,CAAC;AAED,SAAgB,0BAA0B,CACxC,kBAAuC,EACvC,iBAAsC;IAEtC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACnD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/C,OAAO,SAAS,KAAK,MAAM,CAAC,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC;AAC9D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestarc/feature-flag",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "DB-backed feature flags for NestJS + Prisma + PostgreSQL with tenant-aware overrides",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "repository": {
37
37
  "type": "git",
38
- "url": "https://github.com/nestarc/nestjs-feature-flag"
38
+ "url": "git+https://github.com/nestarc/nestjs-feature-flag.git"
39
39
  },
40
40
  "keywords": [
41
41
  "nestjs",
@@ -56,6 +56,8 @@
56
56
  "@nestjs/common": "^10.0.0 || ^11.0.0",
57
57
  "@nestjs/core": "^10.0.0 || ^11.0.0",
58
58
  "@prisma/client": "^5.0.0 || ^6.0.0",
59
+ "class-transformer": "^0.5.1",
60
+ "class-validator": "^0.14.0 || ^0.15.0",
59
61
  "ioredis": "^5.0.0",
60
62
  "reflect-metadata": "^0.1.13 || ^0.2.0",
61
63
  "rxjs": "^7.0.0"
@@ -83,6 +85,8 @@
83
85
  "@types/ioredis-mock": "^8.2.7",
84
86
  "@types/jest": "^29.5.0",
85
87
  "@types/supertest": "^7.2.0",
88
+ "class-transformer": "^0.5.1",
89
+ "class-validator": "^0.15.1",
86
90
  "dotenv-cli": "^11.0.0",
87
91
  "eslint": "^9.0.0",
88
92
  "ioredis": "^5.10.1",
@@ -0,0 +1,59 @@
1
+ ALTER TABLE "feature_flag_overrides"
2
+ ADD COLUMN "attributes" JSONB NOT NULL DEFAULT '{}',
3
+ ADD COLUMN "priority" INTEGER NOT NULL DEFAULT 0;
4
+
5
+ ALTER TABLE "feature_flag_overrides"
6
+ ALTER COLUMN "updated_at" SET DEFAULT CURRENT_TIMESTAMP;
7
+
8
+ UPDATE "feature_flag_overrides"
9
+ SET "attributes" = jsonb_strip_nulls(
10
+ jsonb_build_object(
11
+ 'tenantId', "tenant_id",
12
+ 'userId', "user_id",
13
+ 'environment', "environment"
14
+ )
15
+ );
16
+
17
+ -- v0.3.0 rejects empty targeting attributes. Legacy all-null/global overrides
18
+ -- become empty objects during backfill and are intentionally removed by this
19
+ -- breaking migration.
20
+ DELETE FROM "feature_flag_overrides"
21
+ WHERE "attributes" = '{}'::jsonb;
22
+
23
+ WITH ranked_overrides AS (
24
+ SELECT
25
+ "id",
26
+ row_number() OVER (
27
+ PARTITION BY "flag_id", "attributes"
28
+ ORDER BY "updated_at" DESC, "created_at" DESC, "id" DESC
29
+ ) AS row_number
30
+ FROM "feature_flag_overrides"
31
+ )
32
+ DELETE FROM "feature_flag_overrides" target
33
+ USING ranked_overrides ranked
34
+ WHERE target."id" = ranked."id"
35
+ AND ranked.row_number > 1;
36
+
37
+ DROP INDEX IF EXISTS "uq_override_000";
38
+ DROP INDEX IF EXISTS "uq_override_001";
39
+ DROP INDEX IF EXISTS "uq_override_010";
40
+ DROP INDEX IF EXISTS "uq_override_011";
41
+ DROP INDEX IF EXISTS "uq_override_100";
42
+ DROP INDEX IF EXISTS "uq_override_101";
43
+ DROP INDEX IF EXISTS "uq_override_110";
44
+ DROP INDEX IF EXISTS "uq_override_111";
45
+
46
+ ALTER TABLE "feature_flag_overrides"
47
+ DROP COLUMN "tenant_id",
48
+ DROP COLUMN "user_id",
49
+ DROP COLUMN "environment";
50
+
51
+ ALTER TABLE "feature_flag_overrides"
52
+ ALTER COLUMN "attributes" DROP DEFAULT;
53
+
54
+ CREATE UNIQUE INDEX "uq_feature_flag_override_attributes"
55
+ ON "feature_flag_overrides"("flag_id", "attributes");
56
+
57
+ ALTER TABLE "feature_flag_overrides"
58
+ ADD CONSTRAINT "chk_feature_flag_override_attributes_non_empty"
59
+ CHECK (jsonb_typeof("attributes") = 'object' AND "attributes" <> '{}'::jsonb);
@@ -24,18 +24,16 @@ model FeatureFlag {
24
24
  }
25
25
 
26
26
  model FeatureFlagOverride {
27
- id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
28
- flagId String @map("flag_id") @db.Uuid
29
- tenantId String? @map("tenant_id")
30
- userId String? @map("user_id")
31
- environment String?
32
- enabled Boolean
33
- createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
34
- updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
27
+ id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
28
+ flagId String @map("flag_id") @db.Uuid
29
+ attributes Json
30
+ priority Int @default(0)
31
+ enabled Boolean
32
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
33
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
35
34
 
36
35
  flag FeatureFlag @relation(fields: [flagId], references: [id], onDelete: Cascade)
37
36
 
38
- // Unique indexes defined in raw SQL migration (0001) to handle NULL equality correctly
39
37
  @@index([flagId], map: "idx_override_flag_id")
40
38
  @@map("feature_flag_overrides")
41
39
  }