Skip to content

Commit

Permalink
feat: allow user defined context data (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmb0 committed Oct 18, 2021
1 parent 94cb32c commit cb3b054
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 46 deletions.
53 changes: 47 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@

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

- [Usage](#usage)
- [Setup](#setup)
- [Synchronous configuration](#synchronous-configuration)
- [Asynchronous configuration](#asynchronous-configuration)
- [Usage in controllers or providers](#usage-in-controllers-or-providers)
- [Usage in controllers or providers](#usage-in-controllers-or-providers)
- [Custom context](#custom-context)
- [Configuration](#configuration)
- [Default strategies](#default-strategies)
- [Custom strategies](#custom-strategies)
- [License](#license)

# Usage
# Setup

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

## Synchronous configuration

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

```ts
@Module({
Expand All @@ -52,7 +53,7 @@ export class MyModule {}

## Asynchronous configuration

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`:
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`:

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

## Usage in controllers or providers
# Usage in controllers or providers

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

Expand Down Expand Up @@ -101,6 +102,46 @@ export class AppController {
}
```

## Custom context

The `UnleashContext` grants access to request related information like user ID or IP address.

In addition, the context can be dynamically enriched with further information and subsequently used in a separate strategy:

```ts
export interface MyCustomData {
foo: string;
bar: number;
}

@Injectable()
class SomeProvider {
constructor(private readonly unleash: UnleashService<MyCustomData>) {}

someMethod() {
return this.unleash.isEnabled("someToggleName", undefined, {
foo: "bar",
bar: 123,
})
? "feature is active"
: "feature is not active";
}
}

// Custom strategy with custom data:
@Injectable()
export class MyCustomStrategy implements UnleashStrategy {
name = "MyCustomStrategy";

isEnabled(
_parameters: unknown,
context: UnleashContext<MyCustomData>
): boolean {
return context.customData?.foo === "bar";
}
}
```

## Configuration

NestJS-Unleash can be configured with the following options:
Expand Down
40 changes: 40 additions & 0 deletions e2e/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# https://github.com/Unleash/unleash-docker/blob/master/docker-compose.yml
# admin / unleash4all
version: "3.4"
services:
web:
image: unleashorg/unleash-server
ports:
- "4242:4242"
environment:
DATABASE_URL: "postgres://postgres:unleash@db/postgres"
DATABASE_SSL: "false"
depends_on:
- db
command: npm run start
healthcheck:
test: ["CMD", "nc", "-z", "db", "5432"]
interval: 1s
timeout: 1m
retries: 5
start_period: 15s
db:
expose:
- "5432"
image: postgres:10-alpine
environment:
POSTGRES_DB: "db"
POSTGRES_HOST_AUTH_METHOD: "trust"
healthcheck:
test:
[
"CMD",
"pg_isready",
"--username=postgres",
"--host=127.0.0.1",
"--port=5432",
]
interval: 2s
timeout: 1m
retries: 5
start_period: 10s
13 changes: 4 additions & 9 deletions e2e/specifications.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
GradualRolloutRandomStrategy,
GradualRolloutSessionIdStrategy,
RemoteAddressStrategy,
UnleashContext,
UnleashStrategiesService,
UserWithIdStrategy,
} from '../src'
Expand All @@ -28,8 +27,11 @@ import { CUSTOM_STRATEGIES } from '../src/unleash-strategies/unleash-strategies.
import { ToggleEntity } from '../src/unleash/entity/toggle.entity'
import { MetricsService } from '../src/unleash/metrics.service'
import { ToggleRepository } from '../src/unleash/repository/toggle-repository'
import { UnleashContext } from '../src/unleash/unleash.context'
import { UnleashService } from '../src/unleash/unleash.service'

jest.mock('../src/unleash/unleash.context')

// 09-strategy-constraints.json is an enterprise feature. can't test.
const testSuite = [s1, s2, s3, s4, s5, s6, s7, s10]

Expand All @@ -49,14 +51,7 @@ describe('Specification test', () => {
UnleashService,
{ provide: CUSTOM_STRATEGIES, useValue: [] },
{ provide: MetricsService, useValue: { increase: jest.fn() } },
{
provide: UnleashContext,
useValue: {
getRemoteAddress: jest.fn(),
getSessionId: jest.fn(),
getUserId: jest.fn(),
},
},
UnleashContext,
ApplicationHostnameStrategy,
DefaultStrategy,
FlexibleRolloutStrategy,
Expand Down
16 changes: 14 additions & 2 deletions e2e/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Controller, Get, UseGuards } from '@nestjs/common'
import { Controller, Get, Param, UseGuards } from '@nestjs/common'
import { IfEnabled } from '../../src/unleash'
import { UnleashService } from '../../src/unleash/unleash.service'
import { UserGuard } from './user.guard'

export interface MyCustomData {
foo: string
}

@Controller()
@UseGuards(UserGuard)
export class AppController {
constructor(private readonly unleash: UnleashService) {}
constructor(private readonly unleash: UnleashService<MyCustomData>) {}

@Get('/')
index(): string {
Expand All @@ -20,4 +24,12 @@ export class AppController {
getContent(): string {
return 'my content'
}

@Get('/custom-context/:foo')
customContext(@Param('foo') foo: string): string {
// Provide "foo" as custom context data
return this.unleash.isEnabled('test', undefined, { foo })
? 'feature is active'
: 'feature is not active'
}
}
9 changes: 5 additions & 4 deletions e2e/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ import { UsersService } from './users.service'
UnleashModule.forRootAsync({
useFactory: () => ({
// disableRegistration: true,
// url: 'http://127.0.0.1:3000/unleash',
url: 'https://unleash.herokuapp.com/api/client',
url: 'http://localhost:4242/api/client',
appName: 'my-app-name',
instanceId: 'my-unique-instance', //process.pid.toString(),
refreshInterval: 20_000,
// metricsInterval: 3000,
// strategies: [MyCustomStrategy],
metricsInterval: 3000,
strategies: [MyCustomStrategy],
http: {
headers: {
Authorization:
'8b2d15c99270b809d47eef3bc7d8988059d7215adafa4c5175e2f4fe7b387f60',
'X-Foo': 'bar',
},
},
Expand Down
11 changes: 7 additions & 4 deletions e2e/src/my-custom-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Injectable } from '@nestjs/common'
import { UnleashStrategy } from '../../src'
import { UnleashContext, UnleashStrategy } from '../../src'
import { MyCustomData } from './app.controller'

@Injectable()
export class MyCustomStrategy implements UnleashStrategy {
name = 'MyCustomStrategy'

isEnabled(_parameters: unknown): boolean {
// eslint-disable-next-line no-magic-numbers
return Math.random() < 0.5
isEnabled(
_parameters: unknown,
context: UnleashContext<MyCustomData>,
): boolean {
return context.customData?.foo === 'bar'
}
}
4 changes: 2 additions & 2 deletions src/unleash-strategies/strategy/strategy.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { UnleashContext } from '../../unleash'

export interface UnleashStrategy<T = unknown> {
export interface UnleashStrategy<T = unknown, U = unknown> {
/**
* Must match the name you used to create the strategy in your Unleash
* server UI
Expand All @@ -13,5 +13,5 @@ export interface UnleashStrategy<T = unknown> {
* @param parameters Custom paramemters as configured in Unleash server UI
* @param context applicaton/request context, i.e. UserID
*/
isEnabled(parameters: T, context: UnleashContext): boolean
isEnabled(parameters: T, context: UnleashContext<U>): boolean
}
16 changes: 16 additions & 0 deletions src/unleash/unleash.context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ function createRequest(
return data
}

interface MyCustomData {
foo: boolean
bar: string
}

describe('UnleashContext', () => {
let context: UnleashContext
let req: Request<{
Expand Down Expand Up @@ -50,4 +55,15 @@ describe('UnleashContext', () => {
context.request = { hello: 'world' }
expect(context.getRequest()).toStrictEqual({ hello: 'world' })
})

describe('Custom data', () => {
test('extend()', () => {
const context = new UnleashContext<MyCustomData>(
req,
{} as UnleashModuleOptions,
)
const extendedContext = context.extend({ foo: true, bar: 'baz' })
expect(extendedContext.customData).toEqual({ foo: true, bar: 'baz' })
})
})
})
14 changes: 13 additions & 1 deletion src/unleash/unleash.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const defaultUserIdFactory = (request: Request<{ id: string }>) => {
}

@Injectable({ scope: Scope.REQUEST })
export class UnleashContext {
export class UnleashContext<TCustomData = unknown> {
#customData?: TCustomData

constructor(
@Inject(REQUEST) private request: Request<{ id: string }>,
@Inject(UNLEASH_MODULE_OPTIONS)
Expand All @@ -35,4 +37,14 @@ export class UnleashContext {
getRequest<T = Request<{ id: string }>>(): T {
return this.request as T
}

get customData(): TCustomData | undefined {
return this.#customData
}

extend(customData: TCustomData | undefined): UnleashContext<TCustomData> {
this.#customData = customData

return this
}
}
Loading

0 comments on commit cb3b054

Please sign in to comment.