Skip to content

Commit 1fa53c9

Browse files
lukas-reiningbeeme1mrluizgribeiro
authored
feat: context propagation for nestjs (#736)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR Implements context propagation in the Nest.js SDK. This relates to the discussions from #733 (comment). Open questions are: - How to scope the `contextFactory`? It seems that we should be able to define a common and client specific context factories. - Should the `AsyncLocalStorage` be used in the `FeatureFlagDecorators` too as it is in this draft? - Should we merge the injected `OpenFeatureClient` and the `OpenFeatureContextService` so that you do not have to inject both into the consuming services as we do in the `TestService`? @luizgribeiro this is the draft for what we discussed about, what do you think? --------- Signed-off-by: Lukas Reining <[email protected]> Signed-off-by: Luiz Guilherme Ribeiro <[email protected]> Co-authored-by: Michael Beemer <[email protected]> Co-authored-by: Luiz Guilherme Ribeiro <[email protected]>
1 parent a2d4f3e commit 1fa53c9

12 files changed

+598
-325
lines changed

package-lock.json

Lines changed: 68 additions & 90 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/nest/package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"test": "jest --verbose",
1818
"lint": "eslint ./",
1919
"clean": "shx rm -rf ./dist",
20-
"build:esm": "esbuild src/index.ts --bundle --external:@nestjs/common --external:@nestjs/common --external:@openfeature/server-sdk --sourcemap --target=es2016 --platform=node --format=esm --outfile=./dist/esm/index.js --analyze",
21-
"build:cjs": "esbuild src/index.ts --bundle --external:@nestjs/common --external:@nestjs/common --external:@openfeature/server-sdk --sourcemap --target=es2016 --platform=node --format=cjs --outfile=./dist/cjs/index.js --analyze",
20+
"build:esm": "esbuild src/index.ts --bundle --external:@nestjs/* --external:@openfeature/server-sdk --sourcemap --target=es2016 --platform=node --format=esm --outfile=./dist/esm/index.js --analyze",
21+
"build:cjs": "esbuild src/index.ts --bundle --external:@nestjs/* --external:@openfeature/server-sdk --sourcemap --target=es2016 --platform=node --format=cjs --outfile=./dist/cjs/index.js --analyze",
2222
"build:rollup-types": "rollup -c ../../rollup.config.mjs",
2323
"build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:rollup-types",
2424
"postbuild": "shx cp ./../../package.esm.json ./dist/esm/package.json",
@@ -56,9 +56,7 @@
5656
"@nestjs/testing": "^10.2.10",
5757
"@openfeature/core": "*",
5858
"@openfeature/server-sdk": "*",
59-
"@types/express": "^4.17.21",
6059
"@types/supertest": "^2.0.16",
61-
"express": "^4.18.2",
6260
"supertest": "^6.3.3"
6361
}
6462
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { EvaluationContext } from '@openfeature/core';
2+
import { ExecutionContext, Inject } from '@nestjs/common';
3+
4+
/**
5+
* A factory function for creating an OpenFeature {@link EvaluationContext} from Nest {@link ExecutionContext}.
6+
* This can be used e.g. to get header info from an HTTP request or information from a gRPC call.
7+
*
8+
* Example getting an HTTP header value:
9+
* ```typescript
10+
* async function(context: ExecutionContext) {
11+
* const request = await context.switchToHttp().getRequest();
12+
*
13+
* const userId = request.header('x-user-id');
14+
*
15+
* if (userId) {
16+
* return {
17+
* targetingKey: userId,
18+
* };
19+
* }
20+
*
21+
* return undefined;
22+
* }
23+
* ```
24+
* @param {ExecutionContext} request The {@link ExecutionContext} to get the information from.
25+
* @returns {(Promise<EvaluationContext | undefined> | EvaluationContext | undefined)} The {@link EvaluationContext} new.
26+
*/
27+
export type ContextFactory = (
28+
request: ExecutionContext,
29+
) => Promise<EvaluationContext | undefined> | EvaluationContext | undefined;
30+
31+
/**
32+
* InjectionToken for a {@link ContextFactory}.
33+
* @see {@link Inject}
34+
*/
35+
export const ContextFactoryToken = Symbol('CONTEXT_FACTORY');
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
2+
import { ContextFactory, ContextFactoryToken } from './context-factory';
3+
import { Observable } from 'rxjs';
4+
import { OpenFeature } from '@openfeature/server-sdk';
5+
6+
@Injectable()
7+
export class EvaluationContextInterceptor implements NestInterceptor {
8+
constructor(@Inject(ContextFactoryToken) private contextFactory?: ContextFactory) {}
9+
10+
async intercept(executionContext: ExecutionContext, next: CallHandler) {
11+
const context = await this.contextFactory?.(executionContext);
12+
13+
return new Observable((subscriber) => {
14+
OpenFeature.setTransactionContext(context ?? {}, async () => {
15+
next.handle().subscribe({
16+
next: (res) => subscriber.next(res),
17+
error: (err) => subscriber.error(err),
18+
complete: () => subscriber.complete(),
19+
});
20+
});
21+
});
22+
}
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { TransactionContextPropagator } from '@openfeature/server-sdk';
2+
import { AsyncLocalStorage } from 'async_hooks';
3+
import { EvaluationContext } from '@openfeature/server-sdk';
4+
5+
export class AsyncLocalStorageTransactionContext implements TransactionContextPropagator {
6+
private asyncLocalStorage = new AsyncLocalStorage<EvaluationContext>();
7+
8+
getTransactionContext(): EvaluationContext {
9+
return this.asyncLocalStorage.getStore() ?? {};
10+
}
11+
setTransactionContext(context: EvaluationContext, callback: () => void): void {
12+
this.asyncLocalStorage.run(context, callback);
13+
}
14+
}

0 commit comments

Comments
 (0)