UNPKG

@aws-lambda-powertools/parameters

Version:
305 lines (304 loc) 12.6 kB
import { AppConfigDataClient, GetLatestConfigurationCommand, StartConfigurationSessionCommand, } from '@aws-sdk/client-appconfigdata'; import { BaseProvider } from '../base/BaseProvider.js'; import { APPCONFIG_TOKEN_EXPIRATION } from '../constants.js'; /** * ## Intro * The Parameters utility provides an AppConfigProvider that allows to retrieve configuration profiles from AWS AppConfig. * * ## Getting started * * This utility supports AWS SDK v3 for JavaScript only (`@aws-sdk/client-appconfigdata`). This allows the utility to be modular, and you to install only * the SDK packages you need and keep your bundle size small. * * ## Basic usage * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a configuration profile * const encodedConfig = await configProvider.get('my-config'); * const config = new TextDecoder('utf-8').decode(encodedConfig); * }; * ``` * If you want to retrieve configs without customizing the provider, you can use the {@link getAppConfig} function instead. * * ## Advanced usage * * ### Caching * * By default, the provider will cache parameters retrieved in-memory for 5 seconds. * You can adjust how long values should be kept in cache by using the `maxAge` parameter. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a configuration profile and cache it for 10 seconds * const encodedConfig = await configProvider.get('my-config', { maxAge: 10 }); * const config = new TextDecoder('utf-8').decode(encodedConfig); * }; * ``` * * If instead you'd like to always ensure you fetch the latest parameter from the store regardless if already available in cache, use the `forceFetch` parameter. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a config and always fetch the latest value * const config = await configProvider.get('my-config', { forceFetch: true }); * const config = new TextDecoder('utf-8').decode(encodedConfig); * }; * ``` * * ### Transformations * * For configurations stored as freeform JSON, Freature Flag, you can use the transform argument for deserialization. This will return a JavaScript object instead of a string. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a JSON config or Feature Flag and parse it as JSON * const config = await configProvider.get('my-config', { transform: 'json' }); * }; * ``` * * For configurations that are instead stored as base64-encoded binary data, you can use the transform argument set to `binary` for decoding. This will return a decoded string. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a base64-encoded string and decode it * const config = await configProvider.get('my-config', { transform: 'binary' }); * }; * ``` * * ### Extra SDK options * * When retrieving a configuration profile, you can pass extra options to the AWS SDK v3 for JavaScript client by using the `sdkOptions` parameter. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a config and pass extra options to the AWS SDK v3 for JavaScript client * const config = await configProvider.get('my-config', { * sdkOptions: { * RequiredMinimumPollIntervalInSeconds: 60, * }, * }); * const config = new TextDecoder('utf-8').decode(encodedConfig); * }; * ``` * * This object accepts the same options as the [AWS SDK v3 for JavaScript AppConfigData client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-appconfigdata/interfaces/startconfigurationsessioncommandinput.html). * * ### Customize AWS SDK v3 for JavaScript client * * By default, the provider will create a new AppConfigData client using the default configuration. * * You can customize the client by passing a custom configuration object to the provider. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * clientConfig: { region: 'eu-west-1' }, * }); * ``` * * This object accepts the same options as the [AWS SDK v3 for JavaScript AppConfig Data client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-appconfigdata/interfaces/appconfigdataclientconfig.html). * * Otherwise, if you want to use a custom client altogether, you can pass it to the provider. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * import { AppConfigDataClient } from '@aws-sdk/client-appconfigdata'; * * const client = new AppConfigDataClient({ region: 'eu-west-1' }); * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * awsSdkV3Client: client, * }); * ``` * * This object must be an instance of the [AWS SDK v3 for JavaScript AppConfig Data client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-appconfigdata/classes/appconfigdataclient.html). * * For more usage examples, see [our documentation](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parameters/). */ class AppConfigProvider extends BaseProvider { configurationTokenStore = new Map(); valueStore = new Map(); application; environment; /** * It initializes the AppConfigProvider class. * * * @param {AppConfigProviderOptions} options - The configuration object. */ constructor(options) { super({ awsSdkV3ClientPrototype: AppConfigDataClient, clientConfig: options.clientConfig, awsSdkV3Client: options.awsSdkV3Client, }); const { application, environment } = options; this.application = application ?? this.envVarsService.getServiceName(); if (!this.application || this.application.trim().length === 0) { throw new Error('Application name is not defined or POWERTOOLS_SERVICE_NAME is not set'); } this.environment = environment; } /** * Retrieve a configuration profile from AWS AppConfig. * * @example * ```typescript * import { AppConfigProvider } from '@aws-lambda-powertools/parameters/appconfig'; * * const configProvider = new AppConfigProvider({ * application: 'my-app', * environment: 'prod', * }); * * export const handler = async (): Promise<void> => { * // Retrieve a configuration profile * const encodedConfig = await configProvider.get('my-config'); * const config = new TextDecoder('utf-8').decode(encodedConfig); * }; * ``` * * You can customize the retrieval of the configuration profile by passing options to the function: * * `maxAge` - The maximum age of the value in cache before fetching a new one (in seconds) (default: 5) * * `forceFetch` - Whether to always fetch a new value from the store regardless if already available in cache * * `transform` - Whether to transform the value before returning it. Supported values: `json`, `binary` * * `sdkOptions` - Extra options to pass to the AWS SDK v3 for JavaScript client * * For usage examples check {@link AppConfigProvider}. * * @param {string} name - The name of the configuration profile or its ID * @param {AppConfigGetOptions} options - Options to configure the provider * @see https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/parameters/ */ async get(name, options) { return super.get(name, options); } /** * Retrieving multiple configurations is not supported by AWS AppConfig. */ /* v8 ignore start */ async getMultiple(path, _options) { await super.getMultiple(path); } /* v8 ignore stop */ /** * Retrieve a configuration from AWS AppConfig. * * First we start the session and after that we retrieve the configuration from AppSync. * When starting a session, the service returns a token that can be used to poll for changes * for up to 24hrs, so we cache it for later use together with the expiration date. * * The value of the configuration is also cached internally because AppConfig returns an empty * value if the configuration has not changed since the last poll. This way even if your code * polls the configuration multiple times, we return the most recent value by returning the cached * one if an empty response is returned by AppConfig. * * @param {string} name - Name of the configuration or its ID * @param {AppConfigGetOptions} options - SDK options to propagate to `StartConfigurationSession` API call */ async _get(name, options) { if (!this.configurationTokenStore.has(name) || // biome-ignore lint/style/noNonNullAssertion: we check if the value is in the map before accessing it this.configurationTokenStore.get(name).expiration <= Date.now()) { const sessionOptions = { ...(options?.sdkOptions || {}), ApplicationIdentifier: this.application, ConfigurationProfileIdentifier: name, EnvironmentIdentifier: this.environment, }; const sessionCommand = new StartConfigurationSessionCommand(sessionOptions); const session = await this.client.send(sessionCommand); if (!session.InitialConfigurationToken) throw new Error('Unable to retrieve the configuration token'); this.configurationTokenStore.set(name, { value: session.InitialConfigurationToken, expiration: Date.now() + APPCONFIG_TOKEN_EXPIRATION, }); } const getConfigurationCommand = new GetLatestConfigurationCommand({ ConfigurationToken: this.configurationTokenStore.get(name)?.value, }); const response = await this.client.send(getConfigurationCommand); if (response.NextPollConfigurationToken) { this.configurationTokenStore.set(name, { value: response.NextPollConfigurationToken, expiration: Date.now() + APPCONFIG_TOKEN_EXPIRATION, }); } else { this.configurationTokenStore.delete(name); } /** When the response is not empty, stash the result locally before returning * See AppConfig docs: * {@link https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-retrieving-the-configuration.html} **/ if (response.Configuration !== undefined && response.Configuration?.length > 0) { this.valueStore.set(name, response.Configuration); return response.Configuration; } // Otherwise, use a stashed value return this.valueStore.get(name); } /** * Retrieving multiple configurations is not supported by AWS AppConfig. * * @throws Not Implemented Error. */ async _getMultiple(_path, _sdkOptions) { throw new Error('Method not implemented.'); } } export { AppConfigProvider };