Skip to content

Commit eacb1d9

Browse files
KevenFuentes9vgphoenixcamposjeffrey-baker-vgijemmy
authored
feat(idempotency): Add function wrapper and decorator (#1262)
* feat: initial idempotency classes * feat: refactor persistence layer classes into their own folder * feat: rename idempotency config to differentiate from idempotency options * feat: added type for a generic function * feat: remove idempotency configuration for this FR * feat: refactored type of function to accept any combo of parameters * feat: adding PersistenceLayer * feat: PersistenceLayer unit tests for saveInProgress * feat: added saveSuccess * feat: added getRecord * feat: added delete record * feat: branch coverage and cleaning up imports * feat: added more tests * feat: deleted unused methods * feat: added comments * feat: implement get command for dynamo persistence layer * feat: implement get command for dynamo persistence layer * feat: allow for data attr to be passed and return in persistence layer get * feat: added implementation for delete, update, put * feat: create condition on put for not in progress status * feat: use inprogress enum for status * feat: added error when unable to get record for idempotency key * feat: added error for conditional write of an existing record * feat: tests added for put record on dynamo persistence layer * feat: implemented the idempotency record functions for status, expiry, and json response * test: check if the status is expired * test: idempotency record is not expired and status maintained * feat: added tests for get record * feat: add aws-sdk-client-mock jest assertion library * feat: add unit tests for update record and delete record * feat: remove optional chaining from item made unnecessary with error branch * feat: remove unused block * feat: refactored mock child class to be shared amongst dynamo persistence layer tests * test: add path to get the response data from the data record * feat: added branch to handle conditional check failure * feat: add configuration option to dynamo client creation to remove undefined values * feat: change how time is measured to seconds * feat: change type of the response/result to a record * feat:updated imports * feat: added save in progress to handle already existing records in dynamo * feat: add log message for the already in progress error * feat: change the anyfunction type definition to also include a sync function * refactor: create constructor object for dynamo persistence layer * fix: remove temp eslint disable * fix: adjust verbiage on test blocks Co-authored-by: ijemmy <[email protected]> * style: put constructor parameters onto one line for readability * fix: update dynamo persistence layer tests to use new construtor options * fix: remove unneeded eslint ignore from persistence layer * style: put parameters for dynamo client command object onto one line for readability * fix: move lib-dynamo dep under the correct package * refactor: change idempotency record to use options object in contructor * feat: add consistent read to dynamo persistence layer * fix: revert changes to layer-publisher package-lock * feat added the call to the function in the idempotency handler and add overarching error scenario * feat: add logic to invoke function if it has not already been called; add logic to call the idempotency hander * chore: move and enhance comment on question * chore: update comments * feat: use new record formatting for idempotent function wrapper * test: add test case for issues even getting to save the record in persistence layer * chore: refactoring test suite for idempotent wrapper * chore: clean up comments * feat: added decorator for idempotency * chore: get rid of extra private member on the idempotency handler class * chore: refactor to use class for options * chore: bring in old version of package-locks * chore: update paths for interface * chore: remove env files * chore: rename file * chore: renaming test names and function names for idempotency decorator/wrapper --------- Co-authored-by: vgphoenixcampos <[email protected]> Co-authored-by: jeffrey-baker-vg <[email protected]> Co-authored-by: Phoenix Campos <[email protected]> Co-authored-by: ijemmy <[email protected]>
1 parent 3a8cfa0 commit eacb1d9

12 files changed

+444
-29
lines changed

Diff for: package-lock.json

+16-13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@
8686
"dependencies": {
8787
"hosted-git-info": "^6.1.1"
8888
}
89-
}
89+
}

Diff for: packages/idempotency/src/Exceptions.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,23 @@ class IdempotencyInvalidStatusError extends Error {
1010

1111
}
1212

13+
class IdempotencyInconsistentStateError extends Error {
14+
15+
}
16+
17+
class IdempotencyAlreadyInProgressError extends Error {
18+
19+
}
20+
21+
class IdempotencyPersistenceLayerError extends Error {
22+
23+
}
24+
1325
export {
1426
IdempotencyItemNotFoundError,
1527
IdempotencyItemAlreadyExistsError,
16-
IdempotencyInvalidStatusError
28+
IdempotencyInvalidStatusError,
29+
IdempotencyInconsistentStateError,
30+
IdempotencyAlreadyInProgressError,
31+
IdempotencyPersistenceLayerError
1732
};

