diff --git a/package-lock.json b/package-lock.json index 20321c6c8d..70d62f9199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,8 +52,8 @@ "proxy-agent": "^5.0.0", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", - "typedoc": "^0.23.22", - "typedoc-plugin-missing-exports": "^1.0.0", + "typedoc": "^0.24.7", + "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "^4.9.4", "uuid": "^9.0.0" }, @@ -7919,6 +7919,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz", + "integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==", + "dev": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -15537,9 +15543,10 @@ } }, "node_modules/marked": { - "version": "4.2.4", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, - "license": "MIT", "bin": { "marked": "bin/marked.js" }, @@ -18608,13 +18615,15 @@ } }, "node_modules/shiki": { - "version": "0.11.1", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.2.tgz", + "integrity": "sha512-ltSZlSLOuSY0M0Y75KA+ieRaZ0Trf5Wl3gutE7jzLuIcWxLp5i/uEnLoQWNvgKXQ5OMpGkJnVMRLAuzjc0LJ2A==", "dev": true, - "license": "MIT", "dependencies": { - "jsonc-parser": "^3.0.0", - "vscode-oniguruma": "^1.6.1", - "vscode-textmate": "^6.0.0" + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" } }, "node_modules/shimmer": { @@ -19660,14 +19669,15 @@ "license": "MIT" }, "node_modules/typedoc": { - "version": "0.23.23", + "version": "0.24.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.7.tgz", + "integrity": "sha512-zzfKDFIZADA+XRIp2rMzLe9xZ6pt12yQOhCr7cD7/PBTjhPmMyMvGrkZ2lPNJitg3Hj1SeiYFNzCsSDrlpxpKw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "lunr": "^2.3.9", - "marked": "^4.2.4", - "minimatch": "^5.1.1", - "shiki": "^0.11.1" + "marked": "^4.3.0", + "minimatch": "^9.0.0", + "shiki": "^0.14.1" }, "bin": { "typedoc": "bin/typedoc" @@ -19676,34 +19686,40 @@ "node": ">= 14.14" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" } }, "node_modules/typedoc-plugin-missing-exports": { - "version": "1.0.0", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-missing-exports/-/typedoc-plugin-missing-exports-2.0.0.tgz", + "integrity": "sha512-t0QlKCm27/8DaheJkLo/gInSNjzBXgSciGhoLpL6sLyXZibm7SuwJtHvg4qXI2IjJfFBgW9mJvvszpoxMyB0TA==", "dev": true, - "license": "MIT", "peerDependencies": { - "typedoc": "0.22.x || 0.23.x" + "typedoc": "0.24.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "5.1.2", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/typescript": { @@ -19945,13 +19961,15 @@ }, "node_modules/vscode-oniguruma": { "version": "1.7.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true }, "node_modules/vscode-textmate": { - "version": "6.0.0", - "dev": true, - "license": "MIT" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true }, "node_modules/walk-up-path": { "version": "1.0.0", diff --git a/package.json b/package.json index 154689b3ab..bd40da9b35 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "proxy-agent": "^5.0.0", "ts-jest": "^29.0.3", "ts-node": "^10.9.1", - "typedoc": "^0.23.22", - "typedoc-plugin-missing-exports": "^1.0.0", + "typedoc": "^0.24.7", + "typedoc-plugin-missing-exports": "^2.0.0", "typescript": "^4.9.4", "uuid": "^9.0.0" }, diff --git a/packages/commons/package.json b/packages/commons/package.json index b9e84bf379..70d3222c0a 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -30,7 +30,6 @@ "license": "MIT-0", "main": "./lib/index.js", "types": "./lib/index.d.ts", - "typedocMain": "src/index.ts", "files": [ "lib" ], diff --git a/packages/commons/typedoc.json b/packages/commons/typedoc.json new file mode 100644 index 0000000000..879b1d55e7 --- /dev/null +++ b/packages/commons/typedoc.json @@ -0,0 +1,7 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": [ + "./src/index.ts" + ], + "readme": "README.md" +} \ No newline at end of file diff --git a/packages/idempotency/README.md b/packages/idempotency/README.md index 1b38da475a..80c1da112a 100644 --- a/packages/idempotency/README.md +++ b/packages/idempotency/README.md @@ -1,71 +1,148 @@ -# Powertools for AWS Lambda (TypeScript) +# Powertools for AWS Lambda (TypeScript) - Idempotency Utility + + +| ⚠️ **WARNING: Do not use this utility in production just yet!** ⚠️ | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **This utility is currently released as beta developer preview** and is intended strictly for feedback and testing purposes **and not for production workloads**.. The version and all future versions tagged with the `-beta` suffix should be treated as not stable. Up until before the [General Availability release](https://github.com/awslabs/aws-lambda-powertools-typescript/milestone/10) we might introduce significant breaking changes and improvements in response to customers feedback. | _ | + Powertools for AWS Lambda (TypeScript) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/#features). -You can use the library in both TypeScript and JavaScript code bases. +## Intro + +This package provides a utility to implement idempotency in your Lambda functions. +You can either use it as a decorator on your Lambda handler or as a wrapper on any other function. +If you use middy, we also provide a middleware to make your Lambda handler idempotent. +The current implementation provides a persistance layer for Amazon DynamoDB, which offers a variety of configuration options. +You can also bring your own persistance layer by implementing the `IdempotencyPersistanceLayer` interface. + +## Key features +* Prevent Lambda handler from executing more than once on the same event payload during a time window +* Ensure Lambda handler returns the same result when called with the same payload +* Select a subset of the event as the idempotency key using JMESPath expressions +* Set a time window in which records with the same payload should be considered duplicates +* Expires in-progress executions if the Lambda function times out halfway through + +## Usage + +### Decorators +If you use classes to define your Lambda handlers, you can use the decorators to make your handler idempotent or a specific function idempotent. +We offer two decorators: +* `@idempotentLambdaHandler`: makes the handler idempotent. +* `@idempotentFunction`: makes any function within your class idempotent -[Powertools for AWS Lambda (Python)](https://github.com/awslabs/aws-lambda-powertools-python) and [Powertools for AWS Lambda (Java)](https://github.com/awslabs/aws-lambda-powertools-java) are also available. +The first can only be applied to the handler function with the specific signature of a Lambda handler. +The second can be applied to any function within your class. In this case you need to pass a `Record` object and provide the `dataKeywordArgument` parameter to specify the name of the argument that contains the data to be used as the idempotency key. +In any of both cases yoiu need to pass the persistance layer where we will store the idempotency information. -**[📜 Documentation](https://awslabs.github.io/aws-lambda-powertools-typescript/)** | **[NPM](https://www.npmjs.com/org/aws-lambda-powertools)** | **[Roadmap](https://github.com/awslabs/aws-lambda-powertools-roadmap/projects/1)** | **[Examples](https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/examples)** | **[Serverless TypeScript Demo](https://github.com/aws-samples/serverless-typescript-demo)** -## Table of contents +### Function wrapper -- [Features](#features) -- [Getting started](#getting-started) - - [Installation](#installation) - - [Examples](#examples) - - [Serverless TypeScript Demo application](#serverless-typescript-demo-application) -- [Contribute](#contribute) -- [Roadmap](#roadmap) -- [Connect](#connect) -- [How to support Powertools for AWS Lambda (TypeScript)?](#how-to-support-powertools-for-aws-lambda-typescript) - - [Becoming a reference customer](#becoming-a-reference-customer) - - [Sharing your work](#sharing-your-work) - - [Using Lambda Layer](#using-lambda-layer) -- [Credits](#credits) -- [License](#license) +A more common approach is to use the function wrapper. +Similar to `@idempotentFunction` decorator you need to pass keyword argument to indicate which part of the payload will be hashed. -## Features +### Middy middleware +// TODO: after e2e tests are implemented -* **[Tracer](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/tracer/)** - Utilities to trace Lambda function handlers, and both synchronous and asynchronous functions -* **[Logger](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/logger/)** - Structured logging made easier, and a middleware to enrich log items with key details of the Lambda context -* **[Metrics](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/metrics/)** - Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) -* **[Parameters (beta)](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/utilities/parameters/)** - High-level functions to retrieve one or more parameters from AWS SSM, Secrets Manager, AppConfig, and DynamoDB +### DynamoDB peristance layer +To store the idempotency information offer a DynamoDB persistance layer. +This enables you to store the hash key, payload, status for progress and expiration and much more. +You can customise most of the configuration options of the DynamoDB table, i.e the names of the attributes. +See the [API documentation](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/modules/.index.DynamoDBPersistenceLayer.html) for more details. -## Getting started +## Examples -Find the complete project's [documentation here](https://awslabs.github.io/aws-lambda-powertools-typescript). +### Decorator Lambda handler -### Installation +```ts +import { idempotentLambdaHandler } from "@aws-lambda-powertools/idempotency"; +import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/persistance"; +import type { Context } from 'aws-lambda'; -The Powertools for AWS Lambda (TypeScript) utilities follow a modular approach, similar to the official [AWS SDK v3 for JavaScript](https://github.com/aws/aws-sdk-js-v3). -Each TypeScript utility is installed as standalone NPM package. +const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer(); -Install all three core utilities at once with this single command: +class MyLambdaHandler implements LambdaInterface { + @idempotentLambdaHandler({ persistenceStore: dynamoDBPersistenceLayer }) + public async handler(_event: any, _context: Context): Promise { + // your lambda code here + return "Hello World"; + } +} -```shell -npm install @aws-lambda-powertools/logger @aws-lambda-powertools/tracer @aws-lambda-powertools/metrics +const lambdaClass = new MyLambdaHandler(); +export const handler = lambdaClass.handler.bind(lambdaClass); ``` -Or refer to the installation guide of each utility: +### Decorator function + +```ts +import { idempotentLambdaHandler } from "@aws-lambda-powertools/idempotency"; +import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/persistance"; +import type { Context } from 'aws-lambda'; + + +const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer(); + +class MyLambdaHandler implements LambdaInterface { + + public async handler(_event: any, _context: Context): Promise { + for(const record of _event.Records) { + await this.processRecord(record); + } + } + + @idempotentFunction({ persistenceStore: dynamoDBPersistenceLayer, dataKeywordArgument: "payload" }) + public async process(payload: Record): Promise { + // your lambda code here + } +} +``` + +The `dataKeywordArgument` parameter is optional. If not provided, the whole event will be used as the idempotency key. +Otherwise, you need to specify the string name of the argument that contains the data to be used as the idempotency key. +For example if you have an input like this: + -👉 [Installation guide for the **Tracer** utility](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/tracer#getting-started) +```json +{ + "transactionId": 1235, + "product": "book", + "quantity": 1, + "price": 10 +} +``` + +You can use `transactionId` as the idempotency key. This will ensure that the same transaction is not processed twice. -👉 [Installation guide for the **Logger** utility](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/logger#getting-started) +### Function wrapper -👉 [Installation guide for the **Metrics** utility](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/core/metrics#getting-started) +In case where you don't use classes and decorators you can wrap your function to make it idempotent. -👉 [Installation guide for the **Parameters** utility](https://awslabs.github.io/aws-lambda-powertools-typescript/latest/utilities/parameters/#getting-started) +```ts +import { makeFunctionIdempotent } from "@aws-lambda-powertools/idempotency"; +import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/persistance"; +import type { Context } from 'aws-lambda'; -### Examples -* [CDK](https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/examples/cdk) -* [SAM](https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/examples/sam) +const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer(); +const processingFunction = async (payload: Record): Promise => { + // your lambda code here +}; -### Serverless TypeScript Demo application +const processIdempotently = makeFunctionIdempotent(proccessingFunction, { + persistenceStore: dynamoDBPersistenceLayer, + dataKeywordArgument: "transactionId" +}); -The [Serverless TypeScript Demo](https://github.com/aws-samples/serverless-typescript-demo) shows how to use Powertools for AWS Lambda (TypeScript). -You can find instructions on how to deploy and load test this application in the [repository](https://github.com/aws-samples/serverless-typescript-demo). +export const handler = async ( + _event: any, + _context: Context +): Promise => { + for (const record of _event.Records) { + await processIdempotently(record); + } +}; +``` ## Contribute diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index f2df04aa9c..9b00446ba7 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -71,7 +71,6 @@ }, "main": "./lib/index.js", "types": "./lib/index.d.ts", - "typedocMain": "src/file_that_does_not_exist_so_its_ignored_from_api_docs.ts", "files": [ "lib" ], @@ -91,6 +90,7 @@ "aws", "lambda", "powertools", + "idempotency", "serverless", "nodejs" ], diff --git a/packages/idempotency/src/IdempotencyConfig.ts b/packages/idempotency/src/IdempotencyConfig.ts index f8a00ed8d4..ad494bdc64 100644 --- a/packages/idempotency/src/IdempotencyConfig.ts +++ b/packages/idempotency/src/IdempotencyConfig.ts @@ -2,14 +2,48 @@ import { EnvironmentVariablesService } from './config'; import type { Context } from 'aws-lambda'; import type { IdempotencyConfigOptions } from './types'; +/** + * Configuration for the idempotency feature. + */ class IdempotencyConfig { + /** + * The JMESPath expression used to extract the idempotency key from the event. + * @default '' + */ public eventKeyJmesPath: string; + /** + * The number of seconds the idempotency key is valid. + * @default 3600 (1 hour) + */ public expiresAfterSeconds: number; + /** + * The hash function used to generate the idempotency key. + * @see https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options + * @default 'md5' + */ public hashFunction: string; + /** + * The lambda context object. + */ public lambdaContext?: Context; + /** + * The maximum number of items to store in the local cache. + * @default 1000 + */ public maxLocalCacheSize: number; + /** + * The JMESPath expression used to extract the payload to validate. + */ public payloadValidationJmesPath?: string; + /** + * Throw an error if the idempotency key is not found in the event. + * In some cases, you may want to allow the request to continue without idempotency. + */ public throwOnNoIdempotencyKey: boolean; + /** + * Use the local cache to store idempotency keys. + * @see {@link LRUCache} + */ public useLocalCache: boolean; readonly #envVarsService: EnvironmentVariablesService; readonly #enabled: boolean = true; diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index f8983e59c0..66ecae19eb 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -10,6 +10,9 @@ import { BasePersistenceLayer, IdempotencyRecord } from './persistence'; import { IdempotencyConfig } from './IdempotencyConfig'; import { MAX_RETRIES } from './constants'; +/** + * @internal + */ export class IdempotencyHandler { private readonly fullFunctionPayload: Record; private readonly functionPayloadToBeHashed: Record; diff --git a/packages/idempotency/src/idempotentDecorator.ts b/packages/idempotency/src/idempotentDecorator.ts index 4787ae9f75..020c3e2cd9 100644 --- a/packages/idempotency/src/idempotentDecorator.ts +++ b/packages/idempotency/src/idempotentDecorator.ts @@ -50,6 +50,32 @@ const idempotent = function ( }; }; +/** + * Use this decorator to make your lambda handler itempotent. + * You need to provide a peristance layer to store the idempotency information. + * At the moment we only support `DynamodbPersistenceLayer`. + * > **Note**: + * > decorators are an exeperimental feature in typescript and may change in the future. + * > To enable decoratopr support in your project, you need to enable the `experimentalDecorators` compiler option in your tsconfig.json file. + * @example + * ```ts + * import { + * DynamoDBPersistenceLayer, + * idempotentLambdaHandler + * } from '@aws-lambda-powertools/idempotency' + * + * class MyLambdaFunction { + * @idempotentLambdaHandler({ persistenceStore: new DynamoDBPersistenceLayer() }) + * async handler(event: any, context: any) { + * return "Hello World"; + * } + * } + * export myLambdaHandler new MyLambdaFunction(); + * export const handler = myLambdaHandler.handler.bind(myLambdaHandler); + * ``` + * @see {@link DynamoDBPersistenceLayer} + * @see https://www.typescriptlang.org/docs/handbook/decorators.html + */ const idempotentLambdaHandler = function ( options: IdempotencyLambdaHandlerOptions ): ( @@ -59,6 +85,32 @@ const idempotentLambdaHandler = function ( ) => PropertyDescriptor { return idempotent(options); }; +/** + * Use this decorator to make any class function idempotent. + * Similar to the `idempotentLambdaHandler` decorator, you need to provide a persistence layer to store the idempotency information. + * @example + * ```ts + * import { + * DynamoDBPersistenceLayer, + * idempotentFunction + * } from '@aws-lambda-powertools/idempotency' + * + * class MyClass { + * + * public async handler(_event: any, _context: any) { + * for(const record of _event.records){ + * await this.process(record); + * } + * } + * + * @idempotentFunction({ persistenceStore: new DynamoDBPersistenceLayer() }) + * public async process(record: Record { ); }; +/** + * Use function wrapper to make your function idempotent. + * @example + * ```ts + * // this is your processing function with an example record { transactionId: '123', foo: 'bar' } + * const processRecord = (record: Record): any => { + * // you custom processing logic + * return result; + * }; + * + * // we use wrapper to make processing function idempotent with DynamoDBPersistenceLayer + * const processIdempotently = makeFunctionIdempotent(processRecord, { + * persistenceStore: new DynamoDBPersistenceLayer() + * dataKeywordArgument: 'transactionId', // keyword argument to hash the payload and the result + * }); + * + * export const handler = async ( + * _event: EventRecords, + * _context: Context + * ): Promise => { + * for (const record of _event.records) { + * const result = await processIdempotently(record); + * // do something with the result + * } + * + * return Promise.resolve(); + * }; + * + * ``` + */ const makeFunctionIdempotent = function ( fn: AnyFunctionWithRecord, options: IdempotencyFunctionOptions @@ -50,7 +80,6 @@ const makeFunctionIdempotent = function ( return idempotencyHandler.handle(); }; - if (idempotencyConfig.isEnabled()) return wrappedFn; else return fn; }; diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index b247d83025..708f458756 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -11,6 +11,14 @@ import { } from '../Exceptions'; import { LRUCache } from './LRUCache'; +/** + * Base class for all persistence layers. This class provides the basic functionality for + * saving, retrieving, and deleting idempotency records. It also provides the ability to + * configure the persistence layer from the idempotency config. + * @abstract + * @class + * @implements {BasePersistenceLayerInterface} + */ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { public idempotencyKeyPrefix: string; private cache?: LRUCache; diff --git a/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts b/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts index ec23401b5a..27601fb11a 100644 --- a/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts +++ b/packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts @@ -18,6 +18,15 @@ import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; import { IdempotencyRecord } from './IdempotencyRecord'; import { BasePersistenceLayer } from './BasePersistenceLayer'; +/** + * DynamoDB persistence layer for idempotency records. This class will use the AWS SDK V3 to write and read idempotency records from DynamoDB. + * There are various options to configure the persistence layer, such as the table name, the key attribute, the status attribute, etc. + * With default configuration you don't need to create the table beforehand, the persistence layer will create it for you. + * You can also bring your own AWS SDK V3 client, or configure the client with the `clientConfig` option. + * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/index.html + * @category Persistence Layer + * @implements {BasePersistenceLayer} + */ class DynamoDBPersistenceLayer extends BasePersistenceLayer { private client?: DynamoDBClient; private clientConfig: DynamoDBClientConfig = {}; diff --git a/packages/idempotency/src/persistence/IdempotencyRecord.ts b/packages/idempotency/src/persistence/IdempotencyRecord.ts index 88882caa49..b2fe7a0528 100644 --- a/packages/idempotency/src/persistence/IdempotencyRecord.ts +++ b/packages/idempotency/src/persistence/IdempotencyRecord.ts @@ -1,16 +1,38 @@ -import { IdempotencyRecordStatus } from '../types'; import type { IdempotencyRecordOptions } from '../types'; +import { IdempotencyRecordStatus } from '../types'; import { IdempotencyInvalidStatusError } from '../Exceptions'; /** - * Class representing an idempotency record + * Class representing an idempotency record. + * The properties of this class will be reflected in the persistance layer. */ class IdempotencyRecord { + /** + * The expiry timestamp of the record in milliseconds UTC. + */ public expiryTimestamp?: number; + /** + * The idempotency key of the record that is used to identify the record. + */ public idempotencyKey: string; + /** + * The expiry timestamp of the in progress record in milliseconds UTC. + */ public inProgressExpiryTimestamp?: number; + /** + * The hash of the payload of the request, used for comparing requests. + */ public payloadHash?: string; + /** + * The response data of the request, this will be returned if the payload hash matches. + */ public responseData?: Record; + /** + * The idempotency record status can be COMPLETED, IN_PROGRESS or EXPIRED. + * We check the status during idempotency processing to make sure we don't process an expired record and handle concurrent requests. + * @link {IdempotencyRecordStatus} + * @private + */ private status: IdempotencyRecordStatus; public constructor(config: IdempotencyRecordOptions) { @@ -22,10 +44,17 @@ class IdempotencyRecord { this.status = config.status; } + /** + * Get the response data of the record. + */ public getResponse(): Record | undefined { return this.responseData; } + /** + * Get the status of the record. + * @throws {IdempotencyInvalidStatusError} If the status is not a valid status. + */ public getStatus(): IdempotencyRecordStatus { if (this.isExpired()) { return IdempotencyRecordStatus.EXPIRED; @@ -36,6 +65,9 @@ class IdempotencyRecord { } } + /** + * Returns true if the record is expired or undefined. + */ public isExpired(): boolean { return ( this.expiryTimestamp !== undefined && diff --git a/packages/idempotency/src/types/DynamoDBPersistence.ts b/packages/idempotency/src/types/DynamoDBPersistence.ts index 576a5acdd7..65b1980510 100644 --- a/packages/idempotency/src/types/DynamoDBPersistence.ts +++ b/packages/idempotency/src/types/DynamoDBPersistence.ts @@ -39,8 +39,8 @@ interface DynamoPersistenceOptionsBaseInterface { */ interface DynamoPersistenceOptionsWithClientConfig extends DynamoPersistenceOptionsBaseInterface { - clientConfig?: DynamoDBClientConfig; awsSdkV3Client?: never; + clientConfig?: DynamoDBClientConfig; } /** @@ -58,9 +58,9 @@ interface DynamoPersistenceOptionsWithClientInstance } /** - * Options for the AppConfigProvider class constructor. + * Options for the {@link DynamoDBPersistenceLayer} class constructor. * - * @type AppConfigProviderOptions + * @type DynamoPersistenceOptions * @property {string} tableName - The DynamoDB table name. * @property {string} [keyAttr] - The DynamoDB table key attribute name. Defaults to 'id'. * @property {string} [expiryAttr] - The DynamoDB table expiry attribute name. Defaults to 'expiration'. diff --git a/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts b/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts index a75a72b026..244457aab3 100644 --- a/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/idempotencyDecorator.test.FunctionCode.ts @@ -1,8 +1,8 @@ import type { Context } from 'aws-lambda'; import { LambdaInterface } from '@aws-lambda-powertools/commons'; -import { DynamoDBPersistenceLayer } from '../../src/dynamodb'; import { idempotentFunction, idempotentLambdaHandler } from '../../src'; import { Logger } from '../../../logger'; +import { DynamoDBPersistenceLayer } from '../../src/persistence/DynamoDBPersistenceLayer'; const IDEMPOTENCY_TABLE_NAME = process.env.IDEMPOTENCY_TABLE_NAME || 'table_name'; diff --git a/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts b/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts index 897499ccfc..af2d3062c6 100644 --- a/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/makeFunctionIdempotent.test.FunctionCode.ts @@ -1,5 +1,5 @@ import type { Context } from 'aws-lambda'; -import { DynamoDBPersistenceLayer } from '../../src/dynamodb'; +import { DynamoDBPersistenceLayer } from '../../src/persistence/DynamoDBPersistenceLayer'; import { makeFunctionIdempotent } from '../../src'; import { Logger } from '@aws-lambda-powertools/logger'; diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index dd1a60d9d1..8f4b4c3a0d 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -12,7 +12,7 @@ import { 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/'; import { MAX_RETRIES } from '../../src/constants'; class PersistenceLayerTestClass extends BasePersistenceLayer { diff --git a/packages/idempotency/tests/unit/idempotentDecorator.test.ts b/packages/idempotency/tests/unit/idempotentDecorator.test.ts index 712f7c8552..2e15f8c623 100644 --- a/packages/idempotency/tests/unit/idempotentDecorator.test.ts +++ b/packages/idempotency/tests/unit/idempotentDecorator.test.ts @@ -5,10 +5,7 @@ */ import { BasePersistenceLayer, IdempotencyRecord } from '../../src/persistence'; -import { - idempotentFunction, - idempotentLambdaHandler, -} from '../../src/idempotentDecorator'; +import { idempotentFunction, idempotentLambdaHandler } from '../../src/'; import type { IdempotencyRecordOptions } from '../../src/types'; import { IdempotencyRecordStatus } from '../../src/types'; import { diff --git a/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts b/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts index 9c45384b4d..4310a648a1 100644 --- a/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeFunctionIdempotent.test.ts @@ -3,7 +3,7 @@ * * @group unit/idempotency/makeFunctionIdempotent */ -import { IdempotencyFunctionOptions } from '../../src/types/IdempotencyOptions'; +import { IdempotencyFunctionOptions } from '../../src/types/'; import { BasePersistenceLayer, IdempotencyRecord } from '../../src/persistence'; import { makeFunctionIdempotent } from '../../src'; import type { diff --git a/packages/idempotency/tests/unit/makeHandlerIdempotent.test.ts b/packages/idempotency/tests/unit/makeHandlerIdempotent.test.ts index 387a9e0fd9..c3677032bc 100644 --- a/packages/idempotency/tests/unit/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeHandlerIdempotent.test.ts @@ -13,7 +13,7 @@ import { IdempotencyItemAlreadyExistsError, IdempotencyInconsistentStateError, } from '../../src/Exceptions'; -import { IdempotencyConfig } from '../../src/IdempotencyConfig'; +import { IdempotencyConfig } from '../../src/'; import middy from '@middy/core'; import { MAX_RETRIES } from '../../src/constants'; import { PersistenceLayerTestClass } from '../helpers/idempotencyUtils'; diff --git a/packages/idempotency/typedoc.json b/packages/idempotency/typedoc.json new file mode 100644 index 0000000000..b983e7c7b4 --- /dev/null +++ b/packages/idempotency/typedoc.json @@ -0,0 +1,12 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "./src/index.ts", + "./src/types/index.ts", + "./src/middleware/index.ts", + "./src/persistence/index.ts" + ], + "readme": "README.md" +} \ No newline at end of file diff --git a/packages/logger/package.json b/packages/logger/package.json index 155fc67e58..b4dfa96882 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -33,7 +33,6 @@ "license": "MIT", "main": "./lib/index.js", "types": "./lib/index.d.ts", - "typedocMain": "src/index.ts", "devDependencies": { "@types/lodash.merge": "^4.6.7" }, diff --git a/packages/logger/typedoc.json b/packages/logger/typedoc.json new file mode 100644 index 0000000000..3a1fad0dcf --- /dev/null +++ b/packages/logger/typedoc.json @@ -0,0 +1,8 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": [ + "./src/index.ts", + "./src/types" + ], + "readme": "README.md" +} \ No newline at end of file diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 98731d3d6a..c518112d47 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -33,7 +33,6 @@ "license": "MIT-0", "main": "./lib/index.js", "types": "./lib/index.d.ts", - "typedocMain": "src/index.ts", "devDependencies": { "@aws-sdk/client-cloudwatch": "^3.316.0", "@types/promise-retry": "^1.1.3", diff --git a/packages/metrics/typedoc.json b/packages/metrics/typedoc.json new file mode 100644 index 0000000000..a81c0d5f1d --- /dev/null +++ b/packages/metrics/typedoc.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "../../typedoc.base.json" + ], + "entryPoints": [ + "./src/index.ts", + "./src/types/index.ts" + ], + "readme": "README.md" +} \ No newline at end of file diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 8230003ac8..559cb18cca 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -36,7 +36,6 @@ "license": "MIT-0", "main": "./lib/index.js", "types": "./lib/index.d.ts", - "typedocMain": "src/docs.ts", "files": [ "lib" ], diff --git a/packages/parameters/src/docs.ts b/packages/parameters/src/docs.ts deleted file mode 100644 index f43ccf14aa..0000000000 --- a/packages/parameters/src/docs.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './appconfig'; -export * from './ssm'; -export * from './secrets'; -export * from './dynamodb'; diff --git a/packages/parameters/typedoc.json b/packages/parameters/typedoc.json new file mode 100644 index 0000000000..2464c677ef --- /dev/null +++ b/packages/parameters/typedoc.json @@ -0,0 +1,10 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": [ + "./src/appconfig/index.ts", + "./src/ssm/index.ts", + "./src/dynamodb/index.ts", + "./src/secrets/index.ts", + "./src/types/index.ts"], + "readme": "README.md" +} \ No newline at end of file diff --git a/packages/tracer/typedoc.json b/packages/tracer/typedoc.json new file mode 100644 index 0000000000..8ead571a7f --- /dev/null +++ b/packages/tracer/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["./src/index.ts", "./src/types/index.ts"], + "readme": "README.md" +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..5fefd1f535 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "noImplicitAny": true, + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "outDir": "lib", + "strict": true, + "inlineSourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "pretty": true, + "baseUrl": "src/", + "rootDirs": [ "src/" ], + "esModuleInterop": true + }, + "include": [ "packages" ], + "exclude": [ "./node_modules"], + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority" + }, + "lib": [ "es2020" ], + "types": [ + "node" + ] +} \ No newline at end of file diff --git a/typedoc.base.json b/typedoc.base.json new file mode 100644 index 0000000000..ce978059e9 --- /dev/null +++ b/typedoc.base.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "includeVersion": false, + "excludePrivate": true, + "exclude": [ + "**/node_modules/**", + "**/*.test.ts", + "**/*.json", + "docs/snippets" + ] +} \ No newline at end of file diff --git a/typedoc.js b/typedoc.js deleted file mode 100644 index 7f17fbcc1d..0000000000 --- a/typedoc.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - out: 'api', - exclude: ['**/node_modules/**', '**/*.test.ts', '**/*.json'], - name: 'aws-lambda-powertools-typescript', - excludePrivate: true, - excludeInternal: true, - entryPointStrategy: 'packages', - readme: './README.md', -}; diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000000..c277d0cc80 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,23 @@ +{ + "entryPoints": ["packages/*"], + "entryPointStrategy": "packages", + "name": "Powertools for AWS Lambda (Typescript) API Reference", + "readme": "README.md", + "out": "api", + "exclude": [ + "**/node_modules/**", + "**/*.test.ts", + "**/*.json", + "docs/snippets", + "layers", + "examples/**" + ], + "skipErrorChecking": true, + "excludePrivate": true, + "visibilityFilters": { + "protected": true, + "private": false, + "inherited": true, + "external": true + } +} \ No newline at end of file