From 0d3ccd75cd7aadfcfdf1e3fa2d143463841fde3e Mon Sep 17 00:00:00 2001 From: Kaushal Kapasi Date: Fri, 14 Mar 2025 15:45:40 -0400 Subject: [PATCH] feat: adds `RequireFlagsEnabled` decorator to allow reusable controller & endpoint access based on boolean flag values Signed-off-by: Kaushal Kapasi --- packages/nest/src/index.ts | 1 + .../src/require-flags-enabled.decorator.ts | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 packages/nest/src/require-flags-enabled.decorator.ts diff --git a/packages/nest/src/index.ts b/packages/nest/src/index.ts index 20514df86..7296307e8 100644 --- a/packages/nest/src/index.ts +++ b/packages/nest/src/index.ts @@ -2,5 +2,6 @@ export * from './open-feature.module'; export * from './feature.decorator'; export * from './evaluation-context-interceptor'; export * from './context-factory'; +export * from './require-flags-enabled.decorator'; // re-export the server-sdk so consumers can access that API from the nestjs-sdk export * from '@openfeature/server-sdk'; diff --git a/packages/nest/src/require-flags-enabled.decorator.ts b/packages/nest/src/require-flags-enabled.decorator.ts new file mode 100644 index 000000000..525cc563f --- /dev/null +++ b/packages/nest/src/require-flags-enabled.decorator.ts @@ -0,0 +1,87 @@ +import { + applyDecorators, + CallHandler, + ExecutionContext, + HttpException, + mixin, + NestInterceptor, + NotFoundException, + UseInterceptors, +} from '@nestjs/common'; +import { OpenFeature } from '@openfeature/server-sdk'; + +/** + * Options for injecting a feature flag into a route handler. + */ +interface RequireFlagsEnabledProps { + /** + * The key of the feature flag. + * @see {@link Client#getBooleanValue} + */ + flagKeys: string[]; + /** + * The exception to throw if any of the required feature flags are not enabled. + * Defaults to a 404 Not Found exception. + * @see {@link HttpException} + */ + exception?: HttpException; + + /** + * The domain of the OpenFeature client, if a domain scoped client should be used. + * @see {@link OpenFeature#getClient} + */ + domain?: string; +} + +/** + * Returns a domain scoped or the default OpenFeature client with the given context. + * @param {string} domain The domain of the OpenFeature client. + * @returns {Client} The OpenFeature client. + */ +function getClientForEvaluation(domain?: string) { + return domain ? OpenFeature.getClient(domain) : OpenFeature.getClient(); +} + +/** + * Controller or Route permissions handler decorator. + * + * Requires that the given feature flags are enabled for the request to be processed, else throws an exception. + * + * For example: + * ```typescript + * @RequireFlagsEnabled({ + * flagKeys: ['flagName', 'flagName2'], // Required, an array of Boolean feature flag keys + * exception: new ForbiddenException(), // Optional, defaults to a 404 Not Found exception + * domain: 'my-domain', // Optional, defaults to the default OpenFeature client + * }) + * @Get('/') + * public async handleGetRequest() + * ``` + * @param {RequireFlagsEnabledProps} options The options for injecting the feature flag. + * @returns {Decorator} + */ +export const RequireFlagsEnabled = (props: RequireFlagsEnabledProps): ClassDecorator & MethodDecorator => + applyDecorators(UseInterceptors(FlagsEnabledInterceptor(props))); + +const FlagsEnabledInterceptor = (props: RequireFlagsEnabledProps) => { + class FlagsEnabledInterceptor implements NestInterceptor { + constructor() {} + + async intercept(context: ExecutionContext, next: CallHandler) { + const req = context.switchToHttp().getRequest(); + const client = getClientForEvaluation(props.domain); + + for (const flagKey of props.flagKeys) { + const endpointAccessible = await client.getBooleanValue(flagKey, false); + + if (!endpointAccessible) { + throw props.exception || new NotFoundException(`Cannot ${req.method} ${req.url}`); + } + } + + return next.handle(); + } + } + + return mixin(FlagsEnabledInterceptor); +};