Diff for: packages/idempotency/src/IdempotencyHandler.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
3+
import { AnyFunctionWithRecord, IdempotencyRecordStatus } from './types';
4+
import { IdempotencyOptions } from './types/IdempotencyOptions';
5+
import { IdempotencyRecord } from 'persistence';
6+
import { IdempotencyInconsistentStateError, IdempotencyItemAlreadyExistsError, IdempotencyAlreadyInProgressError, IdempotencyPersistenceLayerError } from './Exceptions';
7+
8+
export class IdempotencyHandler<U> {
9+
10+
public constructor(private functionToMakeIdempotent: AnyFunctionWithRecord<U>, private functionPayloadToBeHashed: unknown,
11+
private idempotencyOptions: IdempotencyOptions, private fullFunctionPayload: Record<string, any>) {}
12+
13+
public determineResultFromIdempotencyRecord(idempotencyRecord: IdempotencyRecord): Promise<U> | U{
14+
if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.EXPIRED) {
15+
throw new IdempotencyInconsistentStateError('Item has expired during processing and may not longer be valid.');
16+
} else if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.INPROGRESS){
17+
throw new IdempotencyAlreadyInProgressError(`There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}`);
18+
} else {
19+
// 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
20+
return this.functionToMakeIdempotent(this.fullFunctionPayload);
21+
}
22+
}
23+
24+
public async processIdempotency(): Promise<U> {
25+
try {
26+
await this.idempotencyOptions.persistenceStore.saveInProgress(this.functionPayloadToBeHashed);
27+
} catch (e) {
28+
if (e instanceof IdempotencyItemAlreadyExistsError) {
29+
const idempotencyRecord: IdempotencyRecord = await this.idempotencyOptions.persistenceStore.getRecord(this.functionPayloadToBeHashed);
30+
31+
return this.determineResultFromIdempotencyRecord(idempotencyRecord);
32+
} else {
33+
throw new IdempotencyPersistenceLayerError();
34+
}
35+
}
36+
37+
return this.functionToMakeIdempotent(this.fullFunctionPayload);
38+
}
39+
}

Diff for: packages/idempotency/src/idempotentDecorator.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { IdempotencyOptions } from './types/IdempotencyOptions';
3+
import { IdempotencyHandler } from './IdempotencyHandler';
4+
5+
const idempotent = function (options: IdempotencyOptions) {
6+
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
7+
const childFunction = descriptor.value;
8+
descriptor.value = function(record: Record<string, any>){
9+
const idempotencyHandler: IdempotencyHandler<unknown> = new IdempotencyHandler<unknown>(childFunction, record[options.dataKeywordArgument], options, record);
10+
11+
return idempotencyHandler.processIdempotency();
12+
};
13+
14+
return descriptor;
15+
};
16+
};
17+
18+
export { idempotent };
19+

Diff for: packages/idempotency/src/makeFunctionIdempotent.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import type { AnyFunction } from './types/AnyFunction';
2-
import type { IdempotencyOptions } from './types/IdempotencyOptions';
3-
4-
const makeFunctionIdempotent = <U>(
5-
fn: AnyFunction<U>,
6-
_options: IdempotencyOptions
7-
// TODO: revisit this with a more specific type if possible
8-
/* eslint-disable @typescript-eslint/no-explicit-any */
9-
): (...args: Array<any>) => Promise<U | void> => (...args) => fn(...args);
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { AnyFunctionWithRecord, AnyIdempotentFunction } from './types/AnyFunction';
3+
import { IdempotencyOptions } from './types/IdempotencyOptions';
4+
import { IdempotencyHandler } from './IdempotencyHandler';
5+
6+
const makeFunctionIdempotent = function <U>(
7+
fn: AnyFunctionWithRecord<U>,
8+
options: IdempotencyOptions
9+
): AnyIdempotentFunction<U> {
10+
const wrappedFn: AnyIdempotentFunction<U> = function (record: Record<string, any>): Promise<U> {
11+
const idempotencyHandler: IdempotencyHandler<U> = new IdempotencyHandler<U>(fn, record[options.dataKeywordArgument], options, record);
12+
13+
return idempotencyHandler.processIdempotency();
14+
};
15+
16+
return wrappedFn;
17+
};
1018

1119
export { makeFunctionIdempotent };

Diff for: packages/idempotency/src/types/AnyFunction.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2-
type AnyFunction<U> = (...args: Array<any>) => Promise<U>;
2+
type AnyFunctionWithRecord<U> = (record: Record<string,any>) => Promise<U> | U;
3+
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
type AnyIdempotentFunction<U> = (record: Record<string,any>) => Promise<U>;
36

47
export {
5-
AnyFunction
8+
// AnyFunction,
9+
AnyFunctionWithRecord,
10+
AnyIdempotentFunction
611
};

Diff for: packages/idempotency/src/types/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './AnyFunction';
22
export * from './IdempotencyRecordStatus';
3-
export * from './PersistenceLayer';
3+
export * from './IdempotencyRecordOptions';
4+
export * from './PersistenceLayer';

Diff for: packages/idempotency/tests/helpers/populateEnvironmentVariables.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128';
66
if (process.env.AWS_REGION === undefined && process.env.CDK_DEFAULT_REGION === undefined) {
77
process.env.AWS_REGION = 'eu-west-1';
88
}
9-
process.env._HANDLER = 'index.handler';
9+
process.env._HANDLER = 'index.handler';

0 commit comments

Comments
 (0)