diff --git a/package-lock.json b/package-lock.json index 015b32f41f..5fe96eff3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16623,7 +16623,7 @@ }, "packages/commons": { "name": "@aws-lambda-powertools/commons", - "version": "1.5.1", + "version": "1.2.1", "license": "MIT-0" }, "packages/idempotency": { @@ -16641,11 +16641,13 @@ }, "packages/logger": { "name": "@aws-lambda-powertools/logger", - "version": "1.5.1", + "version": "1.2.1", "license": "MIT", "dependencies": { - "@aws-lambda-powertools/commons": "^1.5.1", - "lodash.merge": "^4.6.2" + "@aws-lambda-powertools/commons": "^1.2.1", + "lodash.clonedeep": "^4.5.0", + "lodash.merge": "^4.6.2", + "lodash.pickby": "^4.6.0" }, "devDependencies": { "@types/lodash.merge": "^4.6.7" @@ -16653,10 +16655,10 @@ }, "packages/metrics": { "name": "@aws-lambda-powertools/metrics", - "version": "1.5.1", + "version": "1.2.1", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^1.5.1" + "@aws-lambda-powertools/commons": "^1.2.1" }, "devDependencies": { "@types/promise-retry": "^1.1.3", @@ -16694,11 +16696,11 @@ }, "packages/tracer": { "name": "@aws-lambda-powertools/tracer", - "version": "1.5.1", + "version": "1.2.1", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^1.5.1", - "aws-xray-sdk-core": "^3.4.0" + "@aws-lambda-powertools/commons": "^1.2.1", + "aws-xray-sdk-core": "^3.3.6" }, "devDependencies": { "@aws-sdk/client-dynamodb": "^3.231.0", @@ -16943,7 +16945,8 @@ "@aws-lambda-powertools/logger": { "version": "file:packages/logger", "requires": { - "@aws-lambda-powertools/commons": "^1.5.1", + "@aws-lambda-powertools/commons": "^1.2.1", + "@types/lodash.clonedeep": "^4.5.7", "@types/lodash.merge": "^4.6.7", "lodash.merge": "^4.6.2" } @@ -16951,7 +16954,7 @@ "@aws-lambda-powertools/metrics": { "version": "file:packages/metrics", "requires": { - "@aws-lambda-powertools/commons": "^1.5.1", + "@aws-lambda-powertools/commons": "^1.2.1", "@types/promise-retry": "^1.1.3", "promise-retry": "^2.0.1" } @@ -16983,8 +16986,8 @@ "@aws-lambda-powertools/tracer": { "version": "file:packages/tracer", "requires": { - "@aws-lambda-powertools/commons": "^1.5.1", - "@aws-sdk/client-dynamodb": "^3.231.0", + "@aws-lambda-powertools/commons": "^1.2.1", + "@aws-sdk/client-dynamodb": "^3.100.0", "@types/promise-retry": "^1.1.3", "aws-sdk": "^2.1276.0", "aws-xray-sdk-core": "^3.4.0", diff --git a/package.json b/package.json index c7ca00b1e1..b937ffeb0a 100644 --- a/package.json +++ b/package.json @@ -86,4 +86,4 @@ "dependencies": { "hosted-git-info": "^6.1.1" } -} \ No newline at end of file +} diff --git a/packages/idempotency/src/Exceptions.ts b/packages/idempotency/src/Exceptions.ts index 405a59aa66..d39a01b37e 100644 --- a/packages/idempotency/src/Exceptions.ts +++ b/packages/idempotency/src/Exceptions.ts @@ -10,8 +10,23 @@ class IdempotencyInvalidStatusError extends Error { } +class IdempotencyInconsistentStateError extends Error { + +} + +class IdempotencyAlreadyInProgressError extends Error { + +} + +class IdempotencyPersistenceLayerError extends Error { + +} + export { IdempotencyItemNotFoundError, IdempotencyItemAlreadyExistsError, - IdempotencyInvalidStatusError + IdempotencyInvalidStatusError, + IdempotencyInconsistentStateError, + IdempotencyAlreadyInProgressError, + IdempotencyPersistenceLayerError }; \ No newline at end of file diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts new file mode 100644 index 0000000000..e629fa917b --- /dev/null +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { AnyFunctionWithRecord, IdempotencyRecordStatus } from './types'; +import { IdempotencyOptions } from './types/IdempotencyOptions'; +import { IdempotencyRecord } from 'persistence'; +import { IdempotencyInconsistentStateError, IdempotencyItemAlreadyExistsError, IdempotencyAlreadyInProgressError, IdempotencyPersistenceLayerError } from './Exceptions'; + +export class IdempotencyHandler { + + public constructor(private functionToMakeIdempotent: AnyFunctionWithRecord, private functionPayloadToBeHashed: unknown, + private idempotencyOptions: IdempotencyOptions, private fullFunctionPayload: Record) {} + + 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.'); + } else if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.INPROGRESS){ + throw new IdempotencyAlreadyInProgressError(`There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}`); + } else { + // Currently recalling the method as this fulfills FR1. FR3 will address using the previously stored value https://github.com/awslabs/aws-lambda-powertools-typescript/issues/447 + return this.functionToMakeIdempotent(this.fullFunctionPayload); + } + } + + public async processIdempotency(): Promise { + try { + await this.idempotencyOptions.persistenceStore.saveInProgress(this.functionPayloadToBeHashed); + } catch (e) { + if (e instanceof IdempotencyItemAlreadyExistsError) { + const idempotencyRecord: IdempotencyRecord = await this.idempotencyOptions.persistenceStore.getRecord(this.functionPayloadToBeHashed); + + return this.determineResultFromIdempotencyRecord(idempotencyRecord); + } else { + throw new IdempotencyPersistenceLayerError(); + } + } + + return this.functionToMakeIdempotent(this.fullFunctionPayload); + } +} \ No newline at end of file diff --git a/packages/idempotency/src/idempotentDecorator.ts b/packages/idempotency/src/idempotentDecorator.ts new file mode 100644 index 0000000000..51e387b428 --- /dev/null +++ b/packages/idempotency/src/idempotentDecorator.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { IdempotencyOptions } from './types/IdempotencyOptions'; +import { IdempotencyHandler } from './IdempotencyHandler'; + +const idempotent = function (options: IdempotencyOptions) { + return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { + const childFunction = descriptor.value; + descriptor.value = function(record: Record){ + const idempotencyHandler: IdempotencyHandler = new IdempotencyHandler(childFunction, record[options.dataKeywordArgument], options, record); + + return idempotencyHandler.processIdempotency(); + }; + + return descriptor; + }; +}; + +export { idempotent }; + \ No newline at end of file diff --git a/packages/idempotency/src/makeFunctionIdempotent.ts b/packages/idempotency/src/makeFunctionIdempotent.ts index 40b6e52acf..fd416ccfc5 100644 --- a/packages/idempotency/src/makeFunctionIdempotent.ts +++ b/packages/idempotency/src/makeFunctionIdempotent.ts @@ -1,11 +1,19 @@ -import type { AnyFunction } from './types/AnyFunction'; -import type { IdempotencyOptions } from './types/IdempotencyOptions'; - -const makeFunctionIdempotent = ( - fn: AnyFunction, - _options: IdempotencyOptions - // TODO: revisit this with a more specific type if possible - /* eslint-disable @typescript-eslint/no-explicit-any */ -): (...args: Array) => Promise => (...args) => fn(...args); +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AnyFunctionWithRecord, AnyIdempotentFunction } from './types/AnyFunction'; +import { IdempotencyOptions } from './types/IdempotencyOptions'; +import { IdempotencyHandler } from './IdempotencyHandler'; + +const makeFunctionIdempotent = function ( + fn: AnyFunctionWithRecord, + options: IdempotencyOptions +): AnyIdempotentFunction { + const wrappedFn: AnyIdempotentFunction = function (record: Record): Promise { + const idempotencyHandler: IdempotencyHandler = new IdempotencyHandler(fn, record[options.dataKeywordArgument], options, record); + + return idempotencyHandler.processIdempotency(); + }; + + return wrappedFn; +}; export { makeFunctionIdempotent }; diff --git a/packages/idempotency/src/types/AnyFunction.ts b/packages/idempotency/src/types/AnyFunction.ts index bddcd8fc15..4b2a894a13 100644 --- a/packages/idempotency/src/types/AnyFunction.ts +++ b/packages/idempotency/src/types/AnyFunction.ts @@ -1,6 +1,11 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyFunction = (...args: Array) => Promise; +type AnyFunctionWithRecord = (record: Record) => Promise | U; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AnyIdempotentFunction = (record: Record) => Promise; export { - AnyFunction + // AnyFunction, + AnyFunctionWithRecord, + AnyIdempotentFunction }; \ No newline at end of file diff --git a/packages/idempotency/src/types/index.ts b/packages/idempotency/src/types/index.ts index 307c6bc093..c725c5a6ba 100644 --- a/packages/idempotency/src/types/index.ts +++ b/packages/idempotency/src/types/index.ts @@ -1,3 +1,4 @@ export * from './AnyFunction'; export * from './IdempotencyRecordStatus'; -export * from './PersistenceLayer'; \ No newline at end of file +export * from './IdempotencyRecordOptions'; +export * from './PersistenceLayer'; diff --git a/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts b/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts index 0511a61959..cac6f99929 100644 --- a/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts +++ b/packages/idempotency/tests/helpers/populateEnvironmentVariables.ts @@ -6,4 +6,4 @@ process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128'; if (process.env.AWS_REGION === undefined && process.env.CDK_DEFAULT_REGION === undefined) { process.env.AWS_REGION = 'eu-west-1'; } -process.env._HANDLER = 'index.handler'; \ No newline at end of file +process.env._HANDLER = 'index.handler'; diff --git a/packages/idempotency/tests/unit/idempotentDecorator.test.ts b/packages/idempotency/tests/unit/idempotentDecorator.test.ts new file mode 100644 index 0000000000..3df2689116 --- /dev/null +++ b/packages/idempotency/tests/unit/idempotentDecorator.test.ts @@ -0,0 +1,163 @@ +/** + * Test Function Wrapper + * + * @group unit/idempotency/all + */ + +import { IdempotencyOptions } from '../../src/types/IdempotencyOptions'; +import { PersistenceLayer, IdempotencyRecord } from '../../src/persistence'; +import { idempotent } from '../../src/idempotentDecorator'; +import { IdempotencyRecordStatus, IdempotencyRecordOptions } from '../../src/types'; +import { IdempotencyItemAlreadyExistsError, IdempotencyAlreadyInProgressError, IdempotencyInconsistentStateError, IdempotencyPersistenceLayerError } from '../../src/Exceptions'; + +const mockSaveInProgress = jest.spyOn(PersistenceLayer.prototype, 'saveInProgress').mockImplementation(); +const mockGetRecord = jest.spyOn(PersistenceLayer.prototype, 'getRecord').mockImplementation(); + +class PersistenceLayerTestClass extends PersistenceLayer { + protected _deleteRecord = jest.fn(); + protected _getRecord = jest.fn(); + protected _putRecord = jest.fn(); + protected _updateRecord = jest.fn(); +} + +const options: IdempotencyOptions = { persistenceStore: new PersistenceLayerTestClass(), dataKeywordArgument: 'testingKey' }; +const functionalityToDecorate = jest.fn(); + +class TestingClass { + @idempotent(options) + public testing(record: Record): string { + functionalityToDecorate(record); + + return 'Hi'; + } +} + +describe('Given a class with a function to decorate', (classWithFunction = new TestingClass()) => { + const keyValueToBeSaved = 'thisWillBeSaved'; + const inputRecord = { testingKey: keyValueToBeSaved, otherKey: 'thisWillNot' }; + beforeEach(()=> jest.clearAllMocks()); + describe('When wrapping a function with no previous executions', () => { + beforeEach(async () => { + classWithFunction.testing(inputRecord); + }); + + test('Then it will save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will call the function that was decorated', () => { + expect(functionalityToDecorate).toBeCalledWith(inputRecord); + }); + }); + + describe('When decorating a function with previous execution that is INPROGRESS', () => { + let resultingError: Error; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + const idempotencyOptions: IdempotencyRecordOptions = { + idempotencyKey: 'key', + status: IdempotencyRecordStatus.INPROGRESS + }; + mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + try { + await classWithFunction.testing(inputRecord); + } catch (e) { + resultingError = e as Error; + } + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will get the previous execution record', () => { + expect(mockGetRecord).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will not call the function that was decorated', () => { + expect(functionalityToDecorate).not.toBeCalled(); + }); + + test('Then an IdempotencyAlreadyInProgressError is thrown', () => { + expect(resultingError).toBeInstanceOf(IdempotencyAlreadyInProgressError); + }); + }); + + describe('When decorating a function with previous execution that is EXPIRED', () => { + let resultingError: Error; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + const idempotencyOptions: IdempotencyRecordOptions = { + idempotencyKey: 'key', + status: IdempotencyRecordStatus.EXPIRED + }; + mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + try { + await classWithFunction.testing(inputRecord); + } catch (e) { + resultingError = e as Error; + } + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will get the previous execution record', () => { + expect(mockGetRecord).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will not call the function that was decorated', () => { + expect(functionalityToDecorate).not.toBeCalled(); + }); + + test('Then an IdempotencyInconsistentStateError is thrown', () => { + expect(resultingError).toBeInstanceOf(IdempotencyInconsistentStateError); + }); + }); + + describe('When wrapping a function with previous execution that is COMPLETED', () => { + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + const idempotencyOptions: IdempotencyRecordOptions = { + idempotencyKey: 'key', + status: IdempotencyRecordStatus.COMPLETED + }; + mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + await classWithFunction.testing(inputRecord); + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will get the previous execution record', () => { + expect(mockGetRecord).toBeCalledWith(keyValueToBeSaved); + }); + + //This should be the saved record once FR3 is complete https://github.com/awslabs/aws-lambda-powertools-typescript/issues/447 + test('Then it will call the function that was decorated with the whole input record', () => { + expect(functionalityToDecorate).toBeCalledWith(inputRecord); + }); + }); + + describe('When wrapping a function with issues saving the record', () => { + let resultingError: Error; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new Error('RandomError')); + try { + await classWithFunction.testing(inputRecord); + } catch (e) { + resultingError = e as Error; + } + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then an IdempotencyPersistenceLayerError is thrown', () => { + expect(resultingError).toBeInstanceOf(IdempotencyPersistenceLayerError); + }); + }); +}); \ No newline at end of file diff --git a/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts b/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts new file mode 100644 index 0000000000..95fc50b068 --- /dev/null +++ b/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts @@ -0,0 +1,163 @@ +/** + * Test Function Wrapper + * + * @group unit/idempotency/all + */ + +import { IdempotencyOptions } from '../../src/types/IdempotencyOptions'; +import { IdempotencyRecord, PersistenceLayer } from '../../src/persistence'; +import { makeFunctionIdempotent } from '../../src/makeFunctionIdempotent'; +import { AnyIdempotentFunction, IdempotencyRecordStatus, IdempotencyRecordOptions } from '../../src/types'; +import { IdempotencyItemAlreadyExistsError, IdempotencyAlreadyInProgressError, IdempotencyInconsistentStateError, IdempotencyPersistenceLayerError } from '../../src/Exceptions'; + +const mockSaveInProgress = jest.spyOn(PersistenceLayer.prototype, 'saveInProgress').mockImplementation(); +const mockGetRecord = jest.spyOn(PersistenceLayer.prototype, 'getRecord').mockImplementation(); + +class PersistenceLayerTestClass extends PersistenceLayer { + protected _deleteRecord = jest.fn(); + protected _getRecord = jest.fn(); + protected _putRecord = jest.fn(); + protected _updateRecord = jest.fn(); +} + +describe('Given a function to wrap', (functionToWrap = jest.fn()) => { + beforeEach(()=> jest.clearAllMocks()); + describe('Given options for idempotency', (options: IdempotencyOptions = { persistenceStore: new PersistenceLayerTestClass(), dataKeywordArgument: 'testingKey' }) => { + const keyValueToBeSaved = 'thisWillBeSaved'; + const inputRecord = { testingKey: keyValueToBeSaved, otherKey: 'thisWillNot' }; + describe('When wrapping a function with no previous executions', () => { + let resultingFunction: AnyIdempotentFunction; + beforeEach(async () => { + resultingFunction = makeFunctionIdempotent(functionToWrap, options); + await resultingFunction(inputRecord); + }); + + test('Then it will save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will call the function that was wrapped with the whole input record', () => { + expect(functionToWrap).toBeCalledWith(inputRecord); + }); + }); + + describe('When wrapping a function with previous execution that is INPROGRESS', () => { + let resultingFunction: AnyIdempotentFunction; + let resultingError: Error; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + const idempotencyOptions: IdempotencyRecordOptions = { + idempotencyKey: 'key', + status: IdempotencyRecordStatus.INPROGRESS + }; + mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + resultingFunction = makeFunctionIdempotent(functionToWrap, options); + try { + await resultingFunction(inputRecord); + } catch (e) { + resultingError = e as Error; + } + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will get the previous execution record', () => { + expect(mockGetRecord).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then the function that was wrapped is not called again', () => { + expect(functionToWrap).not.toBeCalled(); + }); + + test('Then an IdempotencyAlreadyInProgressError is thrown', ()=> { + expect(resultingError).toBeInstanceOf(IdempotencyAlreadyInProgressError); + }); + }); + + describe('When wrapping a function with previous execution that is EXPIRED', () => { + let resultingFunction: AnyIdempotentFunction; + let resultingError: Error; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + const idempotencyOptions: IdempotencyRecordOptions = { + idempotencyKey: 'key', + status: IdempotencyRecordStatus.EXPIRED + }; + mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + resultingFunction = makeFunctionIdempotent(functionToWrap, options); + try { + await resultingFunction(inputRecord); + } catch (e) { + resultingError = e as Error; + } + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will get the previous execution record', () => { + expect(mockGetRecord).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then the function that was wrapped is not called again', () => { + expect(functionToWrap).not.toBeCalled(); + }); + + test('Then an IdempotencyInconsistentStateError is thrown', ()=> { + expect(resultingError).toBeInstanceOf(IdempotencyInconsistentStateError); + }); + }); + + describe('When wrapping a function with previous execution that is COMPLETED', () => { + let resultingFunction: AnyIdempotentFunction; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + const idempotencyOptions: IdempotencyRecordOptions = { + idempotencyKey: 'key', + status: IdempotencyRecordStatus.COMPLETED + }; + mockGetRecord.mockResolvedValue(new IdempotencyRecord(idempotencyOptions)); + resultingFunction = makeFunctionIdempotent(functionToWrap, options); + await resultingFunction(inputRecord); + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then it will get the previous execution record', () => { + expect(mockGetRecord).toBeCalledWith(keyValueToBeSaved); + }); + + //This should be the saved record once FR3 is complete https://github.com/awslabs/aws-lambda-powertools-typescript/issues/447 + test('Then it will call the function that was wrapped with the whole input record', () => { + expect(functionToWrap).toBeCalledWith(inputRecord); + }); + }); + + describe('When wrapping a function with issues saving the record', () => { + let resultingFunction: AnyIdempotentFunction; + let resultingError: Error; + beforeEach(async () => { + mockSaveInProgress.mockRejectedValue(new Error('RandomError')); + resultingFunction = makeFunctionIdempotent(functionToWrap, options); + try { + await resultingFunction(inputRecord); + } catch (e) { + resultingError = e as Error; + } + }); + + test('Then it will attempt to save the record to INPROGRESS', () => { + expect(mockSaveInProgress).toBeCalledWith(keyValueToBeSaved); + }); + + test('Then an IdempotencyPersistenceLayerError is thrown', ()=> { + expect(resultingError).toBeInstanceOf(IdempotencyPersistenceLayerError); + }); + }); + }); +}); \ 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 46d3b3e23d..8d95de55e2 100644 --- a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts @@ -34,7 +34,6 @@ describe('Class: DynamoDBPersistenceLayer', () => { public _updateRecord(record: IdempotencyRecord): Promise { return super._updateRecord(record); } - } beforeEach(() => {