@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.
- package/README.md +102 -85
- package/dist/admin/feature-flag-admin.controller.d.ts +6 -6
- package/dist/admin/feature-flag-admin.controller.js +10 -4
- package/dist/admin/feature-flag-admin.controller.js.map +1 -1
- package/dist/admin/feature-flag-admin.dto.d.ts +22 -0
- package/dist/admin/feature-flag-admin.dto.js +93 -0
- package/dist/admin/feature-flag-admin.dto.js.map +1 -0
- package/dist/admin/targeting-attributes.validator.d.ts +6 -0
- package/dist/admin/targeting-attributes.validator.js +36 -0
- package/dist/admin/targeting-attributes.validator.js.map +1 -0
- package/dist/events/feature-flag.events.d.ts +4 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/interfaces/evaluation-context.interface.d.ts +6 -3
- package/dist/interfaces/feature-flag-repository.interface.d.ts +8 -6
- package/dist/interfaces/feature-flag.interface.d.ts +9 -9
- package/dist/repositories/prisma-feature-flag.repository.d.ts +3 -3
- package/dist/repositories/prisma-feature-flag.repository.js +18 -14
- package/dist/repositories/prisma-feature-flag.repository.js.map +1 -1
- package/dist/services/feature-flag.service.d.ts +1 -0
- package/dist/services/feature-flag.service.js +23 -14
- package/dist/services/feature-flag.service.js.map +1 -1
- package/dist/services/flag-context-resolver.js +14 -3
- package/dist/services/flag-context-resolver.js.map +1 -1
- package/dist/services/flag-evaluator.service.d.ts +1 -0
- package/dist/services/flag-evaluator.service.js +29 -27
- package/dist/services/flag-evaluator.service.js.map +1 -1
- package/dist/utils/targeting-attributes.d.ts +9 -0
- package/dist/utils/targeting-attributes.js +44 -0
- package/dist/utils/targeting-attributes.js.map +1 -0
- package/package.json +6 -2
- package/prisma/migrations/20260512000000_attribute_targeting/migration.sql +59 -0
- package/prisma/schema.prisma +7 -9
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nestarc.dev/packages/feature-flag/)
|
|
8
8
|
|
|
9
|
-
DB-backed feature flags for NestJS + Prisma + PostgreSQL --
|
|
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
|
-
- **
|
|
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
|
|
95
|
-
flagId
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
351
|
+
## Attribute Targeting
|
|
362
352
|
|
|
363
|
-
|
|
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
|
-
|
|
367
|
-
|
|
356
|
+
const enabled = await this.flags.isEnabled('NEW_CHECKOUT', {
|
|
357
|
+
userId: 'user-123',
|
|
368
358
|
tenantId: 'tenant-1',
|
|
369
|
-
|
|
359
|
+
environment: 'production',
|
|
360
|
+
attributes: {
|
|
361
|
+
plan: 'pro',
|
|
362
|
+
country: 'KR',
|
|
363
|
+
},
|
|
370
364
|
});
|
|
365
|
+
```
|
|
371
366
|
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
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
|
|
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 | **
|
|
493
|
-
| 3 | **
|
|
494
|
-
| 4 | **
|
|
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 {
|
|
3
|
-
import {
|
|
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:
|
|
7
|
+
create(input: CreateFeatureFlagDto): Promise<FeatureFlagWithOverrides>;
|
|
8
8
|
findAll(): Promise<FeatureFlagWithOverrides[]>;
|
|
9
9
|
findByKey(key: string): Promise<FeatureFlagWithOverrides>;
|
|
10
|
-
update(key: string, input:
|
|
10
|
+
update(key: string, input: UpdateFeatureFlagDto): Promise<FeatureFlagWithOverrides>;
|
|
11
11
|
archive(key: string): Promise<FeatureFlagWithOverrides>;
|
|
12
|
-
setOverride(key: string, input:
|
|
13
|
-
removeOverride(key: string, input:
|
|
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", [
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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: '
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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;
|
|
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
|
|
3
|
+
/** User ID - used for user-scoped targeting and percentage hash */
|
|
3
4
|
userId?: string | null;
|
|
4
|
-
/** Tenant ID
|
|
5
|
+
/** Tenant ID - used for tenant-scoped targeting. Ignored if tenancy is not installed */
|
|
5
6
|
tenantId?: string | null;
|
|
6
|
-
/** Environment
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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.
|
|
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
|
|
134
|
+
async updateOverride(id, input) {
|
|
134
135
|
await this.prisma.featureFlagOverride.update({
|
|
135
136
|
where: { id },
|
|
136
|
-
data: {
|
|
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;
|
|
1
|
+
{"version":3,"file":"prisma-feature-flag.repository.js","sourceRoot":"","sources":["../../src/repositories/prisma-feature-flag.repository.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAKwB;AAYxB,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;gBACL,MAAM;gBACN,UAAU,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,UAAU,EAAE;aAC5C;YACD,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;SACrB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,MAAc,EACd,QAA0B,EAC1B,OAAgB,EAChB,QAAgB;QAEhB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;gBAC3C,IAAI,EAAE;oBACJ,MAAM;oBACN,UAAU,EAAE,QAAQ,CAAC,UAAU;oBAC/B,QAAQ;oBACR,OAAO;iBACR;aACF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;gBAClC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;gBAC3D,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;oBAC9D,OAAO;gBACT,CAAC;YACH,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,EAAU,EAAE,KAA0B;QACzD,MAAM,IAAI,CAAC,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC;YAC3C,KAAK,EAAE,EAAE,EAAE,EAAE;YACb,IAAI,EAAE;gBACJ,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB;SACF,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;AAjJY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,mBAAU,GAAE;;GACA,2BAA2B,CAiJvC"}
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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":";;;;;;;;;;;;;;;AAAA,
|
|
1
|
+
{"version":3,"file":"feature-flag.service.js","sourceRoot":"","sources":["../../src/services/feature-flag.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA4F;AAC5F,sEAAgH;AAYhH,qEAAgE;AAChE,mEAA8D;AAC9D,iEAA4D;AAC5D,uEAAsF;AACtF,wEAA6E;AAE7E,MAAM,yBAAyB,GAAG,wCAAwC,CAAC;AAGpE,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAC7B,YACwD,OAAiC,EACrC,UAAiC,EAC3C,YAA0B,EACjD,SAA+B,EAC/B,eAAoC,EACpC,cAAkC;QALG,YAAO,GAAP,OAAO,CAA0B;QACrC,eAAU,GAAV,UAAU,CAAuB;QAC3C,iBAAY,GAAZ,YAAY,CAAc;QACjD,cAAS,GAAT,SAAS,CAAsB;QAC/B,oBAAe,GAAf,eAAe,CAAqB;QACpC,mBAAc,GAAd,cAAc,CAAoB;IAClD,CAAC;IAEJ,IAAY,UAAU;QACpB,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,IAAI,MAAM,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAe,EAAE,eAAmC;QAClE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,IAAI,CAAC,OAAO,CAAC,gBAAgB,IAAI,KAAK,CAAC;QAChD,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC9D,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAClE,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAEhD,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,SAAS,EAAE;YACpD,OAAO;YACP,MAAM;YACN,OAAO;YACP,MAAM;YACN,gBAAgB;SACY,CAAC,CAAC;QAEhC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,eAAmC;QACnD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC9D,MAAM,MAAM,GAA4B,EAAE,CAAC;QAE3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC;QACnE,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAA6B;QACxC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACrD,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACjC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAC/F,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,KAA6B;QACrD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC1D,MAAM,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACzF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACpD,MAAM,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QAC3F,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,GAAW,EAAE,KAAuB;QACpD,MAAM,UAAU,GAAG,IAAI,CAAC,2BAA2B,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACtE,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;QAErC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,GAAG,aAAa,CAAC,CAAC;QACjE,CAAC;QAED,MAAM,QAAQ,GAAG,EAAE,UAAU,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACtE,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,EAAE;gBAChD,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAClF,CAAC;QAED,MAAM,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,YAAY,EAAE;YACvD,OAAO,EAAE,GAAG;YACZ,UAAU;YACV,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,QAAQ;YACR,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,UAAU,CAAC,kBAAkB,EAAE,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;QACrC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,GAAG,aAAa,CAAC,CAAC;QACjE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,GAAW,EAAE,KAA0B;QAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,2BAA2B,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAEtE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,0BAAiB,CAAC,iBAAiB,GAAG,aAAa,CAAC,CAAC;QACjE,CAAC;QAED,MAAM,QAAQ,GAAG,EAAE,UAAU,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACtE,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACpD,CAAC;QAED,MAAM,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,uCAAiB,CAAC,gBAAgB,EAAE;YAC3D,OAAO,EAAE,GAAG;YACZ,UAAU;YACV,MAAM,EAAE,SAAS;SAClB,CAAC,CAAC;IACL,CAAC;IAEO,2BAA2B,CAAC,KAAc;QAChD,IAAI,CAAC;YACH,OAAO,IAAA,mDAA4B,EAAC,KAAK,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QACpE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,4BAAmB,CAAC,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IAED;;;;OAIG;IACK,KAAK,CAAC,mBAAmB,CAAC,GAAY;QAC5C,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,yBAAyB,EAAE;gBAClD,GAAG,EAAE,GAAG,IAAI,SAAS;gBACrB,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;aACrB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,GAAW;QACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QACtD,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1D,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,eAAe;QAC3B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;QAChD,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,kBAAkB,EAAE,CAAC;QACzD,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC;IACf,CAAC;CACF,CAAA;AAtLY,gDAAkB;6BAAlB,kBAAkB;IAD9B,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,oDAA2B,CAAC,CAAA;IACnC,WAAA,IAAA,eAAM,EAAC,gDAAuB,CAAC,CAAA;IAC/B,WAAA,IAAA,eAAM,EAAC,sCAAa,CAAC,CAAA;6DACM,6CAAoB;QACd,2CAAmB;QACpB,yCAAkB;GAP1C,kBAAkB,CAsL9B"}
|
|
@@ -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
|
|
28
|
-
tenantId
|
|
29
|
-
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,
|
|
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"}
|
|
@@ -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
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
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":";;;;;;;;;AAAA,2CAA4C;AAG5C,wCAA4C;
|
|
1
|
+
{"version":3,"file":"flag-evaluator.service.js","sourceRoot":"","sources":["../../src/services/flag-evaluator.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAG5C,wCAA4C;AAE5C,wEAA2E;AAUpE,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAC/B,QAAQ,CAAC,IAA8B,EAAE,OAA0B;QACjE,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QAC7C,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACpE,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;QAC1D,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,IAAI,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;gBAC5B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;YAChD,CAAC;YAED,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;YACzD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;YACpD,CAAC;YAED,MAAM,MAAM,GAAG,IAAA,kBAAW,EAAC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,GAAG,CAAC;YACrD,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;QACpE,CAAC;QAED,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACpD,CAAC;IAEO,oBAAoB,CAC1B,SAAyB,EACzB,OAA0B;QAE1B,MAAM,iBAAiB,GAAG,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;QAEnD,OAAO,CACL,SAAS;aACN,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAA,iDAA0B,EAAC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;aACxF,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CACrC,CAAC;IACJ,CAAC;CACF,CAAA;AAxCY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;GACA,oBAAoB,CAwChC;AAED,SAAS,gBAAgB,CAAC,CAAe,EAAE,CAAe;IACxD,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IAC7F,IAAI,gBAAgB,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IAED,MAAM,aAAa,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC;IAC9C,IAAI,aAAa,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,cAAc,GAAG,wBAAwB,CAAC,CAAC,CAAC,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACjF,IAAI,cAAc,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,OAAO,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,SAAS,wBAAwB,CAAC,QAAsB;IACtD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAA0B,CAAC;IACtD,OAAO,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC;AACzF,CAAC"}
|
|
@@ -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.
|
|
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);
|
package/prisma/schema.prisma
CHANGED
|
@@ -24,18 +24,16 @@ model FeatureFlag {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
model FeatureFlagOverride {
|
|
27
|
-
id
|
|
28
|
-
flagId
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
}
|