Skip to content

chore(maintenance): migrate idempotency utility to biome #2810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/idempotency/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"build:cjs": "tsc --build tsconfig.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json",
"build:esm": "tsc --build tsconfig.esm.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json",
"build": "npm run build:esm & npm run build:cjs",
"lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .",
"lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .",
"lint": "biome lint .",
"lint:fix": "biome check --write .",
"prepack": "node ../../.github/scripts/release_patch_package_json.js ."
},
"homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/idempotency#readme",
Expand Down
53 changes: 25 additions & 28 deletions packages/idempotency/src/IdempotencyHandler.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import type { Handler } from 'aws-lambda';
import type {
JSONValue,
MiddyLikeRequest,
} from '@aws-lambda-powertools/commons/types';
import type {
AnyFunction,
IdempotencyHandlerOptions,
} from './types/IdempotencyOptions.js';
import { search } from '@aws-lambda-powertools/jmespath';
import type { Handler } from 'aws-lambda';
import type { IdempotencyConfig } from './IdempotencyConfig.js';
import { IdempotencyRecordStatus, MAX_RETRIES } from './constants.js';
import {
IdempotencyAlreadyInProgressError,
IdempotencyInconsistentStateError,
IdempotencyItemAlreadyExistsError,
type IdempotencyItemAlreadyExistsError,
IdempotencyPersistenceLayerError,
IdempotencyUnknownError,
} from './errors.js';
import { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js';
import { IdempotencyRecord } from './persistence/IdempotencyRecord.js';
import { IdempotencyConfig } from './IdempotencyConfig.js';
import { MAX_RETRIES, IdempotencyRecordStatus } from './constants.js';
import { search } from '@aws-lambda-powertools/jmespath';
import type { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js';
import type { IdempotencyRecord } from './persistence/IdempotencyRecord.js';
import type {
AnyFunction,
IdempotencyHandlerOptions,
} from './types/IdempotencyOptions.js';

/**
* @internal
Expand Down Expand Up @@ -99,9 +99,8 @@ export class IdempotencyHandler<Func extends AnyFunction> {
throw new IdempotencyInconsistentStateError(
'Item has expired during processing and may not longer be valid.'
);
} else if (
idempotencyRecord.getStatus() === IdempotencyRecordStatus.INPROGRESS
) {
}
if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.INPROGRESS) {
if (
idempotencyRecord.inProgressExpiryTimestamp &&
idempotencyRecord.inProgressExpiryTimestamp <
Expand All @@ -110,11 +109,10 @@ export class IdempotencyHandler<Func extends AnyFunction> {
throw new IdempotencyInconsistentStateError(
'Item is in progress but the in progress expiry timestamp has expired.'
);
} else {
throw new IdempotencyAlreadyInProgressError(
`There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}`
);
}
throw new IdempotencyAlreadyInProgressError(
`There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}`
);
}

return idempotencyRecord.getResponse();
Expand All @@ -129,7 +127,7 @@ export class IdempotencyHandler<Func extends AnyFunction> {
* @returns The result of the function execution
*/
public async getFunctionResult(): Promise<ReturnType<Func>> {
let result;
let result: ReturnType<Func>;
try {
result = await this.#functionToMakeIdempotent.apply(
this.#thisArg,
Expand Down Expand Up @@ -168,7 +166,7 @@ export class IdempotencyHandler<Func extends AnyFunction> {
);
}

let e;
let e: Error | undefined;
for (let retryNo = 0; retryNo <= MAX_RETRIES; retryNo++) {
try {
const { isIdempotent, result } =
Expand Down Expand Up @@ -234,7 +232,7 @@ export class IdempotencyHandler<Func extends AnyFunction> {
public async handleMiddyBefore(
request: MiddyLikeRequest,
callback: (request: MiddyLikeRequest) => Promise<void>
): Promise<ReturnType<Func> | void> {
): Promise<ReturnType<Func> | undefined> {
for (let retryNo = 0; retryNo <= MAX_RETRIES; retryNo++) {
try {
const { isIdempotent, result } =
Expand Down Expand Up @@ -309,9 +307,9 @@ export class IdempotencyHandler<Func extends AnyFunction> {
);

return selection === undefined || selection === null;
} else {
return false;
}

return false;
}

/**
Expand Down Expand Up @@ -388,12 +386,11 @@ export class IdempotencyHandler<Func extends AnyFunction> {
);

return returnValue;
} else {
throw new IdempotencyPersistenceLayerError(
'Failed to save in progress record to idempotency store',
{ cause: error }
);
}
throw new IdempotencyPersistenceLayerError(
'Failed to save in progress record to idempotency store',
{ cause: error }
);
}
};

Expand Down
5 changes: 3 additions & 2 deletions packages/idempotency/src/deepSort.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getType } from '@aws-lambda-powertools/commons';
import {
import type {
JSONArray,
JSONObject,
JSONValue,
Expand Down Expand Up @@ -36,7 +36,8 @@ const deepSort = (data: JSONValue): JSONValue => {
const type = getType(data);
if (type === 'object') {
return sortObject(data as JSONObject);
} else if (type === 'array') {
}
if (type === 'array') {
return (data as JSONArray).map(deepSort);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/idempotency/src/idempotencyDecorator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Handler } from 'aws-lambda';
import {
import { makeIdempotent } from './makeIdempotent.js';
import type {
AnyFunction,
ItempotentFunctionOptions,
} from './types/IdempotencyOptions.js';
import { makeIdempotent } from './makeIdempotent.js';

/**
* Use this decorator to make your lambda handler itempotent.
Expand Down Expand Up @@ -60,6 +60,7 @@ const idempotent = function (
propertyKey: string,
descriptor: PropertyDescriptor
) => PropertyDescriptor {
// biome-ignore lint/complexity/useArrowFunction: this is a decorator function and we need to maintain the `this` context
return function (
_target: unknown,
_propertyKey: string,
Expand Down
10 changes: 5 additions & 5 deletions packages/idempotency/src/makeIdempotent.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
import type { Context, Handler } from 'aws-lambda';
import { IdempotencyConfig } from './IdempotencyConfig.js';
import { IdempotencyHandler } from './IdempotencyHandler.js';
import type {
AnyFunction,
ItempotentFunctionOptions,
IdempotencyLambdaHandlerOptions,
ItempotentFunctionOptions,
} from './types/IdempotencyOptions.js';
import { IdempotencyHandler } from './IdempotencyHandler.js';
import { IdempotencyConfig } from './IdempotencyConfig.js';

const isContext = (arg: unknown): arg is Context => {
return (
Expand Down Expand Up @@ -72,7 +73,6 @@ const isOptionsWithDataIndexArgument = (
*
* ```
*/
// eslint-disable-next-line func-style
function makeIdempotent<Func extends AnyFunction>(
fn: Func,
options: ItempotentFunctionOptions<Parameters<Func>>
Expand All @@ -83,7 +83,7 @@ function makeIdempotent<Func extends AnyFunction>(
if (!idempotencyConfig.isEnabled()) return fn;

return function (this: Handler, ...args: Parameters<Func>): ReturnType<Func> {
let functionPayloadToBeHashed;
let functionPayloadToBeHashed: JSONValue;

if (isFnHandler(fn, args)) {
idempotencyConfig.registerLambdaContext(args[1]);
Expand Down
60 changes: 32 additions & 28 deletions packages/idempotency/src/persistence/BasePersistenceLayer.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { createHash, Hash } from 'node:crypto';
import { type Hash, createHash } from 'node:crypto';
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
import { search } from '@aws-lambda-powertools/jmespath';
import type { JMESPathParsingOptions } from '@aws-lambda-powertools/jmespath/types';
import type {
BasePersistenceLayerOptions,
BasePersistenceLayerInterface,
} from '../types/BasePersistenceLayer.js';
import { IdempotencyRecordStatus } from '../constants.js';
import { EnvironmentVariablesService } from '../config/EnvironmentVariablesService.js';
import { IdempotencyRecord } from './IdempotencyRecord.js';
import { IdempotencyRecordStatus } from '../constants.js';
import { deepSort } from '../deepSort.js';
import {
IdempotencyItemAlreadyExistsError,
IdempotencyKeyError,
IdempotencyValidationError,
} from '../errors.js';
import type {
BasePersistenceLayerInterface,
BasePersistenceLayerOptions,
} from '../types/BasePersistenceLayer.js';
import { IdempotencyRecord } from './IdempotencyRecord.js';
import { LRUCache } from './LRUCache.js';
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
import { deepSort } from '../deepSort.js';

/**
* Base class for all persistence layers. This class provides the basic functionality for
Expand Down Expand Up @@ -282,15 +282,15 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
* @returns the idempotency key
*/
private getHashedIdempotencyKey(data: JSONValue): string {
if (this.eventKeyJmesPath) {
data = search(
this.eventKeyJmesPath,
data,
this.#jmesPathOptions
) as JSONValue;
}

if (BasePersistenceLayer.isMissingIdempotencyKey(data)) {
const payload = this.eventKeyJmesPath
? (search(
this.eventKeyJmesPath,
data,
this.#jmesPathOptions
) as JSONValue)
: data;

if (BasePersistenceLayer.isMissingIdempotencyKey(payload)) {
if (this.throwOnNoIdempotencyKey) {
throw new IdempotencyKeyError(
'No data found to create a hashed idempotency_key'
Expand All @@ -302,7 +302,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
}

return `${this.idempotencyKeyPrefix}#${this.generateHash(
JSON.stringify(deepSort(data))
JSON.stringify(deepSort(payload))
)}`;
}

Expand All @@ -313,16 +313,20 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
*/
private getHashedPayload(data: JSONValue): string {
if (this.isPayloadValidationEnabled() && this.validationKeyJmesPath) {
data = search(
this.validationKeyJmesPath,
data,
this.#jmesPathOptions
) as JSONValue;

return this.generateHash(JSON.stringify(deepSort(data)));
} else {
return '';
return this.generateHash(
JSON.stringify(
deepSort(
search(
this.validationKeyJmesPath,
data,
this.#jmesPathOptions
) as JSONValue
)
)
);
}

return '';
}

private static isMissingIdempotencyKey(data: JSONValue): boolean {
Expand Down
24 changes: 12 additions & 12 deletions packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import {
IdempotencyItemAlreadyExistsError,
IdempotencyItemNotFoundError,
} from '../errors.js';
import { IdempotencyRecordStatus } from '../constants.js';
import type { DynamoDBPersistenceOptions } from '../types/DynamoDBPersistence.js';
addUserAgentMiddleware,
isSdkClient,
} from '@aws-lambda-powertools/commons';
import {
AttributeValue,
type AttributeValue,
ConditionalCheckFailedException,
DeleteItemCommand,
DynamoDBClient,
DynamoDBClientConfig,
type DynamoDBClientConfig,
GetItemCommand,
PutItemCommand,
UpdateItemCommand,
} from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { IdempotencyRecord } from './IdempotencyRecord.js';
import { BasePersistenceLayer } from './BasePersistenceLayer.js';
import { IdempotencyRecordStatus } from '../constants.js';
import {
addUserAgentMiddleware,
isSdkClient,
} from '@aws-lambda-powertools/commons';
IdempotencyItemAlreadyExistsError,
IdempotencyItemNotFoundError,
} from '../errors.js';
import type { DynamoDBPersistenceOptions } from '../types/DynamoDBPersistence.js';
import { BasePersistenceLayer } from './BasePersistenceLayer.js';
import { IdempotencyRecord } from './IdempotencyRecord.js';

/**
* DynamoDB persistence layer for idempotency records.
Expand Down
10 changes: 5 additions & 5 deletions packages/idempotency/src/persistence/IdempotencyRecord.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
import { IdempotencyRecordStatus } from '../constants.js';
import { IdempotencyInvalidStatusError } from '../errors.js';
import type {
IdempotencyRecordOptions,
IdempotencyRecordStatusValue,
} from '../types/IdempotencyRecord.js';
import { IdempotencyRecordStatus } from '../constants.js';
import { IdempotencyInvalidStatusError } from '../errors.js';

/**
* Class representing an idempotency record.
Expand Down Expand Up @@ -62,11 +62,11 @@ class IdempotencyRecord {
public getStatus(): IdempotencyRecordStatusValue {
if (this.isExpired()) {
return IdempotencyRecordStatus.EXPIRED;
} else if (Object.values(IdempotencyRecordStatus).includes(this.status)) {
}
if (Object.values(IdempotencyRecordStatus).includes(this.status)) {
return this.status;
} else {
throw new IdempotencyInvalidStatusError(this.status);
}
throw new IdempotencyInvalidStatusError(this.status);
}

/**
Expand Down
6 changes: 2 additions & 4 deletions packages/idempotency/src/persistence/LRUCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ class LRUCache<K, V> {
public add(key: K, value: V): void {
// If the key already exists, we just update the value and mark it as the most recently used
if (this.map.has(key)) {
// At this point, we know that the key exists in the map, so we can safely use the non-null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// biome-ignore lint/style/noNonNullAssertion: At this point, we know that the key exists in the map, so we can safely use the non-null
const item = this.map.get(key)!;
item.value = value;
this.trackItemUse(item);
Expand Down Expand Up @@ -192,8 +191,7 @@ class LRUCache<K, V> {
* Removes the oldest item from the cache and unlinks it from the linked list.
*/
private shift(): void {
// If this function is called, we know that the least recently used item exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// biome-ignore lint/style/noNonNullAssertion: If this function is called, we know that the least recently used item exists
const item = this.leastRecentlyUsed!;

// If there's a newer item, make it the oldest
Expand Down
4 changes: 2 additions & 2 deletions packages/idempotency/src/types/BasePersistenceLayer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IdempotencyRecord } from '../persistence/IdempotencyRecord.js';
import { IdempotencyConfig } from '../IdempotencyConfig.js';
import type { IdempotencyConfig } from '../IdempotencyConfig.js';
import type { IdempotencyRecord } from '../persistence/IdempotencyRecord.js';

type BasePersistenceLayerOptions = {
config: IdempotencyConfig;
Expand Down
Loading