Skip to content

Commit cb3b054

Browse files
committed
feat: allow user defined context data (#199)
1 parent 94cb32c commit cb3b054

11 files changed

+181
-46
lines changed

README.md

+47-6
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616

1717
# Table of contents <!-- omit in toc -->
1818

19-
- [Usage](#usage)
19+
- [Setup](#setup)
2020
- [Synchronous configuration](#synchronous-configuration)
2121
- [Asynchronous configuration](#asynchronous-configuration)
22-
- [Usage in controllers or providers](#usage-in-controllers-or-providers)
22+
- [Usage in controllers or providers](#usage-in-controllers-or-providers)
23+
- [Custom context](#custom-context)
2324
- [Configuration](#configuration)
2425
- [Default strategies](#default-strategies)
2526
- [Custom strategies](#custom-strategies)
2627
- [License](#license)
2728

28-
# Usage
29+
# Setup
2930

3031
```sh
3132
$ npm install --save nestjs-unleash
@@ -35,7 +36,7 @@ Import the module with `UnleashModule.forRoot(...)` or `UnleashModule.forRootAsy
3536

3637
## Synchronous configuration
3738

38-
Use `UnleashModule.forRoot()`. Available ptions are described in the [UnleashModuleOptions interface](#configuration).
39+
Use `UnleashModule.forRoot()`. Available options are described in the [UnleashModuleOptions interface](#configuration).
3940

4041
```ts
4142
@Module({
@@ -52,7 +53,7 @@ export class MyModule {}
5253

5354
## Asynchronous configuration
5455

55-
If you want to use retrieve you [Unleash options](#configuration) dynamically, use `UnleashModule.forRootAsync()`. Use `useFactory` and `inject` to import your dependencies. Example using the `ConfigService`:
56+
If you want to use your [Unleash options](#configuration) dynamically, use `UnleashModule.forRootAsync()`. Use `useFactory` and `inject` to import your dependencies. Example using the `ConfigService`:
5657

5758
```ts
5859
@Module({
@@ -72,7 +73,7 @@ If you want to use retrieve you [Unleash options](#configuration) dynamically, u
7273
export class MyModule {}
7374
```
7475

75-
## Usage in controllers or providers
76+
# Usage in controllers or providers
7677

7778
In your controller use the `UnleashService` or the `@IfEnabled(...)` route decorator:
7879

@@ -101,6 +102,46 @@ export class AppController {
101102
}
102103
```
103104

105+
## Custom context
106+
107+
The `UnleashContext` grants access to request related information like user ID or IP address.
108+
109+
In addition, the context can be dynamically enriched with further information and subsequently used in a separate strategy:
110+
111+
```ts
112+
export interface MyCustomData {
113+
foo: string;
114+
bar: number;
115+
}
116+
117+
@Injectable()
118+
class SomeProvider {
119+
constructor(private readonly unleash: UnleashService<MyCustomData>) {}
120+
121+
someMethod() {
122+
return this.unleash.isEnabled("someToggleName", undefined, {
123+
foo: "bar",
124+
bar: 123,
125+
})
126+
? "feature is active"
127+
: "feature is not active";
128+
}
129+
}
130+
131+
// Custom strategy with custom data:
132+
@Injectable()
133+
export class MyCustomStrategy implements UnleashStrategy {
134+
name = "MyCustomStrategy";
135+
136+
isEnabled(
137+
_parameters: unknown,
138+
context: UnleashContext<MyCustomData>
139+
): boolean {
140+
return context.customData?.foo === "bar";
141+
}
142+
}
143+
```
144+
104145
## Configuration
105146

106147
NestJS-Unleash can be configured with the following options:

e2e/docker-compose.yml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# https://github.com./Unleash/unleash-docker/blob/master/docker-compose.yml
2+
# admin / unleash4all
3+
version: "3.4"
4+
services:
5+
web:
6+
image: unleashorg/unleash-server
7+
ports:
8+
- "4242:4242"
9+
environment:
10+
DATABASE_URL: "postgres://postgres:unleash@db/postgres"
11+
DATABASE_SSL: "false"
12+
depends_on:
13+
- db
14+
command: npm run start
15+
healthcheck:
16+
test: ["CMD", "nc", "-z", "db", "5432"]
17+
interval: 1s
18+
timeout: 1m
19+
retries: 5
20+
start_period: 15s
21+
db:
22+
expose:
23+
- "5432"
24+
image: postgres:10-alpine
25+
environment:
26+
POSTGRES_DB: "db"
27+
POSTGRES_HOST_AUTH_METHOD: "trust"
28+
healthcheck:
29+
test:
30+
[
31+
"CMD",
32+
"pg_isready",
33+
"--username=postgres",
34+
"--host=127.0.0.1",
35+
"--port=5432",
36+
]
37+
interval: 2s
38+
timeout: 1m
39+
retries: 5
40+
start_period: 10s

e2e/specifications.e2e-spec.ts

+4-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
GradualRolloutRandomStrategy,
2020
GradualRolloutSessionIdStrategy,
2121
RemoteAddressStrategy,
22-
UnleashContext,
2322
UnleashStrategiesService,
2423
UserWithIdStrategy,
2524
} from '../src'
@@ -28,8 +27,11 @@ import { CUSTOM_STRATEGIES } from '../src/unleash-strategies/unleash-strategies.
2827
import { ToggleEntity } from '../src/unleash/entity/toggle.entity'
2928
import { MetricsService } from '../src/unleash/metrics.service'
3029
import { ToggleRepository } from '../src/unleash/repository/toggle-repository'
30+
import { UnleashContext } from '../src/unleash/unleash.context'
3131
import { UnleashService } from '../src/unleash/unleash.service'
3232

33+
jest.mock('../src/unleash/unleash.context')
34+
3335
// 09-strategy-constraints.json is an enterprise feature. can't test.
3436
const testSuite = [s1, s2, s3, s4, s5, s6, s7, s10]
3537

@@ -49,14 +51,7 @@ describe('Specification test', () => {
4951
UnleashService,
5052
{ provide: CUSTOM_STRATEGIES, useValue: [] },
5153
{ provide: MetricsService, useValue: { increase: jest.fn() } },
52-
{
53-
provide: UnleashContext,
54-
useValue: {
55-
getRemoteAddress: jest.fn(),
56-
getSessionId: jest.fn(),
57-
getUserId: jest.fn(),
58-
},
59-
},
54+
UnleashContext,
6055
ApplicationHostnameStrategy,
6156
DefaultStrategy,
6257
FlexibleRolloutStrategy,

e2e/src/app.controller.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { Controller, Get, UseGuards } from '@nestjs/common'
1+
import { Controller, Get, Param, UseGuards } from '@nestjs/common'
22
import { IfEnabled } from '../../src/unleash'
33
import { UnleashService } from '../../src/unleash/unleash.service'
44
import { UserGuard } from './user.guard'
55

6+
export interface MyCustomData {
7+
foo: string
8+
}
9+
610
@Controller()
711
@UseGuards(UserGuard)
812
export class AppController {
9-
constructor(private readonly unleash: UnleashService) {}
13+
constructor(private readonly unleash: UnleashService<MyCustomData>) {}
1014

1115
@Get('/')
1216
index(): string {
@@ -20,4 +24,12 @@ export class AppController {
2024
getContent(): string {
2125
return 'my content'
2226
}
27+
28+
@Get('/custom-context/:foo')
29+
customContext(@Param('foo') foo: string): string {
30+
// Provide "foo" as custom context data
31+
return this.unleash.isEnabled('test', undefined, { foo })
32+
? 'feature is active'
33+
: 'feature is not active'
34+
}
2335
}

e2e/src/app.module.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ import { UsersService } from './users.service'
1919
UnleashModule.forRootAsync({
2020
useFactory: () => ({
2121
// disableRegistration: true,
22-
// url: 'http://127.0.0.1:3000/unleash',
23-
url: 'https://unleash.herokuapp.com/api/client',
22+
url: 'http://localhost:4242/api/client',
2423
appName: 'my-app-name',
2524
instanceId: 'my-unique-instance', //process.pid.toString(),
2625
refreshInterval: 20_000,
27-
// metricsInterval: 3000,
28-
// strategies: [MyCustomStrategy],
26+
metricsInterval: 3000,
27+
strategies: [MyCustomStrategy],
2928
http: {
3029
headers: {
30+
Authorization:
31+
'8b2d15c99270b809d47eef3bc7d8988059d7215adafa4c5175e2f4fe7b387f60',
3132
'X-Foo': 'bar',
3233
},
3334
},

e2e/src/my-custom-strategy.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { Injectable } from '@nestjs/common'
2-
import { UnleashStrategy } from '../../src'
2+
import { UnleashContext, UnleashStrategy } from '../../src'
3+
import { MyCustomData } from './app.controller'
34

45
@Injectable()
56
export class MyCustomStrategy implements UnleashStrategy {
67
name = 'MyCustomStrategy'
78

8-
isEnabled(_parameters: unknown): boolean {
9-
// eslint-disable-next-line no-magic-numbers
10-
return Math.random() < 0.5
9+
isEnabled(
10+
_parameters: unknown,
11+
context: UnleashContext<MyCustomData>,
12+
): boolean {
13+
return context.customData?.foo === 'bar'
1114
}
1215
}

src/unleash-strategies/strategy/strategy.interface.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { UnleashContext } from '../../unleash'
22

3-
export interface UnleashStrategy<T = unknown> {
3+
export interface UnleashStrategy<T = unknown, U = unknown> {
44
/**
55
* Must match the name you used to create the strategy in your Unleash
66
* server UI
@@ -13,5 +13,5 @@ export interface UnleashStrategy<T = unknown> {
1313
* @param parameters Custom paramemters as configured in Unleash server UI
1414
* @param context applicaton/request context, i.e. UserID
1515
*/
16-
isEnabled(parameters: T, context: UnleashContext): boolean
16+
isEnabled(parameters: T, context: UnleashContext<U>): boolean
1717
}

src/unleash/unleash.context.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ function createRequest(
88
return data
99
}
1010

11+
interface MyCustomData {
12+
foo: boolean
13+
bar: string
14+
}
15+
1116
describe('UnleashContext', () => {
1217
let context: UnleashContext
1318
let req: Request<{
@@ -50,4 +55,15 @@ describe('UnleashContext', () => {
5055
context.request = { hello: 'world' }
5156
expect(context.getRequest()).toStrictEqual({ hello: 'world' })
5257
})
58+
59+
describe('Custom data', () => {
60+
test('extend()', () => {
61+
const context = new UnleashContext<MyCustomData>(
62+
req,
63+
{} as UnleashModuleOptions,
64+
)
65+
const extendedContext = context.extend({ foo: true, bar: 'baz' })
66+
expect(extendedContext.customData).toEqual({ foo: true, bar: 'baz' })
67+
})
68+
})
5369
})

src/unleash/unleash.context.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ const defaultUserIdFactory = (request: Request<{ id: string }>) => {
99
}
1010

1111
@Injectable({ scope: Scope.REQUEST })
12-
export class UnleashContext {
12+
export class UnleashContext<TCustomData = unknown> {
13+
#customData?: TCustomData
14+
1315
constructor(
1416
@Inject(REQUEST) private request: Request<{ id: string }>,
1517
@Inject(UNLEASH_MODULE_OPTIONS)
@@ -35,4 +37,14 @@ export class UnleashContext {
3537
getRequest<T = Request<{ id: string }>>(): T {
3638
return this.request as T
3739
}
40+
41+
get customData(): TCustomData | undefined {
42+
return this.#customData
43+
}
44+
45+
extend(customData: TCustomData | undefined): UnleashContext<TCustomData> {
46+
this.#customData = customData
47+
48+
return this
49+
}
3850
}

0 commit comments

Comments
 (0)