@nestarc/feature-flag 0.5.0 → 0.6.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/CHANGELOG.md +186 -0
- package/README.md +130 -724
- package/dist/admin/feature-flag-admin.dto.d.ts +4 -3
- package/dist/admin/feature-flag-admin.dto.js +19 -13
- package/dist/admin/feature-flag-admin.dto.js.map +1 -1
- package/dist/feature-flag.module.d.ts +4 -1
- package/dist/feature-flag.module.js +37 -10
- package/dist/feature-flag.module.js.map +1 -1
- package/dist/flag-registry.js +3 -0
- package/dist/flag-registry.js.map +1 -1
- package/dist/interfaces/evaluation-context.interface.d.ts +5 -5
- package/dist/interfaces/evaluation-details.interface.d.ts +2 -0
- package/dist/interfaces/feature-flag-options.interface.d.ts +19 -5
- package/dist/interfaces/feature-flag.interface.d.ts +2 -2
- package/dist/openfeature.d.ts +6 -9
- package/dist/openfeature.js +38 -8
- package/dist/openfeature.js.map +1 -1
- package/dist/repositories/prisma-feature-flag.repository.js +9 -6
- package/dist/repositories/prisma-feature-flag.repository.js.map +1 -1
- package/dist/services/feature-flag.service.js +4 -2
- package/dist/services/feature-flag.service.js.map +1 -1
- package/dist/services/flag-context-resolver.js +1 -0
- package/dist/services/flag-context-resolver.js.map +1 -1
- package/docs/usage.md +720 -0
- package/package.json +18 -5
package/README.md
CHANGED
|
@@ -1,825 +1,231 @@
|
|
|
1
|
-
#
|
|
1
|
+
# NestJS feature flags with Prisma and PostgreSQL
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nestarc/feature-flag)
|
|
4
4
|
[](https://www.npmjs.com/package/@nestarc/feature-flag)
|
|
5
5
|
[](https://github.com/nestarc/nestjs-feature-flag/actions/workflows/ci.yml)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://nestarc.dev/packages/feature-flag/)
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
`@nestarc/feature-flag` stores feature flags in your PostgreSQL database and evaluates them inside your NestJS application. Use route guards, exact attribute targeting, and deterministic percentage rollouts without a separate feature flag service. NestJS, Prisma, and other [peer dependencies](#installation-and-compatibility) are required.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
**Version scope:** these docs describe **0.6.0**. It includes targeting-key and registry fixes, custom module provider options, and an SDK-compatible OpenFeature provider that are absent from `0.5.0`. See the [0.6.0 changes and upgrade notes](CHANGELOG.md), and consult the [0.5.0 source](https://github.com/nestarc/nestjs-feature-flag/tree/v0.5.0) when working on that release.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
- **Attribute-targeted overrides** -- exact-match targeting for tenants, users, environments, plans, regions, or custom dimensions
|
|
15
|
-
- **Percentage rollouts** -- deterministic hashing (murmurhash3) with explicit `targetingKey` / `bucketBy`
|
|
16
|
-
- **Guard decorator** -- `@FeatureFlag()` automatically gates routes and controllers
|
|
17
|
-
- **Bypass decorator** -- `@BypassFeatureFlag()` exempts health checks and public endpoints
|
|
18
|
-
- **Programmatic evaluation** -- `isEnabled()`, `evaluateBoolean()`, and `evaluateAll()` for service-layer logic
|
|
19
|
-
- **Type-safe registry helpers** -- define flag keys, defaults, rollout bucket keys, exposure tracking, and lifecycle metadata in code
|
|
20
|
-
- **Built-in caching** -- configurable TTL with manual invalidation; Redis Pub/Sub for multi-instance
|
|
21
|
-
- **Pluggable persistence** -- `FeatureFlagRepository` interface for custom backends (Prisma default)
|
|
22
|
-
- **Pluggable tenancy** -- `TenantContextProvider` interface for custom tenant resolution
|
|
23
|
-
- **Admin REST API** -- opt-in `FeatureFlagAdminModule` with guard injection and proper error responses
|
|
24
|
-
- **Event system** -- optional integration with `@nestjs/event-emitter` for audit and observability
|
|
25
|
-
- **OpenFeature adapter** -- optional boolean-only provider at `@nestarc/feature-flag/openfeature`
|
|
26
|
-
- **Testing utilities** -- drop-in `TestFeatureFlagModule` for unit and integration tests
|
|
12
|
+
## Contents
|
|
27
13
|
|
|
28
|
-
|
|
14
|
+
- [Installation and compatibility](#installation-and-compatibility)
|
|
15
|
+
- [Quickstart](#quickstart)
|
|
16
|
+
- [Evaluate a flag](#evaluate-a-flag)
|
|
17
|
+
- [How a flag resolves](#how-a-flag-resolves)
|
|
18
|
+
- [Target users and tenants](#target-users-and-tenants)
|
|
19
|
+
- [Manage flags](#manage-flags)
|
|
20
|
+
- [Caching, events, and integrations](#caching-events-and-integrations)
|
|
21
|
+
- [Documentation and examples](#documentation-and-examples)
|
|
22
|
+
- [For AI agents](#for-ai-agents)
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
npm install @nestarc/feature-flag
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Peer dependencies
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm install @nestjs/common @nestjs/core @prisma/client @prisma/adapter-pg pg class-transformer class-validator rxjs reflect-metadata
|
|
38
|
-
npm install --save-dev prisma
|
|
39
|
-
```
|
|
24
|
+
## Installation and compatibility
|
|
40
25
|
|
|
41
|
-
|
|
42
|
-
same runtime requirement.
|
|
43
|
-
|
|
44
|
-
### Optional
|
|
26
|
+
Install 0.6.0 in an existing NestJS application:
|
|
45
27
|
|
|
46
28
|
```bash
|
|
47
|
-
|
|
48
|
-
npm install @
|
|
49
|
-
|
|
50
|
-
# Required only if you use RedisCacheAdapter
|
|
51
|
-
npm install ioredis
|
|
52
|
-
|
|
53
|
-
# Required only if you use the OpenFeature adapter with the SDK
|
|
54
|
-
npm install @openfeature/server-sdk
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Redis Cache (Multi-Instance)
|
|
58
|
-
|
|
59
|
-
For production deployments with multiple instances, use `RedisCacheAdapter` for shared caching and real-time invalidation via Redis Pub/Sub:
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import { FeatureFlagModule, RedisCacheAdapter } from '@nestarc/feature-flag';
|
|
63
|
-
import { Redis } from 'ioredis';
|
|
64
|
-
|
|
65
|
-
const redisClient = new Redis({ host: 'localhost', port: 6379 });
|
|
66
|
-
|
|
67
|
-
FeatureFlagModule.forRoot({
|
|
68
|
-
environment: 'production',
|
|
69
|
-
prisma,
|
|
70
|
-
cacheAdapter: new RedisCacheAdapter({
|
|
71
|
-
client: redisClient,
|
|
72
|
-
// subscriber is auto-created via client.duplicate()
|
|
73
|
-
// keyPrefix: 'feature-flag:', // default
|
|
74
|
-
// channel: 'feature-flag:invalidate', // default
|
|
75
|
-
}),
|
|
76
|
-
})
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
When a flag is updated on any instance, all other instances are notified via Pub/Sub and invalidate their cache immediately — eliminating the stale-cache window.
|
|
80
|
-
|
|
81
|
-
## Prisma Schema
|
|
82
|
-
|
|
83
|
-
Prisma 7 keeps connection URLs in `prisma.config.ts` and requires a database
|
|
84
|
-
driver adapter at runtime. A minimal PostgreSQL setup is:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
// prisma.config.ts
|
|
88
|
-
import 'dotenv/config';
|
|
89
|
-
import { defineConfig, env } from 'prisma/config';
|
|
90
|
-
|
|
91
|
-
export default defineConfig({
|
|
92
|
-
schema: 'prisma/schema.prisma',
|
|
93
|
-
migrations: { path: 'prisma/migrations' },
|
|
94
|
-
datasource: { url: env('DATABASE_URL') },
|
|
95
|
-
});
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
```prisma
|
|
99
|
-
generator client {
|
|
100
|
-
provider = "prisma-client"
|
|
101
|
-
output = "../src/generated/prisma"
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
datasource db {
|
|
105
|
-
provider = "postgresql"
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Create the client with `@prisma/adapter-pg`, then pass that instance to
|
|
110
|
-
`FeatureFlagModule`:
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import { PrismaPg } from '@prisma/adapter-pg';
|
|
114
|
-
import { PrismaClient } from './generated/prisma/client';
|
|
115
|
-
|
|
116
|
-
const adapter = new PrismaPg({
|
|
117
|
-
connectionString: process.env.DATABASE_URL,
|
|
118
|
-
});
|
|
119
|
-
const prisma = new PrismaClient({ adapter });
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Add the following models to your `schema.prisma`:
|
|
123
|
-
|
|
124
|
-
```prisma
|
|
125
|
-
model FeatureFlag {
|
|
126
|
-
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
|
127
|
-
key String @unique
|
|
128
|
-
description String?
|
|
129
|
-
enabled Boolean @default(false)
|
|
130
|
-
percentage Int @default(0)
|
|
131
|
-
metadata Json @default("{}")
|
|
132
|
-
archivedAt DateTime? @map("archived_at") @db.Timestamptz()
|
|
133
|
-
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
|
134
|
-
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
|
135
|
-
|
|
136
|
-
overrides FeatureFlagOverride[]
|
|
137
|
-
|
|
138
|
-
@@map("feature_flags")
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
model FeatureFlagOverride {
|
|
142
|
-
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
|
143
|
-
flagId String @map("flag_id") @db.Uuid
|
|
144
|
-
attributes Json
|
|
145
|
-
priority Int @default(0)
|
|
146
|
-
enabled Boolean
|
|
147
|
-
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
|
148
|
-
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz()
|
|
149
|
-
|
|
150
|
-
flag FeatureFlag @relation(fields: [flagId], references: [id], onDelete: Cascade)
|
|
151
|
-
|
|
152
|
-
@@index([flagId], map: "idx_override_flag_id")
|
|
153
|
-
@@map("feature_flag_overrides")
|
|
154
|
-
}
|
|
29
|
+
npm install @nestarc/feature-flag@0.6.0
|
|
30
|
+
npm install @prisma/client@^7 @prisma/adapter-pg@^7 pg class-transformer@^0.5.1 class-validator@^0.15.0
|
|
31
|
+
npm install --save-dev prisma@^7
|
|
155
32
|
```
|
|
156
33
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
```sql
|
|
160
|
-
CREATE UNIQUE INDEX "uq_feature_flag_override_attributes"
|
|
161
|
-
ON "feature_flag_overrides"("flag_id", "attributes");
|
|
34
|
+
Keep the Prisma CLI, client, and PostgreSQL adapter on matching versions. The quickstart below uses a locally packed archive to verify this repository checkout.
|
|
162
35
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
36
|
+
| Requirement | Supported range / purpose |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| Node.js | `^20.19.0`, `^22.12.0`, or `>=24.0.0` |
|
|
39
|
+
| NestJS | `@nestjs/common` and `@nestjs/core` 10 or 11 |
|
|
40
|
+
| Prisma | `@prisma/client` 7; Prisma CLI and `@prisma/adapter-pg` for PostgreSQL setup |
|
|
41
|
+
| PostgreSQL | Default persistence backend; integration tests use PostgreSQL 16 |
|
|
42
|
+
| Other required peers | `class-transformer` 0.5, `class-validator` 0.14/0.15, `rxjs` 7, `reflect-metadata` 0.1/0.2 |
|
|
43
|
+
| Optional integrations | `ioredis` 5 for Redis; `@nestjs/event-emitter` for events; `@openfeature/server-sdk` ^1.23.0 for OpenFeature |
|
|
167
44
|
|
|
168
|
-
|
|
45
|
+
A standard Nest application already supplies NestJS, RxJS, reflection support, and an HTTP platform adapter. The [consumer guide](docs/usage.md#database-setup) contains the Prisma schema, SQL constraints, and generated-client setup.
|
|
169
46
|
|
|
170
|
-
|
|
47
|
+
## Quickstart
|
|
171
48
|
|
|
172
|
-
|
|
49
|
+
The [complete basic guard example](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/basic-guard/README.md) includes its own Prisma schema, migrations, seed data, and startup script. To run this checkout from the repository root, use Node.js from the supported range and a local PostgreSQL database. `npm run docker:up` starts the repository's PostgreSQL and Redis development services if you use Docker.
|
|
173
50
|
|
|
174
51
|
```bash
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
52
|
+
npm ci
|
|
53
|
+
npm run build
|
|
54
|
+
npm pack
|
|
55
|
+
npm run docker:up
|
|
56
|
+
cd examples/basic-guard
|
|
57
|
+
npm install ../../nestarc-feature-flag-0.6.0.tgz
|
|
58
|
+
export DATABASE_URL='postgresql://test:test@localhost:5499/feature_flag_test'
|
|
59
|
+
npm run prisma:generate
|
|
60
|
+
npm run db:migrate
|
|
61
|
+
npm run build
|
|
62
|
+
npm run seed -- on
|
|
63
|
+
npm start
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The tarball contains the local checkout, including any local changes. The database URL above is for the repository's development Compose service; substitute your own empty development database if needed. In another terminal:
|
|
185
67
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
```json
|
|
191
|
-
{ "tenantId": "tenant-1", "enabled": true }
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Use an `attributes` object instead:
|
|
68
|
+
```bash
|
|
69
|
+
curl -i http://127.0.0.1:3000/dashboard
|
|
70
|
+
# HTTP 200; {"message":"New dashboard is enabled"}
|
|
195
71
|
|
|
196
|
-
|
|
197
|
-
|
|
72
|
+
# From examples/basic-guard, with the same DATABASE_URL:
|
|
73
|
+
npm run seed -- off
|
|
74
|
+
curl -i http://127.0.0.1:3000/dashboard
|
|
75
|
+
# HTTP 403
|
|
198
76
|
```
|
|
199
77
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
### forRoot (synchronous)
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
import { FeatureFlagModule } from '@nestarc/feature-flag';
|
|
206
|
-
|
|
207
|
-
@Module({
|
|
208
|
-
imports: [
|
|
209
|
-
FeatureFlagModule.forRoot({
|
|
210
|
-
environment: 'production',
|
|
211
|
-
prisma: prismaService,
|
|
212
|
-
userIdExtractor: (req) => req.headers['x-user-id'] as string,
|
|
213
|
-
emitEvents: true,
|
|
214
|
-
cacheTtlMs: 30_000,
|
|
215
|
-
}),
|
|
216
|
-
],
|
|
217
|
-
})
|
|
218
|
-
export class AppModule {}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### forRootAsync (with useFactory)
|
|
78
|
+
The example disables caching so direct seed changes are visible on the next request. Its complete module registration is:
|
|
222
79
|
|
|
80
|
+
<!-- source: examples/basic-guard/src/app.module.ts -->
|
|
223
81
|
```typescript
|
|
82
|
+
import { Module } from '@nestjs/common';
|
|
224
83
|
import { FeatureFlagModule } from '@nestarc/feature-flag';
|
|
84
|
+
import { DashboardController } from './dashboard.controller';
|
|
85
|
+
import { PrismaModule } from './prisma.module';
|
|
86
|
+
import { PrismaService } from './prisma.service';
|
|
225
87
|
|
|
226
88
|
@Module({
|
|
227
89
|
imports: [
|
|
90
|
+
PrismaModule,
|
|
228
91
|
FeatureFlagModule.forRootAsync({
|
|
229
|
-
imports: [
|
|
230
|
-
inject: [
|
|
231
|
-
useFactory: (
|
|
232
|
-
environment: config.get('NODE_ENV'),
|
|
92
|
+
imports: [PrismaModule],
|
|
93
|
+
inject: [PrismaService],
|
|
94
|
+
useFactory: (prisma: PrismaService) => ({
|
|
233
95
|
prisma,
|
|
234
|
-
|
|
96
|
+
cacheTtlMs: 0, // Let local seed changes appear on the next request.
|
|
97
|
+
environment: process.env.NODE_ENV ?? 'development',
|
|
98
|
+
userIdExtractor: (req) => {
|
|
99
|
+
const userId = req.headers['x-user-id'];
|
|
100
|
+
return Array.isArray(userId) ? (userId[0] ?? null) : (userId ?? null);
|
|
101
|
+
},
|
|
235
102
|
}),
|
|
236
103
|
}),
|
|
237
104
|
],
|
|
105
|
+
controllers: [DashboardController],
|
|
238
106
|
})
|
|
239
107
|
export class AppModule {}
|
|
240
108
|
```
|
|
241
109
|
|
|
242
|
-
|
|
110
|
+
The imported [PrismaModule](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/basic-guard/src/prisma.module.ts), [PrismaService](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/basic-guard/src/prisma.service.ts), and [DashboardController](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/basic-guard/src/dashboard.controller.ts) are included in the example. `PrismaModule` exports `PrismaService`; including that module in `forRootAsync.imports` makes the service available to the factory. Events are disabled in this basic configuration.
|
|
243
111
|
|
|
244
|
-
|
|
245
|
-
@Injectable()
|
|
246
|
-
class FeatureFlagConfigService implements FeatureFlagModuleOptionsFactory {
|
|
247
|
-
constructor(
|
|
248
|
-
private readonly config: ConfigService,
|
|
249
|
-
private readonly prisma: PrismaService,
|
|
250
|
-
) {}
|
|
251
|
-
|
|
252
|
-
createFeatureFlagOptions() {
|
|
253
|
-
return {
|
|
254
|
-
environment: this.config.get('NODE_ENV'),
|
|
255
|
-
prisma: this.prisma,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
}
|
|
112
|
+
For an existing app, follow the [database setup and complete synchronous registration recipe](docs/usage.md#database-setup), or reuse the example's Prisma module with `forRootAsync`.
|
|
259
113
|
|
|
260
|
-
|
|
261
|
-
imports: [
|
|
262
|
-
FeatureFlagModule.forRootAsync({
|
|
263
|
-
imports: [ConfigModule, PrismaModule],
|
|
264
|
-
useClass: FeatureFlagConfigService,
|
|
265
|
-
}),
|
|
266
|
-
],
|
|
267
|
-
})
|
|
268
|
-
export class AppModule {}
|
|
269
|
-
```
|
|
114
|
+
## Evaluate a flag
|
|
270
115
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
@Module({
|
|
275
|
-
imports: [
|
|
276
|
-
FeatureFlagModule.forRootAsync({
|
|
277
|
-
useExisting: FeatureFlagConfigService,
|
|
278
|
-
}),
|
|
279
|
-
],
|
|
280
|
-
})
|
|
281
|
-
export class AppModule {}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## Feature Flag Guard
|
|
285
|
-
|
|
286
|
-
The `@FeatureFlag()` decorator automatically applies `UseGuards(FeatureFlagGuard)`, so you do not need to add `@UseGuards()` yourself.
|
|
287
|
-
|
|
288
|
-
### Method-level
|
|
116
|
+
`@FeatureFlag()` adds its guard automatically. A disabled flag returns HTTP 403 by default:
|
|
289
117
|
|
|
290
118
|
```typescript
|
|
119
|
+
import { Controller, Get } from '@nestjs/common';
|
|
291
120
|
import { FeatureFlag } from '@nestarc/feature-flag';
|
|
292
121
|
|
|
293
122
|
@Controller('dashboard')
|
|
294
123
|
export class DashboardController {
|
|
295
|
-
@FeatureFlag('NEW_DASHBOARD')
|
|
296
124
|
@Get()
|
|
125
|
+
@FeatureFlag('EXAMPLE_DASHBOARD')
|
|
297
126
|
getDashboard() {
|
|
298
127
|
return { message: 'Welcome to the new dashboard' };
|
|
299
128
|
}
|
|
300
129
|
}
|
|
301
130
|
```
|
|
302
131
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
```typescript
|
|
306
|
-
@FeatureFlag('BETA_API')
|
|
307
|
-
@Controller('beta')
|
|
308
|
-
export class BetaController {
|
|
309
|
-
@Get('feature-a')
|
|
310
|
-
featureA() { /* guarded */ }
|
|
311
|
-
|
|
312
|
-
@Get('feature-b')
|
|
313
|
-
featureB() { /* guarded */ }
|
|
314
|
-
}
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
### Custom status code and fallback
|
|
318
|
-
|
|
319
|
-
```typescript
|
|
320
|
-
@FeatureFlag('PREMIUM_FEATURE', {
|
|
321
|
-
statusCode: 402,
|
|
322
|
-
fallback: { message: 'Upgrade required' },
|
|
323
|
-
})
|
|
324
|
-
@Get('premium')
|
|
325
|
-
getPremiumContent() { ... }
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
When the flag is disabled, the guard responds with the given `statusCode` (default `403`) and optional `fallback` body.
|
|
329
|
-
|
|
330
|
-
Use `defaultValue` when a route should choose an invocation-specific fallback if a flag is missing or evaluation fails:
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
@FeatureFlag('OPTIONAL_PREVIEW', { defaultValue: true })
|
|
334
|
-
@Get('preview')
|
|
335
|
-
getPreview() { ... }
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Bypassing the guard
|
|
339
|
-
|
|
340
|
-
Use `@BypassFeatureFlag()` on methods that should always be accessible, even when a class-level flag is applied:
|
|
341
|
-
|
|
342
|
-
```typescript
|
|
343
|
-
import { BypassFeatureFlag } from '@nestarc/feature-flag';
|
|
344
|
-
|
|
345
|
-
@FeatureFlag('BETA_API')
|
|
346
|
-
@Controller('beta')
|
|
347
|
-
export class BetaController {
|
|
348
|
-
@Get('docs')
|
|
349
|
-
betaDocs() { /* guarded by BETA_API */ }
|
|
350
|
-
|
|
351
|
-
@BypassFeatureFlag()
|
|
352
|
-
@Get('health')
|
|
353
|
-
healthCheck() {
|
|
354
|
-
return { status: 'ok' };
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
## Programmatic Evaluation
|
|
360
|
-
|
|
361
|
-
Inject `FeatureFlagService` for service-layer checks outside the HTTP request cycle:
|
|
132
|
+
For service logic, inject `FeatureFlagService` and await the result:
|
|
362
133
|
|
|
363
134
|
```typescript
|
|
135
|
+
import { Injectable } from '@nestjs/common';
|
|
364
136
|
import { FeatureFlagService } from '@nestarc/feature-flag';
|
|
365
137
|
|
|
366
138
|
@Injectable()
|
|
367
|
-
export class
|
|
139
|
+
export class CheckoutService {
|
|
368
140
|
constructor(private readonly flags: FeatureFlagService) {}
|
|
369
141
|
|
|
370
|
-
async
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
if (useNewGateway) {
|
|
374
|
-
return this.newGateway.process(order);
|
|
375
|
-
}
|
|
376
|
-
return this.legacyGateway.process(order);
|
|
142
|
+
async checkoutVersion(tenantId: string): Promise<string> {
|
|
143
|
+
const enabled = await this.flags.isEnabled('NEW_CHECKOUT', { tenantId });
|
|
144
|
+
return enabled ? 'new' : 'classic';
|
|
377
145
|
}
|
|
378
146
|
}
|
|
379
147
|
```
|
|
380
148
|
|
|
381
|
-
|
|
149
|
+
Use `evaluateBoolean()` for the value and explanation (`source`, `reason`, `defaultUsed`, and optional bucket details). `evaluateAll()` returns the values of active, stored flags; it does not create entries for registry-only keys, emit evaluation/exposure events, or convert errors to defaults. See the [evaluation reference](docs/usage.md#evaluation-and-defaults).
|
|
382
150
|
|
|
383
|
-
|
|
384
|
-
const allFlags = await this.flags.evaluateAll();
|
|
385
|
-
// { NEW_DASHBOARD: true, PREMIUM_FEATURE: false, ... }
|
|
386
|
-
```
|
|
151
|
+
## How a flag resolves
|
|
387
152
|
|
|
388
|
-
|
|
153
|
+
Evaluation checks **archived status → matching override → percentage rollout → global `enabled`** in that order. In particular, `enabled: false` does not cancel overrides or a percentage rollout.
|
|
389
154
|
|
|
390
|
-
|
|
155
|
+
| Condition | Result |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| Archived | `false`, regardless of other settings |
|
|
158
|
+
| An attribute override matches | The winning override's `enabled` value |
|
|
159
|
+
| No override, `percentage: 100` | `true`, even without a user or tenant |
|
|
160
|
+
| No override, `percentage: 1–99`, usable bucket key | Whether the deterministic bucket is below the percentage; global `enabled` is ignored |
|
|
161
|
+
| No override, `percentage: 1–99`, no usable bucket key | Global `enabled` |
|
|
162
|
+
| No override, `percentage: 0` | Global `enabled` |
|
|
163
|
+
| Missing flag or individual evaluation error | Invocation default → module registry default → `defaultOnMissing` → `false` |
|
|
391
164
|
|
|
392
|
-
|
|
393
|
-
const enabled = await this.flags.isEnabled('MY_FLAG', {
|
|
394
|
-
userId: 'user-123',
|
|
395
|
-
tenantId: 'tenant-abc',
|
|
396
|
-
environment: 'staging',
|
|
397
|
-
});
|
|
398
|
-
```
|
|
165
|
+
For a gradual rollout, use `enabled: false` and a percentage between 1 and 99. To make an active flag false for everyone, set `enabled: false`, set `percentage: 0`, and remove any enabling overrides. Archiving also makes evaluation false, and removes the flag from active listings.
|
|
399
166
|
|
|
400
|
-
|
|
167
|
+
A non-empty `targetingKey` takes precedence for bucketing. Otherwise the evaluator uses the selected `bucketBy` attribute, then falls back to `userId ?? tenantId`. The [reference](docs/usage.md#percentage-bucketing) defines configuration precedence and missing-attribute behavior. Explicit `targetingKey` handling and consistent registry bucketing are fixed in 0.6.0.
|
|
401
168
|
|
|
402
|
-
|
|
403
|
-
// Evaluate as if no user is present, even within a request with x-user-id
|
|
404
|
-
const globalResult = await this.flags.isEnabled('MY_FLAG', { userId: null });
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
### Detailed boolean evaluation
|
|
169
|
+
## Target users and tenants
|
|
408
170
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
```typescript
|
|
412
|
-
const details = await this.flags.evaluateBoolean(
|
|
413
|
-
'NEW_CHECKOUT',
|
|
414
|
-
{ targetingKey: 'tenant-1', tenantId: 'tenant-1' },
|
|
415
|
-
{ defaultValue: false, trackExposure: true },
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
console.log(details);
|
|
419
|
-
// {
|
|
420
|
-
// flagKey: 'NEW_CHECKOUT',
|
|
421
|
-
// value: true,
|
|
422
|
-
// result: true,
|
|
423
|
-
// source: 'percentage',
|
|
424
|
-
// reason: 'PERCENTAGE_MATCH',
|
|
425
|
-
// defaultUsed: false,
|
|
426
|
-
// bucket: 17,
|
|
427
|
-
// targetingKey: 'tenant-1',
|
|
428
|
-
// evaluationTimeMs: 1
|
|
429
|
-
// }
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
Missing flags and evaluation errors return the selected default instead of throwing. Default priority is:
|
|
433
|
-
|
|
434
|
-
1. Invocation `defaultValue`
|
|
435
|
-
2. Registry `defaultValue`
|
|
436
|
-
3. Module `defaultOnMissing`
|
|
437
|
-
4. `false`
|
|
438
|
-
|
|
439
|
-
### Type-safe flag registry
|
|
440
|
-
|
|
441
|
-
```typescript
|
|
442
|
-
import { defineFlags, createFeatureFlagClient } from '@nestarc/feature-flag';
|
|
443
|
-
|
|
444
|
-
export const flags = defineFlags({
|
|
445
|
-
NEW_CHECKOUT: {
|
|
446
|
-
defaultValue: false,
|
|
447
|
-
bucketBy: 'tenantId',
|
|
448
|
-
trackExposure: true,
|
|
449
|
-
owner: 'payments',
|
|
450
|
-
tags: ['checkout'],
|
|
451
|
-
staleAt: '2026-09-01',
|
|
452
|
-
expiresAt: '2026-12-01',
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
const flagClient = createFeatureFlagClient(featureFlagService, flags);
|
|
457
|
-
const enabled = await flagClient.isEnabled('NEW_CHECKOUT', { tenantId: 'tenant-1' });
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
You can also pass the registry to `FeatureFlagModule.forRoot({ flags })` so service-level fallback and `bucketBy` defaults apply to direct `FeatureFlagService` calls.
|
|
461
|
-
|
|
462
|
-
### OpenFeature boolean adapter
|
|
463
|
-
|
|
464
|
-
The optional adapter lives on a separate subpath and delegates boolean resolution to `FeatureFlagService`:
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
import { createOpenFeatureBooleanProvider } from '@nestarc/feature-flag/openfeature';
|
|
468
|
-
|
|
469
|
-
const provider = createOpenFeatureBooleanProvider(featureFlagService);
|
|
470
|
-
const result = await provider.resolveBooleanEvaluation(
|
|
471
|
-
'NEW_CHECKOUT',
|
|
472
|
-
false,
|
|
473
|
-
{ targetingKey: 'tenant-1', tenantId: 'tenant-1', plan: 'pro' },
|
|
474
|
-
);
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
Only boolean evaluation is supported in v0.4.0. Variant flags and string/number/json remote config remain out of scope.
|
|
478
|
-
|
|
479
|
-
## Attribute Targeting
|
|
480
|
-
|
|
481
|
-
Overrides match exact attributes. Every key/value in an override's `attributes` object must exist in the evaluation context attributes for the override to apply.
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
const enabled = await this.flags.isEnabled('NEW_CHECKOUT', {
|
|
485
|
-
userId: 'user-123',
|
|
486
|
-
tenantId: 'tenant-1',
|
|
487
|
-
environment: 'production',
|
|
488
|
-
attributes: {
|
|
489
|
-
plan: 'pro',
|
|
490
|
-
country: 'KR',
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
Top-level `userId`, `tenantId`, and `environment` are merged into targeting attributes. If the same key also appears in `attributes`, the top-level value wins.
|
|
496
|
-
|
|
497
|
-
When multiple overrides match, the evaluator chooses the winner by:
|
|
498
|
-
|
|
499
|
-
1. More attributes
|
|
500
|
-
2. Higher `priority`
|
|
501
|
-
3. Earlier `createdAt`
|
|
502
|
-
4. Lower `id`
|
|
503
|
-
|
|
504
|
-
## Overrides
|
|
505
|
-
|
|
506
|
-
Set attribute-based overrides that take precedence over the global flag value:
|
|
171
|
+
Overrides use exact attribute matches. Every attribute in an override must match; string `"1"` and number `1` are different values.
|
|
507
172
|
|
|
508
173
|
```typescript
|
|
174
|
+
// `flags` is an injected FeatureFlagService.
|
|
509
175
|
await flags.setOverride('NEW_CHECKOUT', {
|
|
510
|
-
attributes: {
|
|
511
|
-
tenantId: 'tenant-1',
|
|
512
|
-
plan: 'pro',
|
|
513
|
-
country: 'KR',
|
|
514
|
-
},
|
|
176
|
+
attributes: { tenantId: 'tenant-acme', plan: 'pro' },
|
|
515
177
|
enabled: true,
|
|
516
178
|
priority: 10,
|
|
517
179
|
});
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
REST Admin API body:
|
|
521
|
-
|
|
522
|
-
```json
|
|
523
|
-
{
|
|
524
|
-
"attributes": {
|
|
525
|
-
"tenantId": "tenant-1",
|
|
526
|
-
"plan": "pro",
|
|
527
|
-
"country": "KR"
|
|
528
|
-
},
|
|
529
|
-
"enabled": true,
|
|
530
|
-
"priority": 10
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
## Events
|
|
535
|
-
|
|
536
|
-
Enable event emission to observe flag lifecycle changes. Requires installing `@nestjs/event-emitter`.
|
|
537
|
-
|
|
538
|
-
**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.
|
|
539
|
-
|
|
540
|
-
### Setup
|
|
541
|
-
|
|
542
|
-
```typescript
|
|
543
|
-
import { EventEmitterModule } from '@nestjs/event-emitter';
|
|
544
|
-
|
|
545
|
-
@Module({
|
|
546
|
-
imports: [
|
|
547
|
-
EventEmitterModule.forRoot(), // must be imported
|
|
548
|
-
FeatureFlagModule.forRoot({
|
|
549
|
-
environment: 'production',
|
|
550
|
-
prisma: prismaService,
|
|
551
|
-
emitEvents: true,
|
|
552
|
-
}),
|
|
553
|
-
],
|
|
554
|
-
})
|
|
555
|
-
export class AppModule {}
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### Event types
|
|
559
|
-
|
|
560
|
-
| Event constant | Event string | Payload type |
|
|
561
|
-
| ---------------------------------------- | ---------------------------------- | -------------------- |
|
|
562
|
-
| `FeatureFlagEvents.EVALUATED` | `feature-flag.evaluated` | `FlagEvaluatedEvent` |
|
|
563
|
-
| `FeatureFlagEvents.EXPOSED` | `feature-flag.exposed` | `FlagExposedEvent` |
|
|
564
|
-
| `FeatureFlagEvents.CREATED` | `feature-flag.created` | `FlagMutationEvent` |
|
|
565
|
-
| `FeatureFlagEvents.UPDATED` | `feature-flag.updated` | `FlagMutationEvent` |
|
|
566
|
-
| `FeatureFlagEvents.ARCHIVED` | `feature-flag.archived` | `FlagMutationEvent` |
|
|
567
|
-
| `FeatureFlagEvents.OVERRIDE_SET` | `feature-flag.override.set` | `FlagOverrideEvent` |
|
|
568
|
-
| `FeatureFlagEvents.OVERRIDE_REMOVED` | `feature-flag.override.removed` | `FlagOverrideEvent` |
|
|
569
|
-
| `FeatureFlagEvents.CACHE_INVALIDATED` | `feature-flag.cache.invalidated` | `{}` |
|
|
570
|
-
|
|
571
|
-
### Listening to events
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
import { OnEvent } from '@nestjs/event-emitter';
|
|
575
|
-
import { FeatureFlagEvents, FlagEvaluatedEvent } from '@nestarc/feature-flag';
|
|
576
|
-
|
|
577
|
-
@Injectable()
|
|
578
|
-
export class FlagAuditListener {
|
|
579
|
-
@OnEvent(FeatureFlagEvents.EVALUATED)
|
|
580
|
-
handleEvaluation(event: FlagEvaluatedEvent) {
|
|
581
|
-
console.log(`Flag ${event.flagKey} = ${event.result} (${event.reason})`);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
Exposure events are opt-in per call, registry entry, or flag metadata via `trackExposure`. They do not persist analytics; attach your own listener if you need sampling, batching, or storage.
|
|
587
|
-
|
|
588
|
-
## Testing
|
|
589
|
-
|
|
590
|
-
Import `TestFeatureFlagModule` from the `/testing` subpath to stub flag values in tests without a database connection:
|
|
591
|
-
|
|
592
|
-
```typescript
|
|
593
|
-
import { TestFeatureFlagModule } from '@nestarc/feature-flag/testing';
|
|
594
|
-
|
|
595
|
-
describe('DashboardController', () => {
|
|
596
|
-
let app: INestApplication;
|
|
597
|
-
|
|
598
|
-
beforeEach(async () => {
|
|
599
|
-
const module = await Test.createTestingModule({
|
|
600
|
-
imports: [
|
|
601
|
-
TestFeatureFlagModule.register({
|
|
602
|
-
NEW_DASHBOARD: true,
|
|
603
|
-
PREMIUM_FEATURE: false,
|
|
604
|
-
}),
|
|
605
|
-
],
|
|
606
|
-
controllers: [DashboardController],
|
|
607
|
-
}).compile();
|
|
608
|
-
|
|
609
|
-
app = module.createNestApplication();
|
|
610
|
-
await app.init();
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('should allow access when flag is enabled', () => {
|
|
614
|
-
return request(app.getHttpServer())
|
|
615
|
-
.get('/dashboard')
|
|
616
|
-
.expect(200);
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
`TestFeatureFlagModule.register()` provides a global mock of `FeatureFlagService`:
|
|
622
|
-
- `isEnabled(key)` returns the boolean you specified (defaulting to `false` for unregistered keys)
|
|
623
|
-
- `evaluateBoolean(key)` returns `BooleanEvaluationDetails`
|
|
624
|
-
- `evaluateAll()` returns the full flag map
|
|
625
|
-
- `create()`, `update()`, `archive()`, `findByKey()`, `findAll()` return full `FeatureFlagWithOverrides` stub objects
|
|
626
|
-
- `findByKey()` throws `NotFoundException` for unknown keys
|
|
627
|
-
|
|
628
|
-
For registry-based tests, use `registerRegistry()` and the injected controller:
|
|
629
|
-
|
|
630
|
-
```typescript
|
|
631
|
-
import {
|
|
632
|
-
TestFeatureFlagController,
|
|
633
|
-
TestFeatureFlagModule,
|
|
634
|
-
} from '@nestarc/feature-flag/testing';
|
|
635
|
-
|
|
636
|
-
const module = await Test.createTestingModule({
|
|
637
|
-
imports: [TestFeatureFlagModule.registerRegistry(flags)],
|
|
638
|
-
}).compile();
|
|
639
|
-
|
|
640
|
-
const testFlags = module.get(TestFeatureFlagController);
|
|
641
|
-
testFlags.set('NEW_CHECKOUT', true);
|
|
642
|
-
testFlags.reset();
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
The testing controller keeps state inside the compiled testing module. CRUD-style write methods on the mocked service still return stub objects and do not persist database rows.
|
|
646
|
-
|
|
647
|
-
## Evaluation Priority
|
|
648
|
-
|
|
649
|
-
When `isEnabled()` is called, flags are evaluated through the current cascade. The first matching layer wins:
|
|
650
|
-
|
|
651
|
-
| Priority | Layer | Description |
|
|
652
|
-
| -------- | ---------------------- | ------------------------------------------------------------------ |
|
|
653
|
-
| 1 | **Archived** | If the flag has `archivedAt` set, evaluation always returns `false` |
|
|
654
|
-
| 2 | **Attribute override** | Best override whose attributes are all present in the evaluation context |
|
|
655
|
-
| 3 | **Percentage rollout** | Deterministic hash of `flagKey + targetingKey` mod 100 |
|
|
656
|
-
| 4 | **Global default** | The flag's `enabled` field |
|
|
657
|
-
|
|
658
|
-
Percentage rollout uses murmurhash3 for deterministic bucketing. The targeting key is resolved in this order: explicit `context.targetingKey`, registry or metadata `bucketBy`, then the legacy `userId ?? tenantId` fallback.
|
|
659
|
-
|
|
660
|
-
## Configuration Reference
|
|
661
|
-
|
|
662
|
-
### FeatureFlagModuleOptions
|
|
663
|
-
|
|
664
|
-
| Option | Type | Default | Description |
|
|
665
|
-
| ------------------- | --------------------------------- | --------- | --------------------------------------------------------------- |
|
|
666
|
-
| `environment` | `string` | *required*| Deployment environment (e.g. `'production'`, `'staging'`) |
|
|
667
|
-
| `cacheTtlMs` | `number` | `30000` | Cache TTL in ms. Set to `0` to disable caching |
|
|
668
|
-
| `userIdExtractor` | `(req: Request) => string \| null`| `undefined`| Extracts user ID from the incoming request |
|
|
669
|
-
| `defaultOnMissing` | `boolean` | `false` | Value returned when a flag key does not exist in the database |
|
|
670
|
-
| `emitEvents` | `boolean` | `false` | Emit lifecycle events via `@nestjs/event-emitter` |
|
|
671
|
-
| `cacheAdapter` | `CacheAdapter` | `MemoryCacheAdapter` | Pluggable cache backend (e.g. `RedisCacheAdapter`) |
|
|
672
|
-
| `flags` | `FlagRegistry` | `undefined` | Optional typed registry for defaults, `bucketBy`, and exposure settings |
|
|
673
|
-
|
|
674
|
-
### FeatureFlagModuleRootOptions
|
|
675
180
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
| ------- | ----- | ------------------------------ |
|
|
680
|
-
| `prisma`| `any` | Prisma client instance |
|
|
681
|
-
|
|
682
|
-
## CRUD Operations
|
|
683
|
-
|
|
684
|
-
`FeatureFlagService` also exposes methods for managing flags programmatically:
|
|
685
|
-
|
|
686
|
-
```typescript
|
|
687
|
-
// Create a flag
|
|
688
|
-
const flag = await this.flags.create({
|
|
689
|
-
key: 'NEW_FEATURE',
|
|
690
|
-
description: 'Enables the new feature',
|
|
691
|
-
enabled: false,
|
|
692
|
-
percentage: 0,
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// Update a flag
|
|
696
|
-
await this.flags.update('NEW_FEATURE', {
|
|
697
|
-
enabled: true,
|
|
698
|
-
percentage: 50,
|
|
181
|
+
const enabled = await flags.isEnabled('NEW_CHECKOUT', {
|
|
182
|
+
tenantId: 'tenant-acme',
|
|
183
|
+
attributes: { plan: 'pro' },
|
|
699
184
|
});
|
|
700
|
-
|
|
701
|
-
// Archive a flag (soft delete -- evaluations return false)
|
|
702
|
-
await this.flags.archive('OLD_FEATURE');
|
|
703
|
-
|
|
704
|
-
// List all active (non-archived) flags
|
|
705
|
-
const allFlags = await this.flags.findAll();
|
|
706
|
-
|
|
707
|
-
// Manually invalidate the cache
|
|
708
|
-
this.flags.invalidateCache();
|
|
709
185
|
```
|
|
710
186
|
|
|
711
|
-
|
|
187
|
+
If several overrides match, the winner has more attributes, then higher priority, then earlier creation time, then the lower ID. Top-level `userId`, `tenantId`, and `environment` become targeting attributes and take precedence over same-named entries in `attributes`. Explicit `null` suppresses the corresponding ambient value; it remains a `null` targeting attribute. A provided `tenantId` works without a tenancy package.
|
|
712
188
|
|
|
713
|
-
|
|
189
|
+
The [registry guide](docs/usage.md#typed-registry) covers typed keys and defaults. Registry entries describe evaluation behavior; they do not create database flags or automatically archive expired flags.
|
|
714
190
|
|
|
715
|
-
|
|
716
|
-
import { FeatureFlagAdminModule } from '@nestarc/feature-flag';
|
|
717
|
-
import { AdminAuthGuard } from './guards/admin-auth.guard';
|
|
718
|
-
|
|
719
|
-
@Module({
|
|
720
|
-
imports: [
|
|
721
|
-
FeatureFlagModule.forRoot({ ... }),
|
|
722
|
-
FeatureFlagAdminModule.register({
|
|
723
|
-
guard: AdminAuthGuard,
|
|
724
|
-
// path: 'feature-flags', // default
|
|
725
|
-
}),
|
|
726
|
-
],
|
|
727
|
-
})
|
|
728
|
-
export class AppModule {}
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
### Endpoints
|
|
732
|
-
|
|
733
|
-
| Method | Route | Description | Error Responses |
|
|
734
|
-
|--------|-------|-------------|-----------------|
|
|
735
|
-
| POST | `/feature-flags` | Create a flag | 409 duplicate key, 400 invalid percentage |
|
|
736
|
-
| GET | `/feature-flags` | List all flags | |
|
|
737
|
-
| GET | `/feature-flags/:key` | Get a single flag | 404 not found |
|
|
738
|
-
| PATCH | `/feature-flags/:key` | Update a flag | 404 not found, 400 invalid percentage |
|
|
739
|
-
| DELETE | `/feature-flags/:key` | Archive a flag | 404 not found |
|
|
740
|
-
| POST | `/feature-flags/:key/evaluate` | Evaluate a flag without mutating it | |
|
|
741
|
-
| POST | `/feature-flags/:key/overrides` | Set an override | 404 flag not found |
|
|
742
|
-
| DELETE | `/feature-flags/:key/overrides` | Remove an override | 404 flag not found |
|
|
743
|
-
|
|
744
|
-
Percentage values must be between 0 and 100 (inclusive). Invalid values return 400 Bad Request.
|
|
745
|
-
|
|
746
|
-
## Custom Persistence (Advanced)
|
|
747
|
-
|
|
748
|
-
The default `PrismaFeatureFlagRepository` can be replaced with any implementation of `FeatureFlagRepository`:
|
|
191
|
+
## Manage flags
|
|
749
192
|
|
|
750
193
|
```typescript
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
@Module({
|
|
758
|
-
imports: [
|
|
759
|
-
FeatureFlagModule.forRoot({
|
|
760
|
-
environment: 'production',
|
|
761
|
-
prisma, // still required for module init, but unused if you override the repository
|
|
762
|
-
}),
|
|
763
|
-
],
|
|
764
|
-
providers: [
|
|
765
|
-
{
|
|
766
|
-
provide: FEATURE_FLAG_REPOSITORY,
|
|
767
|
-
useClass: MyCustomRepository, // implements FeatureFlagRepository
|
|
768
|
-
},
|
|
769
|
-
],
|
|
770
|
-
})
|
|
771
|
-
export class AppModule {}
|
|
194
|
+
// `flags` is an injected FeatureFlagService.
|
|
195
|
+
await flags.create({ key: 'NEW_CHECKOUT', enabled: false, percentage: 0 });
|
|
196
|
+
await flags.update('NEW_CHECKOUT', { percentage: 20 });
|
|
197
|
+
const activeFlags = await flags.findAll();
|
|
198
|
+
await flags.archive('OLD_CHECKOUT');
|
|
199
|
+
await flags.invalidateCache();
|
|
772
200
|
```
|
|
773
201
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
Override the default `@nestarc/tenancy` integration with your own `TenantContextProvider`:
|
|
202
|
+
The optional [Admin REST API](docs/usage.md#admin-rest-api) exposes creation, updates, active listings, evaluation, and overrides. Registration requires an application-supplied authentication/authorization guard. Request bodies, response examples, status codes, and errors are documented in the guide.
|
|
777
203
|
|
|
778
|
-
|
|
779
|
-
import {
|
|
780
|
-
FeatureFlagModule,
|
|
781
|
-
TENANT_CONTEXT_PROVIDER,
|
|
782
|
-
TenantContextProvider,
|
|
783
|
-
} from '@nestarc/feature-flag';
|
|
784
|
-
|
|
785
|
-
@Injectable()
|
|
786
|
-
class MyTenantProvider implements TenantContextProvider {
|
|
787
|
-
getCurrentTenantId(): string | null {
|
|
788
|
-
// your custom tenant resolution logic
|
|
789
|
-
return 'tenant-from-custom-source';
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
@Module({
|
|
794
|
-
imports: [FeatureFlagModule.forRoot({ ... })],
|
|
795
|
-
providers: [
|
|
796
|
-
{ provide: TENANT_CONTEXT_PROVIDER, useClass: MyTenantProvider },
|
|
797
|
-
],
|
|
798
|
-
})
|
|
799
|
-
export class AppModule {}
|
|
800
|
-
```
|
|
204
|
+
## Caching, events, and integrations
|
|
801
205
|
|
|
802
|
-
|
|
206
|
+
The default cache is in memory with a 30,000 ms TTL. Set `cacheTtlMs: 0` to disable writes to the cache, or use Redis for shared storage and Pub/Sub invalidation across instances. Mutation invalidation is best effort: a database write can succeed while cache invalidation fails. TTL expiry limits how long an existing stale entry remains; concurrent reads and failures mean this is not an immediate-consistency guarantee. Choose TTL based on your acceptable staleness and measured workload.
|
|
803
207
|
|
|
804
|
-
- [
|
|
805
|
-
- [
|
|
806
|
-
- [
|
|
208
|
+
- [Redis and cache lifecycle](docs/usage.md#caching): adapter setup, invalidation, and connection ownership.
|
|
209
|
+
- [Events](docs/usage.md#events): import `EventEmitterModule.forRoot()` **and** set `emitEvents: true`; exposure tracking also needs an opt-in setting.
|
|
210
|
+
- [Custom persistence and tenancy](docs/usage.md#custom-persistence-and-tenancy): use module options and Nest factories; these options are new in 0.6.0.
|
|
211
|
+
- [OpenFeature](docs/usage.md#openfeature): boolean evaluation through the optional SDK integration; no string, numeric, or object flag values.
|
|
212
|
+
- [Testing utilities](docs/usage.md#testing): `/testing` provides controlled boolean stubs; targeting behavior should be tested with the actual evaluator.
|
|
807
213
|
|
|
808
|
-
##
|
|
214
|
+
## Documentation and examples
|
|
809
215
|
|
|
810
|
-
|
|
216
|
+
- [Consumer guide](docs/usage.md): schema, complete registration, API semantics, integrations, and troubleshooting.
|
|
217
|
+
- [Basic route guard](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/basic-guard/README.md), [tenant and plan targeting](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/multi-tenant-targeting/README.md), and [Redis with events](https://github.com/nestarc/nestjs-feature-flag/blob/main/examples/redis-events/README.md): runnable applications with seed data and expected HTTP results.
|
|
218
|
+
- [Changelog and 0.5 upgrade notes](CHANGELOG.md): Prisma 7 adapter, generated import path, and configuration changes.
|
|
219
|
+
- [Benchmark method](https://github.com/nestarc/nestjs-feature-flag/blob/main/benchmarks/README.md): reproducible commands and measurement limits; latency depends on your workload and environment.
|
|
220
|
+
- [Documentation index](https://github.com/nestarc/nestjs-feature-flag/blob/main/docs/README.md): current guides versus historical designs and validation reports.
|
|
221
|
+
- [Website](https://nestarc.dev/packages/feature-flag/) and [issue tracker](https://github.com/nestarc/nestjs-feature-flag/issues).
|
|
811
222
|
|
|
812
|
-
|
|
813
|
-
|----------|-----|-----|-----|-----|
|
|
814
|
-
| **isEnabled() — cache hit** | **0.04ms** | **0.04ms** | **0.05ms** | **0.12ms** |
|
|
815
|
-
| isEnabled() — cache miss (DB lookup) | 1.17ms | 1.10ms | 1.62ms | 2.04ms |
|
|
816
|
-
| isEnabled() — override cascade (cold) | 0.95ms | 0.89ms | 1.36ms | 1.87ms |
|
|
817
|
-
| **evaluateAll() — 50 flags (mixed)** | **0.19ms** | **0.04ms** | **1.47ms** | **1.78ms** |
|
|
223
|
+
## For AI agents
|
|
818
224
|
|
|
819
|
-
|
|
225
|
+
Check the installed package version and its `.d.ts` exports first, then use [docs/usage.md](docs/usage.md) for consumer implementation. It identifies the 0.6.0 APIs, version boundaries, prerequisites, context/default semantics, and executable examples. The guide and changelog are included in the package so installed-package workflows can read them without relying on search results. Use the documented `/testing` and `/openfeature` entry points instead of importing internal `dist` paths.
|
|
820
226
|
|
|
821
|
-
|
|
227
|
+
For changes to this repository, follow [AGENTS.md](https://github.com/nestarc/nestjs-feature-flag/blob/main/AGENTS.md). Historical design documents are not the current API contract.
|
|
822
228
|
|
|
823
229
|
## License
|
|
824
230
|
|
|
825
|
-
MIT
|
|
231
|
+
[MIT](LICENSE)
|