Skip to content

Commit

Permalink
feat(application-generic, shared): Add Rate Limiting feature flag (#4667
Browse files Browse the repository at this point in the history
)

* feat(shared): Add feature flag format type checking

* feat(shared): Add request rate limiting feature flag

* fix(api): Update merged digest ID feature flag to use correct format

* feat(application-generic): Add request rate limiting FF use case

* fix: Remove commented code

* fix: Typo

* feat(infra): Add missing .env file values
  • Loading branch information
rifont authored Oct 30, 2023
1 parent 524f18b commit 37183b3
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 12 deletions.
1 change: 1 addition & 0 deletions apps/api/src/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ REDIS_CACHE_FAMILY=
REDIS_CACHE_KEY_PREFIX=
REDIS_CACHE_ENABLE_AUTOPIPELINING=true

IS_REQUEST_RATE_LIMITING_ENABLED=false
IS_IN_MEMORY_CLUSTER_MODE_ENABLED=false
ELASTICACHE_CLUSTER_SERVICE_HOST=
ELASTICACHE_CLUSTER_SERVICE_PORT=
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ REDIS_PORT=6379
REDIS_PREFIX=
REDIS_DB_INDEX=2

IS_REQUEST_RATE_LIMITING_ENABLED=false
IS_IN_MEMORY_CLUSTER_MODE_ENABLED=false
ELASTICACHE_CLUSTER_SERVICE_HOST=
ELASTICACHE_CLUSTER_SERVICE_PORT=
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ REDIS_CACHE_FAMILY=
REDIS_CACHE_KEY_PREFIX=
REDIS_CACHE_ENABLE_AUTOPIPELINING=false

IS_REQUEST_RATE_LIMITING_ENABLED=false
IS_IN_MEMORY_CLUSTER_MODE_ENABLED=false
ELASTICACHE_CLUSTER_SERVICE_HOST=
ELASTICACHE_CLUSTER_SERVICE_PORT=
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ REDIS_CACHE_FAMILY=
REDIS_CACHE_KEY_PREFIX=
REDIS_CACHE_ENABLE_AUTOPIPELINING=

IS_REQUEST_RATE_LIMITING_ENABLED=false
IS_IN_MEMORY_CLUSTER_MODE_ENABLED=false
REDIS_CLUSTER_SERVICE_HOST=
REDIS_CLUSTER_SERVICE_PORT=
Expand Down
6 changes: 6 additions & 0 deletions libs/shared/src/types/feature-flags/feature-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum FeatureFlagsKeysEnum {
IS_TEMPLATE_STORE_ENABLED = 'IS_TEMPLATE_STORE_ENABLED',
IS_TOPIC_NOTIFICATION_ENABLED = 'IS_TOPIC_NOTIFICATION_ENABLED',
IS_MULTI_TENANCY_ENABLED = 'IS_MULTI_TENANCY_ENABLED',
IS_USE_MERGED_DIGEST_ID_ENABLED = 'IS_USE_MERGED_DIGEST_ID_ENABLED',
}
54 changes: 54 additions & 0 deletions libs/shared/src/types/feature-flags/flags.types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { IFlagKey, testFlagEnumValidity } from './flags.types';
import { FeatureFlagsKeysEnum } from './feature-flags';
import { SystemCriticalFlagsEnum } from './system-critical-flags';

/**
* Type Error tests for template literal types - Flag naming
* `export` is specified to avoid false-positive issue of:
* "<value> is declared but its value is never read."
*
* https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
*/

/**
* IFlagKey tests
*/
// Valid
export const validFlag: IFlagKey = 'IS_SOMETHING_ENABLED';

// @ts-expect-error - Missing `IS_` prefix
export const invalidPrefixFlag: IFlagKey = 'SOMETHING_ENABLED';

// @ts-expect-error - Missing `_ENABLED` suffix
export const invalidSuffixFlag: IFlagKey = 'IS_SOMETHING';

// @ts-expect-error - Incorrect subject casing
export const invalidSubjectFlag: IFlagKey = 'IS_something_ENABLED';

/**
* testFlagEnumValidity Tests
*/
enum ValidFlagsEnum {
IS_SOMETHING_ENABLED = 'IS_SOMETHING_ENABLED',
IS_SOMETHING_ELSE_ENABLED = 'IS_SOMETHING_ELSE_ENABLED',
}
testFlagEnumValidity(ValidFlagsEnum);

enum InvalidKeyFlagsEnum {
IS_SOMETHING_ENABLED = 'IS_SOMETHING_ENABLED',
INVALID_ENABLED = 'IS_INVALID_ENABLED',
}
// @ts-expect-error - Invalid key - INVALID_ENABLED
testFlagEnumValidity(InvalidKeyFlagsEnum);
enum InvalidValueFlagsEnum {
IS_SOMETHING_ENABLED = 'IS_SOMETHING_ENABLED',
IS_INVALID_ENABLED = 'INVALID_ENABLED',
}
// @ts-expect-error - Invalid value on IS_INVALID_ENABLED: 'INVALID_ENABLED'
testFlagEnumValidity(InvalidValueFlagsEnum);

