Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Nest SDK #750

Merged
merged 25 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4dd59c4
docs: add quick start
luizgribeiro Jan 9, 2024
d837440
chore: add jsdocs
luizgribeiro Jan 10, 2024
cad673a
Update packages/nest/README.md
luizgribeiro Jan 10, 2024
88bf4f7
Update packages/nest/src/open-feature.module.ts
luizgribeiro Jan 10, 2024
04e92b7
Update packages/nest/README.md
luizgribeiro Jan 10, 2024
f0c9afe
Update packages/nest/README.md
luizgribeiro Jan 16, 2024
c527bc9
Update packages/nest/README.md
luizgribeiro Jan 16, 2024
9f1b5f6
Update packages/nest/src/evaluation-context-interceptor.ts
luizgribeiro Jan 16, 2024
3de4dff
docs: fix minor issues
luizgribeiro Jan 16, 2024
de8aa97
Update packages/nest/src/evaluation-context-interceptor.ts
lukas-reining Jan 22, 2024
87f1b51
docs: specify peer deps
luizgribeiro Jan 23, 2024
34e70d5
Update packages/nest/README.md
luizgribeiro Jan 23, 2024
1f932e6
Update packages/nest/README.md
luizgribeiro Jan 23, 2024
3564be5
Update packages/nest/README.md
luizgribeiro Jan 23, 2024
4035c25
Update packages/nest/README.md
luizgribeiro Jan 23, 2024
d7801d3
Merge branch 'docs/NextSDK' of github.com:luizgribeiro/openfeature-js…
luizgribeiro Jan 23, 2024
3c33c06
docs: add badge
luizgribeiro Jan 23, 2024
988853b
docs: change badge order and remove line breaks
luizgribeiro Jan 23, 2024
73547b4
Merge branch 'main' into docs/NextSDK
luizgribeiro Jan 23, 2024
c759819
docs: add missing peer deps
luizgribeiro Jan 24, 2024
6d62232
Merge branch 'docs/NextSDK' of github.com:luizgribeiro/openfeature-js…
luizgribeiro Jan 24, 2024
49072d6
Update packages/nest/README.md
luizgribeiro Jan 24, 2024
8948eda
Update packages/nest/README.md
luizgribeiro Jan 24, 2024
82ede1e
Update packages/nest/README.md
luizgribeiro Jan 24, 2024
529178b
Update packages/nest/README.md
lukas-reining Jan 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 66 additions & 39 deletions packages/nest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@
</picture>
</p>

<h2 align="center">OpenFeature Nest.js SDK</h2>
<h2 align="center">OpenFeature NestJS SDK</h2>

<!-- x-hide-in-docs-end -->
<!-- The 'github-badges' class is used in the docs -->
<p align="center" class="github-badges">
<a href="https://github.com/open-feature/spec/releases/tag/v0.7.0">
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
</a>
<br/>
<!-- x-release-please-start-version -->
<a href="https://github.com/open-feature/js-sdk/releases/tag/nestjs-sdk-v0.1.0">
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.1.0&color=blue&style=for-the-badge" />
</a>
<!-- x-release-please-end -->
<a href="https://codecov.io/gh/open-feature/js-sdk">
<img alt="codecov" src="https://codecov.io/gh/open-feature/js-sdk/branch/main/graph/badge.svg?token=3DC5XOEHMY" />
</a>
Expand All @@ -25,11 +29,49 @@
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API
for feature flagging that works with your favorite feature flag management tool.

🧪 This SDK is experimental.
<!-- x-hide-in-docs-end -->

## Overview

