From 08caae2ef172e0dc7bbbbe2cefc661956d0968ee Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 May 2025 10:58:24 +0200 Subject: [PATCH 1/7] feat(commons): add env var util fns --- packages/commons/package.json | 8 ++ packages/commons/src/envUtils.ts | 172 +++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 packages/commons/src/envUtils.ts diff --git a/packages/commons/package.json b/packages/commons/package.json index bd06539c3..bed0bfcb2 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -53,6 +53,10 @@ "import": "./lib/esm/unmarshallDynamoDB.js", "require": "./lib/cjs/unmarshallDynamoDB.js" }, + "./utils/env": { + "import": "./lib/esm/envUtils.js", + "require": "./lib/cjs/envUtils.js" + }, "./types": { "import": "./lib/esm/types/index.js", "require": "./lib/cjs/types/index.js" @@ -76,6 +80,10 @@ "lib/cjs/unmarshallDynamoDB.d.ts", "lib/esm/unmarshallDynamoDB.d.ts" ], + "utils/env": [ + "lib/cjs/envUtils.d.ts", + "lib/esm/envUtils.d.ts" + ], "types": [ "lib/cjs/types/index.d.ts", "lib/esm/types/index.d.ts" diff --git a/packages/commons/src/envUtils.ts b/packages/commons/src/envUtils.ts new file mode 100644 index 000000000..a3b01b0a3 --- /dev/null +++ b/packages/commons/src/envUtils.ts @@ -0,0 +1,172 @@ +import { isNumber } from './typeUtils.js'; + +type GetFromEnvOptions = { + key: string; + defaultValue?: unknown; + required?: boolean; +}; + +/** + * Get a string from the environment variables. + * + * @param {Object} options - The options for getting the string. + * @param {string} options.key - The key of the environment variable. + * @param {string} [options.defaultValue] - The default value to return if the environment variable is not set. + * @param {boolean} [options.required] - Whether the environment variable is required. + */ +const getStringFromEnv = ({ + key, + defaultValue, + required, +}: GetFromEnvOptions): string => { + const value = process.env[key]; + + if (value === undefined) { + if (required === true) { + throw new Error(`Environment variable ${key} is required`); + } + + return String(defaultValue) ?? ''; + } + + return value.trim(); +}; + +/** + * Get a number from the environment variables. + * + * @param {Object} options - The options for getting the number. + * @param {string} options.key - The key of the environment variable. + * @param {number} [options.defaultValue] - The default value to return if the environment variable is not set. + * @param {boolean} [options.required] - Whether the environment variable is required. + */ +const getNumberFromEnv = ({ + key, + defaultValue, + required, +}: GetFromEnvOptions): number => { + const value = getStringFromEnv({ + key, + defaultValue: String(defaultValue), + required, + }) as unknown; + + if (!isNumber(value)) { + throw new Error(`Environment variable ${key} must be a number`); + } + + return value; +}; + +/** + * Get a boolean from the environment variables. + * + * @param {Object} options - The options for getting the boolean. + * @param {string} options.key - The key of the environment variable. + * @param {boolean} [options.defaultValue] - The default value to return if the environment variable is not set. + * @param {boolean} [options.required] - Whether the environment variable is required. + */ +const getBooleanFromEnv = ({ + key, + defaultValue, + required, +}: GetFromEnvOptions): boolean => { + const value = getStringFromEnv({ + key, + defaultValue: String(defaultValue), + required, + }); + + const parsedValue = value.toLowerCase(); + + if (parsedValue !== 'true' && parsedValue !== 'false') { + throw new Error(`Environment variable ${key} must be a boolean`); + } + + return Boolean(parsedValue); +}; + +const truthyValues = ['1', 'y', 'yes', 't', 'true', 'on']; + +/** + * Get a truthy boolean from the environment variables. + * + * Truthy values are: `1`, `y`, `yes`, `t`, `true`, `on`. + * + * @param {Object} options - The options for getting the truthy boolean. + * @param {string} options.key - The key of the environment variable. + * @param {boolean} [options.defaultValue] - The default value to return if the environment variable is not set. + * @param {boolean} [options.required] - Whether the environment variable is required. + */ +const getTruthyBooleanFromEnv = ({ + key, + defaultValue, + required, +}: GetFromEnvOptions): boolean => { + const value = getStringFromEnv({ + key, + defaultValue: String(defaultValue), + required, + }); + + return truthyValues.includes(value.toLowerCase()); +}; + +const falsyValues = ['0', 'n', 'no', 'f', 'false', 'off']; + +/** + * Get a falsy boolean from the environment variables. + * + * Falsy values are: `0`, `n`, `no`, `f`, `false`, `off`. + * + * @param {Object} options - The options for getting the falsy boolean. + * @param {string} options.key - The key of the environment variable. + * @param {boolean} [options.defaultValue] - The default value to return if the environment variable is not set. + * @param {boolean} [options.required] - Whether the environment variable is required. + */ +const getFalsyBooleanFromEnv = ({ + key, + defaultValue, + required, +}: GetFromEnvOptions): boolean => { + const value = getStringFromEnv({ + key, + defaultValue: String(defaultValue), + required, + }); + return falsyValues.includes(value.toLowerCase()); +}; + +/** + * Check if the current invocation is running in a development environment. + * + * This is determined by the `POWERTOOLS_DEV` environment variable. + */ +const isDevMode = (): boolean => { + return getTruthyBooleanFromEnv({ + key: 'POWERTOOLS_DEV', + defaultValue: false, + }); +}; + +/** + * Get the service name from the environment variables. + * + * This is determined by the `POWERTOOLS_SERVICE_NAME` environment variable. + */ +const getServiceName = (): string => { + return getStringFromEnv({ + key: 'POWERTOOLS_SERVICE_NAME', + defaultValue: '', + }); +}; + +export { + getStringFromEnv, + getNumberFromEnv, + getBooleanFromEnv, + getTruthyBooleanFromEnv, + getFalsyBooleanFromEnv, + isDevMode, + getServiceName, +}; From 3c01f3517ad61a5b6def82db34eeef858cfe31fb Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 May 2025 11:19:28 +0200 Subject: [PATCH 2/7] chore: add unit tests --- packages/commons/src/envUtils.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/commons/src/envUtils.ts b/packages/commons/src/envUtils.ts index a3b01b0a3..18aaa3d27 100644 --- a/packages/commons/src/envUtils.ts +++ b/packages/commons/src/envUtils.ts @@ -1,10 +1,4 @@ -import { isNumber } from './typeUtils.js'; - -type GetFromEnvOptions = { - key: string; - defaultValue?: unknown; - required?: boolean; -}; +import type { GetFromEnvOptions } from './types/index.js'; /** * Get a string from the environment variables. @@ -26,7 +20,7 @@ const getStringFromEnv = ({ throw new Error(`Environment variable ${key} is required`); } - return String(defaultValue) ?? ''; + return defaultValue ? String(defaultValue) : ''; } return value.trim(); @@ -51,11 +45,13 @@ const getNumberFromEnv = ({ required, }) as unknown; - if (!isNumber(value)) { + const parsedValue = Number(value); + + if (Number.isNaN(parsedValue)) { throw new Error(`Environment variable ${key} must be a number`); } - return value; + return parsedValue; }; /** @@ -83,7 +79,7 @@ const getBooleanFromEnv = ({ throw new Error(`Environment variable ${key} must be a boolean`); } - return Boolean(parsedValue); + return parsedValue === 'true'; }; const truthyValues = ['1', 'y', 'yes', 't', 'true', 'on']; From 0e15177ebe9d853201332cd63652c885c431a81d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 May 2025 14:08:43 +0200 Subject: [PATCH 3/7] chore: unit test coverage --- packages/commons/src/types/index.ts | 7 + packages/commons/tests/unit/envUtils.test.ts | 338 +++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 packages/commons/tests/unit/envUtils.test.ts diff --git a/packages/commons/src/types/index.ts b/packages/commons/src/types/index.ts index 080d13ed1..1ae90cc5e 100644 --- a/packages/commons/src/types/index.ts +++ b/packages/commons/src/types/index.ts @@ -20,3 +20,10 @@ export type { HandlerMethodDecorator, } from './LambdaInterface.js'; export type { ConfigServiceInterface } from './ConfigServiceInterface.js'; + +type GetFromEnvOptions = { + key: string; + defaultValue?: unknown; + required?: boolean; +}; +export type { GetFromEnvOptions }; diff --git a/packages/commons/tests/unit/envUtils.test.ts b/packages/commons/tests/unit/envUtils.test.ts new file mode 100644 index 000000000..8d7e1be89 --- /dev/null +++ b/packages/commons/tests/unit/envUtils.test.ts @@ -0,0 +1,338 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + getBooleanFromEnv, + getFalsyBooleanFromEnv, + getNumberFromEnv, + getServiceName, + getStringFromEnv, + getTruthyBooleanFromEnv, + isDevMode, +} from '../../src/envUtils.js'; + +describe('Functions: envUtils', () => { + const env = process.env; + + beforeEach(() => { + process.env = { ...env }; + }); + + describe('Function: getStringFromEnv', () => { + it('returns the value of the environment variable', () => { + // Prepare + process.env.TEST_ENV = 'testValue'; + + // Act + const result = getStringFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe('testValue'); + }); + + it('returns the default value if the environment variable is not set', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act + const result = getStringFromEnv({ + key: 'TEST_ENV', + defaultValue: 'defaultValue', + }); + + // Assess + expect(result).toBe('defaultValue'); + }); + + it('returns an empty string if the environment variable is not set and no default value is provided', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act + const result = getStringFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(''); + }); + + it('throws an error if the environment variable is not set and required is true', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act & Assess + expect(() => + getStringFromEnv({ key: 'TEST_ENV', required: true }) + ).toThrowError('Environment variable TEST_ENV is required'); + }); + + it('returns the trimmed value of the environment variable', () => { + // Prepare + process.env.TEST_ENV = ' testValue '; + + // Act + const result = getStringFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe('testValue'); + }); + }); + + describe('Function: getNumberFromEnv', () => { + it('returns the value of the environment variable as a number', () => { + // Prepare + process.env.TEST_ENV = '123'; + + // Act + const result = getNumberFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(123); + }); + + it('returns the default value if the environment variable is not set', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act + const result = getNumberFromEnv({ + key: 'TEST_ENV', + defaultValue: 456, + }); + + // Assess + expect(result).toBe(456); + }); + + it('throws an error if the environment variable is not a number', () => { + // Prepare + process.env.TEST_ENV = 'notANumber'; + + // Act & Assess + expect(() => getNumberFromEnv({ key: 'TEST_ENV' })).toThrowError( + 'Environment variable TEST_ENV must be a number' + ); + }); + }); + + describe('Function: getBooleanFromEnv', () => { + it('returns true if the environment variable is set to a truthy value', () => { + // Prepare + process.env.TEST_ENV = 'true'; + + // Act + const result = getBooleanFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(true); + }); + + it('returns false if the environment variable is set to a falsy value', () => { + // Prepare + process.env.TEST_ENV = 'false'; + + // Act + const result = getBooleanFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(false); + }); + + it('returns the default value if the environment variable is not set', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act + const result = getBooleanFromEnv({ + key: 'TEST_ENV', + defaultValue: true, + }); + + // Assess + expect(result).toBe(true); + }); + + it('throws an error if the environment variable value is not a boolean', () => { + // Prepare + process.env.TEST_ENV = 'notABoolean'; + + // Act & Assess + expect(() => getBooleanFromEnv({ key: 'TEST_ENV' })).toThrowError( + 'Environment variable TEST_ENV must be a boolean' + ); + }); + }); + + describe('Function: getTruthyBooleanFromEnv', () => { + it.each([ + ['1', true], + ['y', true], + ['yes', true], + ['t', true], + ['TRUE', true], + ['on', true], + ])( + 'returns true if the environment variable is set to a truthy value: %s', + (value, expected) => { + // Prepare + process.env.TEST_ENV = value; + + // Act + const result = getTruthyBooleanFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(expected); + } + ); + + it.each([ + ['', false], + ['false', false], + ['fasle', false], + ['somethingsilly', false], + ['0', false], + ])( + 'returns false if the environment variable is set to a falsy value: %s', + (value, expected) => { + // Prepare + process.env.TEST_ENV = value; + + // Act + const result = getTruthyBooleanFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(expected); + } + ); + + it('returns the default value if the environment variable is not set', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act + const result = getTruthyBooleanFromEnv({ + key: 'TEST_ENV', + defaultValue: true, + }); + + // Assess + expect(result).toBe(true); + }); + }); + + describe('Function: getFalsyBooleanFromEnv', () => { + it.each([ + ['0', true], + ['n', true], + ['no', true], + ['f', true], + ['FALSE', true], + ['off', true], + ])( + 'returns true if the environment variable is set to a truthy value: %s', + (value, expected) => { + // Prepare + process.env.TEST_ENV = value; + + // Act + const result = getFalsyBooleanFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(expected); + } + ); + + it.each([ + ['1', false], + ['y', false], + ['yes', false], + ['t', false], + ['TRUE', false], + ['on', false], + ['', false], + ['somethingsilly', false], + ])( + 'returns false if the environment variable is set to a falsy value: %s', + (value, expected) => { + // Prepare + process.env.TEST_ENV = value; + + // Act + const result = getFalsyBooleanFromEnv({ key: 'TEST_ENV' }); + + // Assess + expect(result).toBe(expected); + } + ); + + it('returns the default value if the environment variable is not set', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act + const result = getFalsyBooleanFromEnv({ + key: 'TEST_ENV', + defaultValue: false, + }); + + // Assess + expect(result).toBe(true); + }); + }); + + describe('Function: isDevMode', () => { + it('returns true if the environment variable is set to a truthy value', () => { + // Prepare + process.env.POWERTOOLS_DEV = 'true'; + + // Act + const result = isDevMode(); + + // Assess + expect(result).toBe(true); + }); + + it('returns false if the environment variable is set to a falsy value', () => { + // Prepare + process.env.POWERTOOLS_DEV = 'false'; + + // Act + const result = isDevMode(); + + // Assess + expect(result).toBe(false); + }); + + it('returns false if the environment variable is not set', () => { + // Prepare + process.env.POWERTOOLS_DEV = undefined; + + // Act + const result = isDevMode(); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: getServiceName', () => { + it('returns the service name from the environment variable', () => { + // Prepare + process.env.POWERTOOLS_SERVICE_NAME = 'testService'; + + // Act + const result = getServiceName(); + + // Assess + expect(result).toBe('testService'); + }); + + it('returns an empty string if the environment variable is not set', () => { + // Prepare + process.env.POWERTOOLS_SERVICE_NAME = undefined; + + // Act + const result = getServiceName(); + + // Assess + expect(result).toBe(''); + }); + }); +}); From b677110ae94c634738015ac922fa0019bd8329ca Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 May 2025 14:40:52 +0200 Subject: [PATCH 4/7] chore: make env vars required --- packages/commons/src/envUtils.ts | 220 +++++++++++++++---- packages/commons/src/types/envUtils.ts | 56 +++++ packages/commons/src/types/index.ts | 12 +- packages/commons/tests/unit/envUtils.test.ts | 32 +-- packages/commons/typedoc.json | 1 + 5 files changed, 259 insertions(+), 62 deletions(-) create mode 100644 packages/commons/src/types/envUtils.ts diff --git a/packages/commons/src/envUtils.ts b/packages/commons/src/envUtils.ts index 18aaa3d27..3e1c35d33 100644 --- a/packages/commons/src/envUtils.ts +++ b/packages/commons/src/envUtils.ts @@ -1,26 +1,56 @@ -import type { GetFromEnvOptions } from './types/index.js'; +import type { + GetBooleanFromEnvOptions, + GetNumberFromEnvOptions, + GetStringFromEnvOptions, +} from './types/envUtils.js'; /** * Get a string from the environment variables. * - * @param {Object} options - The options for getting the string. - * @param {string} options.key - The key of the environment variable. - * @param {string} [options.defaultValue] - The default value to return if the environment variable is not set. - * @param {boolean} [options.required] - Whether the environment variable is required. + * @example + * ```ts + * import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getStringFromEnv({ + * key: 'MY_ENV_VAR', + * errorMessage: 'MY_ENV_VAR is required for this function', + * }); + * ``` + * + * By default, the value is trimmed and always required. + * + * You can also provide a default value, which will be returned if the environment variable is not set instead of throwing an error. + * + * @example + * ```ts + * import { getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getStringFromEnv({ + * key: 'MY_ENV_VAR', + * defaultValue: 'defaultValue', + * }); + * ``` + * + * @param options - The options for getting the string. + * @param options.key - The key of the environment variable. + * @param options.defaultValue - Optional default value to return if the environment variable is not set. + * @param options.errorMessage - Optional error message to throw if the environment variable is not set and no default value is provided. Defaults to `"Environment variable is required"`. */ const getStringFromEnv = ({ key, defaultValue, - required, -}: GetFromEnvOptions): string => { + errorMessage, +}: GetStringFromEnvOptions): string => { const value = process.env[key]; if (value === undefined) { - if (required === true) { - throw new Error(`Environment variable ${key} is required`); + if (defaultValue !== undefined) { + return defaultValue; } - - return defaultValue ? String(defaultValue) : ''; + if (errorMessage) { + throw new Error(errorMessage); + } + throw new Error(`Environment variable ${key} is required`); } return value.trim(); @@ -29,21 +59,45 @@ const getStringFromEnv = ({ /** * Get a number from the environment variables. * - * @param {Object} options - The options for getting the number. - * @param {string} options.key - The key of the environment variable. - * @param {number} [options.defaultValue] - The default value to return if the environment variable is not set. - * @param {boolean} [options.required] - Whether the environment variable is required. + * @example + * ```ts + * import { getNumberFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getNumberFromEnv({ + * key: 'MY_ENV_VAR', + * errorMessage: 'MY_ENV_VAR is required for this function', + * }); + * ``` + * + * By default, the value is trimmed before being converted to a number and always required. + * + * You can also provide a default value, which will be returned if the environment variable is not set instead of throwing an error. + * + * @example + * ```ts + * import { getNumberFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getNumberFromEnv({ + * key: 'MY_ENV_VAR', + * defaultValue: 42, + * }); + * ``` + * + * @param options - The options for getting the number. + * @param options.key - The key of the environment variable. + * @param options.defaultValue - The default value to return if the environment variable is not set. + * @param options.errorMessage - Optional error message to throw if the environment variable is not set and no default value is provided. Defaults to `"Environment variable is required"`. */ const getNumberFromEnv = ({ key, defaultValue, - required, -}: GetFromEnvOptions): number => { + errorMessage, +}: GetNumberFromEnvOptions): number => { const value = getStringFromEnv({ key, defaultValue: String(defaultValue), - required, - }) as unknown; + errorMessage, + }); const parsedValue = Number(value); @@ -57,20 +111,44 @@ const getNumberFromEnv = ({ /** * Get a boolean from the environment variables. * - * @param {Object} options - The options for getting the boolean. - * @param {string} options.key - The key of the environment variable. - * @param {boolean} [options.defaultValue] - The default value to return if the environment variable is not set. - * @param {boolean} [options.required] - Whether the environment variable is required. + * @example + * ```ts + * import { getBooleanFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getBooleanFromEnv({ + * key: 'MY_ENV_VAR', + * errorMessage: 'MY_ENV_VAR is required for this function', + * }); + * ``` + * + * By default, the value is trimmed before being converted to a boolean and always required. + * + * You can also provide a default value, which will be returned if the environment variable is not set instead of throwing an error. + * + * @example + * ```ts + * import { getBooleanFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getBooleanFromEnv({ + * key: 'MY_ENV_VAR', + * defaultValue: true, + * }); + * ``` + * + * @param options - The options for getting the boolean. + * @param options.key - The key of the environment variable. + * @param options.defaultValue - The default value to return if the environment variable is not set. + * @param options.errorMessage - Optional error message to throw if the environment variable is not set and no default value is provided. Defaults to `"Environment variable is required"`. */ const getBooleanFromEnv = ({ key, defaultValue, - required, -}: GetFromEnvOptions): boolean => { + errorMessage, +}: GetBooleanFromEnvOptions): boolean => { const value = getStringFromEnv({ key, defaultValue: String(defaultValue), - required, + errorMessage, }); const parsedValue = value.toLowerCase(); @@ -89,20 +167,44 @@ const truthyValues = ['1', 'y', 'yes', 't', 'true', 'on']; * * Truthy values are: `1`, `y`, `yes`, `t`, `true`, `on`. * - * @param {Object} options - The options for getting the truthy boolean. - * @param {string} options.key - The key of the environment variable. - * @param {boolean} [options.defaultValue] - The default value to return if the environment variable is not set. - * @param {boolean} [options.required] - Whether the environment variable is required. + * @example + * ```ts + * import { getTruthyBooleanFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getTruthyBooleanFromEnv({ + * key: 'MY_ENV_VAR', + * errorMessage: 'MY_ENV_VAR is required for this function', + * }); + * ``` + * + * By default, the value is trimmed before being converted to a boolean and always required. + * + * You can also provide a default value, which will be returned if the environment variable is not set instead of throwing an error. + * + * @example + * ```ts + * import { getTruthyBooleanFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getTruthyBooleanFromEnv({ + * key: 'MY_ENV_VAR', + * defaultValue: true, + * }); + * ``` + * + * @param options - The options for getting the truthy boolean. + * @param options.key - The key of the environment variable. + * @param options.defaultValue - The default value to return if the environment variable is not set. + * @param options.errorMessage - Optional error message to throw if the environment variable is not set and no default value is provided. Defaults to `"Environment variable is required"`. */ const getTruthyBooleanFromEnv = ({ key, defaultValue, - required, -}: GetFromEnvOptions): boolean => { + errorMessage, +}: GetBooleanFromEnvOptions): boolean => { const value = getStringFromEnv({ key, defaultValue: String(defaultValue), - required, + errorMessage, }); return truthyValues.includes(value.toLowerCase()); @@ -115,20 +217,44 @@ const falsyValues = ['0', 'n', 'no', 'f', 'false', 'off']; * * Falsy values are: `0`, `n`, `no`, `f`, `false`, `off`. * - * @param {Object} options - The options for getting the falsy boolean. - * @param {string} options.key - The key of the environment variable. - * @param {boolean} [options.defaultValue] - The default value to return if the environment variable is not set. - * @param {boolean} [options.required] - Whether the environment variable is required. + * @example + * ```ts + * import { getFalsyBooleanFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getFalsyBooleanFromEnv({ + * key: 'MY_ENV_VAR', + * errorMessage: 'MY_ENV_VAR is required for this function', + * }); + * ``` + * + * By default, the value is trimmed before being converted to a boolean and always required. + * + * You can also provide a default value, which will be returned if the environment variable is not set instead of throwing an error. + * + * @example + * ```ts + * import { getFalsyBooleanFromEnv } from '@aws-lambda-powertools/commons/utils/env'; + * + * const myEnvVar = getFalsyBooleanFromEnv({ + * key: 'MY_ENV_VAR', + * defaultValue: false, + * }); + * ``` + * + * @param options - The options for getting the falsy boolean. + * @param options.key - The key of the environment variable. + * @param options.defaultValue - The default value to return if the environment variable is not set. + * @param options.errorMessage - Optional error message to throw if the environment variable is not set and no default value is provided. Defaults to `"Environment variable is required"`. */ const getFalsyBooleanFromEnv = ({ key, defaultValue, - required, -}: GetFromEnvOptions): boolean => { + errorMessage, +}: GetBooleanFromEnvOptions): boolean => { const value = getStringFromEnv({ key, defaultValue: String(defaultValue), - required, + errorMessage, }); return falsyValues.includes(value.toLowerCase()); }; @@ -137,6 +263,13 @@ const getFalsyBooleanFromEnv = ({ * Check if the current invocation is running in a development environment. * * This is determined by the `POWERTOOLS_DEV` environment variable. + * + * @example + * ```ts + * import { isDevMode } from '@aws-lambda-powertools/commons/utils/env'; + * + * const isDev = isDevMode(); + * ``` */ const isDevMode = (): boolean => { return getTruthyBooleanFromEnv({ @@ -149,6 +282,13 @@ const isDevMode = (): boolean => { * Get the service name from the environment variables. * * This is determined by the `POWERTOOLS_SERVICE_NAME` environment variable. + * + * @example + * ```ts + * import { getServiceName } from '@aws-lambda-powertools/commons/utils/env'; + * + * const serviceName = getServiceName(); + * ``` */ const getServiceName = (): string => { return getStringFromEnv({ diff --git a/packages/commons/src/types/envUtils.ts b/packages/commons/src/types/envUtils.ts new file mode 100644 index 000000000..b7ab42c3a --- /dev/null +++ b/packages/commons/src/types/envUtils.ts @@ -0,0 +1,56 @@ +type GetStringFromEnvOptions = { + /** + * The key of the environment variable. + */ + key: string; + /** + * Optional default value to return if the environment variable is not set. + * @default "" + */ + defaultValue?: string; + /** + * Optional error message to throw if the environment variable is not set and no default value is provided. + * @default "Environment variable is required" + */ + errorMessage?: string; +}; + +type GetNumberFromEnvOptions = { + /** + * The key of the environment variable. + */ + key: string; + /** + * The default value to return if the environment variable is not set. + * @default undefined + */ + defaultValue?: number; + /** + * Optional error message to throw if the environment variable is not set and no default value is provided. + * @default "Environment variable is required" + */ + errorMessage?: string; +}; + +type GetBooleanFromEnvOptions = { + /** + * The key of the environment variable. + */ + key: string; + /** + * The default value to return if the environment variable is not set. + * @default undefined + */ + defaultValue?: boolean; + /** + * Optional error message to throw if the environment variable is not set and no default value is provided. + * @default "Environment variable is required" + */ + errorMessage?: string; +}; + +export type { + GetStringFromEnvOptions, + GetNumberFromEnvOptions, + GetBooleanFromEnvOptions, +}; diff --git a/packages/commons/src/types/index.ts b/packages/commons/src/types/index.ts index 1ae90cc5e..de3d07f01 100644 --- a/packages/commons/src/types/index.ts +++ b/packages/commons/src/types/index.ts @@ -20,10 +20,8 @@ export type { HandlerMethodDecorator, } from './LambdaInterface.js'; export type { ConfigServiceInterface } from './ConfigServiceInterface.js'; - -type GetFromEnvOptions = { - key: string; - defaultValue?: unknown; - required?: boolean; -}; -export type { GetFromEnvOptions }; +export type { + GetStringFromEnvOptions, + GetBooleanFromEnvOptions, + GetNumberFromEnvOptions, +} from './envUtils.js'; diff --git a/packages/commons/tests/unit/envUtils.test.ts b/packages/commons/tests/unit/envUtils.test.ts index 8d7e1be89..276aa49c1 100644 --- a/packages/commons/tests/unit/envUtils.test.ts +++ b/packages/commons/tests/unit/envUtils.test.ts @@ -42,25 +42,14 @@ describe('Functions: envUtils', () => { expect(result).toBe('defaultValue'); }); - it('returns an empty string if the environment variable is not set and no default value is provided', () => { - // Prepare - process.env.TEST_ENV = undefined; - - // Act - const result = getStringFromEnv({ key: 'TEST_ENV' }); - - // Assess - expect(result).toBe(''); - }); - - it('throws an error if the environment variable is not set and required is true', () => { + it('throws an error if the environment variable is not set', () => { // Prepare process.env.TEST_ENV = undefined; // Act & Assess - expect(() => - getStringFromEnv({ key: 'TEST_ENV', required: true }) - ).toThrowError('Environment variable TEST_ENV is required'); + expect(() => getStringFromEnv({ key: 'TEST_ENV' })).toThrowError( + 'Environment variable TEST_ENV is required' + ); }); it('returns the trimmed value of the environment variable', () => { @@ -73,6 +62,19 @@ describe('Functions: envUtils', () => { // Assess expect(result).toBe('testValue'); }); + + it('uses the provided error message if the environment variable is not set', () => { + // Prepare + process.env.TEST_ENV = undefined; + + // Act & Assess + expect(() => + getStringFromEnv({ + key: 'TEST_ENV', + errorMessage: 'Custom error message', + }) + ).toThrowError('Custom error message'); + }); }); describe('Function: getNumberFromEnv', () => { diff --git a/packages/commons/typedoc.json b/packages/commons/typedoc.json index d34f7f99e..1ba85b7f7 100644 --- a/packages/commons/typedoc.json +++ b/packages/commons/typedoc.json @@ -7,6 +7,7 @@ "./src/types/index.ts", "./src/middleware/cleanupMiddlewares.ts", "./src/typeUtils.ts", + "./src/envUtils.ts", "./src/fromBase64.ts", "./src/LRUCache.ts" ], From d92f76ed09fbeb9a8764383c97b44d99428d63d9 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 May 2025 15:31:26 +0200 Subject: [PATCH 5/7] chore: add xray helpers --- .../src/config/EnvironmentVariablesService.ts | 45 ++++--------- packages/commons/src/constants.ts | 9 +++ packages/commons/src/envUtils.ts | 65 ++++++++++++++++++- .../unit/EnvironmentVariablesService.test.ts | 49 -------------- packages/commons/tests/unit/envUtils.test.ts | 65 +++++++++++++++++++ 5 files changed, 151 insertions(+), 82 deletions(-) create mode 100644 packages/commons/src/constants.ts diff --git a/packages/commons/src/config/EnvironmentVariablesService.ts b/packages/commons/src/config/EnvironmentVariablesService.ts index d9d45d8f8..aa1a7de41 100644 --- a/packages/commons/src/config/EnvironmentVariablesService.ts +++ b/packages/commons/src/config/EnvironmentVariablesService.ts @@ -1,3 +1,13 @@ +import { + getFalsyBooleanFromEnv, + getServiceName, + getStringFromEnv, + getTruthyBooleanFromEnv, + getXRayTraceIdFromEnv, + getXrayTraceDataFromEnv, + isDevMode, + isRequestXRaySampled, +} from '../envUtils.js'; import type { ConfigServiceInterface } from '../types/ConfigServiceInterface.js'; /** @@ -46,7 +56,7 @@ class EnvironmentVariablesService implements ConfigServiceInterface { * Get the value of the `POWERTOOLS_SERVICE_NAME` environment variable. */ public getServiceName(): string { - return this.get(this.serviceNameVariable); + return getServiceName(); } /** @@ -58,9 +68,7 @@ class EnvironmentVariablesService implements ConfigServiceInterface { * The actual Trace ID is: `1-5759e988-bd862e3fe1be46a994272793`. */ public getXrayTraceId(): string | undefined { - const xRayTraceData = this.getXrayTraceData(); - - return xRayTraceData?.Root; + return getXRayTraceIdFromEnv(); } /** @@ -70,16 +78,14 @@ class EnvironmentVariablesService implements ConfigServiceInterface { * `Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1`, */ public getXrayTraceSampled(): boolean { - const xRayTraceData = this.getXrayTraceData(); - - return xRayTraceData?.Sampled === '1'; + return isRequestXRaySampled(); } /** * Determine if the current invocation is running in a development environment. */ public isDevMode(): boolean { - return this.isValueTrue(this.get(this.devModeVariable)); + return isDevMode(); } /** @@ -103,29 +109,6 @@ class EnvironmentVariablesService implements ConfigServiceInterface { return falsyValues.includes(value.toLowerCase()); } - - /** - * Get the AWS X-Ray Trace data from the environment variable. - * - * The method parses the environment variable `_X_AMZN_TRACE_ID` and returns an object with the key-value pairs. - */ - private getXrayTraceData(): Record | undefined { - const xRayTraceEnv = this.get(this.xRayTraceIdVariable); - - if (xRayTraceEnv === '') return undefined; - - if (!xRayTraceEnv.includes('=')) return { Root: xRayTraceEnv }; - - const xRayTraceData: Record = {}; - - for (const field of xRayTraceEnv.split(';')) { - const [key, value] = field.split('='); - - xRayTraceData[key] = value; - } - - return xRayTraceData; - } } export { EnvironmentVariablesService }; diff --git a/packages/commons/src/constants.ts b/packages/commons/src/constants.ts new file mode 100644 index 000000000..6a7a3eb7b --- /dev/null +++ b/packages/commons/src/constants.ts @@ -0,0 +1,9 @@ +const POWERTOOLS_DEV_ENV_VAR = 'POWERTOOLS_DEV' as const; +const POWERTOOLS_SERVICE_NAME_ENV_VAR = 'POWERTOOLS_SERVICE_NAME' as const; +const XRAY_TRACE_ID_ENV_VAR = '_X_AMZN_TRACE_ID' as const; + +export { + POWERTOOLS_DEV_ENV_VAR, + POWERTOOLS_SERVICE_NAME_ENV_VAR, + XRAY_TRACE_ID_ENV_VAR, +}; diff --git a/packages/commons/src/envUtils.ts b/packages/commons/src/envUtils.ts index 3e1c35d33..9528ae66d 100644 --- a/packages/commons/src/envUtils.ts +++ b/packages/commons/src/envUtils.ts @@ -1,3 +1,8 @@ +import { + POWERTOOLS_DEV_ENV_VAR, + POWERTOOLS_SERVICE_NAME_ENV_VAR, + XRAY_TRACE_ID_ENV_VAR, +} from './constants.js'; import type { GetBooleanFromEnvOptions, GetNumberFromEnvOptions, @@ -273,7 +278,7 @@ const getFalsyBooleanFromEnv = ({ */ const isDevMode = (): boolean => { return getTruthyBooleanFromEnv({ - key: 'POWERTOOLS_DEV', + key: POWERTOOLS_DEV_ENV_VAR, defaultValue: false, }); }; @@ -292,11 +297,64 @@ const isDevMode = (): boolean => { */ const getServiceName = (): string => { return getStringFromEnv({ - key: 'POWERTOOLS_SERVICE_NAME', + key: POWERTOOLS_SERVICE_NAME_ENV_VAR, defaultValue: '', }); }; +/** + * Get the AWS X-Ray Trace data from the environment variable. + * + * The method parses the environment variable `_X_AMZN_TRACE_ID` and returns an object with the key-value pairs. + */ +const getXrayTraceDataFromEnv = (): Record | undefined => { + const xRayTraceEnv = getStringFromEnv({ + key: XRAY_TRACE_ID_ENV_VAR, + defaultValue: '', + }); + if (xRayTraceEnv === '') { + return undefined; + } + if (!xRayTraceEnv.includes('=')) { + return { + Root: xRayTraceEnv, + }; + } + const xRayTraceData: Record = {}; + + for (const field of xRayTraceEnv.split(';')) { + const [key, value] = field.split('='); + + xRayTraceData[key] = value; + } + + return xRayTraceData; +}; + +/** + * Determine if the current invocation is part of a sampled X-Ray trace. + * + * The AWS X-Ray Trace data available in the environment variable has this format: + * `Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1`, + */ +const isRequestXRaySampled = (): boolean => { + const xRayTraceData = getXrayTraceDataFromEnv(); + return xRayTraceData?.Sampled === '1'; +}; + +/** + * Get the value of the `_X_AMZN_TRACE_ID` environment variable. + * + * The AWS X-Ray Trace data available in the environment variable has this format: + * `Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1`, + * + * The actual Trace ID is: `1-5759e988-bd862e3fe1be46a994272793`. + */ +const getXRayTraceIdFromEnv = (): string | undefined => { + const xRayTraceData = getXrayTraceDataFromEnv(); + return xRayTraceData?.Root; +}; + export { getStringFromEnv, getNumberFromEnv, @@ -305,4 +363,7 @@ export { getFalsyBooleanFromEnv, isDevMode, getServiceName, + getXrayTraceDataFromEnv, + isRequestXRaySampled, + getXRayTraceIdFromEnv, }; diff --git a/packages/commons/tests/unit/EnvironmentVariablesService.test.ts b/packages/commons/tests/unit/EnvironmentVariablesService.test.ts index fcf997452..6f0c50dca 100644 --- a/packages/commons/tests/unit/EnvironmentVariablesService.test.ts +++ b/packages/commons/tests/unit/EnvironmentVariablesService.test.ts @@ -65,30 +65,6 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual('abcd123456789'); }); - it('returns the value of the Root X-Ray segment ID properly formatted', () => { - // Prepare - process.env._X_AMZN_TRACE_ID = - 'Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1'; - const service = new EnvironmentVariablesService(); - - // Act - const value = service.getXrayTraceId(); - - // Assess - expect(value).toEqual('1-5759e988-bd862e3fe1be46a994272793'); - }); - - it('returns the value of the Root X-Ray segment ID properly formatted', () => { - // Prepare - process.env._X_AMZN_TRACE_ID = undefined; - const service = new EnvironmentVariablesService(); - - // Act - const value = service.getXrayTraceId(); - - // Assess - expect(value).toEqual(undefined); - }); }); describe('Method: getXrayTraceSampled', () => { @@ -104,31 +80,6 @@ describe('Class: EnvironmentVariablesService', () => { // Assess expect(value).toEqual(true); }); - - it('returns false if the Sampled flag is not set in the _X_AMZN_TRACE_ID environment variable', () => { - // Prepare - process.env._X_AMZN_TRACE_ID = - 'Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047'; - const service = new EnvironmentVariablesService(); - - // Act - const value = service.getXrayTraceSampled(); - - // Assess - expect(value).toEqual(false); - }); - - it('returns false when no _X_AMZN_TRACE_ID environment variable is present', () => { - // Prepare - process.env._X_AMZN_TRACE_ID = undefined; - const service = new EnvironmentVariablesService(); - - // Act - const value = service.getXrayTraceSampled(); - - // Assess - expect(value).toEqual(false); - }); }); describe('Method: isValueTrue', () => { diff --git a/packages/commons/tests/unit/envUtils.test.ts b/packages/commons/tests/unit/envUtils.test.ts index 276aa49c1..3cf696091 100644 --- a/packages/commons/tests/unit/envUtils.test.ts +++ b/packages/commons/tests/unit/envUtils.test.ts @@ -6,7 +6,10 @@ import { getServiceName, getStringFromEnv, getTruthyBooleanFromEnv, + getXRayTraceIdFromEnv, + getXrayTraceDataFromEnv, isDevMode, + isRequestXRaySampled, } from '../../src/envUtils.js'; describe('Functions: envUtils', () => { @@ -337,4 +340,66 @@ describe('Functions: envUtils', () => { expect(result).toBe(''); }); }); + + describe('Function: getXrayTraceIdFromEnv', () => { + it('returns the value of the environment variable _X_AMZN_TRACE_ID', () => { + // Prepare + process.env._X_AMZN_TRACE_ID = 'abcd123456789'; + + // Act + const value = getXRayTraceIdFromEnv(); + + // Assess + expect(value).toEqual('abcd123456789'); + }); + + it('returns the value of the Root X-Ray segment ID properly formatted', () => { + // Prepare + process.env._X_AMZN_TRACE_ID = + 'Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1'; + + // Act + const value = getXRayTraceIdFromEnv(); + + // Assess + expect(value).toEqual('1-5759e988-bd862e3fe1be46a994272793'); + }); + }); + + describe('Function: isRequestXRaySampled', () => { + it('returns true if the Sampled flag is set in the _X_AMZN_TRACE_ID environment variable', () => { + // Prepare + process.env._X_AMZN_TRACE_ID = + 'Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047;Sampled=1'; + + // Act + const value = isRequestXRaySampled(); + + // Assess + expect(value).toEqual(true); + }); + + it('returns false if the Sampled flag is not set in the _X_AMZN_TRACE_ID environment variable', () => { + // Prepare + process.env._X_AMZN_TRACE_ID = + 'Root=1-5759e988-bd862e3fe1be46a994272793;Parent=557abcec3ee5a047'; + + // Act + const value = isRequestXRaySampled(); + + // Assess + expect(value).toEqual(false); + }); + + it('returns false when no _X_AMZN_TRACE_ID environment variable is present', () => { + // Prepare + process.env._X_AMZN_TRACE_ID = undefined; + + // Act + const value = isRequestXRaySampled(); + + // Assess + expect(value).toEqual(false); + }); + }); }); From 7c1712236f72fec42e9cc4f103a4675a365a18a1 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 May 2025 15:37:15 +0200 Subject: [PATCH 6/7] chore: address sonarqube findings --- packages/commons/src/config/EnvironmentVariablesService.ts | 4 ---- packages/commons/tests/unit/envUtils.test.ts | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/commons/src/config/EnvironmentVariablesService.ts b/packages/commons/src/config/EnvironmentVariablesService.ts index aa1a7de41..a4bfaa2bc 100644 --- a/packages/commons/src/config/EnvironmentVariablesService.ts +++ b/packages/commons/src/config/EnvironmentVariablesService.ts @@ -1,10 +1,6 @@ import { - getFalsyBooleanFromEnv, getServiceName, - getStringFromEnv, - getTruthyBooleanFromEnv, getXRayTraceIdFromEnv, - getXrayTraceDataFromEnv, isDevMode, isRequestXRaySampled, } from '../envUtils.js'; diff --git a/packages/commons/tests/unit/envUtils.test.ts b/packages/commons/tests/unit/envUtils.test.ts index 3cf696091..a58fd8e61 100644 --- a/packages/commons/tests/unit/envUtils.test.ts +++ b/packages/commons/tests/unit/envUtils.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; import { getBooleanFromEnv, getFalsyBooleanFromEnv, @@ -7,7 +7,6 @@ import { getStringFromEnv, getTruthyBooleanFromEnv, getXRayTraceIdFromEnv, - getXrayTraceDataFromEnv, isDevMode, isRequestXRaySampled, } from '../../src/envUtils.js'; From 405172dd3b350fa5069188a442d79ce1a3e37176 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 22 May 2025 10:31:22 +0200 Subject: [PATCH 7/7] refactor: make arrays to set --- packages/commons/src/envUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/commons/src/envUtils.ts b/packages/commons/src/envUtils.ts index 9528ae66d..3c561187a 100644 --- a/packages/commons/src/envUtils.ts +++ b/packages/commons/src/envUtils.ts @@ -165,7 +165,7 @@ const getBooleanFromEnv = ({ return parsedValue === 'true'; }; -const truthyValues = ['1', 'y', 'yes', 't', 'true', 'on']; +const truthyValues = new Set(['1', 'y', 'yes', 't', 'true', 'on']); /** * Get a truthy boolean from the environment variables. @@ -212,10 +212,10 @@ const getTruthyBooleanFromEnv = ({ errorMessage, }); - return truthyValues.includes(value.toLowerCase()); + return truthyValues.has(value.toLowerCase()); }; -const falsyValues = ['0', 'n', 'no', 'f', 'false', 'off']; +const falsyValues = new Set(['0', 'n', 'no', 'f', 'false', 'off']); /** * Get a falsy boolean from the environment variables. @@ -261,7 +261,7 @@ const getFalsyBooleanFromEnv = ({ defaultValue: String(defaultValue), errorMessage, }); - return falsyValues.includes(value.toLowerCase()); + return falsyValues.has(value.toLowerCase()); }; /**