Skip to content

Commit 0d3ccd7

Browse files
committed
feat: adds RequireFlagsEnabled decorator to allow reusable controller & endpoint access based on boolean flag values
Signed-off-by: Kaushal Kapasi <[email protected]>
1 parent ae8fce8 commit 0d3ccd7

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

Diff for: packages/nest/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ export * from './open-feature.module';
22
export * from './feature.decorator';
33
export * from './evaluation-context-interceptor';
44
export * from './context-factory';
5+
export * from './require-flags-enabled.decorator';
56
// re-export the server-sdk so consumers can access that API from the nestjs-sdk
67
export * from '@openfeature/server-sdk';

Diff for: packages/nest/src/require-flags-enabled.decorator.ts

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
applyDecorators,
3+
CallHandler,
4+
ExecutionContext,
5+
HttpException,
6+
mixin,
7+
NestInterceptor,
8+
NotFoundException,
9+
UseInterceptors,
10+
} from '@nestjs/common';
11+
import { OpenFeature } from '@openfeature/server-sdk';
12+
13+
/**
14+
* Options for injecting a feature flag into a route handler.
15+
*/
16+
interface RequireFlagsEnabledProps {
17+
/**
18+
* The key of the feature flag.
19+
* @see {@link Client#getBooleanValue}
20+
*/
21+
flagKeys: string[];
22+
/**
23+
* The exception to throw if any of the required feature flags are not enabled.
24+
* Defaults to a 404 Not Found exception.
25+
* @see {@link HttpException}
26+
*/
27+
exception?: HttpException;
28+
29+
/**
30+
* The domain of the OpenFeature client, if a domain scoped client should be used.
31+
* @see {@link OpenFeature#getClient}
32+
*/
33+
domain?: string;
34+
}
35+
36+
/**
37+
* Returns a domain scoped or the default OpenFeature client with the given context.
38+
* @param {string} domain The domain of the OpenFeature client.
39+
* @returns {Client} The OpenFeature client.
40+
*/
41+
function getClientForEvaluation(domain?: string) {
42+
return domain ? OpenFeature.getClient(domain) : OpenFeature.getClient();
43+
}
44+
45+
/**
46+
* Controller or Route permissions handler decorator.
47+
*
48+
* Requires that the given feature flags are enabled for the request to be processed, else throws an exception.
49+
*
50+
* For example:
51+
* ```typescript
52+
* @RequireFlagsEnabled({
53+
* flagKeys: ['flagName', 'flagName2'], // Required, an array of Boolean feature flag keys
54+
* exception: new ForbiddenException(), // Optional, defaults to a 404 Not Found exception
55+
* domain: 'my-domain', // Optional, defaults to the default OpenFeature client
56+
* })
57+
* @Get('/')
58+
* public async handleGetRequest()
59+
* ```
60+
* @param {RequireFlagsEnabledProps} options The options for injecting the feature flag.
61+
* @returns {Decorator}
62+
*/
63+
export const RequireFlagsEnabled = (props: RequireFlagsEnabledProps): ClassDecorator & MethodDecorator =>
64+
applyDecorators(UseInterceptors(FlagsEnabledInterceptor(props)));
65+
66+
const FlagsEnabledInterceptor = (props: RequireFlagsEnabledProps) => {
67+
class FlagsEnabledInterceptor implements NestInterceptor {
68+
constructor() {}
69+
70+
async intercept(context: ExecutionContext, next: CallHandler) {
71+
const req = context.switchToHttp().getRequest();
72+
const client = getClientForEvaluation(props.domain);
73+
74+
for (const flagKey of props.flagKeys) {
75+
const endpointAccessible = await client.getBooleanValue(flagKey, false);
76+
77+
if (!endpointAccessible) {
78+
throw props.exception || new NotFoundException(`Cannot ${req.method} ${req.url}`);
79+
}
80+
}
81+
82+
return next.handle();
83+
}
84+
}
85+
86+
return mixin(FlagsEnabledInterceptor);
87+
};

0 commit comments

Comments
 (0)