diff --git a/.eslintrc.js b/.eslintrc.js index ef25880334..6fbb20febe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,68 +1,72 @@ module.exports = { - root: true, env: { browser: false, es2020: true, jest: true, node: true, }, - extends: [ 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended' ], + ignorePatterns: ['coverage', 'lib', 'cdk.out', 'dist', 'node_modules'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], + plugins: ['@typescript-eslint', 'prettier'], settings: { 'import/resolver': { node: {}, typescript: { - project: './tsconfig.es.json', + project: './tsconfig.json', alwaysTryTypes: true, }, }, }, rules: { - '@typescript-eslint/ban-ts-ignore': ['off'], - '@typescript-eslint/camelcase': ['off'], - '@typescript-eslint/explicit-function-return-type': [ 'error', { allowExpressions: true } ], - '@typescript-eslint/explicit-member-accessibility': 'error', - '@typescript-eslint/indent': [ 'error', 2, { SwitchCase: 1 } ], - '@typescript-eslint/interface-name-prefix': ['off'], - '@typescript-eslint/member-delimiter-style': [ 'error', { multiline: { delimiter: 'none' } } ], + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { allowExpressions: true }, + ], // Enforce return type definitions for functions + '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) '@typescript-eslint/member-ordering': [ + // Standardize the order of class members 'error', { default: { memberTypes: [ 'signature', - 'public-field', // = ["public-static-field", "public-instance-field"] - 'protected-field', // = ["protected-static-field", "protected-instance-field"] - 'private-field', // = ["private-static-field", "private-instance-field"] + 'public-field', + 'protected-field', + 'private-field', 'constructor', - 'public-method', // = ["public-static-method", "public-instance-method"] - 'protected-method', // = ["protected-static-method", "protected-instance-method"] - 'private-method', // = ["private-static-method", "private-instance-method"] + 'public-method', + 'protected-method', + 'private-method', ], order: 'alphabetically', }, }, ], - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-inferrable-types': ['off'], - '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_' } ], - '@typescript-eslint/no-use-before-define': ['off'], - '@typescript-eslint/semi': [ 'error', 'always' ], - 'array-bracket-spacing': [ 'error', 'always', { singleValue: false } ], - 'arrow-body-style': [ 'error', 'as-needed' ], - 'computed-property-spacing': [ 'error', 'never' ], - 'func-style': [ 'warn', 'expression' ], - indent: [ 'error', 2, { SwitchCase: 1 } ], - 'keyword-spacing': 'error', - 'newline-before-return': 2, - 'no-console': 0, - 'no-multi-spaces': [ 'error', { ignoreEOLComments: false } ], - 'no-multiple-empty-lines': [ 'error', { max: 1, maxBOF: 0 } ], - 'no-throw-literal': 'error', - 'object-curly-spacing': [ 'error', 'always' ], - 'prefer-arrow-callback': 'error', - quotes: [ 'error', 'single', { allowTemplateLiterals: true } ], - semi: [ 'error', 'always' ] - } + '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore + '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed + 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars + // Rules from eslint core https://eslint.org/docs/latest/rules/ + 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets + 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties + 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations + 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) + 'padding-line-between-statements': [ + // Require an empty line before return statements + 'error', + { blankLine: 'always', prev: '*', next: 'return' }, + ], + 'no-console': 0, // Allow console.log statements + 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments + 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements + 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') + 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects + 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks + quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings + semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements + }, }; diff --git a/docs/snippets/.eslintrc.js b/docs/snippets/.eslintrc.js deleted file mode 100644 index 8e1dd1513f..0000000000 --- a/docs/snippets/.eslintrc.js +++ /dev/null @@ -1,71 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/examples/cdk/.eslintrc.js b/examples/cdk/.eslintrc.js deleted file mode 100644 index 8cfae0905f..0000000000 --- a/examples/cdk/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['cdk.out', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/examples/sam/.eslintrc.js b/examples/sam/.eslintrc.js deleted file mode 100644 index 8cfae0905f..0000000000 --- a/examples/sam/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['cdk.out', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/layers/.eslintrc.js b/layers/.eslintrc.js deleted file mode 100644 index 8e1dd1513f..0000000000 --- a/layers/.eslintrc.js +++ /dev/null @@ -1,71 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/packages/commons/.eslintrc.js b/packages/commons/.eslintrc.js deleted file mode 100644 index 2e4ca18fb2..0000000000 --- a/packages/commons/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['coverage', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/packages/commons/tests/utils/e2eUtils.ts b/packages/commons/tests/utils/e2eUtils.ts index 328f7241cc..2a88d29306 100644 --- a/packages/commons/tests/utils/e2eUtils.ts +++ b/packages/commons/tests/utils/e2eUtils.ts @@ -38,7 +38,9 @@ export type StackWithLambdaFunctionOptions = { timeout?: Duration; }; -type FunctionPayload = { [key: string]: string | boolean | number | Array> }; +type FunctionPayload = { + [key: string]: string | boolean | number | Array>; +}; export const isValidRuntimeKey = ( runtime: string @@ -82,17 +84,21 @@ export const generateUniqueName = ( export const invokeFunction = async ( functionName: string, - times: number = 1, + times = 1, invocationMode: 'PARALLEL' | 'SEQUENTIAL' = 'PARALLEL', payload: FunctionPayload = {}, includeIndex = true ): Promise => { const invocationLogs: InvocationLogs[] = []; - const promiseFactory = (index?: number, includeIndex?: boolean): Promise => { - + const promiseFactory = ( + index?: number, + includeIndex?: boolean + ): Promise => { // in some cases we need to send a payload without the index, i.e. idempotency tests - const payloadToSend = includeIndex ? { invocation: index, ...payload } : { ...payload }; + const payloadToSend = includeIndex + ? { invocation: index, ...payload } + : { ...payload }; const invokePromise = lambdaClient .send( @@ -101,7 +107,8 @@ export const invokeFunction = async ( InvocationType: 'RequestResponse', LogType: 'Tail', // Wait until execution completes and return all logs Payload: fromUtf8(JSON.stringify(payloadToSend)), - })) + }) + ) .then((response) => { if (response?.LogResult) { invocationLogs.push(new InvocationLogs(response?.LogResult)); @@ -119,7 +126,9 @@ export const invokeFunction = async ( const invocation = invocationMode == 'PARALLEL' - ? Promise.all(promiseFactories.map((factory, index) => factory(index, includeIndex))) + ? Promise.all( + promiseFactories.map((factory, index) => factory(index, includeIndex)) + ) : chainPromises(promiseFactories); await invocation; diff --git a/packages/idempotency/jest.config.js b/packages/idempotency/jest.config.js index aa65679554..8896fe6dda 100644 --- a/packages/idempotency/jest.config.js +++ b/packages/idempotency/jest.config.js @@ -1,46 +1,32 @@ -module.exports = { - displayName: { - name: 'AWS Lambda Powertools utility: IDEMPOTENCY', - color: 'yellow', - }, - 'runner': 'groups', - 'preset': 'ts-jest', - 'transform': { - '^.+\\.ts?$': 'ts-jest', - }, - moduleFileExtensions: [ 'js', 'ts' ], - 'collectCoverageFrom': [ - '**/src/**/*.ts', - '!**/node_modules/**', - ], - 'testMatch': ['**/?(*.)+(spec|test).ts'], - 'roots': [ - '/src', - '/tests', - ], - 'testPathIgnorePatterns': [ - '/node_modules/', - ], - 'testEnvironment': 'node', - 'coveragePathIgnorePatterns': [ - '/node_modules/', - '/types/', - 'src/makeFunctionIdempotent.ts', // TODO: remove this once makeFunctionIdempotent is implemented - ], - 'coverageThreshold': { - 'global': { - 'statements': 100, - 'branches': 100, - 'functions': 100, - 'lines': 100, - }, - }, - 'coverageReporters': [ - 'json-summary', - 'text', - 'lcov' - ], - 'setupFiles': [ - '/tests/helpers/populateEnvironmentVariables.ts' - ] -}; \ No newline at end of file +module.exports = { + displayName: { + name: 'AWS Lambda Powertools utility: IDEMPOTENCY', + color: 'yellow', + }, + runner: 'groups', + preset: 'ts-jest', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + moduleFileExtensions: ['js', 'ts'], + collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], + testMatch: ['**/?(*.)+(spec|test).ts'], + roots: ['/src', '/tests'], + testPathIgnorePatterns: ['/node_modules/'], + testEnvironment: 'node', + coveragePathIgnorePatterns: [ + '/node_modules/', + '/types/', + 'src/makeFunctionIdempotent.ts', // TODO: remove this once makeFunctionIdempotent is implemented + ], + coverageThreshold: { + global: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + coverageReporters: ['json-summary', 'text', 'lcov'], + setupFiles: ['/tests/helpers/populateEnvironmentVariables.ts'], +}; diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index aa675ee4ff..a6246bd3fc 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -19,8 +19,8 @@ "test:e2e": "jest --group=e2e --detectOpenHandles", "watch": "jest --watch --group=unit", "build": "tsc", - "lint": "eslint --ext .ts --no-error-on-unmatched-pattern src tests", - "lint-fix": "eslint --fix --ext .ts --no-error-on-unmatched-pattern src tests", + "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", + "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .", "package": "mkdir -p dist/ && npm pack && mv *.tgz dist/", "package-bundle": "../../package-bundler.sh idempotency-bundle ./dist", "prepare": "npm run build" diff --git a/packages/idempotency/src/Exceptions.ts b/packages/idempotency/src/Exceptions.ts index 43fa7e8ab7..650d4419d4 100644 --- a/packages/idempotency/src/Exceptions.ts +++ b/packages/idempotency/src/Exceptions.ts @@ -47,4 +47,4 @@ export { IdempotencyInconsistentStateError, IdempotencyPersistenceLayerError, IdempotencyKeyError, -}; \ No newline at end of file +}; diff --git a/packages/idempotency/src/IdempotencyConfig.ts b/packages/idempotency/src/IdempotencyConfig.ts index d6a078e7e3..8483dbd981 100644 --- a/packages/idempotency/src/IdempotencyConfig.ts +++ b/packages/idempotency/src/IdempotencyConfig.ts @@ -27,6 +27,4 @@ class IdempotencyConfig { } } -export { - IdempotencyConfig, -}; \ No newline at end of file +export { IdempotencyConfig }; diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index abeadd2fe5..274d714a20 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -22,7 +22,7 @@ export class IdempotencyHandler { functionPayloadToBeHashed, idempotencyConfig, fullFunctionPayload, - persistenceStore + persistenceStore, } = options; this.functionToMakeIdempotent = functionToMakeIdempotent; this.functionPayloadToBeHashed = functionPayloadToBeHashed; @@ -32,11 +32,13 @@ export class IdempotencyHandler { this.persistenceStore = persistenceStore; this.persistenceStore.configure({ - config: this.idempotencyConfig + config: this.idempotencyConfig, }); } - public determineResultFromIdempotencyRecord(idempotencyRecord: IdempotencyRecord): Promise | U { + public determineResultFromIdempotencyRecord( + idempotencyRecord: IdempotencyRecord + ): Promise | U { if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.EXPIRED) { throw new IdempotencyInconsistentStateError( 'Item has expired during processing and may not longer be valid.' @@ -47,7 +49,7 @@ export class IdempotencyHandler { if ( idempotencyRecord.inProgressExpiryTimestamp && idempotencyRecord.inProgressExpiryTimestamp < - new Date().getUTCMilliseconds() + new Date().getUTCMilliseconds() ) { throw new IdempotencyInconsistentStateError( 'Item is in progress but the in progress expiry timestamp has expired.' @@ -66,19 +68,27 @@ export class IdempotencyHandler { let result: U; try { result = await this.functionToMakeIdempotent(this.fullFunctionPayload); - } catch (e) { try { - await this.persistenceStore.deleteRecord(this.functionPayloadToBeHashed); + await this.persistenceStore.deleteRecord( + this.functionPayloadToBeHashed + ); } catch (e) { - throw new IdempotencyPersistenceLayerError('Failed to delete record from idempotency store'); + throw new IdempotencyPersistenceLayerError( + 'Failed to delete record from idempotency store' + ); } throw e; } try { - await this.persistenceStore.saveSuccess(this.functionPayloadToBeHashed, result as Record); + await this.persistenceStore.saveSuccess( + this.functionPayloadToBeHashed, + result as Record + ); } catch (e) { - throw new IdempotencyPersistenceLayerError('Failed to update success record to idempotency store'); + throw new IdempotencyPersistenceLayerError( + 'Failed to update success record to idempotency store' + ); } return result; @@ -91,13 +101,15 @@ export class IdempotencyHandler { * In most cases we can retry successfully on this exception. */ public async handle(): Promise { - const MAX_RETRIES = 2; for (let i = 1; i <= MAX_RETRIES; i++) { try { return await this.processIdempotency(); } catch (e) { - if (!(e instanceof IdempotencyAlreadyInProgressError) || i === MAX_RETRIES) { + if ( + !(e instanceof IdempotencyAlreadyInProgressError) || + i === MAX_RETRIES + ) { throw e; } } @@ -109,14 +121,12 @@ export class IdempotencyHandler { public async processIdempotency(): Promise { try { await this.persistenceStore.saveInProgress( - this.functionPayloadToBeHashed, + this.functionPayloadToBeHashed ); } catch (e) { if (e instanceof IdempotencyItemAlreadyExistsError) { const idempotencyRecord: IdempotencyRecord = - await this.persistenceStore.getRecord( - this.functionPayloadToBeHashed - ); + await this.persistenceStore.getRecord(this.functionPayloadToBeHashed); return this.determineResultFromIdempotencyRecord(idempotencyRecord); } else { @@ -126,5 +136,4 @@ export class IdempotencyHandler { return this.getFunctionResult(); } - } diff --git a/packages/idempotency/src/config/ConfigServiceInterface.ts b/packages/idempotency/src/config/ConfigServiceInterface.ts index f56dbc0d11..c40e302b73 100644 --- a/packages/idempotency/src/config/ConfigServiceInterface.ts +++ b/packages/idempotency/src/config/ConfigServiceInterface.ts @@ -1,13 +1,9 @@ interface ConfigServiceInterface { + get(name: string): string; - get(name: string): string - - getServiceName(): string - - getFunctionName(): string + getServiceName(): string; + getFunctionName(): string; } -export { - ConfigServiceInterface -}; \ No newline at end of file +export { ConfigServiceInterface }; diff --git a/packages/idempotency/src/config/EnvironmentVariablesService.ts b/packages/idempotency/src/config/EnvironmentVariablesService.ts index a487f0db8a..e73b9775ae 100644 --- a/packages/idempotency/src/config/EnvironmentVariablesService.ts +++ b/packages/idempotency/src/config/EnvironmentVariablesService.ts @@ -1,7 +1,5 @@ import { ConfigServiceInterface } from './ConfigServiceInterface'; -import { - EnvironmentVariablesService as CommonEnvironmentVariablesService -} from '@aws-lambda-powertools/commons'; +import { EnvironmentVariablesService as CommonEnvironmentVariablesService } from '@aws-lambda-powertools/commons'; /** * Class EnvironmentVariablesService @@ -17,8 +15,10 @@ import { * @see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime * @see https://awslabs.github.io/aws-lambda-powertools-typescript/latest/#environment-variables */ -class EnvironmentVariablesService extends CommonEnvironmentVariablesService implements ConfigServiceInterface { - +class EnvironmentVariablesService + extends CommonEnvironmentVariablesService + implements ConfigServiceInterface +{ // Reserved environment variables private functionNameVariable = 'AWS_LAMBDA_FUNCTION_NAME'; @@ -30,9 +30,6 @@ class EnvironmentVariablesService extends CommonEnvironmentVariablesService impl public getFunctionName(): string { return this.get(this.functionNameVariable); } - } -export { - EnvironmentVariablesService -}; \ No newline at end of file +export { EnvironmentVariablesService }; diff --git a/packages/idempotency/src/config/index.ts b/packages/idempotency/src/config/index.ts index b55646be54..3dfae2b192 100644 --- a/packages/idempotency/src/config/index.ts +++ b/packages/idempotency/src/config/index.ts @@ -1 +1 @@ -export * from './EnvironmentVariablesService'; \ No newline at end of file +export * from './EnvironmentVariablesService'; diff --git a/packages/idempotency/src/idempotentDecorator.ts b/packages/idempotency/src/idempotentDecorator.ts index a4a01cf4c5..4787ae9f75 100644 --- a/packages/idempotency/src/idempotentDecorator.ts +++ b/packages/idempotency/src/idempotentDecorator.ts @@ -1,4 +1,8 @@ -import { GenericTempRecord, IdempotencyFunctionOptions, IdempotencyLambdaHandlerOptions, } from './types'; +import { + GenericTempRecord, + IdempotencyFunctionOptions, + IdempotencyLambdaHandlerOptions, +} from './types'; import { IdempotencyHandler } from './IdempotencyHandler'; import { IdempotencyConfig } from './IdempotencyConfig'; @@ -6,20 +10,37 @@ import { IdempotencyConfig } from './IdempotencyConfig'; * use this function to narrow the type of options between IdempotencyHandlerOptions and IdempotencyFunctionOptions * @param options */ -const isFunctionOption = (options: IdempotencyLambdaHandlerOptions | IdempotencyFunctionOptions): boolean => (options as IdempotencyFunctionOptions).dataKeywordArgument !== undefined; +const isFunctionOption = ( + options: IdempotencyLambdaHandlerOptions | IdempotencyFunctionOptions +): boolean => + (options as IdempotencyFunctionOptions).dataKeywordArgument !== undefined; -const idempotent = function (options: IdempotencyLambdaHandlerOptions | IdempotencyFunctionOptions): (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor { - return function (_target: unknown, _propertyKey: string, descriptor: PropertyDescriptor) { +const idempotent = function ( + options: IdempotencyLambdaHandlerOptions | IdempotencyFunctionOptions +): ( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor +) => PropertyDescriptor { + return function ( + _target: unknown, + _propertyKey: string, + descriptor: PropertyDescriptor + ) { const childFunction = descriptor.value; descriptor.value = function (record: GenericTempRecord) { - const functionPayloadtoBeHashed = isFunctionOption(options) ? record[(options as IdempotencyFunctionOptions).dataKeywordArgument] : record; - const idempotencyConfig = options.config ? options.config : new IdempotencyConfig({}); + const functionPayloadtoBeHashed = isFunctionOption(options) + ? record[(options as IdempotencyFunctionOptions).dataKeywordArgument] + : record; + const idempotencyConfig = options.config + ? options.config + : new IdempotencyConfig({}); const idempotencyHandler = new IdempotencyHandler({ functionToMakeIdempotent: childFunction, functionPayloadToBeHashed: functionPayloadtoBeHashed, persistenceStore: options.persistenceStore, idempotencyConfig: idempotencyConfig, - fullFunctionPayload: record + fullFunctionPayload: record, }); return idempotencyHandler.handle(); @@ -29,10 +50,22 @@ const idempotent = function (options: IdempotencyLambdaHandlerOptions | Idempote }; }; -const idempotentLambdaHandler = function (options: IdempotencyLambdaHandlerOptions): (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor { +const idempotentLambdaHandler = function ( + options: IdempotencyLambdaHandlerOptions +): ( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor +) => PropertyDescriptor { return idempotent(options); }; -const idempotentFunction = function (options: IdempotencyFunctionOptions): (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => PropertyDescriptor { +const idempotentFunction = function ( + options: IdempotencyFunctionOptions +): ( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor +) => PropertyDescriptor { return idempotent(options); }; diff --git a/packages/idempotency/src/makeFunctionIdempotent.ts b/packages/idempotency/src/makeFunctionIdempotent.ts index 3ebefbe24b..3415cb0d6d 100644 --- a/packages/idempotency/src/makeFunctionIdempotent.ts +++ b/packages/idempotency/src/makeFunctionIdempotent.ts @@ -9,20 +9,28 @@ import { IdempotencyConfig } from './IdempotencyConfig'; const makeFunctionIdempotent = function ( fn: AnyFunctionWithRecord, - options: IdempotencyFunctionOptions, + options: IdempotencyFunctionOptions ): AnyIdempotentFunction { - const wrappedFn: AnyIdempotentFunction = function (record: GenericTempRecord): Promise { + const wrappedFn: AnyIdempotentFunction = function ( + record: GenericTempRecord + ): Promise { if (options.dataKeywordArgument === undefined) { - throw new Error(`Missing data keyword argument ${options.dataKeywordArgument}`); + throw new Error( + `Missing data keyword argument ${options.dataKeywordArgument}` + ); } - const idempotencyConfig = options.config ? options.config : new IdempotencyConfig({}); - const idempotencyHandler: IdempotencyHandler = new IdempotencyHandler({ - functionToMakeIdempotent: fn, - functionPayloadToBeHashed: record[options.dataKeywordArgument], - idempotencyConfig: idempotencyConfig, - persistenceStore: options.persistenceStore, - fullFunctionPayload: record - }); + const idempotencyConfig = options.config + ? options.config + : new IdempotencyConfig({}); + const idempotencyHandler: IdempotencyHandler = new IdempotencyHandler( + { + functionToMakeIdempotent: fn, + functionPayloadToBeHashed: record[options.dataKeywordArgument], + idempotencyConfig: idempotencyConfig, + persistenceStore: options.persistenceStore, + fullFunctionPayload: record, + } + ); return idempotencyHandler.handle(); }; diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index 39f29a3a07..b247d83025 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -1,30 +1,28 @@ -import { - createHash, - Hash -} from 'node:crypto'; +import { createHash, Hash } from 'node:crypto'; import { search } from 'jmespath'; import { IdempotencyRecordStatus } from '../types'; -import type { - BasePersistenceLayerOptions -} from '../types'; +import type { BasePersistenceLayerOptions } from '../types'; import { EnvironmentVariablesService } from '../config'; import { IdempotencyRecord } from './IdempotencyRecord'; import { BasePersistenceLayerInterface } from './BasePersistenceLayerInterface'; -import { IdempotencyItemAlreadyExistsError, IdempotencyValidationError } from '../Exceptions'; +import { + IdempotencyItemAlreadyExistsError, + IdempotencyValidationError, +} from '../Exceptions'; import { LRUCache } from './LRUCache'; abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { public idempotencyKeyPrefix: string; private cache?: LRUCache; - private configured: boolean = false; + private configured = false; // envVarsService is always initialized in the constructor private envVarsService!: EnvironmentVariablesService; private eventKeyJmesPath?: string; private expiresAfterSeconds: number = 60 * 60; // 1 hour default - private hashFunction: string = 'md5'; - private payloadValidationEnabled: boolean = false; - private throwOnNoIdempotencyKey: boolean = false; - private useLocalCache: boolean = false; + private hashFunction = 'md5'; + private payloadValidationEnabled = false; + private throwOnNoIdempotencyKey = false; + private useLocalCache = false; private validationKeyJmesPath?: string; public constructor() { @@ -34,7 +32,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Initialize the base persistence layer from the configuration settings - * + * * @param {BasePersistenceLayerConfigureOptions} config - configuration object for the persistence layer */ public configure(config: BasePersistenceLayerOptions): void { @@ -53,26 +51,30 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { this.eventKeyJmesPath = idempotencyConfig?.eventKeyJmesPath; this.validationKeyJmesPath = idempotencyConfig?.payloadValidationJmesPath; - this.payloadValidationEnabled = this.validationKeyJmesPath !== undefined || false; - this.throwOnNoIdempotencyKey = idempotencyConfig?.throwOnNoIdempotencyKey || false; + this.payloadValidationEnabled = + this.validationKeyJmesPath !== undefined || false; + this.throwOnNoIdempotencyKey = + idempotencyConfig?.throwOnNoIdempotencyKey || false; this.eventKeyJmesPath = idempotencyConfig.eventKeyJmesPath; this.expiresAfterSeconds = idempotencyConfig.expiresAfterSeconds; // 1 hour default this.useLocalCache = idempotencyConfig.useLocalCache; if (this.useLocalCache) { - this.cache = new LRUCache({ maxSize: idempotencyConfig.maxLocalCacheSize }); + this.cache = new LRUCache({ + maxSize: idempotencyConfig.maxLocalCacheSize, + }); } this.hashFunction = idempotencyConfig.hashFunction; } /** * Deletes a record from the persistence store for the persistence key generated from the data passed in. - * + * * @param data - the data payload that will be hashed to create the hash portion of the idempotency key */ public async deleteRecord(data: Record): Promise { const idempotencyRecord = new IdempotencyRecord({ idempotencyKey: this.getHashedIdempotencyKey(data), - status: IdempotencyRecordStatus.EXPIRED + status: IdempotencyRecordStatus.EXPIRED, }); await this._deleteRecord(idempotencyRecord); @@ -82,10 +84,12 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Retrieves idempotency key for the provided data and fetches data for that key from the persistence store - * + * * @param data - the data payload that will be hashed to create the hash portion of the idempotency key */ - public async getRecord(data: Record): Promise { + public async getRecord( + data: Record + ): Promise { const idempotencyKey = this.getHashedIdempotencyKey(data); const cachedRecord = this.getFromCache(idempotencyKey); @@ -108,11 +112,14 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Saves a record indicating that the function's execution is currently in progress - * + * * @param data - the data payload that will be hashed to create the hash portion of the idempotency key * @param remainingTimeInMillis - the remaining time left in the lambda execution context */ - public async saveInProgress(data: Record, remainingTimeInMillis?: number): Promise { + public async saveInProgress( + data: Record, + remainingTimeInMillis?: number + ): Promise { const idempotencyRecord = new IdempotencyRecord({ idempotencyKey: this.getHashedIdempotencyKey(data), status: IdempotencyRecordStatus.INPROGRESS, @@ -121,7 +128,8 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { }); if (remainingTimeInMillis) { - idempotencyRecord.inProgressExpiryTimestamp = new Date().getTime() + remainingTimeInMillis; + idempotencyRecord.inProgressExpiryTimestamp = + new Date().getTime() + remainingTimeInMillis; } else { console.warn( 'Could not determine remaining time left. Did you call registerLambdaContext on IdempotencyConfig?' @@ -138,11 +146,14 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Saves a record of the function completing successfully. This will create a record with a COMPLETED status * and will save the result of the completed function in the idempotency record. - * + * * @param data - the data payload that will be hashed to create the hash portion of the idempotency key * @param result - the result of the successfully completed function */ - public async saveSuccess(data: Record, result: Record): Promise { + public async saveSuccess( + data: Record, + result: Record + ): Promise { const idempotencyRecord = new IdempotencyRecord({ idempotencyKey: this.getHashedIdempotencyKey(data), status: IdempotencyRecordStatus.COMPLETED, @@ -157,7 +168,9 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { } protected abstract _deleteRecord(record: IdempotencyRecord): Promise; - protected abstract _getRecord(idempotencyKey: string): Promise; + protected abstract _getRecord( + idempotencyKey: string + ): Promise; protected abstract _putRecord(record: IdempotencyRecord): Promise; protected abstract _updateRecord(record: IdempotencyRecord): Promise; @@ -171,7 +184,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Generates a hash of the data and returns the digest of that hash - * + * * @param data the data payload that will generate the hash * @returns the digest of the generated hash */ @@ -192,7 +205,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Creates the expiry timestamp for the idempotency record - * + * * @returns the expiry time for the record expressed as number of seconds past the UNIX epoch */ private getExpiryTimestamp(): number { @@ -214,7 +227,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Generates the idempotency key used to identify records in the persistence store. - * + * * @param data the data payload that will be hashed to create the hash portion of the idempotency key * @returns the idempotency key */ @@ -227,15 +240,19 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { if (this.throwOnNoIdempotencyKey) { throw new Error('No data found to create a hashed idempotency_key'); } - console.warn(`No value found for idempotency_key. jmespath: ${this.eventKeyJmesPath}`); + console.warn( + `No value found for idempotency_key. jmespath: ${this.eventKeyJmesPath}` + ); } - return `${this.idempotencyKeyPrefix}#${this.generateHash(JSON.stringify(data))}`; + return `${this.idempotencyKeyPrefix}#${this.generateHash( + JSON.stringify(data) + )}`; } /** * Extract payload using validation key jmespath and return a hashed representation - * + * * @param data payload */ private getHashedPayload(data: Record): string { @@ -248,7 +265,9 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { return this.generateHash(JSON.stringify(data)); } - private static isMissingIdempotencyKey(data: Record): boolean { + private static isMissingIdempotencyKey( + data: Record + ): boolean { if (Array.isArray(data) || typeof data === 'object') { if (data === null) return true; for (const value of Object.values(data)) { @@ -265,10 +284,10 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Save record to local cache except for when status is `INPROGRESS`. - * + * * We can't cache `INPROGRESS` records because we have no way to reflect updates * that might happen to the record outside of the execution context of the function. - * + * * @param record - record to save */ private saveToCache(record: IdempotencyRecord): void { @@ -277,17 +296,19 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { this.cache?.add(record.idempotencyKey, record); } - private validatePayload(data: Record, record: IdempotencyRecord): void { + private validatePayload( + data: Record, + record: IdempotencyRecord + ): void { if (this.payloadValidationEnabled) { const hashedPayload: string = this.getHashedPayload(data); if (hashedPayload !== record.payloadHash) { - throw new IdempotencyValidationError('Payload does not match stored record for this event key'); + throw new IdempotencyValidationError( + 'Payload does not match stored record for this event key' + ); } } } - } -export { - BasePersistenceLayer -}; \ No newline at end of file +export { BasePersistenceLayer }; diff --git a/packages/idempotency/src/persistence/BasePersistenceLayerInterface.ts b/packages/idempotency/src/persistence/BasePersistenceLayerInterface.ts index 0e4325ec81..ce0d68b5d7 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayerInterface.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayerInterface.ts @@ -2,12 +2,12 @@ import { IdempotencyRecord } from './IdempotencyRecord'; import type { BasePersistenceLayerOptions } from '../types/BasePersistenceLayer'; interface BasePersistenceLayerInterface { - configure(options?: BasePersistenceLayerOptions): void - isPayloadValidationEnabled(): boolean - saveInProgress(data: unknown): Promise - saveSuccess(data: unknown, result: unknown): Promise - deleteRecord(data: unknown): Promise - getRecord(data: unknown): Promise + configure(options?: BasePersistenceLayerOptions): void; + isPayloadValidationEnabled(): boolean; + saveInProgress(data: unknown): Promise; + saveSuccess(data: unknown, result: unknown): Promise; + deleteRecord(data: unknown): Promise; + getRecord(data: unknown): Promise; } export { BasePersistenceLayerInterface }; diff --git a/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts b/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts index 97d40f6fb6..f259c6fac1 100644 --- a/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts +++ b/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts @@ -1,11 +1,9 @@ import { IdempotencyItemAlreadyExistsError, - IdempotencyItemNotFoundError + IdempotencyItemNotFoundError, } from '../Exceptions'; import { IdempotencyRecordStatus } from '../types'; -import type { - DynamoPersistenceOptions -} from '../types'; +import type { DynamoPersistenceOptions } from '../types'; import { DynamoDBClient, DynamoDBClientConfig, @@ -16,10 +14,7 @@ import { UpdateItemCommand, AttributeValue, } from '@aws-sdk/client-dynamodb'; -import { - marshall, - unmarshall, -} from '@aws-sdk/util-dynamodb'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; import { IdempotencyRecord } from './IdempotencyRecord'; import { BasePersistenceLayer } from './BasePersistenceLayer'; @@ -43,7 +38,8 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { this.keyAttr = config.keyAttr ?? 'id'; this.statusAttr = config.statusAttr ?? 'status'; this.expiryAttr = config.expiryAttr ?? 'expiration'; - this.inProgressExpiryAttr = config.inProgressExpiryAttr ?? 'in_progress_expiry_attr'; + this.inProgressExpiryAttr = + config.inProgressExpiryAttr ?? 'in_progress_expiry_attr'; this.dataAttr = config.dataAttr ?? 'data'; this.validationKeyAttr = config.validationKeyAttr ?? 'validation'; if (config.sortKeyAttr === this.keyAttr) { @@ -52,13 +48,16 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { ); } this.sortKeyAttr = config.sortKeyAttr; - this.staticPkValue = config.staticPkValue ?? `idempotency#${this.idempotencyKeyPrefix}`; + this.staticPkValue = + config.staticPkValue ?? `idempotency#${this.idempotencyKeyPrefix}`; if (config?.awsSdkV3Client) { if (config?.awsSdkV3Client instanceof DynamoDBClient) { this.client = config.awsSdkV3Client; } else { - console.warn('Invalid AWS SDK V3 client passed to DynamoDBPersistenceLayer. Using default client.'); + console.warn( + 'Invalid AWS SDK V3 client passed to DynamoDBPersistenceLayer. Using default client.' + ); } } else { this.clientConfig = config?.clientConfig ?? {}; @@ -67,21 +66,24 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { protected async _deleteRecord(record: IdempotencyRecord): Promise { const client = this.getClient(); - await client.send(new DeleteItemCommand({ - TableName: this.tableName, - Key: this.getKey(record.idempotencyKey), - })); + await client.send( + new DeleteItemCommand({ + TableName: this.tableName, + Key: this.getKey(record.idempotencyKey), + }) + ); } - protected async _getRecord(idempotencyKey: string): Promise { + protected async _getRecord( + idempotencyKey: string + ): Promise { const client = this.getClient(); - const result = await client.send(new GetItemCommand( - { + const result = await client.send( + new GetItemCommand({ TableName: this.tableName, Key: this.getKey(idempotencyKey), - ConsistentRead: true - } - ) + ConsistentRead: true, + }) ); if (!result.Item) { @@ -90,11 +92,11 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { const item = unmarshall(result.Item); return new IdempotencyRecord({ - idempotencyKey: item[this.keyAttr], - status: item[this.statusAttr], - expiryTimestamp: item[this.expiryAttr], - inProgressExpiryTimestamp: item[this.inProgressExpiryAttr], - responseData: item[this.dataAttr] + idempotencyKey: item[this.keyAttr], + status: item[this.statusAttr], + expiryTimestamp: item[this.expiryAttr], + inProgressExpiryTimestamp: item[this.inProgressExpiryAttr], + responseData: item[this.dataAttr], }); } @@ -103,21 +105,21 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { const item = { ...this.getKey(record.idempotencyKey), - ...marshall({ - [this.expiryAttr]: record.expiryTimestamp, - [this.statusAttr]: record.getStatus() - }) + ...marshall({ + [this.expiryAttr]: record.expiryTimestamp, + [this.statusAttr]: record.getStatus(), + }), }; if (record.inProgressExpiryTimestamp !== undefined) { item[this.inProgressExpiryAttr] = { - N: record.inProgressExpiryTimestamp.toString() + N: record.inProgressExpiryTimestamp.toString(), }; } if (this.isPayloadValidationEnabled() && record.payloadHash !== undefined) { item[this.validationKeyAttr] = { - S: record.payloadHash as string + S: record.payloadHash as string, }; } @@ -128,7 +130,7 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { * | Lambda Idempotency Record * | Timeout Timeout * | (in_progress_expiry) (expiry) - * + * * Conditions to successfully save a record: * * The idempotency key does not exist: * - first time that this invocation key is used @@ -151,31 +153,33 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { ].join(' OR '); const now = Date.now(); - await client.send(new PutItemCommand({ - TableName: this.tableName, - Item: item, - ExpressionAttributeNames: { - '#id': this.keyAttr, - '#expiry': this.expiryAttr, - '#in_progress_expiry': this.inProgressExpiryAttr, - '#status': this.statusAttr - }, - ExpressionAttributeValues: marshall({ - ':now': now / 1000, - ':now_in_millis': now, - ':inprogress': IdempotencyRecordStatus.INPROGRESS - }), - ConditionExpression: conditionExpression - })); - } catch (error){ + await client.send( + new PutItemCommand({ + TableName: this.tableName, + Item: item, + ExpressionAttributeNames: { + '#id': this.keyAttr, + '#expiry': this.expiryAttr, + '#in_progress_expiry': this.inProgressExpiryAttr, + '#status': this.statusAttr, + }, + ExpressionAttributeValues: marshall({ + ':now': now / 1000, + ':now_in_millis': now, + ':inprogress': IdempotencyRecordStatus.INPROGRESS, + }), + ConditionExpression: conditionExpression, + }) + ); + } catch (error) { if (error instanceof DynamoDBServiceException) { - if (error.name === 'ConditionalCheckFailedException'){ + if (error.name === 'ConditionalCheckFailedException') { throw new IdempotencyItemAlreadyExistsError( `Failed to put record for already existing idempotency key: ${record.idempotencyKey}` ); } } - + throw error; } } @@ -206,15 +210,13 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { } await client.send( - new UpdateItemCommand( - { - TableName: this.tableName, - Key: this.getKey(record.idempotencyKey), - UpdateExpression: `SET ${updateExpressionFields.join(', ')}`, - ExpressionAttributeNames: expressionAttributeNames, - ExpressionAttributeValues: marshall(expressionAttributeValues), - } - ) + new UpdateItemCommand({ + TableName: this.tableName, + Key: this.getKey(record.idempotencyKey), + UpdateExpression: `SET ${updateExpressionFields.join(', ')}`, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: marshall(expressionAttributeValues), + }) ); } @@ -228,27 +230,24 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer { /** * Build primary key attribute simple or composite based on params. - * + * * When sortKeyAttr is set, we must return a composite key with staticPkValue, * otherwise we use the idempotency key given. - * - * @param idempotencyKey + * + * @param idempotencyKey */ private getKey(idempotencyKey: string): Record { if (this.sortKeyAttr) { return marshall({ [this.keyAttr]: this.staticPkValue, - [this.sortKeyAttr]: idempotencyKey + [this.sortKeyAttr]: idempotencyKey, }); } return marshall({ - [this.keyAttr]: idempotencyKey + [this.keyAttr]: idempotencyKey, }); } } -export { - DynamoDBPersistenceLayer -}; - +export { DynamoDBPersistenceLayer }; diff --git a/packages/idempotency/src/persistence/IdempotencyRecord.ts b/packages/idempotency/src/persistence/IdempotencyRecord.ts index 54fab0e93d..88882caa49 100644 --- a/packages/idempotency/src/persistence/IdempotencyRecord.ts +++ b/packages/idempotency/src/persistence/IdempotencyRecord.ts @@ -1,7 +1,5 @@ import { IdempotencyRecordStatus } from '../types'; -import type { - IdempotencyRecordOptions -} from '../types'; +import type { IdempotencyRecordOptions } from '../types'; import { IdempotencyInvalidStatusError } from '../Exceptions'; /** @@ -39,10 +37,11 @@ class IdempotencyRecord { } public isExpired(): boolean { - return this.expiryTimestamp !== undefined && ((Date.now() / 1000) > this.expiryTimestamp); + return ( + this.expiryTimestamp !== undefined && + Date.now() / 1000 > this.expiryTimestamp + ); } } -export { - IdempotencyRecord -}; \ No newline at end of file +export { IdempotencyRecord }; diff --git a/packages/idempotency/src/persistence/LRUCache.ts b/packages/idempotency/src/persistence/LRUCache.ts index 98f2c66d54..583456d4aa 100644 --- a/packages/idempotency/src/persistence/LRUCache.ts +++ b/packages/idempotency/src/persistence/LRUCache.ts @@ -4,7 +4,7 @@ const DEFAULT_MAX_SIZE = 100; const NEWER = Symbol('newer'); const OLDER = Symbol('older'); -class Item{ +class Item { public readonly key: K; public value: V; private [NEWER]: Item | undefined; @@ -21,7 +21,7 @@ class Item{ /** * A simple LRU cache implementation that uses a doubly linked list to track the order of items in * an hash map. - * + * * Illustration of the design: *```text * oldest newest @@ -33,23 +33,23 @@ class Item{ * * removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added * ``` - * + * * Items are added to the cache using the `add()` method. When an item is added, it's marked * as the most recently used item. If the cache is full, the oldest item is removed from the * cache. - * + * * Each item also tracks the item that was added before it, and the item that was added after * it. This allows us to quickly remove the oldest item from the cache without having to * iterate through the entire cache. - * + * * **Note**: This implementation is loosely based on the implementation found in the lru_map package * which is licensed under the MIT license and [recommends users to copy the code into their * own projects](https://github.com/rsms/js-lru/tree/master#usage). - * + * * @typeparam K - The type of the key * @typeparam V - The type of the value */ -class LRUCache{ +class LRUCache { private leastRecentlyUsed?: Item; private readonly map: Map>; private readonly maxSize: number; @@ -58,40 +58,39 @@ class LRUCache{ /** * A simple LRU cache implementation that uses a doubly linked list to track the order of items in * an hash map. - * + * * When instatiating the cache, you can optionally specify the type of the key and value, as well * as the maximum size of the cache. If no maximum size is specified, the cache will default to * a size of 100. - * + * * @example * ```typescript * const cache = new LRUCache({ maxSize: 100 }); * // or * // const cache = new LRUCache(); - * + * * cache.add('a', 1); * cache.add('b', 2); - * + * * cache.get('a'); - * + * * console.log(cache.size()); // 2 * ``` - * + * * @param config - The configuration options for the cache */ public constructor(config?: LRUCacheOptions) { - this.maxSize = config?.maxSize !== undefined ? - config.maxSize : - DEFAULT_MAX_SIZE; + this.maxSize = + config?.maxSize !== undefined ? config.maxSize : DEFAULT_MAX_SIZE; this.map = new Map(); } /** * Adds a new item to the cache. - * + * * If the key already exists, it updates the value and marks the item as the most recently used. * If inserting the new item would exceed the max size, the oldest item is removed from the cache. - * + * * @param key - The key to add to the cache * @param value - The value to add to the cache */ @@ -131,9 +130,9 @@ class LRUCache{ /** * Returns a value from the cache, or undefined if it's not in the cache. - * + * * When a value is returned, it's marked as the most recently used item in the cache. - * + * * @param key - The key to retrieve from the cache */ public get(key: K): V | undefined { @@ -146,7 +145,7 @@ class LRUCache{ /** * Returns `true` if the key exists in the cache, `false` otherwise. - * + * * @param key - The key to check for in the cache */ public has(key: K): boolean { @@ -155,7 +154,7 @@ class LRUCache{ /** * Removes an item from the cache, while doing so it also reconciles the linked list. - * + * * @param key - The key to remove from the cache */ public remove(key: K): void { @@ -211,7 +210,7 @@ class LRUCache{ /** * Marks an item as the most recently used item in the cache. - * + * * @param item - The item to mark as the most recently used */ private trackItemUse(item: Item): void { @@ -237,6 +236,4 @@ class LRUCache{ } } -export { - LRUCache, -}; \ No newline at end of file +export { LRUCache }; diff --git a/packages/idempotency/src/persistence/index.ts b/packages/idempotency/src/persistence/index.ts index c531f87edb..8dd10b0fe2 100644 --- a/packages/idempotency/src/persistence/index.ts +++ b/packages/idempotency/src/persistence/index.ts @@ -1,4 +1,4 @@ export * from './DynamoDBPersistenceLayer'; export * from './BasePersistenceLayer'; export * from './BasePersistenceLayerInterface'; -export * from './IdempotencyRecord'; \ No newline at end of file +export * from './IdempotencyRecord'; diff --git a/packages/idempotency/src/types/AnyFunction.ts b/packages/idempotency/src/types/AnyFunction.ts index a7e9b2f388..744efdf20c 100644 --- a/packages/idempotency/src/types/AnyFunction.ts +++ b/packages/idempotency/src/types/AnyFunction.ts @@ -6,8 +6,4 @@ type AnyFunctionWithRecord = (record: GenericTempRecord) => Promise | U; type AnyIdempotentFunction = (record: GenericTempRecord) => Promise; -export { - GenericTempRecord, - AnyFunctionWithRecord, - AnyIdempotentFunction, -}; \ No newline at end of file +export { GenericTempRecord, AnyFunctionWithRecord, AnyIdempotentFunction }; diff --git a/packages/idempotency/src/types/BasePersistenceLayer.ts b/packages/idempotency/src/types/BasePersistenceLayer.ts index b5456f805e..b11db6f6b7 100644 --- a/packages/idempotency/src/types/BasePersistenceLayer.ts +++ b/packages/idempotency/src/types/BasePersistenceLayer.ts @@ -1,10 +1,8 @@ import { IdempotencyConfig } from '../IdempotencyConfig'; type BasePersistenceLayerOptions = { - config: IdempotencyConfig - functionName?: string + config: IdempotencyConfig; + functionName?: string; }; -export { - BasePersistenceLayerOptions, -}; \ No newline at end of file +export { BasePersistenceLayerOptions }; diff --git a/packages/idempotency/src/types/DynamoDBPersistence.ts b/packages/idempotency/src/types/DynamoDBPersistence.ts index 61db0a5f46..576a5acdd7 100644 --- a/packages/idempotency/src/types/DynamoDBPersistence.ts +++ b/packages/idempotency/src/types/DynamoDBPersistence.ts @@ -18,15 +18,15 @@ import type { * @property {string} [staticPkValue] - The DynamoDB table static partition key value, use only with sortKeyAttr. Defaults to `idempotency#{LAMBDA_FUNCTION_NAME}`. */ interface DynamoPersistenceOptionsBaseInterface { - tableName: string - keyAttr?: string - expiryAttr?: string - inProgressExpiryAttr?: string - statusAttr?: string - dataAttr?: string - validationKeyAttr?: string - sortKeyAttr?: string - staticPkValue?: string + tableName: string; + keyAttr?: string; + expiryAttr?: string; + inProgressExpiryAttr?: string; + statusAttr?: string; + dataAttr?: string; + validationKeyAttr?: string; + sortKeyAttr?: string; + staticPkValue?: string; } /** @@ -37,9 +37,10 @@ interface DynamoPersistenceOptionsBaseInterface { * @property {DynamoDBClientConfig} [clientConfig] - Optional configuration to pass during client initialization, e.g. AWS region. * @property {never} [awsSdkV3Client] - This property should never be passed. */ -interface DynamoPersistenceOptionsWithClientConfig extends DynamoPersistenceOptionsBaseInterface { - clientConfig?: DynamoDBClientConfig - awsSdkV3Client?: never +interface DynamoPersistenceOptionsWithClientConfig + extends DynamoPersistenceOptionsBaseInterface { + clientConfig?: DynamoDBClientConfig; + awsSdkV3Client?: never; } /** @@ -50,9 +51,10 @@ interface DynamoPersistenceOptionsWithClientConfig extends DynamoPersistenceOpti * @property {DynamoDBClient} [awsSdkV3Client] - Optional AWS SDK v3 client to pass during AppConfigProvider class instantiation * @property {never} [clientConfig] - This property should never be passed. */ -interface DynamoPersistenceOptionsWithClientInstance extends DynamoPersistenceOptionsBaseInterface { - awsSdkV3Client?: DynamoDBClient - clientConfig?: never +interface DynamoPersistenceOptionsWithClientInstance + extends DynamoPersistenceOptionsBaseInterface { + awsSdkV3Client?: DynamoDBClient; + clientConfig?: never; } /** @@ -71,8 +73,8 @@ interface DynamoPersistenceOptionsWithClientInstance extends DynamoPersistenceOp * @property {DynamoDBClientConfig} [clientConfig] - Optional configuration to pass during client initialization, e.g. AWS region. Mutually exclusive with awsSdkV3Client. * @property {DynamoDBClient} [awsSdkV3Client] - Optional AWS SDK v3 client to pass during DynamoDBProvider class instantiation. Mutually exclusive with clientConfig. */ -type DynamoPersistenceOptions = DynamoPersistenceOptionsWithClientConfig | DynamoPersistenceOptionsWithClientInstance; +type DynamoPersistenceOptions = + | DynamoPersistenceOptionsWithClientConfig + | DynamoPersistenceOptionsWithClientInstance; -export type { - DynamoPersistenceOptions, -}; \ No newline at end of file +export type { DynamoPersistenceOptions }; diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index 43e983b3c1..830659eaa6 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -4,20 +4,20 @@ import { AnyFunctionWithRecord } from 'types/AnyFunction'; import { IdempotencyConfig } from '../IdempotencyConfig'; type IdempotencyLambdaHandlerOptions = { - persistenceStore: BasePersistenceLayer - config?: IdempotencyConfig + persistenceStore: BasePersistenceLayer; + config?: IdempotencyConfig; }; type IdempotencyFunctionOptions = IdempotencyLambdaHandlerOptions & { - dataKeywordArgument: string + dataKeywordArgument: string; }; type IdempotencyHandlerOptions = { - functionToMakeIdempotent: AnyFunctionWithRecord - functionPayloadToBeHashed: Record - persistenceStore: BasePersistenceLayer - idempotencyConfig: IdempotencyConfig - fullFunctionPayload: Record + functionToMakeIdempotent: AnyFunctionWithRecord; + functionPayloadToBeHashed: Record; + persistenceStore: BasePersistenceLayer; + idempotencyConfig: IdempotencyConfig; + fullFunctionPayload: Record; }; /** @@ -27,40 +27,40 @@ type IdempotencyConfigOptions = { /** * An optional JMESPath expression to extract the idempotency key from the event record */ - eventKeyJmesPath?: string + eventKeyJmesPath?: string; /** * An optional JMESPath expression to extract the payload to be validated from the event record */ - payloadValidationJmesPath?: string + payloadValidationJmesPath?: string; /** * Throw an error if no idempotency key was found in the request, defaults to `false` */ - throwOnNoIdempotencyKey?: boolean + throwOnNoIdempotencyKey?: boolean; /** * The number of seconds to wait before a record is expired, defaults to `3600` (1 hour) */ - expiresAfterSeconds?: number + expiresAfterSeconds?: number; /** * Wheter to locally cache idempotency results, defaults to `false` */ - useLocalCache?: boolean + useLocalCache?: boolean; /** * Number of records to keep in the local cache, defaults to `256` */ - maxLocalCacheSize?: number + maxLocalCacheSize?: number; /** * Function to use for calculating hashes, defaults to `md5` */ - hashFunction?: string + hashFunction?: string; /** * AWS Lambda Context object containing information about the current invocation, function, and execution environment */ - lambdaContext?: Context + lambdaContext?: Context; }; export { IdempotencyConfigOptions, IdempotencyFunctionOptions, IdempotencyLambdaHandlerOptions, - IdempotencyHandlerOptions + IdempotencyHandlerOptions, }; diff --git a/packages/idempotency/src/types/IdempotencyRecord.ts b/packages/idempotency/src/types/IdempotencyRecord.ts index 5e111d5bb7..a9fa880caf 100644 --- a/packages/idempotency/src/types/IdempotencyRecord.ts +++ b/packages/idempotency/src/types/IdempotencyRecord.ts @@ -1,21 +1,19 @@ const IdempotencyRecordStatus = { INPROGRESS: 'INPROGRESS', COMPLETED: 'COMPLETED', - EXPIRED: 'EXPIRED' + EXPIRED: 'EXPIRED', } as const; -type IdempotencyRecordStatus = typeof IdempotencyRecordStatus[keyof typeof IdempotencyRecordStatus]; +type IdempotencyRecordStatus = + (typeof IdempotencyRecordStatus)[keyof typeof IdempotencyRecordStatus]; type IdempotencyRecordOptions = { - idempotencyKey: string - status: IdempotencyRecordStatus - expiryTimestamp?: number - inProgressExpiryTimestamp?: number - responseData?: Record - payloadHash?: string + idempotencyKey: string; + status: IdempotencyRecordStatus; + expiryTimestamp?: number; + inProgressExpiryTimestamp?: number; + responseData?: Record; + payloadHash?: string; }; -export { - IdempotencyRecordStatus, - IdempotencyRecordOptions -}; \ No newline at end of file +export { IdempotencyRecordStatus, IdempotencyRecordOptions }; diff --git a/packages/idempotency/src/types/LRUCache.ts b/packages/idempotency/src/types/LRUCache.ts index 3821befe28..6ab31d260a 100644 --- a/packages/idempotency/src/types/LRUCache.ts +++ b/packages/idempotency/src/types/LRUCache.ts @@ -2,9 +2,7 @@ type LRUCacheOptions = { /** * The maximum number of items to store in the cache. */ - maxSize: number + maxSize: number; }; -export { - LRUCacheOptions -}; \ No newline at end of file +export { LRUCacheOptions }; diff --git a/packages/idempotency/src/types/index.ts b/packages/idempotency/src/types/index.ts index 36579ac56e..4cde1c7882 100644 --- a/packages/idempotency/src/types/index.ts +++ b/packages/idempotency/src/types/index.ts @@ -3,4 +3,4 @@ export * from './IdempotencyRecord'; export * from './BasePersistenceLayer'; export * from './IdempotencyOptions'; export * from './DynamoDBPersistence'; -export * from './LRUCache'; \ No newline at end of file +export * from './LRUCache'; diff --git a/packages/idempotency/tests/e2e/constants.ts b/packages/idempotency/tests/e2e/constants.ts index e581b3c260..12497dc8b4 100644 --- a/packages/idempotency/tests/e2e/constants.ts +++ b/packages/idempotency/tests/e2e/constants.ts @@ -3,4 +3,4 @@ export const RESOURCE_NAME_PREFIX = 'Idempotency-E2E'; export const ONE_MINUTE = 60 * 1_000; export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; export const SETUP_TIMEOUT = 5 * ONE_MINUTE; -export const TEST_CASE_TIMEOUT = 5 * ONE_MINUTE; \ No newline at end of file +export const TEST_CASE_TIMEOUT = 5 * ONE_MINUTE; diff --git a/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts b/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts index 1dda1d338c..9145cc9db8 100644 --- a/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts @@ -1,10 +1,14 @@ import { LambdaInterface } from '@aws-lambda-powertools/commons'; import { DynamoDBPersistenceLayer } from '../../src/persistence'; -import { idempotentFunction, idempotentLambdaHandler } from '../../src/idempotentDecorator'; +import { + idempotentFunction, + idempotentLambdaHandler, +} from '../../src/idempotentDecorator'; import { Context } from 'aws-lambda'; import { Logger } from '../../../logger'; -const IDEMPOTENCY_TABLE_NAME = process.env.IDEMPOTENCY_TABLE_NAME || 'table_name'; +const IDEMPOTENCY_TABLE_NAME = + process.env.IDEMPOTENCY_TABLE_NAME || 'table_name'; const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer({ tableName: IDEMPOTENCY_TABLE_NAME, }); @@ -21,15 +25,14 @@ const ddbPersistenceLayerCustomized = new DynamoDBPersistenceLayer({ }); interface TestEvent { - [key: string]: string + [key: string]: string; } interface EventRecords { - records: Record[] + records: Record[]; } class DefaultLambda implements LambdaInterface { - @idempotentLambdaHandler({ persistenceStore: dynamoDBPersistenceLayer }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -44,7 +47,10 @@ class DefaultLambda implements LambdaInterface { @idempotentLambdaHandler({ persistenceStore: ddbPersistenceLayerCustomized }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handlerCustomized(_event: TestEvent, _context: Context): Promise { + public async handlerCustomized( + _event: TestEvent, + _context: Context + ): Promise { logger.info(`Got test event customized: ${JSON.stringify(_event)}`); // sleep for 5 seconds @@ -54,24 +60,30 @@ class DefaultLambda implements LambdaInterface { @idempotentLambdaHandler({ persistenceStore: dynamoDBPersistenceLayer }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - public async handlerFails(_event: TestEvent, _context: Context): Promise { + public async handlerFails( + _event: TestEvent, + _context: Context + ): Promise { logger.info(`Got test event: ${JSON.stringify(_event)}`); // sleep for 5 seconds throw new Error('Failed'); } - } const defaultLambda = new DefaultLambda(); export const handler = defaultLambda.handler.bind(defaultLambda); -export const handlerCustomized = defaultLambda.handlerCustomized.bind(defaultLambda); +export const handlerCustomized = + defaultLambda.handlerCustomized.bind(defaultLambda); export const handlerFails = defaultLambda.handlerFails.bind(defaultLambda); const logger = new Logger(); class LambdaWithKeywordArgument implements LambdaInterface { - public async handler(_event: EventRecords, _context: Context): Promise { + public async handler( + _event: EventRecords, + _context: Context + ): Promise { logger.info(`Got test event: ${JSON.stringify(_event)}`); for (const record of _event.records) { logger.info(`Processing event: ${JSON.stringify(record)}`); @@ -81,7 +93,10 @@ class LambdaWithKeywordArgument implements LambdaInterface { return 'Hello World Keyword Argument'; } - @idempotentFunction({ persistenceStore: dynamoDBPersistenceLayer, dataKeywordArgument: 'foo' }) + @idempotentFunction({ + persistenceStore: dynamoDBPersistenceLayer, + dataKeywordArgument: 'foo', + }) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore public async process(record: Record): string { @@ -92,4 +107,5 @@ class LambdaWithKeywordArgument implements LambdaInterface { } const lambdaWithKeywordArg = new LambdaWithKeywordArgument(); -export const handlerWithKeywordArgument = lambdaWithKeywordArg.handler.bind(lambdaWithKeywordArg); \ No newline at end of file +export const handlerWithKeywordArgument = + lambdaWithKeywordArg.handler.bind(lambdaWithKeywordArg); diff --git a/packages/idempotency/tests/e2e/idempotencyDecorator.test.ts b/packages/idempotency/tests/e2e/idempotencyDecorator.test.ts index af87c0d270..6f028ac203 100644 --- a/packages/idempotency/tests/e2e/idempotencyDecorator.test.ts +++ b/packages/idempotency/tests/e2e/idempotencyDecorator.test.ts @@ -6,9 +6,21 @@ import { v4 } from 'uuid'; import { App, Stack } from 'aws-cdk-lib'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { generateUniqueName, invokeFunction, isValidRuntimeKey } from '../../../commons/tests/utils/e2eUtils'; -import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT } from './constants'; -import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + generateUniqueName, + invokeFunction, + isValidRuntimeKey, +} from '../../../commons/tests/utils/e2eUtils'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, +} from './constants'; +import { + deployStack, + destroyStack, +} from '../../../commons/tests/utils/cdk-cli'; import { LEVEL } from '../../../commons/tests/utils/InvocationLogs'; import { GetCommand } from '@aws-sdk/lib-dynamodb'; import { createHash } from 'node:crypto'; @@ -21,7 +33,12 @@ if (!isValidRuntimeKey(runtime)) { } const uuid = v4(); -const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'Idempotency'); +const stackName = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'Idempotency' +); const decoratorFunctionFile = 'idempotencyDecorator.test.FunctionCode.ts'; const app = new App(); @@ -29,118 +46,248 @@ const app = new App(); const ddb = new DynamoDBClient({ region: 'eu-west-1' }); const stack = new Stack(app, stackName); -const functionNameDefault = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'default'); +const functionNameDefault = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'default' +); const ddbTableNameDefault = stackName + '-default-table'; -createIdempotencyResources(stack, runtime, ddbTableNameDefault, decoratorFunctionFile, functionNameDefault, 'handler'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameDefault, + decoratorFunctionFile, + functionNameDefault, + 'handler' +); -const functionNameCustom = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'custom'); +const functionNameCustom = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'custom' +); const ddbTableNameCustom = stackName + '-custom-table'; -createIdempotencyResources(stack, runtime, ddbTableNameCustom, decoratorFunctionFile, functionNameCustom, 'handlerCustomized', 'customId'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameCustom, + decoratorFunctionFile, + functionNameCustom, + 'handlerCustomized', + 'customId' +); -const functionNameKeywordArg = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'keywordarg'); +const functionNameKeywordArg = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'keywordarg' +); const ddbTableNameKeywordArg = stackName + '-keywordarg-table'; -createIdempotencyResources(stack, runtime, ddbTableNameKeywordArg, decoratorFunctionFile, functionNameKeywordArg, 'handlerWithKeywordArgument'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameKeywordArg, + decoratorFunctionFile, + functionNameKeywordArg, + 'handlerWithKeywordArgument' +); -const functionNameFails = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'fails'); +const functionNameFails = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'fails' +); const ddbTableNameFails = stackName + '-fails-table'; -createIdempotencyResources(stack, runtime, ddbTableNameFails, decoratorFunctionFile, functionNameFails, 'handlerFails'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameFails, + decoratorFunctionFile, + functionNameFails, + 'handlerFails' +); describe('Idempotency e2e test decorator, default settings', () => { - beforeAll(async () => { await deployStack(app, stack); - }, SETUP_TIMEOUT); - test('when called twice, it returns the same value without calling the inner function', async () => { - const payload = { foo: 'baz' }; - const payloadHash = createHash('md5').update(JSON.stringify(payload)).digest('base64'); - - const invocationLogsSequential = await invokeFunction(functionNameDefault, 2, 'SEQUENTIAL', payload, false); - // create dynamodb client to query the table and check the value - const result = await ddb.send(new GetCommand({ - TableName: ddbTableNameDefault, - Key: { id: `${functionNameDefault}#${payloadHash}` } - })); - expect(result?.Item?.data).toEqual('Hello World'); - expect(result?.Item?.status).toEqual('COMPLETED'); - expect(result?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); - // we log events inside the handler, so the 2nd invocation should not log anything - expect(invocationLogsSequential[0].getFunctionLogs().toString()).toContain('Got test event'); - expect(invocationLogsSequential[1].getFunctionLogs().toString()).not.toContain('Got test event'); - - }, TEST_CASE_TIMEOUT); - - test('when called twice in parallel, it trows an error', async () => { - const payload = { id: '123' }; - const payloadHash = createHash('md5').update(JSON.stringify(payload)).digest('base64'); - const invocationLogs = await invokeFunction(functionNameDefault, 2, 'PARALLEL', payload, false); - - const result = await ddb.send(new GetCommand({ - TableName: ddbTableNameDefault, - Key: { id: `${functionNameDefault}#${payloadHash}` } - })); - expect(result?.Item?.data).toEqual('Hello World'); - expect(result?.Item?.status).toEqual('COMPLETED'); - expect(result?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); - expect(invocationLogs[0].getFunctionLogs(LEVEL.ERROR).toString()).toContain('There is already an execution in progress with idempotency key'); - }, TEST_CASE_TIMEOUT); - - test('when called with customized idempotency decorator, it creates ddb entry with custom attributes', async () => { - const payload = { foo: 'baz' }; - const payloadHash = createHash('md5').update(JSON.stringify(payload)).digest('base64'); - - const invocationLogsCustmozed = await invokeFunction(functionNameCustom, 1, 'PARALLEL', payload, false); - const result = await ddb.send(new GetCommand({ - TableName: ddbTableNameCustom, - Key: { customId: `${functionNameCustom}#${payloadHash}` } - })); - expect(result?.Item?.dataattr).toEqual('Hello World Customized'); - expect(result?.Item?.statusattr).toEqual('COMPLETED'); - expect(result?.Item?.expiryattr).toBeGreaterThan(Date.now() / 1000); - expect(invocationLogsCustmozed[0].getFunctionLogs().toString()).toContain('Got test event customized'); - }, TEST_CASE_TIMEOUT); - - test('when called with a function that fails, it creates ddb entry with error status', async () => { - const payload = { foo: 'baz' }; - const payloadHash = createHash('md5').update(JSON.stringify(payload)).digest('base64'); - - await invokeFunction(functionNameFails, 1, 'PARALLEL', payload, false); - const result = await ddb.send(new GetCommand({ - TableName: ddbTableNameFails, - Key: { id: `${functionNameFails}#${payloadHash}` } - })); - console.log(result); - expect(result?.Item).toBeUndefined(); - }, TEST_CASE_TIMEOUT); - - test('when called with a function that has keyword argument, it creates for every entry of keyword argument', async () => { - const payloadArray = { records: [ { id: 1, foo: 'bar' }, { id: 2, foo: 'baq' }, { id: 3, foo: 'bar' } ] }; - const payloadHashFirst = createHash('md5').update('"bar"').digest('base64'); - - await invokeFunction(functionNameKeywordArg, 2, 'SEQUENTIAL', payloadArray, false); - const resultFirst = await ddb.send(new GetCommand({ - TableName: ddbTableNameKeywordArg, - Key: { id: `${functionNameKeywordArg}#${payloadHashFirst}` } - })); - console.log(resultFirst); - expect(resultFirst?.Item?.data).toEqual('idempotent result: bar'); - expect(resultFirst?.Item?.status).toEqual('COMPLETED'); - expect(resultFirst?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); - - const payloadHashSecond = createHash('md5').update('"baq"').digest('base64'); - const resultSecond = await ddb.send(new GetCommand({ - TableName: ddbTableNameKeywordArg, - Key: { id: `${functionNameKeywordArg}#${payloadHashSecond}` } - })); - console.log(resultSecond); - expect(resultSecond?.Item?.data).toEqual('idempotent result: baq'); - expect(resultSecond?.Item?.status).toEqual('COMPLETED'); - expect(resultSecond?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); - }, TEST_CASE_TIMEOUT); + test( + 'when called twice, it returns the same value without calling the inner function', + async () => { + const payload = { foo: 'baz' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + + const invocationLogsSequential = await invokeFunction( + functionNameDefault, + 2, + 'SEQUENTIAL', + payload, + false + ); + // create dynamodb client to query the table and check the value + const result = await ddb.send( + new GetCommand({ + TableName: ddbTableNameDefault, + Key: { id: `${functionNameDefault}#${payloadHash}` }, + }) + ); + expect(result?.Item?.data).toEqual('Hello World'); + expect(result?.Item?.status).toEqual('COMPLETED'); + expect(result?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); + // we log events inside the handler, so the 2nd invocation should not log anything + expect( + invocationLogsSequential[0].getFunctionLogs().toString() + ).toContain('Got test event'); + expect( + invocationLogsSequential[1].getFunctionLogs().toString() + ).not.toContain('Got test event'); + }, + TEST_CASE_TIMEOUT + ); + + test( + 'when called twice in parallel, it trows an error', + async () => { + const payload = { id: '123' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + const invocationLogs = await invokeFunction( + functionNameDefault, + 2, + 'PARALLEL', + payload, + false + ); + + const result = await ddb.send( + new GetCommand({ + TableName: ddbTableNameDefault, + Key: { id: `${functionNameDefault}#${payloadHash}` }, + }) + ); + expect(result?.Item?.data).toEqual('Hello World'); + expect(result?.Item?.status).toEqual('COMPLETED'); + expect(result?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); + expect( + invocationLogs[0].getFunctionLogs(LEVEL.ERROR).toString() + ).toContain( + 'There is already an execution in progress with idempotency key' + ); + }, + TEST_CASE_TIMEOUT + ); + + test( + 'when called with customized idempotency decorator, it creates ddb entry with custom attributes', + async () => { + const payload = { foo: 'baz' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + + const invocationLogsCustmozed = await invokeFunction( + functionNameCustom, + 1, + 'PARALLEL', + payload, + false + ); + const result = await ddb.send( + new GetCommand({ + TableName: ddbTableNameCustom, + Key: { customId: `${functionNameCustom}#${payloadHash}` }, + }) + ); + expect(result?.Item?.dataattr).toEqual('Hello World Customized'); + expect(result?.Item?.statusattr).toEqual('COMPLETED'); + expect(result?.Item?.expiryattr).toBeGreaterThan(Date.now() / 1000); + expect(invocationLogsCustmozed[0].getFunctionLogs().toString()).toContain( + 'Got test event customized' + ); + }, + TEST_CASE_TIMEOUT + ); + + test( + 'when called with a function that fails, it creates ddb entry with error status', + async () => { + const payload = { foo: 'baz' }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + + await invokeFunction(functionNameFails, 1, 'PARALLEL', payload, false); + const result = await ddb.send( + new GetCommand({ + TableName: ddbTableNameFails, + Key: { id: `${functionNameFails}#${payloadHash}` }, + }) + ); + console.log(result); + expect(result?.Item).toBeUndefined(); + }, + TEST_CASE_TIMEOUT + ); + + test( + 'when called with a function that has keyword argument, it creates for every entry of keyword argument', + async () => { + const payloadArray = { + records: [ + { id: 1, foo: 'bar' }, + { id: 2, foo: 'baq' }, + { id: 3, foo: 'bar' }, + ], + }; + const payloadHashFirst = createHash('md5') + .update('"bar"') + .digest('base64'); + + await invokeFunction( + functionNameKeywordArg, + 2, + 'SEQUENTIAL', + payloadArray, + false + ); + const resultFirst = await ddb.send( + new GetCommand({ + TableName: ddbTableNameKeywordArg, + Key: { id: `${functionNameKeywordArg}#${payloadHashFirst}` }, + }) + ); + console.log(resultFirst); + expect(resultFirst?.Item?.data).toEqual('idempotent result: bar'); + expect(resultFirst?.Item?.status).toEqual('COMPLETED'); + expect(resultFirst?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); + + const payloadHashSecond = createHash('md5') + .update('"baq"') + .digest('base64'); + const resultSecond = await ddb.send( + new GetCommand({ + TableName: ddbTableNameKeywordArg, + Key: { id: `${functionNameKeywordArg}#${payloadHashSecond}` }, + }) + ); + console.log(resultSecond); + expect(resultSecond?.Item?.data).toEqual('idempotent result: baq'); + expect(resultSecond?.Item?.status).toEqual('COMPLETED'); + expect(resultSecond?.Item?.expiration).toBeGreaterThan(Date.now() / 1000); + }, + TEST_CASE_TIMEOUT + ); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await destroyStack(app, stack); } }, TEARDOWN_TIMEOUT); -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts b/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts index 06fbaae9bc..10768e1705 100644 --- a/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts @@ -3,7 +3,8 @@ import { makeFunctionIdempotent } from '../../src/makeFunctionIdempotent'; import { Logger } from '@aws-lambda-powertools/logger'; import { Context } from 'aws-lambda'; -const IDEMPOTENCY_TABLE_NAME = process.env.IDEMPOTENCY_TABLE_NAME || 'table_name'; +const IDEMPOTENCY_TABLE_NAME = + process.env.IDEMPOTENCY_TABLE_NAME || 'table_name'; const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer({ tableName: IDEMPOTENCY_TABLE_NAME, }); @@ -20,7 +21,7 @@ const ddbPersistenceLayerCustomized = new DynamoDBPersistenceLayer({ }); interface EventRecords { - records: Record[] + records: Record[]; } const logger = new Logger(); @@ -31,37 +32,36 @@ const processRecord = (record: Record): string => { return 'Processing done: ' + record['foo']; }; -const processIdempotently = makeFunctionIdempotent( - processRecord, - { - persistenceStore: dynamoDBPersistenceLayer, - dataKeywordArgument: 'foo' - }); +const processIdempotently = makeFunctionIdempotent(processRecord, { + persistenceStore: dynamoDBPersistenceLayer, + dataKeywordArgument: 'foo', +}); -export const handler = async (_event: EventRecords, _context: Context): Promise => { +export const handler = async ( + _event: EventRecords, + _context: Context +): Promise => { for (const record of _event.records) { const result = await processIdempotently(record); logger.info(result.toString()); - } return Promise.resolve(); }; -const processIdempotentlyCustomized = makeFunctionIdempotent( - processRecord, - { - persistenceStore: ddbPersistenceLayerCustomized, - dataKeywordArgument: 'foo' - }); +const processIdempotentlyCustomized = makeFunctionIdempotent(processRecord, { + persistenceStore: ddbPersistenceLayerCustomized, + dataKeywordArgument: 'foo', +}); -export const handlerCustomized = async (_event: EventRecords, _context: Context): Promise => { +export const handlerCustomized = async ( + _event: EventRecords, + _context: Context +): Promise => { for (const record of _event.records) { const result = await processIdempotentlyCustomized(record); logger.info(result.toString()); - } return Promise.resolve(); }; - diff --git a/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.ts b/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.ts index 4e631258d9..8ee55021d9 100644 --- a/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.ts @@ -3,13 +3,25 @@ * * @group e2e/idempotency */ -import { generateUniqueName, invokeFunction, isValidRuntimeKey } from '../../../commons/tests/utils/e2eUtils'; -import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT } from './constants'; +import { + generateUniqueName, + invokeFunction, + isValidRuntimeKey, +} from '../../../commons/tests/utils/e2eUtils'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, +} from './constants'; import { v4 } from 'uuid'; import { App, Stack } from 'aws-cdk-lib'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { createHash } from 'node:crypto'; -import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { + deployStack, + destroyStack, +} from '../../../commons/tests/utils/cdk-cli'; import { GetCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'; import { createIdempotencyResources } from '../helpers/idempotencyUtils'; @@ -19,78 +31,164 @@ if (!isValidRuntimeKey(runtime)) { throw new Error(`Invalid runtime key value: ${runtime}`); } const uuid = v4(); -const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'makeFnIdempotent'); -const makeFunctionIdepmpotentFile = 'makeFunctionIdempotent.test.FunctionCode.ts'; +const stackName = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'makeFnIdempotent' +); +const makeFunctionIdepmpotentFile = + 'makeFunctionIdempotent.test.FunctionCode.ts'; const app = new App(); const ddb = new DynamoDBClient({ region: 'eu-west-1' }); const stack = new Stack(app, stackName); -const functionNameDefault = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'default'); +const functionNameDefault = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'default' +); const ddbTableNameDefault = stackName + '-default-table'; -createIdempotencyResources(stack, runtime, ddbTableNameDefault, makeFunctionIdepmpotentFile, functionNameDefault, 'handler'); - -const functionNameCustom = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'custom'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameDefault, + makeFunctionIdepmpotentFile, + functionNameDefault, + 'handler' +); + +const functionNameCustom = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'custom' +); const ddbTableNameCustom = stackName + '-custom-table'; -createIdempotencyResources(stack, runtime, ddbTableNameCustom, makeFunctionIdepmpotentFile, functionNameCustom, 'handlerCustomized', 'customId'); - -const functionNameKeywordArg = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'keywordarg'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameCustom, + makeFunctionIdepmpotentFile, + functionNameCustom, + 'handlerCustomized', + 'customId' +); + +const functionNameKeywordArg = generateUniqueName( + RESOURCE_NAME_PREFIX, + uuid, + runtime, + 'keywordarg' +); const ddbTableNameKeywordArg = stackName + '-keywordarg-table'; -createIdempotencyResources(stack, runtime, ddbTableNameKeywordArg, makeFunctionIdepmpotentFile, functionNameKeywordArg, 'handlerWithKeywordArgument'); +createIdempotencyResources( + stack, + runtime, + ddbTableNameKeywordArg, + makeFunctionIdepmpotentFile, + functionNameKeywordArg, + 'handlerWithKeywordArgument' +); describe('Idempotency e2e test function wrapper, default settings', () => { - beforeAll(async () => { await deployStack(app, stack); - }, SETUP_TIMEOUT); - it('when called twice, it returns the same result', async () => { - const payload = { records: [ { id: 1, foo: 'bar' }, { id: 2, foo: 'baz' }, { id: 3, foo: 'bar' } ] }; - await invokeFunction(functionNameDefault, 2, 'SEQUENTIAL', payload, false); - - const payloadHashFirst = createHash('md5').update(JSON.stringify('bar')).digest('base64'); - const payloadHashSecond = createHash('md5').update(JSON.stringify('baz')).digest('base64'); - - const result = await ddb.send(new ScanCommand({ TableName: ddbTableNameDefault })); - expect(result?.Items?.length).toEqual(2); - - const resultFirst = await ddb.send(new GetCommand({ - TableName: ddbTableNameDefault, - Key: { id: `${functionNameDefault}#${payloadHashFirst}` } - })); - expect(resultFirst?.Item?.data).toEqual('Processing done: bar'); - expect(resultFirst?.Item?.status).toEqual('COMPLETED'); - - const resultSecond = await ddb.send(new GetCommand({ - TableName: ddbTableNameDefault, - Key: { id: `${functionNameDefault}#${payloadHashSecond}` } - })); - expect(resultSecond?.Item?.data).toEqual('Processing done: baz'); - expect(resultSecond?.Item?.status).toEqual('COMPLETED'); - - }, TEST_CASE_TIMEOUT); - - test('when called with customized function wrapper, it creates ddb entry with custom attributes', async () => { - const payload = { records: [ { id: 1, foo: 'bar' }, { id: 2, foo: 'baq' }, { id: 3, foo: 'bar' } ] }; - const payloadHash = createHash('md5').update('"bar"').digest('base64'); - - const invocationLogsCustmozed = await invokeFunction(functionNameCustom, 2, 'SEQUENTIAL', payload, false); - const result = await ddb.send(new GetCommand({ - TableName: ddbTableNameCustom, - Key: { customId: `${functionNameCustom}#${payloadHash}` } - })); - console.log(result); - expect(result?.Item?.dataattr).toEqual('Processing done: bar'); - expect(result?.Item?.statusattr).toEqual('COMPLETED'); - expect(result?.Item?.expiryattr).toBeGreaterThan(Date.now() / 1000); - expect(invocationLogsCustmozed[0].getFunctionLogs().toString()).toContain('Got test event'); - }, TEST_CASE_TIMEOUT); + it( + 'when called twice, it returns the same result', + async () => { + const payload = { + records: [ + { id: 1, foo: 'bar' }, + { id: 2, foo: 'baz' }, + { id: 3, foo: 'bar' }, + ], + }; + await invokeFunction( + functionNameDefault, + 2, + 'SEQUENTIAL', + payload, + false + ); + + const payloadHashFirst = createHash('md5') + .update(JSON.stringify('bar')) + .digest('base64'); + const payloadHashSecond = createHash('md5') + .update(JSON.stringify('baz')) + .digest('base64'); + + const result = await ddb.send( + new ScanCommand({ TableName: ddbTableNameDefault }) + ); + expect(result?.Items?.length).toEqual(2); + + const resultFirst = await ddb.send( + new GetCommand({ + TableName: ddbTableNameDefault, + Key: { id: `${functionNameDefault}#${payloadHashFirst}` }, + }) + ); + expect(resultFirst?.Item?.data).toEqual('Processing done: bar'); + expect(resultFirst?.Item?.status).toEqual('COMPLETED'); + + const resultSecond = await ddb.send( + new GetCommand({ + TableName: ddbTableNameDefault, + Key: { id: `${functionNameDefault}#${payloadHashSecond}` }, + }) + ); + expect(resultSecond?.Item?.data).toEqual('Processing done: baz'); + expect(resultSecond?.Item?.status).toEqual('COMPLETED'); + }, + TEST_CASE_TIMEOUT + ); + + test( + 'when called with customized function wrapper, it creates ddb entry with custom attributes', + async () => { + const payload = { + records: [ + { id: 1, foo: 'bar' }, + { id: 2, foo: 'baq' }, + { id: 3, foo: 'bar' }, + ], + }; + const payloadHash = createHash('md5').update('"bar"').digest('base64'); + + const invocationLogsCustmozed = await invokeFunction( + functionNameCustom, + 2, + 'SEQUENTIAL', + payload, + false + ); + const result = await ddb.send( + new GetCommand({ + TableName: ddbTableNameCustom, + Key: { customId: `${functionNameCustom}#${payloadHash}` }, + }) + ); + console.log(result); + expect(result?.Item?.dataattr).toEqual('Processing done: bar'); + expect(result?.Item?.statusattr).toEqual('COMPLETED'); + expect(result?.Item?.expiryattr).toBeGreaterThan(Date.now() / 1000); + expect(invocationLogsCustmozed[0].getFunctionLogs().toString()).toContain( + 'Got test event' + ); + }, + TEST_CASE_TIMEOUT + ); afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await destroyStack(app, stack); } }, TEARDOWN_TIMEOUT); -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/helpers/idempotencyUtils.ts b/packages/idempotency/tests/helpers/idempotencyUtils.ts index 91235cd506..fbf0e3f4b1 100644 --- a/packages/idempotency/tests/helpers/idempotencyUtils.ts +++ b/packages/idempotency/tests/helpers/idempotencyUtils.ts @@ -5,7 +5,15 @@ import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { TEST_RUNTIMES } from '../../../commons/tests/utils/e2eUtils'; import path from 'path'; -export const createIdempotencyResources = (stack: Stack, runtime: string, ddbTableName: string, pathToFunction: string, functionName: string, handler: string, ddbPkId?: string): void => { +export const createIdempotencyResources = ( + stack: Stack, + runtime: string, + ddbTableName: string, + pathToFunction: string, + functionName: string, + handler: string, + ddbPkId?: string +): void => { const uniqueTableId = ddbTableName + v4().substring(0, 5); const ddbTable = new Table(stack, uniqueTableId, { tableName: ddbTableName, @@ -14,7 +22,7 @@ export const createIdempotencyResources = (stack: Stack, runtime: string, ddbTab type: AttributeType.STRING, }, billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY + removalPolicy: RemovalPolicy.DESTROY, }); const uniqueFunctionId = functionName + v4().substring(0, 5); @@ -27,9 +35,8 @@ export const createIdempotencyResources = (stack: Stack, runtime: string, ddbTab environment: { IDEMPOTENCY_TABLE_NAME: ddbTableName, POWERTOOLS_LOGGER_LOG_EVENT: 'true', - } + }, }); ddbTable.grantReadWriteData(nodeJsFunction); - -}; \ No newline at end of file +}; diff --git a/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts b/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts index cac6f99929..9d95c74e9c 100644 --- a/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts +++ b/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts @@ -3,7 +3,10 @@ process.env._X_AMZN_TRACE_ID = '1-abcdef12-3456abcdef123456abcdef12'; process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-lambda-function'; process.env.AWS_EXECUTION_ENV = 'nodejs16.x'; process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128'; -if (process.env.AWS_REGION === undefined && process.env.CDK_DEFAULT_REGION === undefined) { +if ( + process.env.AWS_REGION === undefined && + process.env.CDK_DEFAULT_REGION === undefined +) { process.env.AWS_REGION = 'eu-west-1'; } process.env._HANDLER = 'index.handler'; diff --git a/packages/idempotency/tests/unit/IdempotencyConfig.test.ts b/packages/idempotency/tests/unit/IdempotencyConfig.test.ts index 6155787883..0da250a058 100644 --- a/packages/idempotency/tests/unit/IdempotencyConfig.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyConfig.test.ts @@ -3,18 +3,11 @@ * * @group unit/idempotency/config */ -import { - ContextExamples as dummyContext -} from '@aws-lambda-powertools/commons'; -import { - IdempotencyConfig, -} from '../../src/IdempotencyConfig'; -import type { - IdempotencyConfigOptions, -} from '../../src/types'; +import { ContextExamples as dummyContext } from '@aws-lambda-powertools/commons'; +import { IdempotencyConfig } from '../../src/IdempotencyConfig'; +import type { IdempotencyConfigOptions } from '../../src/types'; describe('Class: IdempotencyConfig', () => { - const ENVIRONMENT_VARIABLES = process.env; const context = dummyContext.helloworldContext; @@ -29,30 +22,28 @@ describe('Class: IdempotencyConfig', () => { }); describe('Method: configure', () => { - test('when configured with an empty config object, it initializes the config with default values', () => { - // Prepare const configOptions = {}; - + // Act - const config = new IdempotencyConfig(configOptions); + const config = new IdempotencyConfig(configOptions); // Assess - expect(config).toEqual(expect.objectContaining({ - eventKeyJmesPath: '', - payloadValidationJmesPath: undefined, - throwOnNoIdempotencyKey: false, - expiresAfterSeconds: 3600, - useLocalCache: false, - hashFunction: 'md5', - lambdaContext: undefined, - })); - + expect(config).toEqual( + expect.objectContaining({ + eventKeyJmesPath: '', + payloadValidationJmesPath: undefined, + throwOnNoIdempotencyKey: false, + expiresAfterSeconds: 3600, + useLocalCache: false, + hashFunction: 'md5', + lambdaContext: undefined, + }) + ); }); test('when configured with a config object, it initializes the config with the provided configs', () => { - // Prepare const configOptions: IdempotencyConfigOptions = { eventKeyJmesPath: 'eventKeyJmesPath', @@ -68,37 +59,34 @@ describe('Class: IdempotencyConfig', () => { const config = new IdempotencyConfig(configOptions); // Assess - expect(config).toEqual(expect.objectContaining({ - eventKeyJmesPath: 'eventKeyJmesPath', - payloadValidationJmesPath: 'payloadValidationJmesPath', - throwOnNoIdempotencyKey: true, - expiresAfterSeconds: 100, - useLocalCache: true, - hashFunction: 'hashFunction', - lambdaContext: context, - })); - + expect(config).toEqual( + expect.objectContaining({ + eventKeyJmesPath: 'eventKeyJmesPath', + payloadValidationJmesPath: 'payloadValidationJmesPath', + throwOnNoIdempotencyKey: true, + expiresAfterSeconds: 100, + useLocalCache: true, + hashFunction: 'hashFunction', + lambdaContext: context, + }) + ); }); - }); describe('Method: registerLambdaContext', () => { - test('when called, it stores the provided context', async () => { - // Prepare - const config = new IdempotencyConfig({}); - + const config = new IdempotencyConfig({}); + // Act config.registerLambdaContext(context); // Assess - expect(config).toEqual(expect.objectContaining({ - lambdaContext: context - })); - + expect(config).toEqual( + expect.objectContaining({ + lambdaContext: context, + }) + ); }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index 5b35785080..6e43e01cc6 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -3,17 +3,16 @@ * * @group unit/idempotency/IdempotencyHandler */ - import { IdempotencyAlreadyInProgressError, IdempotencyInconsistentStateError, IdempotencyItemAlreadyExistsError, - IdempotencyPersistenceLayerError + IdempotencyPersistenceLayerError, } from '../../src/Exceptions'; -import { IdempotencyFunctionOptions, IdempotencyRecordStatus, } from '../../src/types'; +import { IdempotencyRecordStatus } from '../../src/types'; import { BasePersistenceLayer, IdempotencyRecord } from '../../src/persistence'; import { IdempotencyHandler } from '../../src/IdempotencyHandler'; -import { IdempotencyConfig } from '../..//src/IdempotencyConfig'; +import { IdempotencyConfig } from '../../src/IdempotencyConfig'; class PersistenceLayerTestClass extends BasePersistenceLayer { protected _deleteRecord = jest.fn(); @@ -24,82 +23,86 @@ class PersistenceLayerTestClass extends BasePersistenceLayer { const mockFunctionToMakeIdempotent = jest.fn(); const mockFunctionPayloadToBeHashed = {}; -const mockIdempotencyOptions: IdempotencyFunctionOptions = { +const mockIdempotencyOptions = { persistenceStore: new PersistenceLayerTestClass(), dataKeywordArgument: 'testKeywordArgument', - config: new IdempotencyConfig({}) + config: new IdempotencyConfig({}), }; const mockFullFunctionPayload = {}; -const idempotentHandler = new IdempotencyHandler( - mockFunctionToMakeIdempotent, - mockFunctionPayloadToBeHashed, - mockIdempotencyOptions.persistenceStore, - mockFullFunctionPayload, -); +const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + fullFunctionPayload: mockFullFunctionPayload, + idempotencyConfig: mockIdempotencyOptions.config, +}); describe('Class IdempotencyHandler', () => { beforeEach(() => jest.resetAllMocks()); describe('Method: determineResultFromIdempotencyRecord', () => { test('when record is in progress and within expiry window, it rejects with IdempotencyAlreadyInProgressError', async () => { - const stubRecord = new IdempotencyRecord({ idempotencyKey: 'idempotencyKey', expiryTimestamp: Date.now() + 1000, // should be in the future inProgressExpiryTimestamp: 0, // less than current time in milliseconds responseData: { responseData: 'responseData' }, payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.INPROGRESS + status: IdempotencyRecordStatus.INPROGRESS, }); expect(stubRecord.isExpired()).toBe(false); expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); try { - await idempotentHandler.determineResultFromIdempotencyRecord(stubRecord); + await idempotentHandler.determineResultFromIdempotencyRecord( + stubRecord + ); } catch (e) { expect(e).toBeInstanceOf(IdempotencyAlreadyInProgressError); } }); test('when record is in progress and outside expiry window, it rejects with IdempotencyInconsistentStateError', async () => { - const stubRecord = new IdempotencyRecord({ idempotencyKey: 'idempotencyKey', expiryTimestamp: Date.now() + 1000, // should be in the future inProgressExpiryTimestamp: new Date().getUTCMilliseconds() - 1000, // should be in the past responseData: { responseData: 'responseData' }, payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.INPROGRESS + status: IdempotencyRecordStatus.INPROGRESS, }); expect(stubRecord.isExpired()).toBe(false); expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); try { - await idempotentHandler.determineResultFromIdempotencyRecord(stubRecord); + await idempotentHandler.determineResultFromIdempotencyRecord( + stubRecord + ); } catch (e) { expect(e).toBeInstanceOf(IdempotencyInconsistentStateError); } }); test('when record is expired, it rejects with IdempotencyInconsistentStateError', async () => { - const stubRecord = new IdempotencyRecord({ idempotencyKey: 'idempotencyKey', expiryTimestamp: new Date().getUTCMilliseconds() - 1000, // should be in the past inProgressExpiryTimestamp: 0, // less than current time in milliseconds responseData: { responseData: 'responseData' }, payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.EXPIRED + status: IdempotencyRecordStatus.EXPIRED, }); expect(stubRecord.isExpired()).toBe(true); expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.EXPIRED); try { - await idempotentHandler.determineResultFromIdempotencyRecord(stubRecord); + await idempotentHandler.determineResultFromIdempotencyRecord( + stubRecord + ); } catch (e) { expect(e).toBeInstanceOf(IdempotencyInconsistentStateError); } @@ -107,55 +110,74 @@ describe('Class IdempotencyHandler', () => { }); describe('Method: handle', () => { - afterAll(() => jest.restoreAllMocks()); // restore processIdempotency for other tests test('when IdempotencyAlreadyInProgressError is thrown, it retries two times', async () => { - const mockProcessIdempotency = jest.spyOn(IdempotencyHandler.prototype, 'processIdempotency').mockRejectedValue(new IdempotencyAlreadyInProgressError('There is already an execution in progress')); - await expect( - idempotentHandler.handle() - ).rejects.toThrow(IdempotencyAlreadyInProgressError); + const mockProcessIdempotency = jest + .spyOn(IdempotencyHandler.prototype, 'processIdempotency') + .mockRejectedValue( + new IdempotencyAlreadyInProgressError( + 'There is already an execution in progress' + ) + ); + await expect(idempotentHandler.handle()).rejects.toThrow( + IdempotencyAlreadyInProgressError + ); expect(mockProcessIdempotency).toHaveBeenCalledTimes(2); }); test('when non IdempotencyAlreadyInProgressError is thrown, it rejects', async () => { + const mockProcessIdempotency = jest + .spyOn(IdempotencyHandler.prototype, 'processIdempotency') + .mockRejectedValue(new Error('Some other error')); - const mockProcessIdempotency = jest.spyOn(IdempotencyHandler.prototype, 'processIdempotency').mockRejectedValue(new Error('Some other error')); - - await expect( - idempotentHandler.handle() - ).rejects.toThrow(Error); + await expect(idempotentHandler.handle()).rejects.toThrow(Error); expect(mockProcessIdempotency).toHaveBeenCalledTimes(1); }); - }); describe('Method: processIdempotency', () => { - test('when persistenceStore saves successfuly, it resolves', async () => { - const mockSaveInProgress = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'saveInProgress').mockResolvedValue(); + const mockSaveInProgress = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'saveInProgress') + .mockResolvedValue(); - mockFunctionToMakeIdempotent.mockImplementation(() => Promise.resolve('result')); + mockFunctionToMakeIdempotent.mockImplementation(() => + Promise.resolve('result') + ); - await expect( - idempotentHandler.processIdempotency() - ).resolves.toBe('result'); + await expect(idempotentHandler.processIdempotency()).resolves.toBe( + 'result' + ); expect(mockSaveInProgress).toHaveBeenCalledTimes(1); }); test('when persistences store throws any error, it wraps the error to IdempotencyPersistencesLayerError', async () => { - const mockSaveInProgress = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'saveInProgress').mockRejectedValue(new Error('Some error')); - const mockDetermineResultFromIdempotencyRecord = jest.spyOn(IdempotencyHandler.prototype, 'determineResultFromIdempotencyRecord').mockResolvedValue('result'); - - await expect( - idempotentHandler.processIdempotency() - ).rejects.toThrow(IdempotencyPersistenceLayerError); + const mockSaveInProgress = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'saveInProgress') + .mockRejectedValue(new Error('Some error')); + const mockDetermineResultFromIdempotencyRecord = jest + .spyOn( + IdempotencyHandler.prototype, + 'determineResultFromIdempotencyRecord' + ) + .mockResolvedValue('result'); + + await expect(idempotentHandler.processIdempotency()).rejects.toThrow( + IdempotencyPersistenceLayerError + ); expect(mockSaveInProgress).toHaveBeenCalledTimes(1); expect(mockDetermineResultFromIdempotencyRecord).toHaveBeenCalledTimes(0); }); test('when idempotency item already exists, it returns the existing record', async () => { - const mockSaveInProgress = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'saveInProgress').mockRejectedValue(new IdempotencyItemAlreadyExistsError('There is already an execution in progress')); + const mockSaveInProgress = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'saveInProgress') + .mockRejectedValue( + new IdempotencyItemAlreadyExistsError( + 'There is already an execution in progress' + ) + ); const stubRecord = new IdempotencyRecord({ idempotencyKey: 'idempotencyKey', @@ -163,14 +185,21 @@ describe('Class IdempotencyHandler', () => { inProgressExpiryTimestamp: 0, responseData: { responseData: 'responseData' }, payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.INPROGRESS + status: IdempotencyRecordStatus.INPROGRESS, }); - const mockGetRecord = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'getRecord').mockImplementation(() => Promise.resolve(stubRecord)); - const mockDetermineResultFromIdempotencyRecord = jest.spyOn(IdempotencyHandler.prototype, 'determineResultFromIdempotencyRecord').mockResolvedValue('result'); - - await expect( - idempotentHandler.processIdempotency() - ).resolves.toBe('result'); + const mockGetRecord = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'getRecord') + .mockImplementation(() => Promise.resolve(stubRecord)); + const mockDetermineResultFromIdempotencyRecord = jest + .spyOn( + IdempotencyHandler.prototype, + 'determineResultFromIdempotencyRecord' + ) + .mockResolvedValue('result'); + + await expect(idempotentHandler.processIdempotency()).resolves.toBe( + 'result' + ); expect(mockSaveInProgress).toHaveBeenCalledTimes(1); expect(mockGetRecord).toHaveBeenCalledTimes(1); expect(mockDetermineResultFromIdempotencyRecord).toHaveBeenCalledTimes(1); @@ -179,48 +208,62 @@ describe('Class IdempotencyHandler', () => { describe('Method: getFunctionResult', () => { test('when function returns a result, it saves the successful result and returns it', async () => { - const mockSaveSuccessfulResult = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'saveSuccess').mockResolvedValue(); - mockFunctionToMakeIdempotent.mockImplementation(() => Promise.resolve('result')); - - await expect( - idempotentHandler.getFunctionResult() - ).resolves.toBe('result'); + const mockSaveSuccessfulResult = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'saveSuccess') + .mockResolvedValue(); + mockFunctionToMakeIdempotent.mockImplementation(() => + Promise.resolve('result') + ); + + await expect(idempotentHandler.getFunctionResult()).resolves.toBe( + 'result' + ); expect(mockSaveSuccessfulResult).toHaveBeenCalledTimes(1); }); test('when function throws an error, it deletes the in progress record and throws the error', async () => { - mockFunctionToMakeIdempotent.mockImplementation(() => Promise.reject(new Error('Some error'))); + mockFunctionToMakeIdempotent.mockImplementation(() => + Promise.reject(new Error('Some error')) + ); - const mockDeleteInProgress = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'deleteRecord').mockResolvedValue(); + const mockDeleteInProgress = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'deleteRecord') + .mockResolvedValue(); - await expect( - idempotentHandler.getFunctionResult() - ).rejects.toThrow(Error); + await expect(idempotentHandler.getFunctionResult()).rejects.toThrow( + Error + ); expect(mockDeleteInProgress).toHaveBeenCalledTimes(1); }); test('when deleteRecord throws an error, it wraps the error to IdempotencyPersistenceLayerError', async () => { - mockFunctionToMakeIdempotent.mockImplementation(() => Promise.reject(new Error('Some error'))); + mockFunctionToMakeIdempotent.mockImplementation(() => + Promise.reject(new Error('Some error')) + ); - const mockDeleteInProgress = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'deleteRecord').mockRejectedValue(new Error('Some error')); + const mockDeleteInProgress = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'deleteRecord') + .mockRejectedValue(new Error('Some error')); - await expect( - idempotentHandler.getFunctionResult() - ).rejects.toThrow(IdempotencyPersistenceLayerError); + await expect(idempotentHandler.getFunctionResult()).rejects.toThrow( + IdempotencyPersistenceLayerError + ); expect(mockDeleteInProgress).toHaveBeenCalledTimes(1); }); test('when saveSuccessfulResult throws an error, it wraps the error to IdempotencyPersistenceLayerError', async () => { - mockFunctionToMakeIdempotent.mockImplementation(() => Promise.resolve('result')); + mockFunctionToMakeIdempotent.mockImplementation(() => + Promise.resolve('result') + ); - const mockSaveSuccessfulResult = jest.spyOn(mockIdempotencyOptions.persistenceStore, 'saveSuccess').mockRejectedValue(new Error('Some error')); + const mockSaveSuccessfulResult = jest + .spyOn(mockIdempotencyOptions.persistenceStore, 'saveSuccess') + .mockRejectedValue(new Error('Some error')); - await expect( - idempotentHandler.getFunctionResult() - ).rejects.toThrow(IdempotencyPersistenceLayerError); + await expect(idempotentHandler.getFunctionResult()).rejects.toThrow( + IdempotencyPersistenceLayerError + ); expect(mockSaveSuccessfulResult).toHaveBeenCalledTimes(1); }); - }); }); - diff --git a/packages/idempotency/tests/unit/config/EnvironmentVariableService.test.ts b/packages/idempotency/tests/unit/config/EnvironmentVariableService.test.ts index 85f0c8b116..85f44eb60a 100644 --- a/packages/idempotency/tests/unit/config/EnvironmentVariableService.test.ts +++ b/packages/idempotency/tests/unit/config/EnvironmentVariableService.test.ts @@ -6,7 +6,6 @@ import { EnvironmentVariablesService } from '../../../src/config'; describe('Class: EnvironmentVariableService', () => { - const ENVIRONMENT_VARIABLES = process.env; beforeEach(() => { @@ -19,9 +18,7 @@ describe('Class: EnvironmentVariableService', () => { }); describe('Method: getFunctionName', () => { - test('When called, it gets the Lambda function name from the environment variable', () => { - // Prepare const expectedName = process.env.AWS_LAMBDA_FUNCTION_NAME; @@ -30,11 +27,9 @@ describe('Class: EnvironmentVariableService', () => { // Assess expect(lambdaName).toEqual(expectedName); - }); test('When called without the environment variable set, it returns an empty string', () => { - // Prepare delete process.env.AWS_LAMBDA_FUNCTION_NAME; @@ -43,8 +38,6 @@ describe('Class: EnvironmentVariableService', () => { // Assess expect(lambdaName).toEqual(''); - }); - }); -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/unit/idempotentDecorator.test.ts b/packages/idempotency/tests/unit/idempotentDecorator.test.ts index 65644a7690..42f8d92cba 100644 --- a/packages/idempotency/tests/unit/idempotentDecorator.test.ts +++ b/packages/idempotency/tests/unit/idempotentDecorator.test.ts @@ -5,19 +5,29 @@ */ import { BasePersistenceLayer, IdempotencyRecord } from '../../src/persistence'; -import { idempotentFunction, idempotentLambdaHandler } from '../../src/idempotentDecorator'; +import { + idempotentFunction, + idempotentLambdaHandler, +} from '../../src/idempotentDecorator'; import type { IdempotencyRecordOptions } from '../../src/types'; import { IdempotencyRecordStatus } from '../../src/types'; import { IdempotencyAlreadyInProgressError, IdempotencyInconsistentStateError, IdempotencyItemAlreadyExistsError, - IdempotencyPersistenceLayerError + IdempotencyPersistenceLayerError, } from '../../src/Exceptions'; - -const mockSaveInProgress = jest.spyOn(BasePersistenceLayer.prototype, 'saveInProgress').mockImplementation(); -const mockSaveSuccess = jest.spyOn(BasePersistenceLayer.prototype, 'saveSuccess').mockImplementation(); -const mockGetRecord = jest.spyOn(BasePersistenceLayer.prototype, 'getRecord').mockImplementation(); +import { IdempotencyConfig } from '../../src/IdempotencyConfig'; + +const mockSaveInProgress = jest + .spyOn(BasePersistenceLayer.prototype, 'saveInProgress') + .mockImplementation(); +const mockSaveSuccess = jest + .spyOn(BasePersistenceLayer.prototype, 'saveSuccess') + .mockImplementation(); +const mockGetRecord = jest + .spyOn(BasePersistenceLayer.prototype, 'getRecord') + .mockImplementation(); class PersistenceLayerTestClass extends BasePersistenceLayer { protected _deleteRecord = jest.fn(); @@ -29,9 +39,9 @@ class PersistenceLayerTestClass extends BasePersistenceLayer { const functionalityToDecorate = jest.fn(); class TestinClassWithLambdaHandler { - @idempotentLambdaHandler({ persistenceStore: new PersistenceLayerTestClass() }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + @idempotentLambdaHandler({ + persistenceStore: new PersistenceLayerTestClass(), + }) public testing(record: Record): string { functionalityToDecorate(record); @@ -40,16 +50,14 @@ class TestinClassWithLambdaHandler { } class TestingClassWithFunctionDecorator { - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore public handler(record: Record): string { return this.proccessRecord(record); } - @idempotentFunction({ persistenceStore: new PersistenceLayerTestClass(), dataKeywordArgument: 'testingKey' }) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + @idempotentFunction({ + persistenceStore: new PersistenceLayerTestClass(), + dataKeywordArgument: 'testingKey', + }) public proccessRecord(record: Record): string { functionalityToDecorate(record); @@ -57,10 +65,12 @@ class TestingClassWithFunctionDecorator { } } -describe('Given a class with a function to decorate', (classWithLambdaHandler = new TestinClassWithLambdaHandler(), - classWithFunctionDecorator = new TestingClassWithFunctionDecorator()) => { +describe('Given a class with a function to decorate', (classWithLambdaHandler = new TestinClassWithLambdaHandler(), classWithFunctionDecorator = new TestingClassWithFunctionDecorator()) => { const keyValueToBeSaved = 'thisWillBeSaved'; - const inputRecord = { testingKey: keyValueToBeSaved, otherKey: 'thisWillNot' }; + const inputRecord = { + testingKey: keyValueToBeSaved, + otherKey: 'thisWillNot', + }; beforeEach(() => jest.clearAllMocks()); describe('When wrapping a function with no previous executions', () => { @@ -77,9 +87,11 @@ describe('Given a class with a function to decorate', (classWithLambdaHandler = }); test('Then it will save the record to COMPLETED with function return value', () => { - expect(mockSaveSuccess).toBeCalledWith(keyValueToBeSaved, 'Processed Record'); + expect(mockSaveSuccess).toBeCalledWith( + keyValueToBeSaved, + 'Processed Record' + ); }); - }); describe('When wrapping a function with no previous executions', () => { beforeEach(async () => { @@ -102,12 +114,16 @@ describe('Given a class with a function to decorate', (classWithLambdaHandler = describe('When decorating a function with previous execution that is INPROGRESS', () => { let resultingError: Error; beforeEach(async () => { - mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + mockSaveInProgress.mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const idempotencyOptions: IdempotencyRecordOptions = { idempotencyKey: 'key', - status: IdempotencyRecordStatus.INPROGRESS + status: IdempotencyRecordStatus.INPROGRESS, }; - mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + mockGetRecord.mockResolvedValue( + new IdempotencyRecord(idempotencyOptions) + ); try { await classWithLambdaHandler.testing(inputRecord); } catch (e) { @@ -135,12 +151,16 @@ describe('Given a class with a function to decorate', (classWithLambdaHandler = describe('When decorating a function with previous execution that is EXPIRED', () => { let resultingError: Error; beforeEach(async () => { - mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + mockSaveInProgress.mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const idempotencyOptions: IdempotencyRecordOptions = { idempotencyKey: 'key', - status: IdempotencyRecordStatus.EXPIRED + status: IdempotencyRecordStatus.EXPIRED, }; - mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + mockGetRecord.mockResolvedValue( + new IdempotencyRecord(idempotencyOptions) + ); try { await classWithLambdaHandler.testing(inputRecord); } catch (e) { @@ -167,13 +187,17 @@ describe('Given a class with a function to decorate', (classWithLambdaHandler = describe('When wrapping a function with previous execution that is COMPLETED', () => { beforeEach(async () => { - mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + mockSaveInProgress.mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const idempotencyOptions: IdempotencyRecordOptions = { idempotencyKey: 'key', status: IdempotencyRecordStatus.COMPLETED, }; - mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + mockGetRecord.mockResolvedValue( + new IdempotencyRecord(idempotencyOptions) + ); await classWithLambdaHandler.testing(inputRecord); }); @@ -188,15 +212,28 @@ describe('Given a class with a function to decorate', (classWithLambdaHandler = test('Then it will not call decorated functionality', () => { expect(functionalityToDecorate).not.toBeCalledWith(inputRecord); }); - }); describe('When wrapping a function with issues saving the record', () => { + class TestinClassWithLambdaHandlerWithConfig { + @idempotentLambdaHandler({ + persistenceStore: new PersistenceLayerTestClass(), + config: new IdempotencyConfig({}), + }) + public testing(record: Record): string { + functionalityToDecorate(record); + + return 'Hi'; + } + } + let resultingError: Error; beforeEach(async () => { mockSaveInProgress.mockRejectedValue(new Error('RandomError')); + const classWithLambdaHandlerWithConfig = + new TestinClassWithLambdaHandlerWithConfig(); try { - await classWithLambdaHandler.testing(inputRecord); + await classWithLambdaHandlerWithConfig.testing(inputRecord); } catch (e) { resultingError = e as Error; } @@ -210,6 +247,4 @@ describe('Given a class with a function to decorate', (classWithLambdaHandler = expect(resultingError).toBeInstanceOf(IdempotencyPersistenceLayerError); }); }); - }); - diff --git a/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts b/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts index 07aef22351..b0c77acbf1 100644 --- a/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts @@ -6,17 +6,24 @@ import { IdempotencyFunctionOptions } from '../../src/types/IdempotencyOptions'; import { BasePersistenceLayer, IdempotencyRecord } from '../../src/persistence'; import { makeFunctionIdempotent } from '../../src/makeFunctionIdempotent'; -import type { AnyIdempotentFunction, IdempotencyRecordOptions } from '../../src/types'; +import type { + AnyIdempotentFunction, + IdempotencyRecordOptions, +} from '../../src/types'; import { IdempotencyRecordStatus } from '../../src/types'; import { IdempotencyAlreadyInProgressError, IdempotencyInconsistentStateError, IdempotencyItemAlreadyExistsError, - IdempotencyPersistenceLayerError + IdempotencyPersistenceLayerError, } from '../../src/Exceptions'; -const mockSaveInProgress = jest.spyOn(BasePersistenceLayer.prototype, 'saveInProgress').mockImplementation(); -const mockGetRecord = jest.spyOn(BasePersistenceLayer.prototype, 'getRecord').mockImplementation(); +const mockSaveInProgress = jest + .spyOn(BasePersistenceLayer.prototype, 'saveInProgress') + .mockImplementation(); +const mockGetRecord = jest + .spyOn(BasePersistenceLayer.prototype, 'getRecord') + .mockImplementation(); class PersistenceLayerTestClass extends BasePersistenceLayer { protected _deleteRecord = jest.fn(); @@ -29,10 +36,13 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { beforeEach(() => jest.clearAllMocks()); describe('Given options for idempotency', (options: IdempotencyFunctionOptions = { persistenceStore: new PersistenceLayerTestClass(), - dataKeywordArgument: 'testingKey' + dataKeywordArgument: 'testingKey', }) => { const keyValueToBeSaved = 'thisWillBeSaved'; - const inputRecord = { testingKey: keyValueToBeSaved, otherKey: 'thisWillNot' }; + const inputRecord = { + testingKey: keyValueToBeSaved, + otherKey: 'thisWillNot', + }; describe('When wrapping a function with no previous executions', () => { let resultingFunction: AnyIdempotentFunction; beforeEach(async () => { @@ -53,12 +63,16 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { let resultingFunction: AnyIdempotentFunction; let resultingError: Error; beforeEach(async () => { - mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + mockSaveInProgress.mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const idempotencyOptions: IdempotencyRecordOptions = { idempotencyKey: 'key', - status: IdempotencyRecordStatus.INPROGRESS + status: IdempotencyRecordStatus.INPROGRESS, }; - mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + mockGetRecord.mockResolvedValue( + new IdempotencyRecord(idempotencyOptions) + ); resultingFunction = makeFunctionIdempotent(functionToWrap, options); try { await resultingFunction(inputRecord); @@ -80,7 +94,9 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { }); test('Then an IdempotencyAlreadyInProgressError is thrown', () => { - expect(resultingError).toBeInstanceOf(IdempotencyAlreadyInProgressError); + expect(resultingError).toBeInstanceOf( + IdempotencyAlreadyInProgressError + ); }); }); @@ -88,12 +104,16 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { let resultingFunction: AnyIdempotentFunction; let resultingError: Error; beforeEach(async () => { - mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + mockSaveInProgress.mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const idempotencyOptions: IdempotencyRecordOptions = { idempotencyKey: 'key', - status: IdempotencyRecordStatus.EXPIRED + status: IdempotencyRecordStatus.EXPIRED, }; - mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + mockGetRecord.mockResolvedValue( + new IdempotencyRecord(idempotencyOptions) + ); resultingFunction = makeFunctionIdempotent(functionToWrap, options); try { await resultingFunction(inputRecord); @@ -115,19 +135,25 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { }); test('Then an IdempotencyInconsistentStateError is thrown', () => { - expect(resultingError).toBeInstanceOf(IdempotencyInconsistentStateError); + expect(resultingError).toBeInstanceOf( + IdempotencyInconsistentStateError + ); }); }); describe('When wrapping a function with previous execution that is COMPLETED', () => { let resultingFunction: AnyIdempotentFunction; beforeEach(async () => { - mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + mockSaveInProgress.mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const idempotencyOptions: IdempotencyRecordOptions = { idempotencyKey: 'key', - status: IdempotencyRecordStatus.COMPLETED + status: IdempotencyRecordStatus.COMPLETED, }; - mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + mockGetRecord.mockResolvedValue( + new IdempotencyRecord(idempotencyOptions) + ); resultingFunction = makeFunctionIdempotent(functionToWrap, options); await resultingFunction(inputRecord); }); @@ -143,7 +169,6 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { test('Then it will not call the function that was wrapped with the whole input record', () => { expect(functionToWrap).not.toBeCalledWith(inputRecord); }); - }); describe('When wrapping a function with issues saving the record', () => { @@ -168,4 +193,4 @@ describe('Given a function to wrap', (functionToWrap = jest.fn()) => { }); }); }); -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts b/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts index d63388c285..347d8e22d3 100644 --- a/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts @@ -3,43 +3,31 @@ * * @group unit/idempotency/persistence/base */ -import { - ContextExamples as dummyContext -} from '@aws-lambda-powertools/commons'; -import { - IdempotencyConfig, -} from '../../../src/IdempotencyConfig'; +import { ContextExamples as dummyContext } from '@aws-lambda-powertools/commons'; +import { IdempotencyConfig } from '../../../src/IdempotencyConfig'; import { IdempotencyRecord, - BasePersistenceLayer + BasePersistenceLayer, } from '../../../src/persistence'; import { IdempotencyItemAlreadyExistsError, IdempotencyValidationError, } from '../../../src/Exceptions'; -import type { - IdempotencyConfigOptions -} from '../../../src/types'; -import { - IdempotencyRecordStatus -} from '../../../src/types'; +import type { IdempotencyConfigOptions } from '../../../src/types'; +import { IdempotencyRecordStatus } from '../../../src/types'; jest.mock('node:crypto', () => ({ - createHash: jest.fn().mockReturnValue( - { - update: jest.fn(), - digest: jest.fn().mockReturnValue('mocked-hash') - } - ), + createHash: jest.fn().mockReturnValue({ + update: jest.fn(), + digest: jest.fn().mockReturnValue('mocked-hash'), + }), })); describe('Class: BasePersistenceLayer', () => { - const ENVIRONMENT_VARIABLES = process.env; const context = dummyContext.helloworldContext; class PersistenceLayerTestClass extends BasePersistenceLayer { - public _deleteRecord = jest.fn(); public _getRecord = jest.fn(); public _putRecord = jest.fn(); @@ -47,9 +35,7 @@ describe('Class: BasePersistenceLayer', () => { } beforeAll(() => { - jest - .useFakeTimers() - .setSystemTime(new Date()); + jest.useFakeTimers().setSystemTime(new Date()); }); beforeEach(() => { @@ -64,31 +50,27 @@ describe('Class: BasePersistenceLayer', () => { }); describe('Method: constructor', () => { - test('when initialized with no arguments, it initializes with default values', () => { - // Prepare & Act const persistenceLayer = new PersistenceLayerTestClass(); // Assess expect(persistenceLayer.idempotencyKeyPrefix).toBe('my-lambda-function'); - expect(persistenceLayer).toEqual(expect.objectContaining({ - configured: false, - expiresAfterSeconds: 3600, - hashFunction: 'md5', - payloadValidationEnabled: false, - throwOnNoIdempotencyKey: false, - useLocalCache: false, - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + configured: false, + expiresAfterSeconds: 3600, + hashFunction: 'md5', + payloadValidationEnabled: false, + throwOnNoIdempotencyKey: false, + useLocalCache: false, + }) + ); }); - }); describe('Method: configure', () => { - test('when configured with a function name, it appends the function name to the idempotency key prefix', () => { - // Prepare const config = new IdempotencyConfig({}); const persistenceLayer = new PersistenceLayerTestClass(); @@ -97,12 +79,12 @@ describe('Class: BasePersistenceLayer', () => { persistenceLayer.configure({ config, functionName: 'my-function' }); // Assess - expect(persistenceLayer.idempotencyKeyPrefix).toBe('my-lambda-function.my-function'); - + expect(persistenceLayer.idempotencyKeyPrefix).toBe( + 'my-lambda-function.my-function' + ); }); test('when configured with an empty config object, it initializes the persistence layer with default configs', () => { - // Prepare const config = new IdempotencyConfig({}); const persistenceLayer = new PersistenceLayerTestClass(); @@ -111,21 +93,21 @@ describe('Class: BasePersistenceLayer', () => { persistenceLayer.configure({ config }); // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ - configured: true, - validationKeyJmesPath: undefined, - payloadValidationEnabled: false, - expiresAfterSeconds: 3600, - throwOnNoIdempotencyKey: false, - eventKeyJmesPath: '', - useLocalCache: false, - hashFunction: 'md5', - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + configured: true, + validationKeyJmesPath: undefined, + payloadValidationEnabled: false, + expiresAfterSeconds: 3600, + throwOnNoIdempotencyKey: false, + eventKeyJmesPath: '', + useLocalCache: false, + hashFunction: 'md5', + }) + ); }); test('when configured with a config object, it initializes the persistence layer with the provided configs', () => { - // Prepare const configOptions: IdempotencyConfigOptions = { eventKeyJmesPath: 'eventKeyJmesPath', @@ -144,21 +126,21 @@ describe('Class: BasePersistenceLayer', () => { persistenceLayer.configure({ config }); // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ - configured: true, - eventKeyJmesPath: 'eventKeyJmesPath', - validationKeyJmesPath: 'payloadValidationJmesPath', - payloadValidationEnabled: true, - throwOnNoIdempotencyKey: true, - expiresAfterSeconds: 100, - useLocalCache: true, - hashFunction: 'hashFunction', - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + configured: true, + eventKeyJmesPath: 'eventKeyJmesPath', + validationKeyJmesPath: 'payloadValidationJmesPath', + payloadValidationEnabled: true, + throwOnNoIdempotencyKey: true, + expiresAfterSeconds: 100, + useLocalCache: true, + hashFunction: 'hashFunction', + }) + ); }); test('when called twice, it returns without reconfiguring the persistence layer', () => { - // Prepare const config = new IdempotencyConfig({ eventKeyJmesPath: 'eventKeyJmesPath', @@ -173,19 +155,17 @@ describe('Class: BasePersistenceLayer', () => { persistenceLayer.configure({ config: secondConfig }); // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ - configured: true, - eventKeyJmesPath: 'eventKeyJmesPath', - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + configured: true, + eventKeyJmesPath: 'eventKeyJmesPath', + }) + ); }); - }); describe('Method: deleteRecord', () => { - test('when called, it calls the _deleteRecord method with the correct arguments', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); const baseIdempotencyRecord = new IdempotencyRecord({ @@ -202,14 +182,12 @@ describe('Class: BasePersistenceLayer', () => { expect.objectContaining({ ...baseIdempotencyRecord, idempotencyKey: 'my-lambda-function#mocked-hash', - status: IdempotencyRecordStatus.EXPIRED - }), + status: IdempotencyRecordStatus.EXPIRED, + }) ); - }); test('when called, it deletes the record from the local cache', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -232,18 +210,14 @@ describe('Class: BasePersistenceLayer', () => { expect.objectContaining({ ...baseIdempotencyRecord, idempotencyKey: 'my-lambda-function#mocked-hash', - status: IdempotencyRecordStatus.EXPIRED - }), + status: IdempotencyRecordStatus.EXPIRED, + }) ); - }); - }); describe('Method: getRecord', () => { - test('when called, it calls the _getRecord method with the correct arguments', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -257,12 +231,12 @@ describe('Class: BasePersistenceLayer', () => { await persistenceLayer.getRecord({ foo: 'bar' }); // Assess - expect(getRecordSpy).toHaveBeenCalledWith('my-lambda-function#mocked-hash'); - + expect(getRecordSpy).toHaveBeenCalledWith( + 'my-lambda-function#mocked-hash' + ); }); test('when called and payload validation fails due to hash mismatch, it throws an IdempotencyValidationError', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -270,23 +244,23 @@ describe('Class: BasePersistenceLayer', () => { payloadValidationJmesPath: 'foo', }), }); - jest.spyOn(persistenceLayer, '_getRecord').mockReturnValue(new IdempotencyRecord({ - idempotencyKey: 'my-lambda-function#mocked-hash', - status: IdempotencyRecordStatus.INPROGRESS, - payloadHash: 'different-hash', - })); + jest.spyOn(persistenceLayer, '_getRecord').mockReturnValue( + new IdempotencyRecord({ + idempotencyKey: 'my-lambda-function#mocked-hash', + status: IdempotencyRecordStatus.INPROGRESS, + payloadHash: 'different-hash', + }) + ); // Act & Assess await expect(persistenceLayer.getRecord({ foo: 'bar' })).rejects.toThrow( new IdempotencyValidationError( - 'Payload does not match stored record for this event key', + 'Payload does not match stored record for this event key' ) ); - }); test('when called and the hash generation fails, and throwOnNoIdempotencyKey is disabled, it logs a warning', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -304,11 +278,9 @@ describe('Class: BasePersistenceLayer', () => { expect(logWarningSpy).toHaveBeenCalledWith( 'No value found for idempotency_key. jmespath: bar' ); - }); test('when called and the hash generation fails, and throwOnNoIdempotencyKey is enabled, it throws', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -320,14 +292,14 @@ describe('Class: BasePersistenceLayer', () => { }); // Act & Assess - await expect(persistenceLayer.getRecord({ foo: { bar: [] } })).rejects.toThrow( + await expect( + persistenceLayer.getRecord({ foo: { bar: [] } }) + ).rejects.toThrow( new Error('No data found to create a hashed idempotency_key') ); - }); test('when called twice with the same payload, it retrieves the record from the local cache', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -335,11 +307,15 @@ describe('Class: BasePersistenceLayer', () => { useLocalCache: true, }), }); - const getRecordSpy = jest.spyOn(persistenceLayer, '_getRecord').mockReturnValue(new IdempotencyRecord({ - idempotencyKey: 'my-lambda-function#mocked-hash', - status: IdempotencyRecordStatus.COMPLETED, - payloadHash: 'different-hash', - })); + const getRecordSpy = jest + .spyOn(persistenceLayer, '_getRecord') + .mockReturnValue( + new IdempotencyRecord({ + idempotencyKey: 'my-lambda-function#mocked-hash', + status: IdempotencyRecordStatus.COMPLETED, + payloadHash: 'different-hash', + }) + ); // Act await persistenceLayer.getRecord({ foo: 'bar' }); @@ -347,11 +323,9 @@ describe('Class: BasePersistenceLayer', () => { // Assess expect(getRecordSpy).toHaveBeenCalledTimes(1); - }); test('when called twice with the same payload, if it founds an expired record in the local cache, it retrieves it', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -359,12 +333,16 @@ describe('Class: BasePersistenceLayer', () => { useLocalCache: true, }), }); - const getRecordSpy = jest.spyOn(persistenceLayer, '_getRecord').mockReturnValue(new IdempotencyRecord({ - idempotencyKey: 'my-lambda-function#mocked-hash', - status: IdempotencyRecordStatus.EXPIRED, - payloadHash: 'different-hash', - expiryTimestamp: Date.now() / 1000 - 1, - })); + const getRecordSpy = jest + .spyOn(persistenceLayer, '_getRecord') + .mockReturnValue( + new IdempotencyRecord({ + idempotencyKey: 'my-lambda-function#mocked-hash', + status: IdempotencyRecordStatus.EXPIRED, + payloadHash: 'different-hash', + expiryTimestamp: Date.now() / 1000 - 1, + }) + ); // Act await persistenceLayer.getRecord({ foo: 'bar' }); @@ -372,15 +350,11 @@ describe('Class: BasePersistenceLayer', () => { // Assess expect(getRecordSpy).toHaveBeenCalledTimes(2); - }); - }); describe('Method: saveInProgress', () => { - test('when called, it calls the _putRecord method with the correct arguments', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); const putRecordSpy = jest.spyOn(persistenceLayer, '_putRecord'); @@ -398,17 +372,17 @@ describe('Class: BasePersistenceLayer', () => { payloadHash: 'mocked-hash', inProgressExpiryTimestamp: Date.now() + remainingTimeInMs, responseData: undefined, - }), + }) ); - }); test('when called without remainingTimeInMillis, it logs a warning and then calls the _putRecord method', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); const putRecordSpy = jest.spyOn(persistenceLayer, '_putRecord'); - const logWarningSpy = jest.spyOn(console, 'warn').mockImplementation(() => ({})); + const logWarningSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => ({})); // Act await persistenceLayer.saveInProgress({ foo: 'bar' }); @@ -416,13 +390,11 @@ describe('Class: BasePersistenceLayer', () => { // Assess expect(putRecordSpy).toHaveBeenCalledTimes(1); expect(logWarningSpy).toHaveBeenCalledWith( - 'Could not determine remaining time left. Did you call registerLambdaContext on IdempotencyConfig?', + 'Could not determine remaining time left. Did you call registerLambdaContext on IdempotencyConfig?' ); - }); test('when called and there is already a completed record in the cache, it throws an IdempotencyItemAlreadyExistsError', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -434,15 +406,13 @@ describe('Class: BasePersistenceLayer', () => { await persistenceLayer.saveSuccess({ foo: 'bar' }, { bar: 'baz' }); // Act & Assess - await expect(persistenceLayer.saveInProgress({ foo: 'bar' })).rejects.toThrow( - new IdempotencyItemAlreadyExistsError() - ); + await expect( + persistenceLayer.saveInProgress({ foo: 'bar' }) + ).rejects.toThrow(new IdempotencyItemAlreadyExistsError()); expect(putRecordSpy).toHaveBeenCalledTimes(0); - }); test('when called and there is an in-progress record in the cache, it returns', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); persistenceLayer.configure({ @@ -451,27 +421,26 @@ describe('Class: BasePersistenceLayer', () => { }), }); jest.spyOn(persistenceLayer, '_getRecord').mockImplementationOnce( - () => new IdempotencyRecord({ - idempotencyKey: 'my-lambda-function#mocked-hash', - status: IdempotencyRecordStatus.INPROGRESS, - payloadHash: 'different-hash', - expiryTimestamp: Date.now() / 1000 + 3600, - inProgressExpiryTimestamp: Date.now() + 2000, - }) + () => + new IdempotencyRecord({ + idempotencyKey: 'my-lambda-function#mocked-hash', + status: IdempotencyRecordStatus.INPROGRESS, + payloadHash: 'different-hash', + expiryTimestamp: Date.now() / 1000 + 3600, + inProgressExpiryTimestamp: Date.now() + 2000, + }) ); await persistenceLayer.getRecord({ foo: 'bar' }); // Act & Assess - await expect(persistenceLayer.saveInProgress({ foo: 'bar' })).resolves.toBeUndefined(); - + await expect( + persistenceLayer.saveInProgress({ foo: 'bar' }) + ).resolves.toBeUndefined(); }); - }); describe('Method: saveSuccess', () => { - test('when called, it calls the _updateRecord method with the correct arguments', async () => { - // Prepare const persistenceLayer = new PersistenceLayerTestClass(); const updateRecordSpy = jest.spyOn(persistenceLayer, '_updateRecord'); @@ -489,11 +458,8 @@ describe('Class: BasePersistenceLayer', () => { payloadHash: 'mocked-hash', inProgressExpiryTimestamp: undefined, responseData: result, - }), + }) ); - }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts index 3968f1f51e..09bfb999bc 100644 --- a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts @@ -3,22 +3,14 @@ * * @group unit/idempotency/persistence/dynamodb */ -import { - DynamoDBPersistenceLayer -} from '../../../src/persistence/DynamoDBPersistenceLayer'; +import { DynamoDBPersistenceLayer } from '../../../src/persistence/DynamoDBPersistenceLayer'; import { IdempotencyItemAlreadyExistsError, - IdempotencyItemNotFoundError + IdempotencyItemNotFoundError, } from '../../../src/Exceptions'; -import { - IdempotencyRecord -} from '../../../src/persistence/IdempotencyRecord'; -import type { - DynamoPersistenceOptions -} from '../../../src/types'; -import { - IdempotencyRecordStatus -} from '../../../src/types'; +import { IdempotencyRecord } from '../../../src/persistence/IdempotencyRecord'; +import type { DynamoPersistenceOptions } from '../../../src/types'; +import { IdempotencyRecordStatus } from '../../../src/types'; import { DynamoDBClient, DynamoDBServiceException, @@ -27,23 +19,20 @@ import { UpdateItemCommand, DeleteItemCommand, } from '@aws-sdk/client-dynamodb'; -import { - marshall, -} from '@aws-sdk/util-dynamodb'; +import { marshall } from '@aws-sdk/util-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; -const getFutureTimestamp = (seconds: number): number => new Date().getTime() + (seconds * 1000); +const getFutureTimestamp = (seconds: number): number => + new Date().getTime() + seconds * 1000; describe('Class: DynamoDBPersistenceLayer', () => { - const ENVIRONMENT_VARIABLES = process.env; const client = mockClient(DynamoDBClient); const dummyTableName = 'someTable'; const dummyKey = 'someKey'; class TestDynamoDBPersistenceLayer extends DynamoDBPersistenceLayer { - public _deleteRecord(record: IdempotencyRecord): Promise { return super._deleteRecord(record); } @@ -62,9 +51,7 @@ describe('Class: DynamoDBPersistenceLayer', () => { } beforeAll(() => { - jest - .useFakeTimers() - .setSystemTime(new Date()); + jest.useFakeTimers().setSystemTime(new Date()); }); beforeEach(() => { @@ -82,28 +69,28 @@ describe('Class: DynamoDBPersistenceLayer', () => { }); describe('Method: constructor', () => { - test('when instantiated with minimum options it creates an instance with default values', () => { - // Prepare & Act - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); - - // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ + const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName, - keyAttr: 'id', - statusAttr: 'status', - expiryAttr: 'expiration', - inProgressExpiryAttr: 'in_progress_expiry_attr', - dataAttr: 'data', - validationKeyAttr: 'validation', - staticPkValue: 'idempotency#my-lambda-function', - })); + }); + // Assess + expect(persistenceLayer).toEqual( + expect.objectContaining({ + tableName: dummyTableName, + keyAttr: 'id', + statusAttr: 'status', + expiryAttr: 'expiration', + inProgressExpiryAttr: 'in_progress_expiry_attr', + dataAttr: 'data', + validationKeyAttr: 'validation', + staticPkValue: 'idempotency#my-lambda-function', + }) + ); }); test('when instantiated with specific options it creates an instance with correct values', () => { - // Prepare const testDynamoDBPersistenceLayerOptions: DynamoPersistenceOptions = { tableName: dummyTableName, @@ -118,25 +105,29 @@ describe('Class: DynamoDBPersistenceLayer', () => { }; // Act - const persistenceLayer = new TestDynamoDBPersistenceLayer(testDynamoDBPersistenceLayerOptions); + const persistenceLayer = new TestDynamoDBPersistenceLayer( + testDynamoDBPersistenceLayerOptions + ); // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ - tableName: dummyTableName, - keyAttr: dummyKey, - statusAttr: testDynamoDBPersistenceLayerOptions.statusAttr, - expiryAttr: testDynamoDBPersistenceLayerOptions.expiryAttr, - inProgressExpiryAttr: testDynamoDBPersistenceLayerOptions.inProgressExpiryAttr, - dataAttr: testDynamoDBPersistenceLayerOptions.dataAttr, - validationKeyAttr: testDynamoDBPersistenceLayerOptions.validationKeyAttr, - staticPkValue: testDynamoDBPersistenceLayerOptions.staticPkValue, - sortKeyAttr: testDynamoDBPersistenceLayerOptions.sortKeyAttr, - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + tableName: dummyTableName, + keyAttr: dummyKey, + statusAttr: testDynamoDBPersistenceLayerOptions.statusAttr, + expiryAttr: testDynamoDBPersistenceLayerOptions.expiryAttr, + inProgressExpiryAttr: + testDynamoDBPersistenceLayerOptions.inProgressExpiryAttr, + dataAttr: testDynamoDBPersistenceLayerOptions.dataAttr, + validationKeyAttr: + testDynamoDBPersistenceLayerOptions.validationKeyAttr, + staticPkValue: testDynamoDBPersistenceLayerOptions.staticPkValue, + sortKeyAttr: testDynamoDBPersistenceLayerOptions.sortKeyAttr, + }) + ); }); test('when instantiated with a sortKeyAttr that has same value of keyAttr, it throws', () => { - // Prepare const testDynamoDBPersistenceLayerOptions: DynamoPersistenceOptions = { tableName: dummyTableName, @@ -145,84 +136,83 @@ describe('Class: DynamoDBPersistenceLayer', () => { }; // Act & Assess - expect(() => new TestDynamoDBPersistenceLayer(testDynamoDBPersistenceLayerOptions)).toThrowError( + expect( + () => + new TestDynamoDBPersistenceLayer(testDynamoDBPersistenceLayerOptions) + ).toThrowError( `keyAttr [${dummyKey}] and sortKeyAttr [${dummyKey}] cannot be the same!` ); - }); test('when instantiated with a custom AWS SDK client it uses that client', () => { - // Prepare const awsSdkV3Client = new DynamoDBClient({}); // Act const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName, - awsSdkV3Client + awsSdkV3Client, }); // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ - tableName: dummyTableName, - client: awsSdkV3Client - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + tableName: dummyTableName, + client: awsSdkV3Client, + }) + ); }); test('when passed an invalid AWS SDK client it logs a warning', () => { - // Prepare const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); // Act new TestDynamoDBPersistenceLayer({ tableName: dummyTableName, - awsSdkV3Client: {} as DynamoDBClient + awsSdkV3Client: {} as DynamoDBClient, }); // Assess expect(consoleWarnSpy).toHaveBeenCalledWith( 'Invalid AWS SDK V3 client passed to DynamoDBPersistenceLayer. Using default client.' ); - }); test('when passed a client config it stores it for later use', () => { - // Prepare const clientConfig = { - region: 'someRegion' + region: 'someRegion', }; - + // Act const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName, - clientConfig + clientConfig, }); - + // Assess - expect(persistenceLayer).toEqual(expect.objectContaining({ - tableName: dummyTableName, - clientConfig - })); - + expect(persistenceLayer).toEqual( + expect.objectContaining({ + tableName: dummyTableName, + clientConfig, + }) + ); }); - }); describe('Method: _putRecord', () => { - test('when called with a record that meets conditions, it puts record in DynamoDB table', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = 0; const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status, - expiryTimestamp, + idempotencyKey: dummyKey, + status, + expiryTimestamp, }); // Act @@ -240,32 +230,31 @@ describe('Class: DynamoDBPersistenceLayer', () => { '#id': 'id', '#expiry': 'expiration', '#status': 'status', - '#in_progress_expiry': 'in_progress_expiry_attr' + '#in_progress_expiry': 'in_progress_expiry_attr', }, ExpressionAttributeValues: marshall({ ':now': Date.now() / 1000, ':now_in_millis': Date.now(), - ':inprogress': IdempotencyRecordStatus.INPROGRESS + ':inprogress': IdempotencyRecordStatus.INPROGRESS, }), - ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)' + ConditionExpression: + 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)', }); - }); test('when called with a record that uses composite key, it puts record in DynamoDB table', async () => { - // Prepare const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName, staticPkValue: 'idempotency#my-lambda-function', - sortKeyAttr: 'sortKey' + sortKeyAttr: 'sortKey', }); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = 0; const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status, - expiryTimestamp, + idempotencyKey: dummyKey, + status, + expiryTimestamp, }); // Act @@ -284,22 +273,23 @@ describe('Class: DynamoDBPersistenceLayer', () => { '#id': 'id', '#expiry': 'expiration', '#status': 'status', - '#in_progress_expiry': 'in_progress_expiry_attr' + '#in_progress_expiry': 'in_progress_expiry_attr', }, ExpressionAttributeValues: marshall({ ':now': Date.now() / 1000, ':now_in_millis': Date.now(), - ':inprogress': IdempotencyRecordStatus.INPROGRESS + ':inprogress': IdempotencyRecordStatus.INPROGRESS, }), - ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)' + ConditionExpression: + 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)', }); - }); test('when called with a record that has inProgressExpiryTimestamp, it puts record in DynamoDB table', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); const status = IdempotencyRecordStatus.INPROGRESS; const expiryTimestamp = getFutureTimestamp(10); const inProgressExpiryTimestamp = getFutureTimestamp(5); @@ -307,7 +297,7 @@ describe('Class: DynamoDBPersistenceLayer', () => { idempotencyKey: dummyKey, status, expiryTimestamp, - inProgressExpiryTimestamp + inProgressExpiryTimestamp, }); // Act @@ -320,29 +310,32 @@ describe('Class: DynamoDBPersistenceLayer', () => { id: dummyKey, expiration: expiryTimestamp, status, - in_progress_expiry_attr: inProgressExpiryTimestamp + in_progress_expiry_attr: inProgressExpiryTimestamp, }), ExpressionAttributeNames: { '#id': 'id', '#expiry': 'expiration', '#status': 'status', - '#in_progress_expiry': 'in_progress_expiry_attr' + '#in_progress_expiry': 'in_progress_expiry_attr', }, ExpressionAttributeValues: marshall({ ':now': Date.now() / 1000, ':now_in_millis': Date.now(), - ':inprogress': IdempotencyRecordStatus.INPROGRESS + ':inprogress': IdempotencyRecordStatus.INPROGRESS, }), - ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)' + ConditionExpression: + 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)', }); - }); test('when called and and payload validation is enabled it puts record in DynamoDB table', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); - jest.spyOn(persistenceLayer, 'isPayloadValidationEnabled').mockReturnValue(true); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); + jest + .spyOn(persistenceLayer, 'isPayloadValidationEnabled') + .mockReturnValue(true); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = 0; const record = new IdempotencyRecord({ @@ -362,33 +355,34 @@ describe('Class: DynamoDBPersistenceLayer', () => { id: dummyKey, expiration: expiryTimestamp, status, - validation: record.payloadHash + validation: record.payloadHash, }), ExpressionAttributeNames: { '#id': 'id', '#expiry': 'expiration', '#status': 'status', - '#in_progress_expiry': 'in_progress_expiry_attr' + '#in_progress_expiry': 'in_progress_expiry_attr', }, ExpressionAttributeValues: marshall({ ':now': Date.now() / 1000, ':now_in_millis': Date.now(), - ':inprogress': IdempotencyRecordStatus.INPROGRESS + ':inprogress': IdempotencyRecordStatus.INPROGRESS, }), - ConditionExpression: 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)' + ConditionExpression: + 'attribute_not_exists(#id) OR #expiry < :now OR (#status = :inprogress AND attribute_exists(#in_progress_expiry) AND #in_progress_expiry < :now_in_millis)', }); - }); test('when called with a record that fails any condition, it throws IdempotencyItemAlreadyExistsError', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); - const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status: IdempotencyRecordStatus.EXPIRED, - expiryTimestamp: 0, + const record = new IdempotencyRecord({ + idempotencyKey: dummyKey, + status: IdempotencyRecordStatus.EXPIRED, + expiryTimestamp: 0, }); client.on(PutItemCommand).rejects( new DynamoDBServiceException({ @@ -397,52 +391,50 @@ describe('Class: DynamoDBPersistenceLayer', () => { httpStatusCode: 400, requestId: 'someRequestId', }, - name: 'ConditionalCheckFailedException' + name: 'ConditionalCheckFailedException', }) ); // Act & Assess - await expect( - persistenceLayer._putRecord(record) - ).rejects.toThrowError(IdempotencyItemAlreadyExistsError); - + await expect(persistenceLayer._putRecord(record)).rejects.toThrowError( + IdempotencyItemAlreadyExistsError + ); }); test('when encountering an unknown error, it throws the causing error', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = 0; const inProgressExpiryTimestamp = 0; - const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status, - expiryTimestamp, - inProgressExpiryTimestamp + const record = new IdempotencyRecord({ + idempotencyKey: dummyKey, + status, + expiryTimestamp, + inProgressExpiryTimestamp, }); client.on(PutItemCommand).rejects(new Error()); // Act & Assess await expect(persistenceLayer._putRecord(record)).rejects.toThrow(); - }); - }); describe('Method: _getRecord', () => { - test('it calls DynamoDB with correct parameters', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); - client.on(GetItemCommand).resolves({ + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); + client.on(GetItemCommand).resolves({ Item: marshall({ id: dummyKey, status: IdempotencyRecordStatus.INPROGRESS, expiration: getFutureTimestamp(15), in_progress_expiry_attr: getFutureTimestamp(10), - data: {} + data: {}, }), }); @@ -453,28 +445,28 @@ describe('Class: DynamoDBPersistenceLayer', () => { expect(client).toReceiveCommandWith(GetItemCommand, { TableName: dummyTableName, Key: marshall({ - id: dummyKey + id: dummyKey, }), - ConsistentRead: true + ConsistentRead: true, }); - }); test('when called with a record whose key exists, it gets the correct record', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); const status = IdempotencyRecordStatus.INPROGRESS; const expiryTimestamp = getFutureTimestamp(15); const inProgressExpiryTimestamp = getFutureTimestamp(10); const responseData = {}; - client.on(GetItemCommand).resolves({ + client.on(GetItemCommand).resolves({ Item: marshall({ - id: dummyKey, - status, - expiration: expiryTimestamp, - in_progress_expiry_attr: inProgressExpiryTimestamp, - data: responseData + id: dummyKey, + status, + expiration: expiryTimestamp, + in_progress_expiry_attr: inProgressExpiryTimestamp, + data: responseData, }), }); @@ -485,36 +477,38 @@ describe('Class: DynamoDBPersistenceLayer', () => { expect(record).toBeInstanceOf(IdempotencyRecord); expect(record.getStatus()).toEqual(IdempotencyRecordStatus.INPROGRESS); expect(record.idempotencyKey).toEqual(dummyKey); - expect(record.inProgressExpiryTimestamp).toEqual(inProgressExpiryTimestamp); + expect(record.inProgressExpiryTimestamp).toEqual( + inProgressExpiryTimestamp + ); expect(record.responseData).toEqual(responseData); expect(record.expiryTimestamp).toEqual(expiryTimestamp); - }); test('when called with a record whose key does not exist, it throws IdempotencyItemNotFoundError', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); client.on(GetItemCommand).resolves({ Item: undefined }); - - // Act & Assess - await expect(persistenceLayer._getRecord(dummyKey)).rejects.toThrowError(IdempotencyItemNotFoundError); + // Act & Assess + await expect(persistenceLayer._getRecord(dummyKey)).rejects.toThrowError( + IdempotencyItemNotFoundError + ); }); test('when called with a record in a table that use composite key, it builds the request correctly', async () => { - // Prepare const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName, staticPkValue: 'idempotency#my-lambda-function', - sortKeyAttr: 'sortKey' + sortKeyAttr: 'sortKey', }); - client.on(GetItemCommand).resolves({ + client.on(GetItemCommand).resolves({ Item: marshall({ - id: dummyKey, + id: dummyKey, status: IdempotencyRecordStatus.INPROGRESS, - expiration: getFutureTimestamp(15), + expiration: getFutureTimestamp(15), data: {}, }), }); @@ -527,28 +521,26 @@ describe('Class: DynamoDBPersistenceLayer', () => { TableName: dummyTableName, Key: marshall({ id: 'idempotency#my-lambda-function', - sortKey: dummyKey + sortKey: dummyKey, }), - ConsistentRead: true + ConsistentRead: true, }); - }); - }); describe('Method: _updateRecord', () => { - test('when called to update a record, it updates the item with the correct parameters', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = Date.now(); const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status, - expiryTimestamp, - responseData: {} + idempotencyKey: dummyKey, + status, + expiryTimestamp, + responseData: {}, }); // Act @@ -556,11 +548,12 @@ describe('Class: DynamoDBPersistenceLayer', () => { // Assess expect(client).toReceiveCommandWith(UpdateItemCommand, { - TableName: dummyTableName, + TableName: dummyTableName, Key: marshall({ - id: dummyKey + id: dummyKey, }), - UpdateExpression: 'SET #response_data = :response_data, #expiry = :expiry, #status = :status', + UpdateExpression: + 'SET #response_data = :response_data, #expiry = :expiry, #status = :status', ExpressionAttributeNames: { '#status': 'status', '#expiry': 'expiration', @@ -569,25 +562,27 @@ describe('Class: DynamoDBPersistenceLayer', () => { ExpressionAttributeValues: marshall({ ':status': IdempotencyRecordStatus.EXPIRED, ':expiry': expiryTimestamp, - ':response_data': {} + ':response_data': {}, }), }); - }); test('when called to update a record and payload validation is enabled, it adds the payload hash to the update expression', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); - jest.spyOn(persistenceLayer, 'isPayloadValidationEnabled').mockImplementation(() => true); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); + jest + .spyOn(persistenceLayer, 'isPayloadValidationEnabled') + .mockImplementation(() => true); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = Date.now(); const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status, - expiryTimestamp, + idempotencyKey: dummyKey, + status, + expiryTimestamp, responseData: {}, - payloadHash: 'someHash' + payloadHash: 'someHash', }); // Act @@ -597,38 +592,37 @@ describe('Class: DynamoDBPersistenceLayer', () => { expect(client).toReceiveCommandWith(UpdateItemCommand, { TableName: dummyTableName, Key: marshall({ - id: dummyKey + id: dummyKey, }), - UpdateExpression: 'SET #response_data = :response_data, #expiry = :expiry, #status = :status, #validation_key = :validation_key', + UpdateExpression: + 'SET #response_data = :response_data, #expiry = :expiry, #status = :status, #validation_key = :validation_key', ExpressionAttributeNames: { '#status': 'status', '#expiry': 'expiration', '#response_data': 'data', - '#validation_key': 'validation' + '#validation_key': 'validation', }, ExpressionAttributeValues: marshall({ ':status': IdempotencyRecordStatus.EXPIRED, ':expiry': expiryTimestamp, ':response_data': {}, - ':validation_key': record.payloadHash + ':validation_key': record.payloadHash, }), }); - }); - }); describe('Method: _deleteRecord', () => { - test('when called with a valid record, it calls the delete operation with the correct parameters', async () => { - // Prepare - const persistenceLayer = new TestDynamoDBPersistenceLayer({ tableName: dummyTableName }); + const persistenceLayer = new TestDynamoDBPersistenceLayer({ + tableName: dummyTableName, + }); const status = IdempotencyRecordStatus.EXPIRED; const expiryTimestamp = Date.now(); - const record = new IdempotencyRecord({ - idempotencyKey: dummyKey, - status, + const record = new IdempotencyRecord({ + idempotencyKey: dummyKey, + status, expiryTimestamp, }); @@ -637,12 +631,9 @@ describe('Class: DynamoDBPersistenceLayer', () => { // Assess expect(client).toReceiveCommandWith(DeleteItemCommand, { - TableName: dummyTableName, - Key: marshall({ id: dummyKey }) + TableName: dummyTableName, + Key: marshall({ id: dummyKey }), }); - }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/unit/persistence/IdempotencyRecord.test.ts b/packages/idempotency/tests/unit/persistence/IdempotencyRecord.test.ts index 5b1a7531b1..4836ae3e9e 100644 --- a/packages/idempotency/tests/unit/persistence/IdempotencyRecord.test.ts +++ b/packages/idempotency/tests/unit/persistence/IdempotencyRecord.test.ts @@ -19,12 +19,12 @@ describe('Given an INPROGRESS record that has already expired', () => { const expiryTimeBeforeNow = 1487076707; Date.now = jest.fn(() => mockNowAfterExpiryTime); idempotencyRecord = new IdempotencyRecord({ - idempotencyKey: mockIdempotencyKey, - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: expiryTimeBeforeNow, - inProgressExpiryTimestamp: mockInProgressExpiry, - responseData: mockData, - payloadHash: mockPayloadHash + idempotencyKey: mockIdempotencyKey, + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: expiryTimeBeforeNow, + inProgressExpiryTimestamp: mockInProgressExpiry, + responseData: mockData, + payloadHash: mockPayloadHash, }); }); describe('When checking the status of the idempotency record', () => { @@ -34,50 +34,52 @@ describe('Given an INPROGRESS record that has already expired', () => { }); test('Then the status is EXPIRED', () => { - expect(resultingStatus).toEqual(IdempotencyRecordStatus.EXPIRED); + expect(resultingStatus).toEqual(IdempotencyRecordStatus.EXPIRED); }); }); }); describe('Given an idempotency record that is not expired', () => { - let idempotencyRecord: IdempotencyRecord; + let idempotencyRecord: IdempotencyRecord; beforeEach(() => { const mockNowBeforeExiryTime = 1487076707000; const expiryTimeAfterNow = 1487076708; Date.now = jest.fn(() => mockNowBeforeExiryTime); idempotencyRecord = new IdempotencyRecord({ - idempotencyKey: mockIdempotencyKey, - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: expiryTimeAfterNow, - inProgressExpiryTimestamp: mockInProgressExpiry, - responseData: mockData, - payloadHash: mockPayloadHash + idempotencyKey: mockIdempotencyKey, + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: expiryTimeAfterNow, + inProgressExpiryTimestamp: mockInProgressExpiry, + responseData: mockData, + payloadHash: mockPayloadHash, }); }); - describe('When checking the status of the idempotency record', () => { + describe('When checking the status of the idempotency record', () => { test('Then the status is EXPIRED', () => { - expect(idempotencyRecord.getStatus()).toEqual(IdempotencyRecordStatus.INPROGRESS); + expect(idempotencyRecord.getStatus()).toEqual( + IdempotencyRecordStatus.INPROGRESS + ); }); test('Then the record is returned', () => { - expect(idempotencyRecord.getResponse()).toEqual(mockData); + expect(idempotencyRecord.getResponse()).toEqual(mockData); }); }); }); describe('Given an idempotency record that has a status not in the IdempotencyRecordStatus enum', () => { - let idempotencyRecord: IdempotencyRecord; + let idempotencyRecord: IdempotencyRecord; beforeEach(() => { const mockNowBeforeExiryTime = 1487076707000; const expiryTimeAfterNow = 1487076708; Date.now = jest.fn(() => mockNowBeforeExiryTime); idempotencyRecord = new IdempotencyRecord({ - idempotencyKey: mockIdempotencyKey, - status: 'NOT_A_STATUS' as IdempotencyRecordStatus, - expiryTimestamp: expiryTimeAfterNow, - inProgressExpiryTimestamp: mockInProgressExpiry, - responseData: mockData, - payloadHash: mockPayloadHash + idempotencyKey: mockIdempotencyKey, + status: 'NOT_A_STATUS' as IdempotencyRecordStatus, + expiryTimestamp: expiryTimeAfterNow, + inProgressExpiryTimestamp: mockInProgressExpiry, + responseData: mockData, + payloadHash: mockPayloadHash, }); }); describe('When checking the status of the idempotency record', () => { @@ -88,11 +90,10 @@ describe('Given an idempotency record that has a status not in the IdempotencyRe } catch (e: unknown) { resultingError = e as Error; } - }); - + test('Then an IdempotencyInvalidStatusError is thrown ', () => { - expect(resultingError).toBeInstanceOf(IdempotencyInvalidStatusError); + expect(resultingError).toBeInstanceOf(IdempotencyInvalidStatusError); }); }); -}); \ No newline at end of file +}); diff --git a/packages/idempotency/tests/unit/persistence/LRUCache.test.ts b/packages/idempotency/tests/unit/persistence/LRUCache.test.ts index 238083a406..83a4e445ff 100644 --- a/packages/idempotency/tests/unit/persistence/LRUCache.test.ts +++ b/packages/idempotency/tests/unit/persistence/LRUCache.test.ts @@ -3,16 +3,11 @@ * * @group unit/idempotency/persistence/lru-cache */ -import { - LRUCache, -} from '../../../src/persistence/LRUCache'; +import { LRUCache } from '../../../src/persistence/LRUCache'; describe('Class: LRUMap', () => { - describe('Method: add', () => { - test('when called it adds items to the cache', () => { - // Prepare const cache = new LRUCache(); @@ -24,11 +19,9 @@ describe('Class: LRUMap', () => { expect(cache.size()).toBe(2); expect(cache.get('a')).toBe(1); expect(cache.get('b')).toBe(2); - }); test('when called it updates the value of an existing key', () => { - // Prepare const cache = new LRUCache(); cache.add('a', 1); @@ -39,11 +32,9 @@ describe('Class: LRUMap', () => { // Assess expect(cache.size()).toBe(1); expect(cache.get('a')).toBe(2); - }); test('when called it removes the oldest item when the cache is full', () => { - // Prepare const cache = new LRUCache({ maxSize: 2 }); cache.add('a', 1); @@ -57,11 +48,9 @@ describe('Class: LRUMap', () => { expect(cache.get('a')).toBeUndefined(); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); - }); test('when called and maxSize is 0, it skips cache', () => { - // Prepare const cache = new LRUCache({ maxSize: 0 }); @@ -70,15 +59,11 @@ describe('Class: LRUMap', () => { // Assess expect(cache.size()).toBe(0); - }); - }); describe('Method: get', () => { - test('when called it returns the value of an existing key', () => { - // Prepare const cache = new LRUCache(); cache.add('a', 1); @@ -88,11 +73,9 @@ describe('Class: LRUMap', () => { // Assess expect(value).toBe(1); - }); test('when called it returns undefined for a non-existing key', () => { - // Prepare const cache = new LRUCache(); @@ -101,11 +84,9 @@ describe('Class: LRUMap', () => { // Assess expect(value).toBeUndefined(); - }); test('when called it marks the item as the most recently used', () => { - // Prepare const cache = new LRUCache(); cache.add('a', 1); @@ -119,15 +100,11 @@ describe('Class: LRUMap', () => { expect(cache.get('a')).toBe(1); expect(cache.get('b')).toBe(2); expect(cache.get('c')).toBe(3); - }); - }); describe('Method: has', () => { - test('when called it returns true for an existing key', () => { - // Prepare const cache = new LRUCache(); cache.add('a', 1); @@ -137,11 +114,9 @@ describe('Class: LRUMap', () => { // Assess expect(hasKey).toBe(true); - }); test('when called it returns false for a non-existing key', () => { - // Prepare const cache = new LRUCache(); @@ -150,15 +125,11 @@ describe('Class: LRUMap', () => { // Assess expect(hasKey).toBe(false); - }); - }); describe('Method: remove', () => { - test('when called it removes the item from the cache', () => { - // Prepare const cache = new LRUCache(); cache.add('a', 1); @@ -173,11 +144,9 @@ describe('Class: LRUMap', () => { // Assess expect(cache.size()).toBe(0); expect(cache.get('a')).toBeUndefined(); - }); test('when called on an empty cache it does nothing', () => { - // Prepare const cache = new LRUCache(); cache.add('a', 1); @@ -190,9 +159,6 @@ describe('Class: LRUMap', () => { // Assess expect(cache.size()).toBe(0); - }); - }); - -}); \ No newline at end of file +}); diff --git a/packages/logger/.eslintrc.js b/packages/logger/.eslintrc.js deleted file mode 100644 index 2e4ca18fb2..0000000000 --- a/packages/logger/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['coverage', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/packages/metrics/.eslintrc.js b/packages/metrics/.eslintrc.js deleted file mode 100644 index 2e4ca18fb2..0000000000 --- a/packages/metrics/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['coverage', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/packages/parameters/.eslintrc.js b/packages/parameters/.eslintrc.js deleted file mode 100644 index 2e4ca18fb2..0000000000 --- a/packages/parameters/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['coverage', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/packages/tracer/.eslintrc.js b/packages/tracer/.eslintrc.js deleted file mode 100644 index 2e4ca18fb2..0000000000 --- a/packages/tracer/.eslintrc.js +++ /dev/null @@ -1,72 +0,0 @@ -module.exports = { - env: { - browser: false, - es2020: true, - jest: true, - node: true, - }, - ignorePatterns: ['coverage', 'lib'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - node: {}, - typescript: { - project: './tsconfig.json', - alwaysTryTypes: true, - }, - }, - }, - rules: { - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { allowExpressions: true }, - ], // Enforce return type definitions for functions - '@typescript-eslint/explicit-member-accessibility': 'error', // Enforce explicit accessibility modifiers on class properties and methods (public, private, protected) - '@typescript-eslint/member-ordering': [ - // Standardize the order of class members - 'error', - { - default: { - memberTypes: [ - 'signature', - 'public-field', - 'protected-field', - 'private-field', - 'constructor', - 'public-method', - 'protected-method', - 'private-method', - ], - order: 'alphabetically', - }, - }, - ], - '@typescript-eslint/no-explicit-any': 'error', // Disallow usage of the any type - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // Disallow unused variables, except for variables starting with an underscore - '@typescript-eslint/no-use-before-define': ['off'], // Check if this rule is needed - 'no-unused-vars': 'off', // Disable eslint core rule, since it's replaced by @typescript-eslint/no-unused-vars - // Rules from eslint core https://eslint.org/docs/latest/rules/ - 'array-bracket-spacing': ['error', 'never'], // Disallow spaces inside of array brackets - 'computed-property-spacing': ['error', 'never'], // Disallow spaces inside of computed properties - 'func-style': ['warn', 'expression'], // Enforce function expressions instead of function declarations - 'keyword-spacing': 'error', // Enforce spaces after keywords and before parenthesis, e.g. if (condition) instead of if(condition) - 'padding-line-between-statements': [ - // Require an empty line before return statements - 'error', - { blankLine: 'always', prev: '*', next: 'return' }, - ], - 'no-console': 0, // Allow console.log statements - 'no-multi-spaces': ['error', { ignoreEOLComments: false }], // Disallow multiple spaces except for comments - 'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 0 }], // Enforce no empty line at the beginning & end of files and max 1 empty line between consecutive statements - 'no-throw-literal': 'error', // Disallow throwing literals as exceptions, e.g. throw 'error' instead of throw new Error('error') - 'object-curly-spacing': ['error', 'always'], // Enforce spaces inside of curly braces in objects - 'prefer-arrow-callback': 'error', // Enforce arrow functions instead of anonymous functions for callbacks - quotes: ['error', 'single', { allowTemplateLiterals: true }], // Enforce single quotes except for template strings - semi: ['error', 'always'], // Require semicolons instead of ASI (automatic semicolon insertion) at the end of statements - }, -}; diff --git a/typedoc.js b/typedoc.js index d3e9d58c9d..7f17fbcc1d 100644 --- a/typedoc.js +++ b/typedoc.js @@ -1,9 +1,9 @@ module.exports = { out: 'api', - exclude: [ '**/node_modules/**', '**/*.test.ts', '**/*.json' ], + exclude: ['**/node_modules/**', '**/*.test.ts', '**/*.json'], name: 'aws-lambda-powertools-typescript', excludePrivate: true, excludeInternal: true, entryPointStrategy: 'packages', readme: './README.md', -}; \ No newline at end of file +};