Skip to content

Commit 7be7a83

Browse files
shdqdreamorosi
andauthored
feat(idempotency): add support for custom key prefix (#3532)
Co-authored-by: Andrea Amorosi <[email protected]>
1 parent 6740fa6 commit 7be7a83

9 files changed

+132
-9
lines changed

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

+7
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export class IdempotencyHandler<Func extends AnyFunction> {
5151
* Idempotency configuration options.
5252
*/
5353
readonly #idempotencyConfig: IdempotencyConfig;
54+
/**
55+
* Custom prefix to be used when generating the idempotency key.
56+
*/
57+
readonly #keyPrefix: string | undefined;
5458
/**
5559
* Persistence layer used to store the idempotency records.
5660
*/
@@ -69,18 +73,21 @@ export class IdempotencyHandler<Func extends AnyFunction> {
6973
idempotencyConfig,
7074
functionArguments,
7175
persistenceStore,
76+
keyPrefix,
7277
thisArg,
7378
} = options;
7479
this.#functionToMakeIdempotent = functionToMakeIdempotent;
7580
this.#functionPayloadToBeHashed = functionPayloadToBeHashed;
7681
this.#idempotencyConfig = idempotencyConfig;
82+
this.#keyPrefix = keyPrefix;
7783
this.#functionArguments = functionArguments;
7884
this.#thisArg = thisArg;
7985

8086
this.#persistenceStore = persistenceStore;
8187

8288
this.#persistenceStore.configure({
8389
config: this.#idempotencyConfig,
90+
keyPrefix: this.#keyPrefix,
8491
});
8592
}
8693

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function makeIdempotent<Func extends AnyFunction>(
7979
fn: Func,
8080
options: ItempotentFunctionOptions<Parameters<Func>>
8181
): (...args: Parameters<Func>) => ReturnType<Func> {
82-
const { persistenceStore, config } = options;
82+
const { persistenceStore, config, keyPrefix } = options;
8383
const idempotencyConfig = config ? config : new IdempotencyConfig({});
8484

8585
if (!idempotencyConfig.isEnabled()) return fn;
@@ -102,6 +102,7 @@ function makeIdempotent<Func extends AnyFunction>(
102102
functionToMakeIdempotent: fn,
103103
idempotencyConfig: idempotencyConfig,
104104
persistenceStore: persistenceStore,
105+
keyPrefix: keyPrefix,
105106
functionArguments: args,
106107
functionPayloadToBeHashed,
107108
thisArg: this,

Diff for: packages/idempotency/src/middleware/makeHandlerIdempotent.ts

+3
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,18 @@ const makeHandlerIdempotent = (
117117
? options.config
118118
: new IdempotencyConfig({});
119119
const persistenceStore = options.persistenceStore;
120+
const keyPrefix = options.keyPrefix;
120121
persistenceStore.configure({
121122
config: idempotencyConfig,
123+
keyPrefix: keyPrefix,
122124
});
123125

124126
const idempotencyHandler = new IdempotencyHandler({
125127
functionToMakeIdempotent: /* v8 ignore next */ () => ({}),
126128
functionArguments: [],
127129
idempotencyConfig,
128130
persistenceStore,
131+
keyPrefix,
129132
functionPayloadToBeHashed: undefined,
130133
});
131134
setIdempotencyHandlerInRequestInternal(request, idempotencyHandler);

Diff for: packages/idempotency/src/persistence/BasePersistenceLayer.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
4646
/**
4747
* Initialize the base persistence layer from the configuration settings
4848
*
49-
* @param {BasePersistenceLayerConfigureOptions} config - configuration object for the persistence layer
49+
* @param {BasePersistenceLayerConfigureOptions} options - configuration object for the persistence layer
5050
*/
51-
public configure(config: BasePersistenceLayerOptions): void {
52-
// Extracting the idempotency config from the config object for easier access
53-
const { config: idempotencyConfig } = config;
51+
public configure(options: BasePersistenceLayerOptions): void {
52+
const { config: idempotencyConfig, keyPrefix, functionName } = options;
5453

55-
if (config?.functionName && config.functionName.trim() !== '') {
56-
this.idempotencyKeyPrefix = `${this.idempotencyKeyPrefix}.${config.functionName}`;
54+
if (keyPrefix?.trim()) {
55+
this.idempotencyKeyPrefix = keyPrefix.trim();
56+
} else if (functionName?.trim()) {
57+
this.idempotencyKeyPrefix = `${this.idempotencyKeyPrefix}.${functionName.trim()}`;
5758
}
5859

5960
// Prevent reconfiguration

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

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { IdempotencyRecord } from '../persistence/IdempotencyRecord.js';
44
type BasePersistenceLayerOptions = {
55
config: IdempotencyConfig;
66
functionName?: string;
7+
keyPrefix?: string;
78
};
89

910
interface BasePersistenceLayerInterface {

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

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { IdempotencyRecord } from '../persistence/IdempotencyRecord.js';
1919
type IdempotencyLambdaHandlerOptions = {
2020
persistenceStore: BasePersistenceLayer;
2121
config?: IdempotencyConfig;
22+
keyPrefix?: string;
2223
};
2324

2425
/**
@@ -137,6 +138,10 @@ type IdempotencyHandlerOptions = {
137138
* Idempotency configuration options.
138139
*/
139140
idempotencyConfig: IdempotencyConfig;
141+
/**
142+
* The custom idempotency key prefix.
143+
*/
144+
keyPrefix?: string;
140145
/**
141146
* Persistence layer used to store the idempotency records.
142147
*/

Diff for: packages/idempotency/tests/unit/idempotencyDecorator.test.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { LambdaInterface } from '@aws-lambda-powertools/commons/types';
22
import context from '@aws-lambda-powertools/testing-utils/context';
33
import type { Context } from 'aws-lambda';
4-
import { describe, expect, it } from 'vitest';
5-
import { idempotent } from '../../src/index.js';
4+
import { describe, expect, it, vi } from 'vitest';
5+
import { idempotent, IdempotencyConfig } from '../../src/index.js';
66
import { PersistenceLayerTestClass } from '../helpers/idempotencyUtils.js';
7+
import { BasePersistenceLayer } from '../../src/persistence/BasePersistenceLayer.js';
78

89
describe('Given a class with a function to decorate', () => {
910
it('maintains the scope of the decorated function', async () => {
@@ -35,4 +36,41 @@ describe('Given a class with a function to decorate', () => {
3536
// Assess
3637
expect(result).toBe('private foo');
3738
});
39+
40+
it('passes the custom keyPrefix to the persistenceStore', async () => {
41+
// Prepare
42+
const configureSpy = vi.spyOn(BasePersistenceLayer.prototype, 'configure');
43+
const idempotencyConfig = new IdempotencyConfig({});
44+
45+
class TestClass implements LambdaInterface {
46+
@idempotent({
47+
persistenceStore: new PersistenceLayerTestClass(),
48+
config: idempotencyConfig,
49+
keyPrefix: 'my-custom-prefix',
50+
})
51+
public async handler(
52+
_event: unknown,
53+
_context: Context
54+
): Promise<boolean> {
55+
return true;
56+
}
57+
}
58+
59+
const handlerClass = new TestClass();
60+
const handler = handlerClass.handler.bind(handlerClass);
61+
62+
// Act
63+
const result = await handler({}, context);
64+
65+
// Assess
66+
expect(result).toBeTruthy();
67+
68+
expect(configureSpy).toHaveBeenCalled();
69+
const configureCallArgs = configureSpy.mock.calls[0][0]; // Extract first call's arguments
70+
expect(configureCallArgs.config).toBe(idempotencyConfig);
71+
expect(configureCallArgs.keyPrefix).toBe('my-custom-prefix');
72+
73+
// Restore the spy
74+
configureSpy.mockRestore();
75+
});
3876
});

Diff for: packages/idempotency/tests/unit/makeIdempotent.test.ts

+39
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,45 @@ describe('Function: makeIdempotent', () => {
393393
expect(saveSuccessSpy).toHaveBeenCalledTimes(0);
394394
}
395395
);
396+
397+
it.each([
398+
{
399+
type: 'wrapper',
400+
},
401+
{ type: 'middleware' },
402+
])(
403+
'passes keyPrefix correctly in idempotency handler ($type)',
404+
async ({ type }) => {
405+
// Prepare
406+
const keyPrefix = 'my-custom-prefix';
407+
const options = {
408+
...mockIdempotencyOptions,
409+
keyPrefix,
410+
config: new IdempotencyConfig({
411+
eventKeyJmesPath: 'idempotencyKey',
412+
}),
413+
};
414+
const handler =
415+
type === 'wrapper'
416+
? makeIdempotent(fnSuccessfull, options)
417+
: middy(fnSuccessfull).use(makeHandlerIdempotent(options));
418+
419+
const configureSpy = vi.spyOn(
420+
mockIdempotencyOptions.persistenceStore,
421+
'configure'
422+
);
423+
424+
// Act
425+
const result = await handler(event, context);
426+
427+
// Assess
428+
expect(result).toBe(true);
429+
expect(configureSpy).toHaveBeenCalledWith(
430+
expect.objectContaining({ keyPrefix })
431+
);
432+
}
433+
);
434+
396435
it('uses the first argument when when wrapping an arbitrary function', async () => {
397436
// Prepare
398437
const config = new IdempotencyConfig({});

Diff for: packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@ describe('Class: BasePersistenceLayer', () => {
8080
);
8181
});
8282

83+
it('trims the function name before appending as key prefix', () => {
84+
// Prepare
85+
const config = new IdempotencyConfig({});
86+
const persistenceLayer = new PersistenceLayerTestClass();
87+
88+
// Act
89+
persistenceLayer.configure({ config, functionName: ' my-function ' });
90+
91+
// Assess
92+
expect(persistenceLayer.idempotencyKeyPrefix).toBe(
93+
'my-lambda-function.my-function'
94+
);
95+
});
96+
97+
it('appends custom prefix to the idempotence key prefix', () => {
98+
// Prepare
99+
const config = new IdempotencyConfig({});
100+
const persistenceLayer = new PersistenceLayerTestClass();
101+
102+
// Act
103+
persistenceLayer.configure({ config, keyPrefix: 'my-custom-prefix' });
104+
105+
// Assess
106+
expect(persistenceLayer.idempotencyKeyPrefix).toBe(
107+
'my-custom-prefix'
108+
);
109+
});
110+
83111
it('uses default config when no option is provided', () => {
84112
// Prepare
85113
const config = new IdempotencyConfig({});

0 commit comments

Comments
 (0)