From ad876a4df710691618274396cb5d5c6040b7cc31 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Wed, 21 Dec 2022 19:28:36 +0400 Subject: [PATCH 1/8] feat(parameters): add appconfig provider with types --- packages/parameters/src/AppConfigProvider.ts | 111 ++++++++++++++++++ .../parameters/src/types/AppConfigProvider.ts | 18 +++ 2 files changed, 129 insertions(+) create mode 100644 packages/parameters/src/AppConfigProvider.ts create mode 100644 packages/parameters/src/types/AppConfigProvider.ts diff --git a/packages/parameters/src/AppConfigProvider.ts b/packages/parameters/src/AppConfigProvider.ts new file mode 100644 index 0000000000..05b77aad72 --- /dev/null +++ b/packages/parameters/src/AppConfigProvider.ts @@ -0,0 +1,111 @@ +import { BaseProvider, DEFAULT_PROVIDERS } from 'BaseProvider'; +import { + AppConfigDataClient, + StartConfigurationSessionCommand, + GetLatestConfigurationCommand, +} from '@aws-sdk/client-appconfigdata'; +import type { + StartConfigurationSessionCommandInput, + GetLatestConfigurationCommandInput, +} from '@aws-sdk/client-appconfigdata'; +import type { AppConfigGetOptionsInterface } from './types/AppConfigProvider'; + +class AppConfigProvider extends BaseProvider { + public client: AppConfigDataClient; + private application?: string; + private environment?: string; + private token: string | undefined; + + /** + * It initializes the AppConfigProvider class with an optional set of options like region: 'us-west-1'. + * * + * @param {AppConfigDataClientConfig} options + */ + public constructor(options: AppConfigGetOptionsInterface = {}) { + super(); + this.client = new AppConfigDataClient(options.config || {}); + this.environment = options.environment; + this.application = options.application || 'service_undefined'; // TODO: get value from ENV VARIABLES + } + + /** + * Retrieve a parameter value from AWS App config. + * + * @param {string} name - Name of the configuration + * @param {StartConfigurationSessionCommandInput} [sdkOptions] - SDK options to propagate to `StartConfigurationSession` API call + * @returns {Promise} + */ + protected async _get( + name: string, + options?: AppConfigGetOptionsInterface + ): Promise { + /** + * The new AppConfig APIs require two API calls to return the configuration + * First we start the session and after that we retrieve the configuration + * We need to store the token to use in the next execution + **/ + if (!this.token) { + const sessionOptions: StartConfigurationSessionCommandInput = { + ConfigurationProfileIdentifier: name, + EnvironmentIdentifier: this.environment, + ApplicationIdentifier: this.application, + }; + if (options && options.hasOwnProperty('sdkOptions')) { + Object.assign(sessionOptions, options.sdkOptions); + } + + const sessionCommand = new StartConfigurationSessionCommand( + sessionOptions + ); + + const session = await this.client.send(sessionCommand); + this.token = session.InitialConfigurationToken; + } + + const getConfigurationCommand = new GetLatestConfigurationCommand({ + ConfigurationToken: this.token, + }); + const response = await this.client.send(getConfigurationCommand); + + // Should we cache and flush the token after 24 hours? + // NextPollConfigurationToken expires in 24 hours after the last request and causes `BadRequestException` + this.token = response.NextPollConfigurationToken; + + const configuration = response.Configuration; + + // should we convert Uint8Array to string? + const utf8decoder = new TextDecoder(); + const value = configuration ? utf8decoder.decode(configuration) : undefined; + + return value; + } + + /** + * Retrieving multiple parameter values is not supported with AWS App Config Provider. + * + * @throws Not Implemented Error. + */ + protected async _getMultiple( + path: string, + sdkOptions?: Partial + ): Promise> { + return this._notImplementedError(); + } + + private _notImplementedError(): never { + throw new Error('Not Implemented'); + } +} + +const getAppConfig = ( + name: string, + options?: AppConfigGetOptionsInterface +): Promise> => { + if (!DEFAULT_PROVIDERS.hasOwnProperty('appconfig')) { + DEFAULT_PROVIDERS.appconfig = new AppConfigProvider(options); + } + + return DEFAULT_PROVIDERS.appconfig.get(name, options); +}; + +export { AppConfigProvider, getAppConfig }; diff --git a/packages/parameters/src/types/AppConfigProvider.ts b/packages/parameters/src/types/AppConfigProvider.ts new file mode 100644 index 0000000000..ade0e7a2f2 --- /dev/null +++ b/packages/parameters/src/types/AppConfigProvider.ts @@ -0,0 +1,18 @@ +import type { StartConfigurationSessionCommandInput, AppConfigDataClientConfig } from '@aws-sdk/client-appconfigdata'; +import type { GetOptionsInterface } from 'types/BaseProvider'; + +/** + * Options for the AppConfigProvider get method. + * + * @interface AppConfigGetOptionsInterface + * @extends {GetOptionsInterface} + * @property {Partial} sdkOptions - Options for the AWS SDK. + */ +interface AppConfigGetOptionsInterface extends Omit { + application?: string + config?: AppConfigDataClientConfig + environment?: string + sdkOptions?: Partial +} + +export { AppConfigGetOptionsInterface }; From 58cecefca5f485dfa6edc15c212e01b86514712f Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 23 Dec 2022 14:39:27 +0400 Subject: [PATCH 2/8] feat: save the latest retrived configuration --- packages/parameters/src/AppConfigProvider.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/parameters/src/AppConfigProvider.ts b/packages/parameters/src/AppConfigProvider.ts index 05b77aad72..006183565d 100644 --- a/packages/parameters/src/AppConfigProvider.ts +++ b/packages/parameters/src/AppConfigProvider.ts @@ -15,6 +15,7 @@ class AppConfigProvider extends BaseProvider { private application?: string; private environment?: string; private token: string | undefined; + private latestConfiguration: string | undefined; /** * It initializes the AppConfigProvider class with an optional set of options like region: 'us-west-1'. @@ -77,7 +78,11 @@ class AppConfigProvider extends BaseProvider { const utf8decoder = new TextDecoder(); const value = configuration ? utf8decoder.decode(configuration) : undefined; - return value; + if (value) { + this.latestConfiguration = value; + } + + return this.latestConfiguration; } /** From f36b06f1ad8bf51119183d06e984d3030ff2c38a Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 23 Dec 2022 14:43:00 +0400 Subject: [PATCH 3/8] refactor: refactor interface --- packages/parameters/src/types/BaseProvider.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/parameters/src/types/BaseProvider.ts b/packages/parameters/src/types/BaseProvider.ts index 8f6754d2d3..b7e95e602b 100644 --- a/packages/parameters/src/types/BaseProvider.ts +++ b/packages/parameters/src/types/BaseProvider.ts @@ -7,11 +7,7 @@ interface GetOptionsInterface { transform?: TransformOptions } -interface GetMultipleOptionsInterface { - maxAge?: number - forceFetch?: boolean - sdkOptions?: unknown - transform?: string +interface GetMultipleOptionsInterface extends GetOptionsInterface { throwOnTransformError?: boolean } From 184d4b335e1fe11a42e5255e3861223d56eb29bb Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 30 Dec 2022 13:03:03 +0400 Subject: [PATCH 4/8] feat: AppConfigProvider --- package-lock.json | 689 +++++++++++++++++- packages/parameters/package.json | 10 +- packages/parameters/src/AppConfigProvider.ts | 55 +- packages/parameters/src/BaseProvider.ts | 4 +- .../parameters/src/types/AppConfigProvider.ts | 19 +- .../tests/unit/AppConfigProvider.test.ts | 60 ++ 6 files changed, 797 insertions(+), 40 deletions(-) create mode 100644 packages/parameters/tests/unit/AppConfigProvider.test.ts diff --git a/package-lock.json b/package-lock.json index 7bcb6cbcd4..1a967c5e9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -346,6 +346,365 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-appconfigdata": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-appconfigdata/-/client-appconfigdata-3.241.0.tgz", + "integrity": "sha512-5AVfSc6ZQ17dF0cdIrmpEe0H6vmGumBvsFGn89e2clSPtqqtsFDpBeAsS5jCMbxO9pakkCM0RPAh+sci6MnlYg==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.241.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/client-sso": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.241.0.tgz", + "integrity": "sha512-Jm4HR+RYAqKMEYZvvWaq0NYUKKonyInOeubObXH4BLXZpmUBSdYCSjjLdNJY3jkQoxbDVPVMIurVNh5zT5SMRw==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.241.0.tgz", + "integrity": "sha512-/Ml2QBGpGfUEeBrPzBZhSTBkHuXFD2EAZEIHGCBH4tKaURDI6/FoGI8P1Rl4BzoFt+II/Cr91Eox6YT9EwChsQ==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/client-sts": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.241.0.tgz", + "integrity": "sha512-vmlG8cJzRf8skCtTJbA2wBvD2c3NQ5gZryzJvTKDS06KzBzcEpnjlLseuTekcnOiRNekbFUX5hRu5Zj3N2ReLg==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-sdk-sts": "3.226.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "fast-xml-parser": "4.0.11", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/config-resolver": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.234.0.tgz", + "integrity": "sha512-uZxy4wzllfvgCQxVc+Iqhde0NGAnfmV2hWR6ejadJaAFTuYNvQiRg9IqJy3pkyDPqXySiJ8Bom5PoJfgn55J/A==", + "dev": true, + "dependencies": { + "@aws-sdk/signature-v4": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-config-provider": "3.208.0", + "@aws-sdk/util-middleware": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.241.0.tgz", + "integrity": "sha512-CI+mu6h74Kzmscw35TvNkc/wYHsHPGAwP7humSHoWw53H9mVw21Ggft/dT1iFQQZWQ8BNXkzuXlNo1IlqwMgOA==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.241.0.tgz", + "integrity": "sha512-08zPQcD5o9brQmzEipWHeHgU85aQcEF8MWLfpeyjO6e1/l7ysQ35NsS+PYtv77nLpGCx/X+ZuW/KXWoRrbw77w==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-ini": "3.241.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.241.0.tgz", + "integrity": "sha512-6Bjd6eEIrVomRTrPrM4dlxusQm+KMJ9hLYKECCpFkwDKIK+pTgZNLRtQdalHyzwneHJPdimrm8cOv1kUQ8hPoA==", + "dev": true, + "dependencies": { + "@aws-sdk/client-sso": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/token-providers": "3.241.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/middleware-retry": { + "version": "3.235.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.235.0.tgz", + "integrity": "sha512-50WHbJGpD3SNp9763MAlHqIhXil++JdQbKejNpHg7HsJne/ao3ub+fDOfx//mMBjpzBV25BGd5UlfL6blrClSg==", + "dev": true, + "dependencies": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/service-error-classification": "3.229.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-middleware": "3.226.0", + "@aws-sdk/util-retry": "3.229.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/smithy-client": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.234.0.tgz", + "integrity": "sha512-8AtR/k4vsFvjXeQbIzq/Wy7Nbk48Ou0wUEeVYPHWHPSU8QamFWORkOwmKtKMfHAyZvmqiAPeQqHFkq+UJhWyyQ==", + "dev": true, + "dependencies": { + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/token-providers": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.241.0.tgz", + "integrity": "sha512-79okvuOS7V559OIL/RalIPG98wzmWxeFOChFnbEjn2pKOyGQ6FJRwLPYZaVRtNdAtnkBNgRpmFq9dX843QxhtQ==", + "dev": true, + "dependencies": { + "@aws-sdk/client-sso-oidc": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/util-defaults-mode-browser": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.234.0.tgz", + "integrity": "sha512-IHMKXjTbOD8XMz5+2oCOsVP94BYb9YyjXdns0aAXr2NAo7k2+RCzXQ2DebJXppGda1F6opFutoKwyVSN0cmbMw==", + "dev": true, + "dependencies": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/util-defaults-mode-node": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.234.0.tgz", + "integrity": "sha512-UGjQ+OjBYYhxFVtUY+jtr0ZZgzZh6OHtYwRhFt8IHewJXFCfZTyfsbX20szBj5y1S4HRIUJ7cwBLIytTqMbI5w==", + "dev": true, + "dependencies": { + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/@aws-sdk/util-endpoints": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.241.0.tgz", + "integrity": "sha512-jVf8bKrN22Ey0xLmj75sL7EUvm5HFpuOMkXsZkuXycKhCwLBcEUWlvtJYtRjOU1zScPQv9GMJd2QXQglp34iOQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-appconfigdata/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.231.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.231.0.tgz", @@ -16468,10 +16827,16 @@ }, "packages/parameters": { "name": "@aws-lambda-powertools/parameters", - "version": "1.4.1", + "version": "1.5.0", "license": "MIT-0", "dependencies": { "@aws-sdk/util-base64": "^3.208.0" + }, + "devDependencies": { + "@aws-sdk/client-appconfigdata": "^3.241.0", + "@aws-sdk/types": "^3.226.0", + "aws-sdk-client-mock": "^2.0.1", + "aws-sdk-client-mock-jest": "^2.0.1" } }, "packages/tracer": { @@ -16741,7 +17106,11 @@ "@aws-lambda-powertools/parameters": { "version": "file:packages/parameters", "requires": { - "@aws-sdk/util-base64": "^3.208.0" + "@aws-sdk/client-appconfigdata": "*", + "@aws-sdk/types": "*", + "@aws-sdk/util-base64": "^3.208.0", + "aws-sdk-client-mock": "*", + "aws-sdk-client-mock-jest": "*" } }, "@aws-lambda-powertools/tracer": { @@ -16765,6 +17134,322 @@ "tslib": "^2.3.1" } }, + "@aws-sdk/client-appconfigdata": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-appconfigdata/-/client-appconfigdata-3.241.0.tgz", + "integrity": "sha512-5AVfSc6ZQ17dF0cdIrmpEe0H6vmGumBvsFGn89e2clSPtqqtsFDpBeAsS5jCMbxO9pakkCM0RPAh+sci6MnlYg==", + "dev": true, + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/client-sts": "3.241.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + }, + "dependencies": { + "@aws-sdk/client-sso": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.241.0.tgz", + "integrity": "sha512-Jm4HR+RYAqKMEYZvvWaq0NYUKKonyInOeubObXH4BLXZpmUBSdYCSjjLdNJY3jkQoxbDVPVMIurVNh5zT5SMRw==", + "dev": true, + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/client-sso-oidc": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.241.0.tgz", + "integrity": "sha512-/Ml2QBGpGfUEeBrPzBZhSTBkHuXFD2EAZEIHGCBH4tKaURDI6/FoGI8P1Rl4BzoFt+II/Cr91Eox6YT9EwChsQ==", + "dev": true, + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/client-sts": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.241.0.tgz", + "integrity": "sha512-vmlG8cJzRf8skCtTJbA2wBvD2c3NQ5gZryzJvTKDS06KzBzcEpnjlLseuTekcnOiRNekbFUX5hRu5Zj3N2ReLg==", + "dev": true, + "requires": { + "@aws-crypto/sha256-browser": "2.0.0", + "@aws-crypto/sha256-js": "2.0.0", + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-node": "3.241.0", + "@aws-sdk/fetch-http-handler": "3.226.0", + "@aws-sdk/hash-node": "3.226.0", + "@aws-sdk/invalid-dependency": "3.226.0", + "@aws-sdk/middleware-content-length": "3.226.0", + "@aws-sdk/middleware-endpoint": "3.226.0", + "@aws-sdk/middleware-host-header": "3.226.0", + "@aws-sdk/middleware-logger": "3.226.0", + "@aws-sdk/middleware-recursion-detection": "3.226.0", + "@aws-sdk/middleware-retry": "3.235.0", + "@aws-sdk/middleware-sdk-sts": "3.226.0", + "@aws-sdk/middleware-serde": "3.226.0", + "@aws-sdk/middleware-signing": "3.226.0", + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/middleware-user-agent": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/node-http-handler": "3.226.0", + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/smithy-client": "3.234.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/url-parser": "3.226.0", + "@aws-sdk/util-base64": "3.208.0", + "@aws-sdk/util-body-length-browser": "3.188.0", + "@aws-sdk/util-body-length-node": "3.208.0", + "@aws-sdk/util-defaults-mode-browser": "3.234.0", + "@aws-sdk/util-defaults-mode-node": "3.234.0", + "@aws-sdk/util-endpoints": "3.241.0", + "@aws-sdk/util-retry": "3.229.0", + "@aws-sdk/util-user-agent-browser": "3.226.0", + "@aws-sdk/util-user-agent-node": "3.226.0", + "@aws-sdk/util-utf8-browser": "3.188.0", + "@aws-sdk/util-utf8-node": "3.208.0", + "fast-xml-parser": "4.0.11", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/config-resolver": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.234.0.tgz", + "integrity": "sha512-uZxy4wzllfvgCQxVc+Iqhde0NGAnfmV2hWR6ejadJaAFTuYNvQiRg9IqJy3pkyDPqXySiJ8Bom5PoJfgn55J/A==", + "dev": true, + "requires": { + "@aws-sdk/signature-v4": "3.226.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-config-provider": "3.208.0", + "@aws-sdk/util-middleware": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.241.0.tgz", + "integrity": "sha512-CI+mu6h74Kzmscw35TvNkc/wYHsHPGAwP7humSHoWw53H9mVw21Ggft/dT1iFQQZWQ8BNXkzuXlNo1IlqwMgOA==", + "dev": true, + "requires": { + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.241.0.tgz", + "integrity": "sha512-08zPQcD5o9brQmzEipWHeHgU85aQcEF8MWLfpeyjO6e1/l7ysQ35NsS+PYtv77nLpGCx/X+ZuW/KXWoRrbw77w==", + "dev": true, + "requires": { + "@aws-sdk/credential-provider-env": "3.226.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/credential-provider-ini": "3.241.0", + "@aws-sdk/credential-provider-process": "3.226.0", + "@aws-sdk/credential-provider-sso": "3.241.0", + "@aws-sdk/credential-provider-web-identity": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.241.0.tgz", + "integrity": "sha512-6Bjd6eEIrVomRTrPrM4dlxusQm+KMJ9hLYKECCpFkwDKIK+pTgZNLRtQdalHyzwneHJPdimrm8cOv1kUQ8hPoA==", + "dev": true, + "requires": { + "@aws-sdk/client-sso": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/token-providers": "3.241.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-retry": { + "version": "3.235.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.235.0.tgz", + "integrity": "sha512-50WHbJGpD3SNp9763MAlHqIhXil++JdQbKejNpHg7HsJne/ao3ub+fDOfx//mMBjpzBV25BGd5UlfL6blrClSg==", + "dev": true, + "requires": { + "@aws-sdk/protocol-http": "3.226.0", + "@aws-sdk/service-error-classification": "3.229.0", + "@aws-sdk/types": "3.226.0", + "@aws-sdk/util-middleware": "3.226.0", + "@aws-sdk/util-retry": "3.229.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + } + }, + "@aws-sdk/smithy-client": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.234.0.tgz", + "integrity": "sha512-8AtR/k4vsFvjXeQbIzq/Wy7Nbk48Ou0wUEeVYPHWHPSU8QamFWORkOwmKtKMfHAyZvmqiAPeQqHFkq+UJhWyyQ==", + "dev": true, + "requires": { + "@aws-sdk/middleware-stack": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/token-providers": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.241.0.tgz", + "integrity": "sha512-79okvuOS7V559OIL/RalIPG98wzmWxeFOChFnbEjn2pKOyGQ6FJRwLPYZaVRtNdAtnkBNgRpmFq9dX843QxhtQ==", + "dev": true, + "requires": { + "@aws-sdk/client-sso-oidc": "3.241.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/shared-ini-file-loader": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-defaults-mode-browser": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.234.0.tgz", + "integrity": "sha512-IHMKXjTbOD8XMz5+2oCOsVP94BYb9YyjXdns0aAXr2NAo7k2+RCzXQ2DebJXppGda1F6opFutoKwyVSN0cmbMw==", + "dev": true, + "requires": { + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-defaults-mode-node": { + "version": "3.234.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.234.0.tgz", + "integrity": "sha512-UGjQ+OjBYYhxFVtUY+jtr0ZZgzZh6OHtYwRhFt8IHewJXFCfZTyfsbX20szBj5y1S4HRIUJ7cwBLIytTqMbI5w==", + "dev": true, + "requires": { + "@aws-sdk/config-resolver": "3.234.0", + "@aws-sdk/credential-provider-imds": "3.226.0", + "@aws-sdk/node-config-provider": "3.226.0", + "@aws-sdk/property-provider": "3.226.0", + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.241.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.241.0.tgz", + "integrity": "sha512-jVf8bKrN22Ey0xLmj75sL7EUvm5HFpuOMkXsZkuXycKhCwLBcEUWlvtJYtRjOU1zScPQv9GMJd2QXQglp34iOQ==", + "dev": true, + "requires": { + "@aws-sdk/types": "3.226.0", + "tslib": "^2.3.1" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, "@aws-sdk/client-dynamodb": { "version": "3.231.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.231.0.tgz", diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 0ade3eabe7..88cb26a508 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -52,5 +52,11 @@ "secrets", "serverless", "nodejs" - ] -} \ No newline at end of file + ], + "devDependencies": { + "@aws-sdk/client-appconfigdata": "^3.241.0", + "@aws-sdk/types": "^3.226.0", + "aws-sdk-client-mock": "^2.0.1", + "aws-sdk-client-mock-jest": "^2.0.1" + } +} diff --git a/packages/parameters/src/AppConfigProvider.ts b/packages/parameters/src/AppConfigProvider.ts index 006183565d..b819ee1a50 100644 --- a/packages/parameters/src/AppConfigProvider.ts +++ b/packages/parameters/src/AppConfigProvider.ts @@ -1,4 +1,4 @@ -import { BaseProvider, DEFAULT_PROVIDERS } from 'BaseProvider'; +import { BaseProvider, DEFAULT_PROVIDERS } from './BaseProvider'; import { AppConfigDataClient, StartConfigurationSessionCommand, @@ -12,34 +12,38 @@ import type { AppConfigGetOptionsInterface } from './types/AppConfigProvider'; class AppConfigProvider extends BaseProvider { public client: AppConfigDataClient; - private application?: string; - private environment?: string; + private application: string; + private environment: string; + private latestConfiguration: Uint8Array | undefined; private token: string | undefined; - private latestConfiguration: string | undefined; /** - * It initializes the AppConfigProvider class with an optional set of options like region: 'us-west-1'. + * It initializes the AppConfigProvider class'. * * - * @param {AppConfigDataClientConfig} options + * @param {AppConfigGetOptionsInterface} config */ - public constructor(options: AppConfigGetOptionsInterface = {}) { + public constructor(options: AppConfigGetOptionsInterface) { super(); - this.client = new AppConfigDataClient(options.config || {}); - this.environment = options.environment; - this.application = options.application || 'service_undefined'; // TODO: get value from ENV VARIABLES + this.client = new AppConfigDataClient(options.clientConfig || {}); + this.application = options?.sdkOptions?.application || 'app_undefined'; // TODO: make it optional when we add retrieving from env var + this.environment = options?.sdkOptions?.environment || 'env_undefined'; + } + + public async get(name: string, options?: AppConfigGetOptionsInterface): Promise> { + return super.get(name, options); } /** * Retrieve a parameter value from AWS App config. * * @param {string} name - Name of the configuration - * @param {StartConfigurationSessionCommandInput} [sdkOptions] - SDK options to propagate to `StartConfigurationSession` API call - * @returns {Promise} + * @param {AppConfigGetOptionsInterface} options - SDK options to propagate to `StartConfigurationSession` API call + * @returns {Promise} */ protected async _get( name: string, options?: AppConfigGetOptionsInterface - ): Promise { + ): Promise { /** * The new AppConfig APIs require two API calls to return the configuration * First we start the session and after that we retrieve the configuration @@ -48,10 +52,11 @@ class AppConfigProvider extends BaseProvider { if (!this.token) { const sessionOptions: StartConfigurationSessionCommandInput = { ConfigurationProfileIdentifier: name, - EnvironmentIdentifier: this.environment, - ApplicationIdentifier: this.application, + EnvironmentIdentifier: this.application, + ApplicationIdentifier: this.environment, }; - if (options && options.hasOwnProperty('sdkOptions')) { + + if (options?.sdkOptions) { Object.assign(sessionOptions, options.sdkOptions); } @@ -68,18 +73,12 @@ class AppConfigProvider extends BaseProvider { }); const response = await this.client.send(getConfigurationCommand); - // Should we cache and flush the token after 24 hours? - // NextPollConfigurationToken expires in 24 hours after the last request and causes `BadRequestException` this.token = response.NextPollConfigurationToken; const configuration = response.Configuration; - // should we convert Uint8Array to string? - const utf8decoder = new TextDecoder(); - const value = configuration ? utf8decoder.decode(configuration) : undefined; - - if (value) { - this.latestConfiguration = value; + if (configuration) { + this.latestConfiguration = configuration; } return this.latestConfiguration; @@ -91,8 +90,8 @@ class AppConfigProvider extends BaseProvider { * @throws Not Implemented Error. */ protected async _getMultiple( - path: string, - sdkOptions?: Partial + _path: string, + _sdkOptions?: Partial ): Promise> { return this._notImplementedError(); } @@ -104,8 +103,8 @@ class AppConfigProvider extends BaseProvider { const getAppConfig = ( name: string, - options?: AppConfigGetOptionsInterface -): Promise> => { + options: AppConfigGetOptionsInterface +): Promise> => { if (!DEFAULT_PROVIDERS.hasOwnProperty('appconfig')) { DEFAULT_PROVIDERS.appconfig = new AppConfigProvider(options); } diff --git a/packages/parameters/src/BaseProvider.ts b/packages/parameters/src/BaseProvider.ts index d664217d06..c9ab6feaa6 100644 --- a/packages/parameters/src/BaseProvider.ts +++ b/packages/parameters/src/BaseProvider.ts @@ -5,6 +5,7 @@ import { ExpirableValue } from './ExpirableValue'; import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants'; import { GetParameterError, TransformParameterError } from './Exceptions'; import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types'; +import type { AppConfigGetOptionsInterface } from 'types/AppConfigProvider'; // These providers are dinamycally intialized on first use of the helper functions const DEFAULT_PROVIDERS: Record = {}; @@ -38,8 +39,9 @@ abstract class BaseProvider implements BaseProviderInterface { * this should be an acceptable tradeoff. * * @param {string} name - Parameter name - * @param {GetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch + * @param {GetOptionsInterface | Partial} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch */ + public async get(name: string, options?: Partial): Promise>; public async get(name: string, options?: GetOptionsInterface): Promise> { const configs = new GetOptions(options); const key = [ name, configs.transform ].toString(); diff --git a/packages/parameters/src/types/AppConfigProvider.ts b/packages/parameters/src/types/AppConfigProvider.ts index ade0e7a2f2..35a954c61c 100644 --- a/packages/parameters/src/types/AppConfigProvider.ts +++ b/packages/parameters/src/types/AppConfigProvider.ts @@ -1,4 +1,6 @@ -import type { StartConfigurationSessionCommandInput, AppConfigDataClientConfig } from '@aws-sdk/client-appconfigdata'; +import type { + AppConfigDataClientConfig, +} from '@aws-sdk/client-appconfigdata'; import type { GetOptionsInterface } from 'types/BaseProvider'; /** @@ -6,13 +8,16 @@ import type { GetOptionsInterface } from 'types/BaseProvider'; * * @interface AppConfigGetOptionsInterface * @extends {GetOptionsInterface} - * @property {Partial} sdkOptions - Options for the AWS SDK. + * @property {} [clientConfig] - optional configuration to pass during client initialization + * @property {} sdkOptions - required options to start configuration session. */ -interface AppConfigGetOptionsInterface extends Omit { - application?: string - config?: AppConfigDataClientConfig - environment?: string - sdkOptions?: Partial +interface AppConfigGetOptionsInterface + extends Omit { + clientConfig?: AppConfigDataClientConfig + sdkOptions?: { + application: string + environment: string + } } export { AppConfigGetOptionsInterface }; diff --git a/packages/parameters/tests/unit/AppConfigProvider.test.ts b/packages/parameters/tests/unit/AppConfigProvider.test.ts new file mode 100644 index 0000000000..48cb08751a --- /dev/null +++ b/packages/parameters/tests/unit/AppConfigProvider.test.ts @@ -0,0 +1,60 @@ +/** + * Test AppConfigProvider class + * + * @group unit/parameters/AppConfigProvider/class + */ +import { AppConfigProvider } from '../../src/AppConfigProvider'; +import { + AppConfigDataClient, + StartConfigurationSessionCommand, + GetLatestConfigurationCommand, +} from '@aws-sdk/client-appconfigdata'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; + +const encoder = new TextEncoder(); + +describe('Class: AppConfigProvider', () => { + const client = mockClient(AppConfigDataClient); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Method: _get', () => { + test('when called with name and options, it gets binary configuration', async () => { + // Prepare + const options = { + sdkOptions: { + application: 'MyApp', + environment: 'MyAppProdEnv', + }, + }; + const provider = new AppConfigProvider(options); + const name = 'MyAppFeatureFlag'; + + const mockInitialToken = + 'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1'; + const mockNextToken = + 'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw=='; + const mockData = encoder.encode('myAppConfiguration'); + + client + .on(StartConfigurationSessionCommand) + .resolves({ + InitialConfigurationToken: mockInitialToken, + }) + .on(GetLatestConfigurationCommand) + .resolves({ + Configuration: mockData, + NextPollConfigurationToken: mockNextToken, + }); + + // Act + const result = await provider.get(name); + + // Assess + expect(result).toBe(mockData); + }); + }); +}); From 96434b44e3c5043841f555f63fa9a7d40947e199 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 6 Jan 2023 10:23:54 +0400 Subject: [PATCH 5/8] feat(appconfig): add getAddConfig utility function, types, and tests --- .../parameters/src/appconfig/getAppConfig.ts | 22 +++ packages/parameters/src/appconfig/index.ts | 2 + .../parameters/src/types/AppConfigProvider.ts | 47 +++++-- .../tests/unit/getAppConfig.test.ts | 126 ++++++++++++++++++ 4 files changed, 187 insertions(+), 10 deletions(-) create mode 100644 packages/parameters/src/appconfig/getAppConfig.ts create mode 100644 packages/parameters/src/appconfig/index.ts create mode 100644 packages/parameters/tests/unit/getAppConfig.test.ts diff --git a/packages/parameters/src/appconfig/getAppConfig.ts b/packages/parameters/src/appconfig/getAppConfig.ts new file mode 100644 index 0000000000..9f8694021f --- /dev/null +++ b/packages/parameters/src/appconfig/getAppConfig.ts @@ -0,0 +1,22 @@ +import { AppConfigProvider, DEFAULT_PROVIDERS } from './AppConfigProvider'; +import type { getAppConfigCombinedInterface } from '../types/AppConfigProvider'; + +/** + * Gets the AppConfig data for the specified name. + * + * @param {string} name - The configuration profile ID or the configuration profile name. + * @param {getAppConfigCombinedInterface} options - Options for the AppConfigProvider and the get method. + * @returns {Promise>} A promise that resolves to the AppConfig data or undefined if not found. + */ +const getAppConfig = ( + name: string, + options: getAppConfigCombinedInterface +): Promise> => { + if (!DEFAULT_PROVIDERS.hasOwnProperty('appconfig')) { + DEFAULT_PROVIDERS.appconfig = new AppConfigProvider(options); + } + + return DEFAULT_PROVIDERS.appconfig.get(name, options); +}; + +export { getAppConfig }; diff --git a/packages/parameters/src/appconfig/index.ts b/packages/parameters/src/appconfig/index.ts new file mode 100644 index 0000000000..c0215de062 --- /dev/null +++ b/packages/parameters/src/appconfig/index.ts @@ -0,0 +1,2 @@ +export * from './AppConfigProvider'; +export * from './getAppConfig'; \ No newline at end of file diff --git a/packages/parameters/src/types/AppConfigProvider.ts b/packages/parameters/src/types/AppConfigProvider.ts index 35a954c61c..02b0ae0bc9 100644 --- a/packages/parameters/src/types/AppConfigProvider.ts +++ b/packages/parameters/src/types/AppConfigProvider.ts @@ -1,23 +1,50 @@ import type { AppConfigDataClientConfig, + StartConfigurationSessionCommandInput, } from '@aws-sdk/client-appconfigdata'; import type { GetOptionsInterface } from 'types/BaseProvider'; +/** + * Options for the AppConfigProvider class constructor. + * + * @interface AppConfigProviderOptions + * @property {string} environment - The environment ID or the environment name. + * @property {string} [application] - The application ID or the application name. + * @property {AppConfigDataClientConfig} [clientConfig] - Optional configuration to pass during client initialization, e.g. AWS region. + */ +interface AppConfigProviderOptions { + environment: string + application?: string + clientConfig?: AppConfigDataClientConfig +} + /** * Options for the AppConfigProvider get method. * * @interface AppConfigGetOptionsInterface * @extends {GetOptionsInterface} - * @property {} [clientConfig] - optional configuration to pass during client initialization - * @property {} sdkOptions - required options to start configuration session. + * @property {StartConfigurationSessionCommandInput} [sdkOptions] - Required options to start configuration session. */ -interface AppConfigGetOptionsInterface - extends Omit { - clientConfig?: AppConfigDataClientConfig - sdkOptions?: { - application: string - environment: string - } +interface AppConfigGetOptionsInterface extends Omit { + sdkOptions?: Omit< + Partial, + | 'ApplicationIdentifier' + | 'EnvironmentIdentifier | ConfigurationProfileIdentifier' + > } -export { AppConfigGetOptionsInterface }; +/** + * Combined options for the getAppConfig utility function. + * + * @interface getAppConfigCombinedInterface + * @extends {AppConfigProviderOptions, AppConfigGetOptionsInterface} + */ +interface getAppConfigCombinedInterface + extends AppConfigProviderOptions, + AppConfigGetOptionsInterface {} + +export { + AppConfigProviderOptions, + AppConfigGetOptionsInterface, + getAppConfigCombinedInterface, +}; diff --git a/packages/parameters/tests/unit/getAppConfig.test.ts b/packages/parameters/tests/unit/getAppConfig.test.ts new file mode 100644 index 0000000000..88db9f72a0 --- /dev/null +++ b/packages/parameters/tests/unit/getAppConfig.test.ts @@ -0,0 +1,126 @@ +/** + * Test getAppConfig function + * + * @group unit/parameters/AppConfigProvider/getAppConfig/function + */ +import { + AppConfigProvider, + getAppConfig, + DEFAULT_PROVIDERS, +} from '../../src/appconfig'; +import { + AppConfigDataClient, + StartConfigurationSessionCommand, + GetLatestConfigurationCommand, +} from '@aws-sdk/client-appconfigdata'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; +import type { getAppConfigCombinedInterface } from '../../src/types/AppConfigProvider'; + +describe('Function: getAppConfig', () => { + const client = mockClient(AppConfigDataClient); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('when called and a default provider doesn\'t exist, it instantiates one and returns the value', async () => { + // Prepare + const options: getAppConfigCombinedInterface = { + application: 'MyApp', + environment: 'MyAppProdEnv', + }; + const name = 'MyAppFeatureFlag'; + const mockInitialToken = + 'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1'; + const mockNextToken = + 'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw=='; + const mockData = encoder.encode('myAppConfiguration'); + + client + .on(StartConfigurationSessionCommand) + .resolves({ + InitialConfigurationToken: mockInitialToken, + }) + .on(GetLatestConfigurationCommand) + .resolves({ + Configuration: mockData, + NextPollConfigurationToken: mockNextToken, + }); + + // Act + const result = await getAppConfig(name, options); + + // Assess + expect(result).toBe(mockData); + }); + + test('when called and a default provider exists, it uses it and returns the value', async () => { + // Prepare + const options: getAppConfigCombinedInterface = { + application: 'MyApp', + environment: 'MyAppProdEnv', + }; + const provider = new AppConfigProvider(options); + DEFAULT_PROVIDERS.appconfig = provider; + const name = 'MyAppFeatureFlag'; + const mockInitialToken = + 'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1'; + const mockNextToken = + 'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw=='; + const mockData = encoder.encode('myAppConfiguration'); + + client + .on(StartConfigurationSessionCommand) + .resolves({ + InitialConfigurationToken: mockInitialToken, + }) + .on(GetLatestConfigurationCommand) + .resolves({ + Configuration: mockData, + NextPollConfigurationToken: mockNextToken, + }); + + // Act + const result = await getAppConfig(name, options); + + // Assess + expect(result).toBe(mockData); + }); + + test('when called with transform: \'binary\' option, it returns string value', async () => { + // Prepare + const options: getAppConfigCombinedInterface = { + application: 'MyApp', + environment: 'MyAppProdEnv', + transform: 'binary', + }; + + const name = 'MyAppFeatureFlag'; + const mockInitialToken = + 'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1'; + const mockNextToken = + 'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw=='; + const mockData = encoder.encode('myAppConfiguration'); + const decodedData = decoder.decode(mockData); + + client + .on(StartConfigurationSessionCommand) + .resolves({ + InitialConfigurationToken: mockInitialToken, + }) + .on(GetLatestConfigurationCommand) + .resolves({ + Configuration: mockData, + NextPollConfigurationToken: mockNextToken, + }); + + // Act + const result = await getAppConfig(name, options); + + // Assess + expect(result).toBe(decodedData); + }); +}); From 6298d2ad5deed1a224a9e034417ef826242073ab Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 6 Jan 2023 12:00:34 +0400 Subject: [PATCH 6/8] feat(appconfig): update AppConfigProvider, tests --- packages/parameters/src/AppConfigProvider.ts | 115 --------------- packages/parameters/src/BaseProvider.ts | 4 +- .../src/appconfig/AppConfigProvider.ts | 127 ++++++++++++++++ .../tests/unit/AppConfigProvider.test.ts | 137 ++++++++++++++++-- 4 files changed, 250 insertions(+), 133 deletions(-) delete mode 100644 packages/parameters/src/AppConfigProvider.ts create mode 100644 packages/parameters/src/appconfig/AppConfigProvider.ts diff --git a/packages/parameters/src/AppConfigProvider.ts b/packages/parameters/src/AppConfigProvider.ts deleted file mode 100644 index b819ee1a50..0000000000 --- a/packages/parameters/src/AppConfigProvider.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { BaseProvider, DEFAULT_PROVIDERS } from './BaseProvider'; -import { - AppConfigDataClient, - StartConfigurationSessionCommand, - GetLatestConfigurationCommand, -} from '@aws-sdk/client-appconfigdata'; -import type { - StartConfigurationSessionCommandInput, - GetLatestConfigurationCommandInput, -} from '@aws-sdk/client-appconfigdata'; -import type { AppConfigGetOptionsInterface } from './types/AppConfigProvider'; - -class AppConfigProvider extends BaseProvider { - public client: AppConfigDataClient; - private application: string; - private environment: string; - private latestConfiguration: Uint8Array | undefined; - private token: string | undefined; - - /** - * It initializes the AppConfigProvider class'. - * * - * @param {AppConfigGetOptionsInterface} config - */ - public constructor(options: AppConfigGetOptionsInterface) { - super(); - this.client = new AppConfigDataClient(options.clientConfig || {}); - this.application = options?.sdkOptions?.application || 'app_undefined'; // TODO: make it optional when we add retrieving from env var - this.environment = options?.sdkOptions?.environment || 'env_undefined'; - } - - public async get(name: string, options?: AppConfigGetOptionsInterface): Promise> { - return super.get(name, options); - } - - /** - * Retrieve a parameter value from AWS App config. - * - * @param {string} name - Name of the configuration - * @param {AppConfigGetOptionsInterface} options - SDK options to propagate to `StartConfigurationSession` API call - * @returns {Promise} - */ - protected async _get( - name: string, - options?: AppConfigGetOptionsInterface - ): Promise { - /** - * The new AppConfig APIs require two API calls to return the configuration - * First we start the session and after that we retrieve the configuration - * We need to store the token to use in the next execution - **/ - if (!this.token) { - const sessionOptions: StartConfigurationSessionCommandInput = { - ConfigurationProfileIdentifier: name, - EnvironmentIdentifier: this.application, - ApplicationIdentifier: this.environment, - }; - - if (options?.sdkOptions) { - Object.assign(sessionOptions, options.sdkOptions); - } - - const sessionCommand = new StartConfigurationSessionCommand( - sessionOptions - ); - - const session = await this.client.send(sessionCommand); - this.token = session.InitialConfigurationToken; - } - - const getConfigurationCommand = new GetLatestConfigurationCommand({ - ConfigurationToken: this.token, - }); - const response = await this.client.send(getConfigurationCommand); - - this.token = response.NextPollConfigurationToken; - - const configuration = response.Configuration; - - if (configuration) { - this.latestConfiguration = configuration; - } - - return this.latestConfiguration; - } - - /** - * Retrieving multiple parameter values is not supported with AWS App Config Provider. - * - * @throws Not Implemented Error. - */ - protected async _getMultiple( - _path: string, - _sdkOptions?: Partial - ): Promise> { - return this._notImplementedError(); - } - - private _notImplementedError(): never { - throw new Error('Not Implemented'); - } -} - -const getAppConfig = ( - name: string, - options: AppConfigGetOptionsInterface -): Promise> => { - if (!DEFAULT_PROVIDERS.hasOwnProperty('appconfig')) { - DEFAULT_PROVIDERS.appconfig = new AppConfigProvider(options); - } - - return DEFAULT_PROVIDERS.appconfig.get(name, options); -}; - -export { AppConfigProvider, getAppConfig }; diff --git a/packages/parameters/src/BaseProvider.ts b/packages/parameters/src/BaseProvider.ts index c9ab6feaa6..d664217d06 100644 --- a/packages/parameters/src/BaseProvider.ts +++ b/packages/parameters/src/BaseProvider.ts @@ -5,7 +5,6 @@ import { ExpirableValue } from './ExpirableValue'; import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants'; import { GetParameterError, TransformParameterError } from './Exceptions'; import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types'; -import type { AppConfigGetOptionsInterface } from 'types/AppConfigProvider'; // These providers are dinamycally intialized on first use of the helper functions const DEFAULT_PROVIDERS: Record = {}; @@ -39,9 +38,8 @@ abstract class BaseProvider implements BaseProviderInterface { * this should be an acceptable tradeoff. * * @param {string} name - Parameter name - * @param {GetOptionsInterface | Partial} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch + * @param {GetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch */ - public async get(name: string, options?: Partial): Promise>; public async get(name: string, options?: GetOptionsInterface): Promise> { const configs = new GetOptions(options); const key = [ name, configs.transform ].toString(); diff --git a/packages/parameters/src/appconfig/AppConfigProvider.ts b/packages/parameters/src/appconfig/AppConfigProvider.ts new file mode 100644 index 0000000000..0042722aec --- /dev/null +++ b/packages/parameters/src/appconfig/AppConfigProvider.ts @@ -0,0 +1,127 @@ +import { BaseProvider, DEFAULT_PROVIDERS } from '../BaseProvider'; +import { + AppConfigDataClient, + StartConfigurationSessionCommand, + GetLatestConfigurationCommand, +} from '@aws-sdk/client-appconfigdata'; +import type { StartConfigurationSessionCommandInput } from '@aws-sdk/client-appconfigdata'; +import type { + AppConfigProviderOptions, + AppConfigGetOptionsInterface, +} from '../types/AppConfigProvider'; + +class AppConfigProvider extends BaseProvider { + public client: AppConfigDataClient; + protected configurationTokenStore: Map = new Map(); + private application?: string; + private environment: string; + + /** + * It initializes the AppConfigProvider class'. + * * + * @param {AppConfigProviderOptions} options + */ + public constructor(options: AppConfigProviderOptions) { + super(); + this.client = new AppConfigDataClient(options.clientConfig || {}); + if (!options?.application && !process.env['POWERTOOLS_SERVICE_NAME']) { + throw new Error( + 'Application name is not defined or POWERTOOLS_SERVICE_NAME is not set' + ); + } + this.application = + options.application || process.env['POWERTOOLS_SERVICE_NAME']; + this.environment = options.environment; + } + + /** + * Retrieve a configuration from AWS App config. + */ + public async get( + name: string, + options?: AppConfigGetOptionsInterface + ): Promise> { + return super.get(name, options); + } + + /** + * Retrieving multiple configurations is not supported by AWS App Config Provider. + */ + public async getMultiple( + path: string, + _options?: unknown + ): Promise> { + return super.getMultiple(path); + } + + /** + * Retrieve a configuration from AWS App config. + * + * @param {string} name - Name of the configuration + * @param {AppConfigGetOptionsInterface} options - SDK options to propagate to `StartConfigurationSession` API call + * @returns {Promise} + */ + protected async _get( + name: string, + options?: AppConfigGetOptionsInterface + ): Promise { + + /** + * The new AppConfig APIs require two API calls to return the configuration + * First we start the session and after that we retrieve the configuration + * We need to store { name: token } pairs to use in the next execution + **/ + + if (!this.configurationTokenStore.has(name)) { + + const sessionOptions: StartConfigurationSessionCommandInput = { + ...(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, session.InitialConfigurationToken); + } + + const getConfigurationCommand = new GetLatestConfigurationCommand({ + ConfigurationToken: this.configurationTokenStore.get(name), + }); + + const response = await this.client.send(getConfigurationCommand); + + if (response.NextPollConfigurationToken) { + this.configurationTokenStore.set(name, response.NextPollConfigurationToken); + } else { + this.configurationTokenStore.delete(name); + } + + return response.Configuration; + } + + /** + * Retrieving multiple configurations is not supported by AWS App Config Provider API. + * + * @throws Not Implemented Error. + */ + protected async _getMultiple( + _path: string, + _sdkOptions?: unknown + ): Promise> { + return this._notImplementedError(); + } + + private _notImplementedError(): never { + throw new Error('Not Implemented'); + } +} + +export { AppConfigProvider, DEFAULT_PROVIDERS }; diff --git a/packages/parameters/tests/unit/AppConfigProvider.test.ts b/packages/parameters/tests/unit/AppConfigProvider.test.ts index 48cb08751a..5762a145dd 100644 --- a/packages/parameters/tests/unit/AppConfigProvider.test.ts +++ b/packages/parameters/tests/unit/AppConfigProvider.test.ts @@ -3,7 +3,8 @@ * * @group unit/parameters/AppConfigProvider/class */ -import { AppConfigProvider } from '../../src/AppConfigProvider'; +import { AppConfigProvider } from '../../src/appconfig/index'; + import { AppConfigDataClient, StartConfigurationSessionCommand, @@ -11,43 +12,39 @@ import { } from '@aws-sdk/client-appconfigdata'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; - -const encoder = new TextEncoder(); +import { AppConfigProviderOptions } from '../../src/types/AppConfigProvider'; describe('Class: AppConfigProvider', () => { const client = mockClient(AppConfigDataClient); + const encoder = new TextEncoder(); beforeEach(() => { jest.clearAllMocks(); }); describe('Method: _get', () => { - test('when called with name and options, it gets binary configuration', async () => { + test('when called with name and options, it returns binary configuration', async () => { // Prepare - const options = { - sdkOptions: { - application: 'MyApp', - environment: 'MyAppProdEnv', - }, + const options: AppConfigProviderOptions = { + application: 'MyApp', + environment: 'MyAppProdEnv', }; const provider = new AppConfigProvider(options); const name = 'MyAppFeatureFlag'; - const mockInitialToken = - 'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1'; - const mockNextToken = - 'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw=='; + const fakeInitialToken = 'aW5pdGlhbFRva2Vu'; + const fakeNextToken = 'bmV4dFRva2Vu'; const mockData = encoder.encode('myAppConfiguration'); client .on(StartConfigurationSessionCommand) .resolves({ - InitialConfigurationToken: mockInitialToken, + InitialConfigurationToken: fakeInitialToken, }) .on(GetLatestConfigurationCommand) .resolves({ Configuration: mockData, - NextPollConfigurationToken: mockNextToken, + NextPollConfigurationToken: fakeNextToken, }); // Act @@ -56,5 +53,115 @@ describe('Class: AppConfigProvider', () => { // Assess expect(result).toBe(mockData); }); + + test('when called without application option, it will be retrieved from POWERTOOLS_SERVICE_NAME and provider successfully return configuration', async () => { + // Prepare + process.env.POWERTOOLS_SERVICE_NAME = 'MyApp'; + const config = { + environment: 'MyAppProdEnv', + }; + const provider = new AppConfigProvider(config); + const name = 'MyAppFeatureFlag'; + + const fakeInitialToken = 'aW5pdGlhbFRva2Vu'; + const fakeNextToken = 'bmV4dFRva2Vu'; + const mockData = encoder.encode('myAppConfiguration'); + + client + .on(StartConfigurationSessionCommand) + .resolves({ + InitialConfigurationToken: fakeInitialToken, + }) + .on(GetLatestConfigurationCommand) + .resolves({ + Configuration: mockData, + NextPollConfigurationToken: fakeNextToken, + }); + + // Act + const result = await provider.get(name); + + // Assess + expect(result).toBe(mockData); + }); + + test('when called without application option and POWERTOOLS_SERVICE_NAME is not set, it throws an Error', async () => { + // Prepare + process.env.POWERTOOLS_SERVICE_NAME = ''; + const options = { + environment: 'MyAppProdEnv', + }; + + // Act & Assess + expect(() => { + new AppConfigProvider(options); + }).toThrow(); + }); + + test('when configuration response doesn\'t have the next token it should force a new session by removing the stored token', async () => { + // Prepare + class AppConfigProviderMock extends AppConfigProvider { + public _addToStore(key: string, value: string): void { + this.configurationTokenStore.set(key, value); + } + public _storeHas(key: string): boolean { + return this.configurationTokenStore.has(key); + } + } + + const options: AppConfigProviderOptions = { + application: 'MyApp', + environment: 'MyAppProdEnv', + }; + const provider = new AppConfigProviderMock(options); + const name = 'MyAppFeatureFlag'; + const fakeToken = 'ZmFrZVRva2Vu'; + const mockData = encoder.encode('myAppConfiguration'); + + client.on(GetLatestConfigurationCommand).resolves({ + Configuration: mockData, + NextPollConfigurationToken: undefined, + }); + + // Act + provider._addToStore(name, fakeToken); + await provider.get(name); + + // Assess + expect(provider._storeHas(name)).toBe(false); + }); + + test('when session response doesn\'t have an initial token, it throws an error', async () => { + // Prepare + const options: AppConfigProviderOptions = { + application: 'MyApp', + environment: 'MyAppProdEnv', + }; + const provider = new AppConfigProvider(options); + const name = 'MyAppFeatureFlag'; + + client.on(StartConfigurationSessionCommand).resolves({ + InitialConfigurationToken: undefined, + }); + + // Act & Assess + await expect(provider.get(name)).rejects.toThrow(); + }); + }); + + describe('Method: _getMultiple', () => { + test('when called it throws an Error, because this method is not supported by AppConfig API', async () => { + // Prepare + const config = { + application: 'MyApp', + environment: 'MyAppProdEnv', + }; + const path = '/my/path'; + const provider = new AppConfigProvider(config); + const errorMessage = 'Not Implemented'; + + // Act & Assess + await expect(provider.getMultiple(path)).rejects.toThrow(errorMessage); + }); }); }); From f6025a4ed8780002a81db1cb32f426941e92b82f Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 6 Jan 2023 12:26:05 +0400 Subject: [PATCH 7/8] resolve merge conflicts --- package-lock.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1f201bddb4..27785fef63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18240,16 +18240,13 @@ "@aws-lambda-powertools/parameters": { "version": "file:packages/parameters", "requires": { + "@aws-sdk/client-appconfigdata": "^3.241.0", "@aws-sdk/client-secrets-manager": "^3.238.0", "@aws-sdk/client-ssm": "^3.244.0", + "@aws-sdk/types": "^3.226.0", "@aws-sdk/util-base64": "^3.208.0", "aws-sdk-client-mock": "^2.0.1", "aws-sdk-client-mock-jest": "^2.0.1" - "@aws-sdk/client-appconfigdata": "*", - "@aws-sdk/types": "*", - "@aws-sdk/util-base64": "^3.208.0", - "aws-sdk-client-mock": "*", - "aws-sdk-client-mock-jest": "*" } }, "@aws-lambda-powertools/tracer": { From 5f20b0a352409babd8ceb914da563ca9b3a1dd7d Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Fri, 6 Jan 2023 18:45:32 +0400 Subject: [PATCH 8/8] fix interface --- packages/parameters/src/appconfig/getAppConfig.ts | 6 +++--- packages/parameters/src/types/AppConfigProvider.ts | 6 +++--- packages/parameters/tests/unit/getAppConfig.test.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/parameters/src/appconfig/getAppConfig.ts b/packages/parameters/src/appconfig/getAppConfig.ts index 9f8694021f..ca336ea777 100644 --- a/packages/parameters/src/appconfig/getAppConfig.ts +++ b/packages/parameters/src/appconfig/getAppConfig.ts @@ -1,16 +1,16 @@ import { AppConfigProvider, DEFAULT_PROVIDERS } from './AppConfigProvider'; -import type { getAppConfigCombinedInterface } from '../types/AppConfigProvider'; +import type { GetAppConfigCombinedInterface } from '../types/AppConfigProvider'; /** * Gets the AppConfig data for the specified name. * * @param {string} name - The configuration profile ID or the configuration profile name. - * @param {getAppConfigCombinedInterface} options - Options for the AppConfigProvider and the get method. + * @param {GetAppConfigCombinedInterface} options - Options for the AppConfigProvider and the get method. * @returns {Promise>} A promise that resolves to the AppConfig data or undefined if not found. */ const getAppConfig = ( name: string, - options: getAppConfigCombinedInterface + options: GetAppConfigCombinedInterface ): Promise> => { if (!DEFAULT_PROVIDERS.hasOwnProperty('appconfig')) { DEFAULT_PROVIDERS.appconfig = new AppConfigProvider(options); diff --git a/packages/parameters/src/types/AppConfigProvider.ts b/packages/parameters/src/types/AppConfigProvider.ts index 02b0ae0bc9..89d148f4ce 100644 --- a/packages/parameters/src/types/AppConfigProvider.ts +++ b/packages/parameters/src/types/AppConfigProvider.ts @@ -39,12 +39,12 @@ interface AppConfigGetOptionsInterface extends Omit, AppConfigGetOptionsInterface {} export { AppConfigProviderOptions, AppConfigGetOptionsInterface, - getAppConfigCombinedInterface, + GetAppConfigCombinedInterface, }; diff --git a/packages/parameters/tests/unit/getAppConfig.test.ts b/packages/parameters/tests/unit/getAppConfig.test.ts index 88db9f72a0..af03a8ec32 100644 --- a/packages/parameters/tests/unit/getAppConfig.test.ts +++ b/packages/parameters/tests/unit/getAppConfig.test.ts @@ -15,7 +15,7 @@ import { } from '@aws-sdk/client-appconfigdata'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; -import type { getAppConfigCombinedInterface } from '../../src/types/AppConfigProvider'; +import type { GetAppConfigCombinedInterface } from '../../src/types/AppConfigProvider'; describe('Function: getAppConfig', () => { const client = mockClient(AppConfigDataClient); @@ -28,7 +28,7 @@ describe('Function: getAppConfig', () => { test('when called and a default provider doesn\'t exist, it instantiates one and returns the value', async () => { // Prepare - const options: getAppConfigCombinedInterface = { + const options: GetAppConfigCombinedInterface = { application: 'MyApp', environment: 'MyAppProdEnv', }; @@ -59,7 +59,7 @@ describe('Function: getAppConfig', () => { test('when called and a default provider exists, it uses it and returns the value', async () => { // Prepare - const options: getAppConfigCombinedInterface = { + const options: GetAppConfigCombinedInterface = { application: 'MyApp', environment: 'MyAppProdEnv', }; @@ -92,7 +92,7 @@ describe('Function: getAppConfig', () => { test('when called with transform: \'binary\' option, it returns string value', async () => { // Prepare - const options: getAppConfigCombinedInterface = { + const options: GetAppConfigCombinedInterface = { application: 'MyApp', environment: 'MyAppProdEnv', transform: 'binary',