/**
* Verifying declared FlagEnums
*/
testFlagEnumValidity(FeatureFlagsKeysEnum);
testFlagEnumValidity(SystemCriticalFlagsEnum);
20 changes: 20 additions & 0 deletions libs/shared/src/types/feature-flags/flags.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* The required format for a boolean flag key.
*/
export type IFlagKey = `IS_${Uppercase<string>}_ENABLED`;

/**
* Helper function to test that enum keys and values match correct format.
*
* It is not possible as of Typescript 5.2 to declare a type for an enum key or value in-line.
* Therefore we must test the enum via a helper function that abstracts the enum to an object.
*
* If the test fails, you should review your `enum` to verify that both the
* keys and values match the format specified by the `IFlagKey` template literal type.
* ref: https://stackoverflow.com/a/58181315
*
* @param testEnum - the Enum to type check
*/
export declare function testFlagEnumValidity<TEnum extends IFlags, IFlags = Record<IFlagKey, IFlagKey>>(
testEnum: TEnum & Record<Exclude<keyof TEnum, keyof IFlags>, ['Key must follow `IFlagKey` format']>
): true;
13 changes: 3 additions & 10 deletions libs/shared/src/types/feature-flags/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
export enum FeatureFlagsKeysEnum {
IS_TEMPLATE_STORE_ENABLED = 'IS_TEMPLATE_STORE_ENABLED',
IS_TOPIC_NOTIFICATION_ENABLED = 'IS_TOPIC_NOTIFICATION_ENABLED',
IS_MULTI_TENANCY_ENABLED = 'IS_MULTI_TENANCY_ENABLED',
IS_USE_MERGED_DIGEST_ID = 'IS_USE_MERGED_DIGEST_ID_ENABLED',
}

export enum SystemCriticalFlagsEnum {
IS_IN_MEMORY_CLUSTER_MODE_ENABLED = 'IS_IN_MEMORY_CLUSTER_MODE_ENABLED',
}
export * from './feature-flags';
export * from './system-critical-flags';
export * from './flags.types';
4 changes: 4 additions & 0 deletions libs/shared/src/types/feature-flags/system-critical-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum SystemCriticalFlagsEnum {
IS_IN_MEMORY_CLUSTER_MODE_ENABLED = 'IS_IN_MEMORY_CLUSTER_MODE_ENABLED',
IS_REQUEST_RATE_LIMITING_ENABLED = 'IS_REQUEST_RATE_LIMITING_ENABLED',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { SystemCriticalFlagsEnum } from '@novu/shared';

import { GetSystemCriticalFlag } from './get-system-critical-flag.use-case';

@Injectable()
export class GetIsRequestRateLimitingEnabled extends GetSystemCriticalFlag {
execute(): boolean {
const value = process.env.IS_REQUEST_RATE_LIMITING_ENABLED;
const fallbackValue = false;
const defaultValue = this.prepareBooleanStringSystemCriticalFlag(
value,
fallbackValue
);
const key = SystemCriticalFlagsEnum.IS_REQUEST_RATE_LIMITING_ENABLED;

this.log<boolean>(key, defaultValue);

return defaultValue;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { GetIsInMemoryClusterModeEnabled } from './index';
import {
GetIsInMemoryClusterModeEnabled,
GetIsRequestRateLimitingEnabled,
} from './index';

describe('Get System Critical Flag', () => {
describe('SystemCriticalFlagEnum.IS_IN_MEMORY_CLUSTER_MODE_ENABLED', () => {
Expand Down Expand Up @@ -38,4 +41,26 @@ describe('Get System Critical Flag', () => {
expect(result).toEqual(true);
});
});

describe('SystemCriticalFlagEnum.IS_REQUEST_RATE_LIMITING_ENABLED', () => {
it('should return default hardcoded value when no environment variable is set', async () => {
process.env.IS_REQUEST_RATE_LIMITING_ENABLED = '';

const getIsRequestRateLimitingEnabled =
new GetIsRequestRateLimitingEnabled();

const result = getIsRequestRateLimitingEnabled.execute();
expect(result).toEqual(false);
});

it('should return environment variable value when environment variable is set', async () => {
process.env.IS_REQUEST_RATE_LIMITING_ENABLED = 'true';

const getIsRequestRateLimitingEnabled =
new GetIsRequestRateLimitingEnabled();

const result = getIsRequestRateLimitingEnabled.execute();
expect(result).toEqual(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class GetUseMergedDigestId extends GetFeatureFlag {
value,
fallbackValue
);
const key = FeatureFlagsKeysEnum.IS_USE_MERGED_DIGEST_ID;
const key = FeatureFlagsKeysEnum.IS_USE_MERGED_DIGEST_ID_ENABLED;

const command = this.buildCommand(key, defaultValue, featureFlagCommand);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
} from './get-feature-flag.command';
export { GetFeatureFlag } from './get-feature-flag.use-case';
export { GetIsInMemoryClusterModeEnabled } from './get-is-in-memory-cluster-mode-enabled.use-case';
export { GetIsRequestRateLimitingEnabled } from './get-is-request-rate-limiting-enabled.use-case';
export { GetIsTemplateStoreEnabled } from './get-is-template-store-enabled.use-case';
export { GetIsTopicNotificationEnabled } from './get-is-topic-notification-enabled.use-case';
export { GetUseMergedDigestId } from './get-use-merged-digest-id.use-case';

1 comment on commit 37183b3

@github-actions
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.