The OpenFeature NestJS SDK is a package that provides a NestJS wrapper for the [OpenFeature Server SDK](https://openfeature.dev/docs/reference/technologies/server/javascript/).

Capabilities include:

- Provide a NestJS global module to simplify OpenFeature configuration and usage within NestJS;
- Injecting feature flags directly into controller route handlers by using decorators;
- Injecting transaction evaluation context for flag evaluations directly from [execution context](https://docs.nestjs.com/fundamentals/execution-context) (HTTP header values, client IPs, etc.);
- Injecting OpenFeature clients into NestJS services and controllers by using decorators;
- Setting up logging, event handling, hooks and providers directly when registering the module.

## 🚀 Quick start

### Requirements

- Node.js version 16+
- NestJS version 8+

### Install

#### npm

```sh
npm install --save @openfeature/nestjs-sdk
```

#### Required peer dependencies

#### Here's a basic example of how to use the OpenFeature NestJS API with `InMemoryProvider`.
The following list contains the peer dependencies of `@openfeature/nestjs-sdk` with it's expected and compatible versions:

#### Registering the Nest.js SDK module in the App Module:
* `@openfeature/server-sdk`: >=1.7.5
* `@nestjs/common`: ^8.0.0 || ^9.0.0 || ^10.0.0
* `@nestjs/core`: ^8.0.0 || ^9.0.0 || ^10.0.0
* `rxjs`: ^6.0.0 || ^7.0.0
lukas-reining marked this conversation as resolved.
Show resolved Hide resolved

The minimum required version of `@openfeature/server-sdk` currently is `1.7.5`.

### Usage

The example below shows how to use the `OpenFeatureModule` with OpenFeature's `InMemoryProvider`.

```ts
import { Module } from '@nestjs/common';
Expand All @@ -44,24 +86,19 @@ import { InMemoryProvider } from '@openfeature/web-sdk';
testBooleanFlag: {
defaultVariant: 'default',
variants: { default: true },
disabled: false
disabled: false,
},
companyName: {
defaultVariant: 'default',
variants: { default: "BigCorp" },
disabled: false
}
}),
providers: {
differentProvider: new InMemoryProvider()
}
})
]
differentProvider: new InMemoryProvider(),
},
}),
],
})
export class AppModule {}
```

#### Injecting a feature flag with header value in evaluation context into an endpoint handler method
With the `OpenFeatureModule` configured, it's possible to inject flag evaluation details into route handlers like in the following code snippet.

```ts
import { Controller, ExecutionContext, Get } from '@nestjs/common';
Expand All @@ -70,40 +107,26 @@ import { BooleanFeatureFlag } from '@openfeature/nestjs-sdk';
import { EvaluationDetails } from '@openfeature/server-sdk';
import { Request } from 'express';

function getContext(executionContext: ExecutionContext) {
const request = executionContext.switchToHttp().getRequest<Request>();
const userId = request.header('x-user-id');

if (!userId) {
return undefined;
}

return {
targetingKey: userId,
};
}

@Controller()
export class OpenFeatureController {
@Get('/welcome')
public async welcome(
@BooleanFeatureFlag({
flagKey: 'testBooleanFlag',
defaultValue: false,
contextFactory: getContext,
})
feature: Observable<EvaluationDetails<boolean>>,
feature: Observable<EvaluationDetails<boolean>>,
) {
return feature.pipe(
map((details) =>
details.value ? 'Welcome to this OpenFeature-enabled Nest.js app!' : 'Welcome to this Nest.js app!',
details.value ? 'Welcome to this OpenFeature-enabled NestJS app!' : 'Welcome to this NestJS app!',
),
);
}
}
```

#### Injecting the default and a named client into a service:
It is also possible to inject the default or named OpenFeature clients into a service via Nest dependency injection system.

```ts
import { Injectable } from '@nestjs/common';
Expand All @@ -114,15 +137,19 @@ import { FeatureClient } from '@openfeature/nestjs-sdk';
export class OpenFeatureTestService {
constructor(
@FeatureClient() private defaultClient: Client,
@FeatureClient({ name: 'differentServer' }) private namedClient: Client,
) {
}
@FeatureClient({ name: 'differentProvider' }) private namedClient: Client,
Comment on lines 139 to +140
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should rename this to OpenFeatureClient before we remove the experimental. I think just FeatureClient is a bit disjointed, since we have tended to either add the entire "OpenFeature" prefix, or leave it off. I think OpenFeatureClient would be best, because the client inteface is called Client so it won't clash, and it's consistent with the equivalent in react (</OpenFeatureProvider>).

Copy link
Member

@lukas-reining lukas-reining Jan 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has been OpenFeatureClient before, but OpenFeatureClient is used for the implementation of the Client and it is also exported from the Server SDK.

We could remove the export of OpenFeatureClient from the Web SDK, as no one should use and import this directly anyways I would say. I have seen it several times that this type was used instead of Client in projects.
If we remove this I would be happy calling the interface OpenFeatureClienthere. What do you think @toddbaert?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think @toddbaert?

Copy link
Member

@toddbaert toddbaert Jan 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the late reply - if we can remove that export, I'm fine with it! It's an improvement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @toddbaert, removing this should be considered a breaking change I would say.
I am not sure how we want to continue on this then. What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, well, if I understand correctly, it would be technically breaking, but I think it could be argued it was a misuse. All our doc recommends using the interface.

If we want to be really strict, we could consider it a break and include it with some others I have in mind in a 2.0.0 (meaning to ask you guys about this soon... there's a small list of breaking improvements I think might be worth considering)...

) {}

public async getMessage() {
const companyName = await this.defaultClient.getStringValue('companyName', 'Unknown Company');
return `Hey User from ${companyName}`;
public async getBoolean() {
return await this.defaultClient.getBooleanValue('testBooleanFlag', false);
}
}
```

## Module aditional information

### Flag evaluation context injection

Whenever a flag evaluation occurs, context can be provided with information like user e-mail, role, targeting key, etc in order to trigger specific evaluation rules or logic. The `OpenFeatureModule` provides a way to configure context for each request using the `contextFactory` option.
The `contextFactory` is ran in a NestJS interceptor scope to configure the evaluation context and than it is used in every flag evaluation related to this request.
lukas-reining marked this conversation as resolved.
Show resolved Hide resolved
By default, the interceptor is configured globally, but it can be changed by setting the `useGlobalInterceptor` to `false`. In this case, it is still possible to configure a `contextFactory` that can be injected into route, module or controller bound interceptors.
24 changes: 24 additions & 0 deletions packages/nest/src/evaluation-context-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,31 @@ import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } fr
import { ContextFactory, ContextFactoryToken } from './context-factory';
import { Observable } from 'rxjs';
import { OpenFeature } from '@openfeature/server-sdk';
luizgribeiro marked this conversation as resolved.
Show resolved Hide resolved
import { OpenFeatureModule } from './open-feature.module';

/**
* NestJS interceptor used in {@link OpenFeatureModule}
* to configure flag evaluation context.
*
* This interceptor is configured globally by default.
* If `useGlobalInterceptor` is set to `false` in {@link OpenFeatureModule} it needs to be configured for the specific controllers or routes.
*
* If just the interceptor class is passed to the `UseInterceptors` like below, the `contextFactory` provided in the {@link OpenFeatureModule} will be injected and used in order to create the context.
* ```ts
* //route interceptor
* @UseInterceptors(EvaluationContextInterceptor)
* @Get('/user-info')
* getUserInfo(){}
* ```
*
* A different `contextFactory` can also be provided, but the interceptor instance has to be instantiated like in the following example.
* ```ts
* //route interceptor
* @UseInterceptors(new EvaluationContextInterceptor(<context factory>))
* @Get('/user-info')
* getUserInfo(){}
* ```
*/
@Injectable()
export class EvaluationContextInterceptor implements NestInterceptor {
constructor(@Inject(ContextFactoryToken) private contextFactory?: ContextFactory) {}
Expand Down
3 changes: 1 addition & 2 deletions packages/nest/src/evaluation-context-propagator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TransactionContextPropagator } from '@openfeature/server-sdk';
import { TransactionContextPropagator, EvaluationContext } from '@openfeature/server-sdk';
import { AsyncLocalStorage } from 'async_hooks';
import { EvaluationContext } from '@openfeature/server-sdk';

export class AsyncLocalStorageTransactionContext implements TransactionContextPropagator {
private asyncLocalStorage = new AsyncLocalStorage<EvaluationContext>();
Expand Down
3 changes: 3 additions & 0 deletions packages/nest/src/open-feature.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { AsyncLocalStorageTransactionContext } from './evaluation-context-propag
import { EvaluationContextInterceptor } from './evaluation-context-interceptor';
import { ShutdownService } from './shutdown.service';

/**
* OpenFeatureModule is a NestJS wrapper for OpenFeature Server-SDK.
*/
@Module({})
export class OpenFeatureModule {
static forRoot({ useGlobalInterceptor = true, ...options }: OpenFeatureModuleOptions): DynamicModule {
Expand Down