From 5264d03f98ce5892b0584eed21b6f07429e4fdaf Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 11:34:31 +0000 Subject: [PATCH 1/8] docs(idempotency): bring your own persistent store --- .../advancedBringYourOwnPersistenceLayer.ts | 204 +++++++++++++++ ...vancedBringYourOwnPersistenceLayerUsage.ts | 53 ++++ .../idempotency/makeIdempotentJmes.ts | 2 +- .../idempotency/templates/tableCdk.ts | 28 +++ .../idempotency/templates/tableSam.yaml | 31 +++ .../idempotency/templates/tableTerraform.tf | 78 ++++++ docs/snippets/idempotency/types.ts | 22 +- docs/snippets/package.json | 1 + docs/utilities/idempotency.md | 143 +++-------- package-lock.json | 232 +++++++++++++++++- 10 files changed, 674 insertions(+), 120 deletions(-) create mode 100644 docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts create mode 100644 docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts create mode 100644 docs/snippets/idempotency/templates/tableCdk.ts create mode 100644 docs/snippets/idempotency/templates/tableSam.yaml create mode 100644 docs/snippets/idempotency/templates/tableTerraform.tf diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts new file mode 100644 index 0000000000..4f095111c4 --- /dev/null +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts @@ -0,0 +1,204 @@ +import { + IdempotencyItemAlreadyExistsError, + IdempotencyItemNotFoundError, + IdempotencyRecordStatus, +} from '@aws-lambda-powertools/idempotency'; +import { + IdempotencyRecordOptions, + IdempotencyRecordStatusValue, +} from '@aws-lambda-powertools/idempotency/types'; +import { + IdempotencyRecord, + BasePersistenceLayer, +} from '@aws-lambda-powertools/idempotency/persistence'; +import { getSecret } from '@aws-lambda-powertools/parameters/secrets'; +import { Transform } from '@aws-lambda-powertools/parameters'; +import { + CacheClient, + CredentialProvider, + Configurations, + CacheGet, + CacheKeyExists, + CollectionTtl, + CacheDictionarySetFields, + CacheDictionaryGetFields, +} from '@gomomento/sdk'; +import type { MomentoApiSecret, Item } from './types'; + +class MomentoCachePersistenceLayer extends BasePersistenceLayer { + #cacheName: string; + #client?: CacheClient; + + public constructor(config: { cacheName: string }) { + super(); + this.#cacheName = config.cacheName; + } + + protected async _deleteRecord(record: IdempotencyRecord): Promise { + await ( + await this.#getClient() + ).delete(this.#cacheName, record.idempotencyKey); + } + + protected async _getRecord( + idempotencyKey: string + ): Promise { + const response = await ( + await this.#getClient() + ).dictionaryFetch(this.#cacheName, idempotencyKey); + + if ( + response instanceof CacheGet.Error || + response instanceof CacheGet.Miss + ) { + throw new IdempotencyItemNotFoundError(); + } + const { data, ...rest } = + response.value() as unknown as IdempotencyRecordOptions & { + data: string; + }; + + return new IdempotencyRecord({ + responseData: JSON.parse(data), + ...rest, + }); + } + + protected async _putRecord(record: IdempotencyRecord): Promise { + const item: Partial = { + status: record.getStatus(), + }; + + if (record.inProgressExpiryTimestamp !== undefined) { + item.in_progress_expiration = record.inProgressExpiryTimestamp.toString(); + } + + if (this.isPayloadValidationEnabled() && record.payloadHash !== undefined) { + item.validation = record.payloadHash; + } + + try { + const lock = await this.#lookupItem(record.idempotencyKey); + + if ( + lock.getStatus() !== IdempotencyRecordStatus.INPROGRESS && + (lock.inProgressExpiryTimestamp || 0) < Date.now() + ) { + throw new IdempotencyItemAlreadyExistsError( + `Failed to put record for already existing idempotency key: ${record.idempotencyKey}` + ); + } + } catch (error) { + if (error instanceof IdempotencyItemAlreadyExistsError) { + throw error; + } + } + + try { + const ttl = + Math.floor(new Date(record.expiryTimestamp! * 1000).getTime() / 1000) - + Math.floor(new Date().getTime() / 1000); + + const response = await ( + await this.#getClient() + ).dictionarySetFields(this.#cacheName, record.idempotencyKey, item, { + ttl: CollectionTtl.of(ttl).withNoRefreshTtlOnUpdates(), + }); + + if (response instanceof CacheDictionarySetFields.Error) { + throw new Error(`Unable to put item: ${response.errorCode()}`); + } + } catch (error) { + throw error; + } + } + + protected async _updateRecord(record: IdempotencyRecord): Promise { + const value: Partial = { + data: JSON.stringify(record.responseData), + status: record.getStatus(), + }; + + if (this.isPayloadValidationEnabled()) { + value.validation = record.payloadHash; + } + + await this.#checkItemExists(record.idempotencyKey); + + await ( + await this.#getClient() + ).dictionarySetFields(this.#cacheName, record.idempotencyKey, value, { + ttl: CollectionTtl.refreshTtlIfProvided().withNoRefreshTtlOnUpdates(), + }); + } + + async #getMomentoApiSecret(): Promise { + const secretName = process.env.MOMENTO_API_SECRET; + if (!secretName) { + throw new Error('MOMENTO_API_SECRET environment variable is not set'); + } + + const apiSecret = await getSecret(secretName, { + transform: Transform.JSON, + }); + + if (!apiSecret) { + throw new Error(`Could not retrieve secret ${secretName}`); + } + + return apiSecret; + } + + async #getClient(): Promise { + if (this.#client) return this.#client; + + const apiSecret = await this.#getMomentoApiSecret(); + this.#client = await CacheClient.create({ + configuration: Configurations.InRegion.LowLatency.latest(), + credentialProvider: CredentialProvider.fromString({ + apiKey: apiSecret.apiKey, + }), + defaultTtlSeconds: this.getExpiresAfterSeconds(), + }); + + return this.#client; + } + + async #checkItemExists(idempotencyKey: string): Promise { + const response = await ( + await this.#getClient() + ).keysExist(this.#cacheName, [idempotencyKey]); + + return response instanceof CacheKeyExists.Success; + } + + async #lookupItem(idempotencyKey: string): Promise { + const response = await ( + await this.#getClient() + ).dictionaryGetFields(this.#cacheName, idempotencyKey, [ + 'in_progress_expiration', + 'status', + ]); + + if (response instanceof CacheDictionaryGetFields.Miss) { + throw new IdempotencyItemNotFoundError(); + } else if (response instanceof CacheDictionaryGetFields.Error) { + throw new Error('Unable to get item'); + } else { + const { status, in_progress_expiration: inProgressExpiryTimestamp } = + response.value() || {}; + + if (status !== undefined || inProgressExpiryTimestamp !== undefined) { + throw new Error('Unable'); + } + + return new IdempotencyRecord({ + idempotencyKey, + status: status as IdempotencyRecordStatusValue, + inProgressExpiryTimestamp: parseFloat(inProgressExpiryTimestamp), + }); + } + } +} + +export { MomentoCachePersistenceLayer }; diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts new file mode 100644 index 0000000000..d03abb56a1 --- /dev/null +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts @@ -0,0 +1,53 @@ +import type { Context } from 'aws-lambda'; +import { randomUUID } from 'node:crypto'; +import { MomentoCachePersistenceLayer } from './advancedBringYourOwnPersistenceLayer'; +import { + IdempotencyConfig, + makeIdempotent, +} from '@aws-lambda-powertools/idempotency'; +import type { Request, Response, SubscriptionResult } from './types'; + +const persistenceStore = new MomentoCachePersistenceLayer({ + cacheName: 'powertools', +}); +const config = new IdempotencyConfig({ + expiresAfterSeconds: 60, +}); + +const createSubscriptionPayment = makeIdempotent( + async ( + _transactionId: string, + event: Request + ): Promise => { + // ... create payment + return { + id: randomUUID(), + productId: event.productId, + }; + }, + { + persistenceStore, + dataIndexArgument: 1, + config, + } +); + +export const handler = async ( + event: Request, + context: Context +): Promise => { + config.registerLambdaContext(context); + + try { + const transactionId = randomUUID(); + const payment = await createSubscriptionPayment(transactionId, event); + + return { + paymentId: payment.id, + message: 'success', + statusCode: 200, + }; + } catch (error) { + throw new Error('Error creating payment'); + } +}; diff --git a/docs/snippets/idempotency/makeIdempotentJmes.ts b/docs/snippets/idempotency/makeIdempotentJmes.ts index 32675c1687..b4d0d165d7 100644 --- a/docs/snippets/idempotency/makeIdempotentJmes.ts +++ b/docs/snippets/idempotency/makeIdempotentJmes.ts @@ -12,7 +12,7 @@ const persistenceStore = new DynamoDBPersistenceLayer({ }); const createSubscriptionPayment = async ( - user: string, + _user: string, productId: string ): Promise => { // ... create payment diff --git a/docs/snippets/idempotency/templates/tableCdk.ts b/docs/snippets/idempotency/templates/tableCdk.ts new file mode 100644 index 0000000000..284b6b095a --- /dev/null +++ b/docs/snippets/idempotency/templates/tableCdk.ts @@ -0,0 +1,28 @@ +import { Stack, type StackProps } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; + +export class IdempotencyMomentoStack extends Stack { + public constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const table = new Table(this, 'idempotencyTable', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + timeToLiveAttribute: 'expiration', + billingMode: BillingMode.PAY_PER_REQUEST, + }); + + const fnHandler = new NodejsFunction(this, 'helloWorldFunction', { + runtime: Runtime.NODEJS_18_X, + environment: { + IDEMPOTENCY_TABLE_NAME: table.tableName, + }, + }); + table.grantReadWriteData(fnHandler); + } +} diff --git a/docs/snippets/idempotency/templates/tableSam.yaml b/docs/snippets/idempotency/templates/tableSam.yaml new file mode 100644 index 0000000000..010ecc89ca --- /dev/null +++ b/docs/snippets/idempotency/templates/tableSam.yaml @@ -0,0 +1,31 @@ +Transform: AWS::Serverless-2016-10-31 +Resources: + IdempotencyTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + TimeToLiveSpecification: + AttributeName: expiration + Enabled: true + BillingMode: PAY_PER_REQUEST + + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Runtime: python3.11 + Handler: app.py + Policies: + - Statement: + - Sid: AllowDynamodbReadWrite + Effect: Allow + Action: + - dynamodb:PutItem + - dynamodb:GetItem + - dynamodb:UpdateItem + - dynamodb:DeleteItem + Resource: !GetAtt IdempotencyTable.Arn diff --git a/docs/snippets/idempotency/templates/tableTerraform.tf b/docs/snippets/idempotency/templates/tableTerraform.tf new file mode 100644 index 0000000000..4856f2b0e6 --- /dev/null +++ b/docs/snippets/idempotency/templates/tableTerraform.tf @@ -0,0 +1,78 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + } +} + +provider "aws" { + region = "us-east-1" # Replace with your desired AWS region +} + +resource "aws_dynamodb_table" "IdempotencyTable" { + name = "IdempotencyTable" + billing_mode = "PAY_PER_REQUEST" + hash_key = "id" + attribute { + name = "id" + type = "S" + } + ttl { + attribute_name = "expiration" + enabled = true + } +} + +resource "aws_lambda_function" "IdempotencyFunction" { + function_name = "IdempotencyFunction" + role = aws_iam_role.IdempotencyFunctionRole.arn + runtime = "nodejs18.x" + handler = "index.handler" + filename = "lambda.zip" +} + +resource "aws_iam_role" "IdempotencyFunctionRole" { + name = "IdempotencyFunctionRole" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "" + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + }, + ] + }) +} + +resource "aws_iam_policy" "LambdaDynamoDBPolicy" { + name = "LambdaDynamoDBPolicy" + description = "IAM policy for Lambda function to access DynamoDB" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowDynamodbReadWrite" + Effect = "Allow" + Action = [ + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ] + Resource = aws_dynamodb_table.IdempotencyTable.arn + }, + ] + }) +} + +resource "aws_iam_role_policy_attachment" "IdempotencyFunctionRoleAttachment" { + role = aws_iam_role.IdempotencyFunctionRole.name + policy_arn = aws_iam_policy.LambdaDynamoDBPolicy.arn +} \ No newline at end of file diff --git a/docs/snippets/idempotency/types.ts b/docs/snippets/idempotency/types.ts index 42d2cd63bd..196f19da5b 100644 --- a/docs/snippets/idempotency/types.ts +++ b/docs/snippets/idempotency/types.ts @@ -1,15 +1,29 @@ -type Request = { +import { IdempotencyRecordStatusValue } from '@aws-lambda-powertools/idempotency/types'; + +export type Request = { user: string; productId: string; }; -type Response = { +export type Response = { [key: string]: unknown; }; -type SubscriptionResult = { +export type SubscriptionResult = { id: string; productId: string; }; -export { Request, Response, SubscriptionResult }; +export type MomentoApiSecret = { + apiKey: string; + refreshToken: string; + validUntil: number; + restEndpoint: string; +}; + +export type Item = { + validation?: string; + in_progress_expiration?: string; + status: IdempotencyRecordStatusValue; + data: string; +}; diff --git a/docs/snippets/package.json b/docs/snippets/package.json index 0258196766..259d13246b 100644 --- a/docs/snippets/package.json +++ b/docs/snippets/package.json @@ -33,6 +33,7 @@ "@aws-sdk/client-secrets-manager": "^3.360.0", "@aws-sdk/client-ssm": "^3.360.0", "@aws-sdk/util-dynamodb": "^3.360.0", + "@gomomento/sdk": "^1.39.3", "aws-sdk": "^2.1405.0", "aws-sdk-client-mock": "^2.2.0", "aws-sdk-client-mock-jest": "^2.2.0", diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index c06f28a149..043afeadcc 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -88,124 +88,22 @@ If you're not [changing the default configuration for the DynamoDB persistence l ???+ tip "Tip: You can share a single state table for all functions" You can reuse the same DynamoDB table to store idempotency state. We add the Lambda function name in addition to the idempotency key as a hash key. - -=== "AWS Serverless Application Model (SAM) example" +=== "AWS Cloud Development Kit (CDK) example" - ```yaml hl_lines="6-14 24-31" - Transform: AWS::Serverless-2016-10-31 - Resources: - IdempotencyTable: - Type: AWS::DynamoDB::Table - Properties: - AttributeDefinitions: - - AttributeName: id - AttributeType: S - KeySchema: - - AttributeName: id - KeyType: HASH - TimeToLiveSpecification: - AttributeName: expiration - Enabled: true - BillingMode: PAY_PER_REQUEST - - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - Runtime: nodejs18.x - Handler: index.handler - Policies: - - Statement: - - Sid: AllowDynamodbReadWrite - Effect: Allow - Action: - - dynamodb:PutItem - - dynamodb:GetItem - - dynamodb:UpdateItem - - dynamodb:DeleteItem - Resource: !GetAtt IdempotencyTable.Arn + ```typescript title="template.tf" hl_lines="11-18 26" + --8<-- "docs/snippets/idempotency/templates/tableCdk.ts" ``` -=== "Terraform" - - ```terraform hl_lines="14-26 64-70" - terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 4.0" - } - } - } - - provider "aws" { - region = "us-east-1" # Replace with your desired AWS region - } - - resource "aws_dynamodb_table" "IdempotencyTable" { - name = "IdempotencyTable" - billing_mode = "PAY_PER_REQUEST" - hash_key = "id" - attribute { - name = "id" - type = "S" - } - ttl { - attribute_name = "expiration" - enabled = true - } - } - - resource "aws_lambda_function" "IdempotencyFunction" { - function_name = "IdempotencyFunction" - role = aws_iam_role.IdempotencyFunctionRole.arn - runtime = "nodejs18.x" - handler = "index.handler" - filename = "lambda.zip" - } +=== "AWS Serverless Application Model (SAM) example" - resource "aws_iam_role" "IdempotencyFunctionRole" { - name = "IdempotencyFunctionRole" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "" - Effect = "Allow" - Principal = { - Service = "lambda.amazonaws.com" - } - Action = "sts:AssumeRole" - }, - ] - }) - } + ```yaml title="template.yaml" hl_lines="6-14 24-31" + --8<-- "docs/snippets/idempotency/templates/tableSam.yaml" + ``` - resource "aws_iam_policy" "LambdaDynamoDBPolicy" { - name = "LambdaDynamoDBPolicy" - description = "IAM policy for Lambda function to access DynamoDB" - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "AllowDynamodbReadWrite" - Effect = "Allow" - Action = [ - "dynamodb:PutItem", - "dynamodb:GetItem", - "dynamodb:UpdateItem", - "dynamodb:DeleteItem", - ] - Resource = aws_dynamodb_table.IdempotencyTable.arn - }, - ] - }) - } +=== "Terraform example" - resource "aws_iam_role_policy_attachment" "IdempotencyFunctionRoleAttachment" { - role = aws_iam_role.IdempotencyFunctionRole.name - policy_arn = aws_iam_policy.LambdaDynamoDBPolicy.arn - } + ```terraform title="template.tf" hl_lines="14-26 64-70" + --8<-- "docs/snippets/idempotency/templates/tableTerraform.tf" ``` ???+ warning "Warning: Large responses with DynamoDB persistence layer" @@ -854,6 +752,27 @@ The example function above would cause data to be stored in DynamoDB like this: | idempotency#MyLambdaFunction | 2b2cdb5f86361e97b4383087c1ffdf27 | 1636549571 | COMPLETED | {"paymentId": "527212", "message": "success", "statusCode": 200} | | idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | | +### Bring your own persistent store + +This utility provides an abstract base class (ABC), so that you can implement your choice of persistent storage layer. + +You can create your own persistent store from scratch by inheriting the `BasePersistenceLayer` class, and implementing `_getRecord()`, `_putRecord()`, `_updateRecord()` and `_deleteRecord()`. +- `_getRecord()` – Retrieves an item from the persistence store using an idempotency key and returns it as a DataRecord instance. +- `_putRecord()` – Adds a DataRecord to the persistence store if it doesn't already exist with that key. Throws an ItemAlreadyExists error if a non-expired entry already exists. +- `_updateRecord()` – Updates an item in the persistence store. +- `_deleteRecord()` – Removes an item from the persistence store. + +Below an example of an alternative persistence layer backed by Momento Cache: + +```typescript title="Bring your own persistence store" hl_lines="9" +--8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts" +``` + +???+ danger + Pay attention to the documentation for each - you may need to perform additional checks inside these methods to ensure the idempotency guarantees remain intact. + + For example, the `_put_Record()` method needs to throw an error if a non-expired record already exists in the data store with a matching key. + ## Extra resources If you're interested in a deep dive on how Amazon uses idempotency when building our APIs, check out diff --git a/package-lock.json b/package-lock.json index 1e9966a1b5..033f6293cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "@aws-sdk/client-secrets-manager": "^3.360.0", "@aws-sdk/client-ssm": "^3.360.0", "@aws-sdk/util-dynamodb": "^3.360.0", + "@gomomento/sdk": "^1.39.3", "aws-sdk": "^2.1405.0", "aws-sdk-client-mock": "^2.2.0", "aws-sdk-client-mock-jest": "^2.2.0", @@ -2292,6 +2293,101 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@gomomento/generated-types": { + "version": "0.77.0", + "resolved": "https://registry.npmjs.org/@gomomento/generated-types/-/generated-types-0.77.0.tgz", + "integrity": "sha512-b6ZIoB2N3efhLOUoBxbt+qmE46lNCJErxF2A+g5RcDVgQrx143HcIMXpTghnEA4p6mmVOCQWYGsXVp0PHWz75Q==", + "dev": true, + "dependencies": { + "@grpc/grpc-js": "1.9.0", + "google-protobuf": "3.21.2" + } + }, + "node_modules/@gomomento/sdk": { + "version": "1.39.3", + "resolved": "https://registry.npmjs.org/@gomomento/sdk/-/sdk-1.39.3.tgz", + "integrity": "sha512-VBuBTTsXMlNRFi1cYF6ieP9N1RiUnsy2Bdu62LO57l8KwxnOgSTBcYalr7fwN/vijI2si3kNexUmYK8fXm/aHA==", + "dev": true, + "dependencies": { + "@gomomento/generated-types": "0.77.0", + "@gomomento/sdk-core": "1.39.3", + "@grpc/grpc-js": "1.9.0", + "@types/google-protobuf": "3.15.6", + "google-protobuf": "3.21.2", + "jwt-decode": "3.1.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@gomomento/sdk-core": { + "version": "1.39.3", + "resolved": "https://registry.npmjs.org/@gomomento/sdk-core/-/sdk-core-1.39.3.tgz", + "integrity": "sha512-FoR+oiES2nOQ9hyriyFT6VfFbqS8Bh7w8fH2rNOcfHvvcC5u1CBGFTpU4PSkuktdFSuQ7XVri/SLLvHN6BYudA==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "jwt-decode": "3.1.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@gomomento/sdk-core/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.0.tgz", + "integrity": "sha512-H8+iZh+kCE6VR/Krj6W28Y/ZlxoZ1fOzsNt77nrdE3knkbSelW1Uus192xOFCxHyeszLj8i4APQkSIXjAoOxXg==", + "dev": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.9.tgz", + "integrity": "sha512-YJsOehVXzgurc+lLAxYnlSMc1p/Gu6VAvnfx0ATi2nzvr0YZcjhmZDeY8SeAKv1M7zE3aEJH0Xo9mK1iZ8GYoQ==", + "dev": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.4", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -4361,6 +4457,70 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true + }, "node_modules/@sigstore/bundle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.1.0.tgz", @@ -5139,6 +5299,12 @@ "@types/node": "*" } }, + "node_modules/@types/google-protobuf": { + "version": "3.15.6", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz", + "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -5936,6 +6102,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, "engines": { "node": ">= 4.0.0" } @@ -6728,7 +6895,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -6834,6 +7002,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7497,7 +7666,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/concat-stream": { "version": "2.0.0", @@ -9517,6 +9687,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -9992,6 +10163,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==", + "dev": true + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -10007,7 +10184,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -10273,6 +10451,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, "engines": { "node": ">= 4" } @@ -11740,6 +11919,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -11790,6 +11970,12 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -12873,6 +13059,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -13068,6 +13260,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -13484,6 +13682,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -15440,6 +15639,30 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, + "node_modules/protobufjs": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", + "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protocols": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", @@ -15456,6 +15679,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } @@ -17486,6 +17710,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, "engines": { "node": ">= 10.0.0" } @@ -17936,6 +18161,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, "engines": { "node": ">= 6" } From 3993e69aa0acf5e7ff19eb4437b3608180d4ddf0 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 11:51:30 +0000 Subject: [PATCH 2/8] docs: moved all snippets to dedicated files --- .../samples/makeIdempotentJmes.json | 30 +++++ ...orkingWIthIdempotencyRequiredKeyError.json | 7 ++ ...kingWIthIdempotencyRequiredKeySuccess.json | 7 ++ .../idempotency/samples/workingWithBatch.json | 26 ++++ docs/utilities/idempotency.md | 115 +++++------------- 5 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 docs/snippets/idempotency/samples/makeIdempotentJmes.json create mode 100644 docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeyError.json create mode 100644 docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeySuccess.json create mode 100644 docs/snippets/idempotency/samples/workingWithBatch.json diff --git a/docs/snippets/idempotency/samples/makeIdempotentJmes.json b/docs/snippets/idempotency/samples/makeIdempotentJmes.json new file mode 100644 index 0000000000..9f608983da --- /dev/null +++ b/docs/snippets/idempotency/samples/makeIdempotentJmes.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "routeKey": "ANY /createpayment", + "rawPath": "/createpayment", + "rawQueryString": "", + "headers": { + "Header1": "value1", + "X-Idempotency-Key": "abcdefg" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/createpayment", + "protocol": "HTTP/1.1", + "sourceIp": "ip", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "ANY /createpayment", + "stage": "$default", + "time": "10/Feb/2021:13:40:43 +0000", + "timeEpoch": 1612964443723 + }, + "body": "{\"user\":\"xyz\",\"productId\":\"123456789\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeyError.json b/docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeyError.json new file mode 100644 index 0000000000..a905b83e7a --- /dev/null +++ b/docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeyError.json @@ -0,0 +1,7 @@ +{ + "user": { + "uid": "BB0D045C-8878-40C8-889E-38B3CB0A61B1", + "name": "foo", + "productId": 10000 + } +} \ No newline at end of file diff --git a/docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeySuccess.json b/docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeySuccess.json new file mode 100644 index 0000000000..e721b2c24c --- /dev/null +++ b/docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeySuccess.json @@ -0,0 +1,7 @@ +{ + "user": { + "uid": "BB0D045C-8878-40C8-889E-38B3CB0A61B1", + "name": "Foo" + }, + "productId": 10000 +} \ No newline at end of file diff --git a/docs/snippets/idempotency/samples/workingWithBatch.json b/docs/snippets/idempotency/samples/workingWithBatch.json new file mode 100644 index 0000000000..44bd07a141 --- /dev/null +++ b/docs/snippets/idempotency/samples/workingWithBatch.json @@ -0,0 +1,26 @@ +{ + "Records": [ + { + "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", + "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", + "body": "Test message.", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1545082649183", + "SenderId": "AIDAIENQZJOLO23YVJ4VO", + "ApproximateFirstReceiveTimestamp": "1545082649185" + }, + "messageAttributes": { + "testAttr": { + "stringValue": "100", + "binaryValue": "base64Str", + "dataType": "Number" + } + }, + "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", + "awsRegion": "us-east-2" + } + ] +} \ No newline at end of file diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 043afeadcc..a246203273 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -130,7 +130,7 @@ You can quickly start by initializing the `DynamoDBPersistenceLayer` class and u === "types.ts" ```typescript - --8<-- "docs/snippets/idempotency/types.ts::13" + --8<-- "docs/snippets/idempotency/types.ts:3:16" ``` After processing this request successfully, a second request containing the exact same payload above will now return the same response, ensuring our customer isn't charged twice. @@ -158,7 +158,7 @@ When using `makeIdempotent` on arbitrary functions, you can tell us which argume === "types.ts" ```typescript - --8<-- "docs/snippets/idempotency/types.ts::13" + --8<-- "docs/snippets/idempotency/types.ts:3:16" ``` The function this example has two arguments, note that while wrapping it with the `makeIdempotent` high-order function, we specify the `dataIndexArgument` as `1` to tell the decorator that the second argument is the one that contains the data we should use to make the function idempotent. Remember that arguments are zero-indexed, so the first argument is `0`, the second is `1`, and so on. @@ -181,7 +181,7 @@ If you are using [Middy](https://middy.js.org){target="_blank"} as your middlewa === "types.ts" ```typescript - --8<-- "docs/snippets/idempotency/types.ts::13" + --8<-- "docs/snippets/idempotency/types.ts:3:16" ``` ### Choosing a payload subset for idempotency @@ -208,42 +208,13 @@ Imagine the function executes successfully, but the client never receives the re === "Example event" ```json hl_lines="28" - { - "version":"2.0", - "routeKey":"ANY /createpayment", - "rawPath":"/createpayment", - "rawQueryString":"", - "headers": { - "Header1": "value1", - "X-Idempotency-Key": "abcdefg" - }, - "requestContext":{ - "accountId":"123456789012", - "apiId":"api-id", - "domainName":"id.execute-api.us-east-1.amazonaws.com", - "domainPrefix":"id", - "http":{ - "method":"POST", - "path":"/createpayment", - "protocol":"HTTP/1.1", - "sourceIp":"ip", - "userAgent":"agent" - }, - "requestId":"id", - "routeKey":"ANY /createpayment", - "stage":"$default", - "time":"10/Feb/2021:13:40:43 +0000", - "timeEpoch":1612964443723 - }, - "body":"{\"user\":\"xyz\",\"productId\":\"123456789\"}", - "isBase64Encoded":false - } + --8<-- "docs/snippets/idempotency/samples/makeIdempotentJmes.json" ``` === "types.ts" ```typescript - --8<-- "docs/snippets/idempotency/types.ts::13" + --8<-- "docs/snippets/idempotency/types.ts:3:16" ``` ### Lambda timeouts @@ -645,25 +616,13 @@ This means that we will raise **`IdempotencyKeyError`** if the evaluation of **` === "Success Event" ```json hl_lines="3 6" - { - "user": { - "uid": "BB0D045C-8878-40C8-889E-38B3CB0A61B1", - "name": "Foo" - }, - "productId": 10000 - } + --8<-- "docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeySuccess.json" ``` === "Failure Event" ```json hl_lines="3 5" - { - "user": { - "uid": "BB0D045C-8878-40C8-889E-38B3CB0A61B1", - "name": "foo", - "productId": 10000 - } - } + --8<-- "docs/snippets/idempotency/samples/workingWIthIdempotencyRequiredKeyError.json" ``` ### Batch integration @@ -686,32 +645,7 @@ This ensures that you process each record in an idempotent manner, and guard aga === "Sample event" ```json hl_lines="4" - { - "Records": [ - { - "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d", - "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a...", - "body": "Test message.", - "attributes": { - "ApproximateReceiveCount": "1", - "SentTimestamp": "1545082649183", - "SenderId": "AIDAIENQZJOLO23YVJ4VO", - "ApproximateFirstReceiveTimestamp": "1545082649185" - }, - "messageAttributes": { - "testAttr": { - "stringValue": "100", - "binaryValue": "base64Str", - "dataType": "Number" - } - }, - "md5OfBody": "e4e68fb7bd0e697a0ae8f1bb342846b3", - "eventSource": "aws:sqs", - "eventSourceARN": "arn:aws:sqs:us-east-2:123456789012:my-queue", - "awsRegion": "us-east-2" - } - ] - } + --8<-- "docs/snippets/idempotency/samples/workingWithBatch.json" ``` ### Customizing AWS SDK configuration @@ -757,21 +691,36 @@ The example function above would cause data to be stored in DynamoDB like this: This utility provides an abstract base class (ABC), so that you can implement your choice of persistent storage layer. You can create your own persistent store from scratch by inheriting the `BasePersistenceLayer` class, and implementing `_getRecord()`, `_putRecord()`, `_updateRecord()` and `_deleteRecord()`. -- `_getRecord()` – Retrieves an item from the persistence store using an idempotency key and returns it as a DataRecord instance. -- `_putRecord()` – Adds a DataRecord to the persistence store if it doesn't already exist with that key. Throws an ItemAlreadyExists error if a non-expired entry already exists. -- `_updateRecord()` – Updates an item in the persistence store. -- `_deleteRecord()` – Removes an item from the persistence store. -Below an example of an alternative persistence layer backed by Momento Cache: +* `_getRecord()` – Retrieves an item from the persistence store using an idempotency key and returns it as a `IdempotencyRecord` instance. +* `_putRecord()` – Adds a `IdempotencyRecord` to the persistence store if it doesn't already exist with that key. Throws an `IdempotencyItemAlreadyExistsError` error if a non-expired entry already exists. +* `_updateRecord()` – Updates an item in the persistence store. +* `_deleteRecord()` – Removes an item from the persistence store. -```typescript title="Bring your own persistence store" hl_lines="9" ---8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts" -``` +Below an example of an alternative persistence layer backed by [Momento Cache](https://www.gomomento.com): + +=== "MomentoCachePersistenceLayer" + + ```typescript hl_lines="12 28 37 43 67 116" + --8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts" + ``` + +=== "index.ts" + + ```typescript hl_lines="10" + --8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts" + ``` + +=== "types.ts" + + ```typescript + --8<-- "docs/snippets/idempotency/types.ts" + ``` ???+ danger Pay attention to the documentation for each - you may need to perform additional checks inside these methods to ensure the idempotency guarantees remain intact. - For example, the `_put_Record()` method needs to throw an error if a non-expired record already exists in the data store with a matching key. + For example, the `_putRecord()` method needs to throw an error if a non-expired record already exists in the data store with a matching key. ## Extra resources From ec017f7c57449c89425eaf63f232373f70fc968e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 11:57:17 +0000 Subject: [PATCH 3/8] chore: fix non-null assertion --- .../idempotency/advancedBringYourOwnPersistenceLayer.ts | 7 ++++--- docs/utilities/idempotency.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts index 4f095111c4..96d0ccfebc 100644 --- a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts @@ -95,9 +95,10 @@ class MomentoCachePersistenceLayer extends BasePersistenceLayer { } try { - const ttl = - Math.floor(new Date(record.expiryTimestamp! * 1000).getTime() / 1000) - - Math.floor(new Date().getTime() / 1000); + const ttl = record.expiryTimestamp + ? Math.floor(new Date(record.expiryTimestamp * 1000).getTime() / 1000) - + Math.floor(new Date().getTime() / 1000) + : this.getExpiresAfterSeconds(); const response = await ( await this.#getClient() diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index a246203273..fb3642358b 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -701,7 +701,7 @@ Below an example of an alternative persistence layer backed by [Momento Cache](h === "MomentoCachePersistenceLayer" - ```typescript hl_lines="12 28 37 43 67 116" + ```typescript hl_lines="12 28 37 43 67 117" --8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts" ``` From c6f65a5ab489859d1a5f33d8926d21eb30b3468a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 12:17:18 +0000 Subject: [PATCH 4/8] chore: remove redundant try/catch --- docs/utilities/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index fb3642358b..dbb6a145f6 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -701,7 +701,7 @@ Below an example of an alternative persistence layer backed by [Momento Cache](h === "MomentoCachePersistenceLayer" - ```typescript hl_lines="12 28 37 43 67 117" + ```typescript hl_lines="12 28 37 43 67 113" --8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts" ``` From 7ab092017b6fda15deed84ebd327681183e31333 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 12:20:39 +0000 Subject: [PATCH 5/8] chore: remove redundant try/catch --- .../advancedBringYourOwnPersistenceLayer.ts | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts index 96d0ccfebc..263ab213da 100644 --- a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts @@ -94,23 +94,19 @@ class MomentoCachePersistenceLayer extends BasePersistenceLayer { } } - try { - const ttl = record.expiryTimestamp - ? Math.floor(new Date(record.expiryTimestamp * 1000).getTime() / 1000) - - Math.floor(new Date().getTime() / 1000) - : this.getExpiresAfterSeconds(); - - const response = await ( - await this.#getClient() - ).dictionarySetFields(this.#cacheName, record.idempotencyKey, item, { - ttl: CollectionTtl.of(ttl).withNoRefreshTtlOnUpdates(), - }); + const ttl = record.expiryTimestamp + ? Math.floor(new Date(record.expiryTimestamp * 1000).getTime() / 1000) - + Math.floor(new Date().getTime() / 1000) + : this.getExpiresAfterSeconds(); - if (response instanceof CacheDictionarySetFields.Error) { - throw new Error(`Unable to put item: ${response.errorCode()}`); - } - } catch (error) { - throw error; + const response = await ( + await this.#getClient() + ).dictionarySetFields(this.#cacheName, record.idempotencyKey, item, { + ttl: CollectionTtl.of(ttl).withNoRefreshTtlOnUpdates(), + }); + + if (response instanceof CacheDictionarySetFields.Error) { + throw new Error(`Unable to put item: ${response.errorCode()}`); } } From d98ca6243664f3852f379cd91c2690d4460c10d3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 13:49:47 +0000 Subject: [PATCH 6/8] chore: refactor generic persistence layer --- .../advancedBringYourOwnPersistenceLayer.ts | 177 ++++++------------ ...cedBringYourOwnPersistenceLayerProvider.ts | 44 +++++ ...vancedBringYourOwnPersistenceLayerUsage.ts | 6 +- .../idempotency/templates/tableCdk.ts | 2 +- docs/snippets/idempotency/types.ts | 6 +- docs/snippets/package.json | 1 - docs/utilities/idempotency.md | 6 +- 7 files changed, 107 insertions(+), 135 deletions(-) create mode 100644 docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerProvider.ts diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts index 263ab213da..d65d2f5b8c 100644 --- a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts @@ -3,10 +3,7 @@ import { IdempotencyItemNotFoundError, IdempotencyRecordStatus, } from '@aws-lambda-powertools/idempotency'; -import { - IdempotencyRecordOptions, - IdempotencyRecordStatusValue, -} from '@aws-lambda-powertools/idempotency/types'; +import { IdempotencyRecordOptions } from '@aws-lambda-powertools/idempotency/types'; import { IdempotencyRecord, BasePersistenceLayer, @@ -14,104 +11,84 @@ import { import { getSecret } from '@aws-lambda-powertools/parameters/secrets'; import { Transform } from '@aws-lambda-powertools/parameters'; import { - CacheClient, - CredentialProvider, - Configurations, - CacheGet, - CacheKeyExists, - CollectionTtl, - CacheDictionarySetFields, - CacheDictionaryGetFields, -} from '@gomomento/sdk'; -import type { MomentoApiSecret, Item } from './types'; - -class MomentoCachePersistenceLayer extends BasePersistenceLayer { - #cacheName: string; - #client?: CacheClient; - - public constructor(config: { cacheName: string }) { + ProviderClient, + ProviderItemAlreadyExists, +} from './advancedBringYourOwnPersistenceLayerProvider'; +import type { ApiSecret, ProviderItem } from './types'; + +class CustomPersistenceLayer extends BasePersistenceLayer { + #collectionName: string; + #client?: ProviderClient; + + public constructor(config: { collectionName: string }) { super(); - this.#cacheName = config.cacheName; + this.#collectionName = config.collectionName; } protected async _deleteRecord(record: IdempotencyRecord): Promise { await ( await this.#getClient() - ).delete(this.#cacheName, record.idempotencyKey); + ).delete(this.#collectionName, record.idempotencyKey); } protected async _getRecord( idempotencyKey: string ): Promise { - const response = await ( - await this.#getClient() - ).dictionaryFetch(this.#cacheName, idempotencyKey); + try { + const item = await ( + await this.#getClient() + ).get(this.#collectionName, idempotencyKey); - if ( - response instanceof CacheGet.Error || - response instanceof CacheGet.Miss - ) { + return new IdempotencyRecord({ + ...(item as unknown as IdempotencyRecordOptions), + }); + } catch (error) { throw new IdempotencyItemNotFoundError(); } - const { data, ...rest } = - response.value() as unknown as IdempotencyRecordOptions & { - data: string; - }; - - return new IdempotencyRecord({ - responseData: JSON.parse(data), - ...rest, - }); } protected async _putRecord(record: IdempotencyRecord): Promise { - const item: Partial = { + const item: Partial = { status: record.getStatus(), }; if (record.inProgressExpiryTimestamp !== undefined) { - item.in_progress_expiration = record.inProgressExpiryTimestamp.toString(); + item.in_progress_expiration = record.inProgressExpiryTimestamp; } if (this.isPayloadValidationEnabled() && record.payloadHash !== undefined) { item.validation = record.payloadHash; } - try { - const lock = await this.#lookupItem(record.idempotencyKey); - - if ( - lock.getStatus() !== IdempotencyRecordStatus.INPROGRESS && - (lock.inProgressExpiryTimestamp || 0) < Date.now() - ) { - throw new IdempotencyItemAlreadyExistsError( - `Failed to put record for already existing idempotency key: ${record.idempotencyKey}` - ); - } - } catch (error) { - if (error instanceof IdempotencyItemAlreadyExistsError) { - throw error; - } - } - const ttl = record.expiryTimestamp ? Math.floor(new Date(record.expiryTimestamp * 1000).getTime() / 1000) - Math.floor(new Date().getTime() / 1000) : this.getExpiresAfterSeconds(); - const response = await ( - await this.#getClient() - ).dictionarySetFields(this.#cacheName, record.idempotencyKey, item, { - ttl: CollectionTtl.of(ttl).withNoRefreshTtlOnUpdates(), - }); - - if (response instanceof CacheDictionarySetFields.Error) { - throw new Error(`Unable to put item: ${response.errorCode()}`); + let existingItem: ProviderItem | undefined; + try { + existingItem = await ( + await this.#getClient() + ).put(this.#collectionName, record.idempotencyKey, item, { + ttl, + }); + } catch (error) { + if (error instanceof ProviderItemAlreadyExists) { + if ( + existingItem && + existingItem.status !== IdempotencyRecordStatus.INPROGRESS && + (existingItem.in_progress_expiration || 0) < Date.now() + ) { + throw new IdempotencyItemAlreadyExistsError( + `Failed to put record for already existing idempotency key: ${record.idempotencyKey}` + ); + } + } } } protected async _updateRecord(record: IdempotencyRecord): Promise { - const value: Partial = { + const value: Partial = { data: JSON.stringify(record.responseData), status: record.getStatus(), }; @@ -120,22 +97,20 @@ class MomentoCachePersistenceLayer extends BasePersistenceLayer { value.validation = record.payloadHash; } - await this.#checkItemExists(record.idempotencyKey); - await ( await this.#getClient() - ).dictionarySetFields(this.#cacheName, record.idempotencyKey, value, { - ttl: CollectionTtl.refreshTtlIfProvided().withNoRefreshTtlOnUpdates(), - }); + ).update(this.#collectionName, record.idempotencyKey, value); } - async #getMomentoApiSecret(): Promise { - const secretName = process.env.MOMENTO_API_SECRET; + async #getClient(): Promise { + if (this.#client) return this.#client; + + const secretName = process.env.API_SECRET; if (!secretName) { - throw new Error('MOMENTO_API_SECRET environment variable is not set'); + throw new Error('API_SECRET environment variable is not set'); } - const apiSecret = await getSecret(secretName, { + const apiSecret = await getSecret(secretName, { transform: Transform.JSON, }); @@ -143,59 +118,13 @@ class MomentoCachePersistenceLayer extends BasePersistenceLayer { throw new Error(`Could not retrieve secret ${secretName}`); } - return apiSecret; - } - - async #getClient(): Promise { - if (this.#client) return this.#client; - - const apiSecret = await this.#getMomentoApiSecret(); - this.#client = await CacheClient.create({ - configuration: Configurations.InRegion.LowLatency.latest(), - credentialProvider: CredentialProvider.fromString({ - apiKey: apiSecret.apiKey, - }), + this.#client = new ProviderClient({ + apiKey: apiSecret.apiKey, defaultTtlSeconds: this.getExpiresAfterSeconds(), }); return this.#client; } - - async #checkItemExists(idempotencyKey: string): Promise { - const response = await ( - await this.#getClient() - ).keysExist(this.#cacheName, [idempotencyKey]); - - return response instanceof CacheKeyExists.Success; - } - - async #lookupItem(idempotencyKey: string): Promise { - const response = await ( - await this.#getClient() - ).dictionaryGetFields(this.#cacheName, idempotencyKey, [ - 'in_progress_expiration', - 'status', - ]); - - if (response instanceof CacheDictionaryGetFields.Miss) { - throw new IdempotencyItemNotFoundError(); - } else if (response instanceof CacheDictionaryGetFields.Error) { - throw new Error('Unable to get item'); - } else { - const { status, in_progress_expiration: inProgressExpiryTimestamp } = - response.value() || {}; - - if (status !== undefined || inProgressExpiryTimestamp !== undefined) { - throw new Error('Unable'); - } - - return new IdempotencyRecord({ - idempotencyKey, - status: status as IdempotencyRecordStatusValue, - inProgressExpiryTimestamp: parseFloat(inProgressExpiryTimestamp), - }); - } - } } -export { MomentoCachePersistenceLayer }; +export { CustomPersistenceLayer }; diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerProvider.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerProvider.ts new file mode 100644 index 0000000000..3dedecf864 --- /dev/null +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerProvider.ts @@ -0,0 +1,44 @@ +import type { ProviderItem } from './types'; + +/** + * This is a mock implementation of an SDK client for a generic key-value store. + */ +class ProviderClient { + public constructor(_config: { apiKey: string; defaultTtlSeconds: number }) { + // ... + } + + public async delete(_collectionName: string, _key: string): Promise { + // ... + } + + public async get( + _collectionName: string, + _key: string + ): Promise { + // ... + return {} as ProviderItem; + } + + public async put( + _collectionName: string, + _key: string, + _value: Partial, + _options: { ttl: number } + ): Promise { + // ... + return {} as ProviderItem; + } + + public async update( + _collectionName: string, + _key: string, + _value: Partial + ): Promise { + // ... + } +} + +class ProviderItemAlreadyExists extends Error {} + +export { ProviderClient, ProviderItemAlreadyExists }; diff --git a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts index d03abb56a1..2e8b5fa29e 100644 --- a/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts +++ b/docs/snippets/idempotency/advancedBringYourOwnPersistenceLayerUsage.ts @@ -1,14 +1,14 @@ import type { Context } from 'aws-lambda'; import { randomUUID } from 'node:crypto'; -import { MomentoCachePersistenceLayer } from './advancedBringYourOwnPersistenceLayer'; +import { CustomPersistenceLayer } from './advancedBringYourOwnPersistenceLayer'; import { IdempotencyConfig, makeIdempotent, } from '@aws-lambda-powertools/idempotency'; import type { Request, Response, SubscriptionResult } from './types'; -const persistenceStore = new MomentoCachePersistenceLayer({ - cacheName: 'powertools', +const persistenceStore = new CustomPersistenceLayer({ + collectionName: 'powertools', }); const config = new IdempotencyConfig({ expiresAfterSeconds: 60, diff --git a/docs/snippets/idempotency/templates/tableCdk.ts b/docs/snippets/idempotency/templates/tableCdk.ts index 284b6b095a..2e6e4bb21f 100644 --- a/docs/snippets/idempotency/templates/tableCdk.ts +++ b/docs/snippets/idempotency/templates/tableCdk.ts @@ -4,7 +4,7 @@ import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -export class IdempotencyMomentoStack extends Stack { +export class IdempotencyStack extends Stack { public constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); diff --git a/docs/snippets/idempotency/types.ts b/docs/snippets/idempotency/types.ts index 196f19da5b..e769dcf2dd 100644 --- a/docs/snippets/idempotency/types.ts +++ b/docs/snippets/idempotency/types.ts @@ -14,16 +14,16 @@ export type SubscriptionResult = { productId: string; }; -export type MomentoApiSecret = { +export type ApiSecret = { apiKey: string; refreshToken: string; validUntil: number; restEndpoint: string; }; -export type Item = { +export type ProviderItem = { validation?: string; - in_progress_expiration?: string; + in_progress_expiration?: number; status: IdempotencyRecordStatusValue; data: string; }; diff --git a/docs/snippets/package.json b/docs/snippets/package.json index 259d13246b..0258196766 100644 --- a/docs/snippets/package.json +++ b/docs/snippets/package.json @@ -33,7 +33,6 @@ "@aws-sdk/client-secrets-manager": "^3.360.0", "@aws-sdk/client-ssm": "^3.360.0", "@aws-sdk/util-dynamodb": "^3.360.0", - "@gomomento/sdk": "^1.39.3", "aws-sdk": "^2.1405.0", "aws-sdk-client-mock": "^2.2.0", "aws-sdk-client-mock-jest": "^2.2.0", diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index dbb6a145f6..0bb45ac7b9 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -697,11 +697,11 @@ You can create your own persistent store from scratch by inheriting the `BasePer * `_updateRecord()` – Updates an item in the persistence store. * `_deleteRecord()` – Removes an item from the persistence store. -Below an example of an alternative persistence layer backed by [Momento Cache](https://www.gomomento.com): +Below an example implementation of a custom persistence layer backed by a generic key-value store. -=== "MomentoCachePersistenceLayer" +=== "CustomPersistenceLayer" - ```typescript hl_lines="12 28 37 43 67 113" + ```typescript hl_lines="9 19 28 34 50 90" --8<-- "docs/snippets/idempotency/advancedBringYourOwnPersistenceLayer.ts" ``` From 93d0aa868b088f3e4f2a2c2cae78231b91ba23a4 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 13:50:47 +0000 Subject: [PATCH 7/8] chore: refactor generic persistence layer --- package-lock.json | 214 ---------------------------------------------- 1 file changed, 214 deletions(-) diff --git a/package-lock.json b/package-lock.json index 033f6293cc..d3cbbe8aa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,6 @@ "@aws-sdk/client-secrets-manager": "^3.360.0", "@aws-sdk/client-ssm": "^3.360.0", "@aws-sdk/util-dynamodb": "^3.360.0", - "@gomomento/sdk": "^1.39.3", "aws-sdk": "^2.1405.0", "aws-sdk-client-mock": "^2.2.0", "aws-sdk-client-mock-jest": "^2.2.0", @@ -2293,101 +2292,6 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, - "node_modules/@gomomento/generated-types": { - "version": "0.77.0", - "resolved": "https://registry.npmjs.org/@gomomento/generated-types/-/generated-types-0.77.0.tgz", - "integrity": "sha512-b6ZIoB2N3efhLOUoBxbt+qmE46lNCJErxF2A+g5RcDVgQrx143HcIMXpTghnEA4p6mmVOCQWYGsXVp0PHWz75Q==", - "dev": true, - "dependencies": { - "@grpc/grpc-js": "1.9.0", - "google-protobuf": "3.21.2" - } - }, - "node_modules/@gomomento/sdk": { - "version": "1.39.3", - "resolved": "https://registry.npmjs.org/@gomomento/sdk/-/sdk-1.39.3.tgz", - "integrity": "sha512-VBuBTTsXMlNRFi1cYF6ieP9N1RiUnsy2Bdu62LO57l8KwxnOgSTBcYalr7fwN/vijI2si3kNexUmYK8fXm/aHA==", - "dev": true, - "dependencies": { - "@gomomento/generated-types": "0.77.0", - "@gomomento/sdk-core": "1.39.3", - "@grpc/grpc-js": "1.9.0", - "@types/google-protobuf": "3.15.6", - "google-protobuf": "3.21.2", - "jwt-decode": "3.1.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@gomomento/sdk-core": { - "version": "1.39.3", - "resolved": "https://registry.npmjs.org/@gomomento/sdk-core/-/sdk-core-1.39.3.tgz", - "integrity": "sha512-FoR+oiES2nOQ9hyriyFT6VfFbqS8Bh7w8fH2rNOcfHvvcC5u1CBGFTpU4PSkuktdFSuQ7XVri/SLLvHN6BYudA==", - "dev": true, - "dependencies": { - "buffer": "^6.0.3", - "jwt-decode": "3.1.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@gomomento/sdk-core/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.0.tgz", - "integrity": "sha512-H8+iZh+kCE6VR/Krj6W28Y/ZlxoZ1fOzsNt77nrdE3knkbSelW1Uus192xOFCxHyeszLj8i4APQkSIXjAoOxXg==", - "dev": true, - "dependencies": { - "@grpc/proto-loader": "^0.7.0", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.9.tgz", - "integrity": "sha512-YJsOehVXzgurc+lLAxYnlSMc1p/Gu6VAvnfx0ATi2nzvr0YZcjhmZDeY8SeAKv1M7zE3aEJH0Xo9mK1iZ8GYoQ==", - "dev": true, - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.4", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", @@ -4457,70 +4361,6 @@ "node": ">=14" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dev": true - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "dev": true - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dev": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dev": true - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "dev": true - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dev": true - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dev": true - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "dev": true - }, "node_modules/@sigstore/bundle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.1.0.tgz", @@ -5299,12 +5139,6 @@ "@types/node": "*" } }, - "node_modules/@types/google-protobuf": { - "version": "3.15.6", - "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.6.tgz", - "integrity": "sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==", - "dev": true - }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", @@ -10163,12 +9997,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-protobuf": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", - "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==", - "dev": true - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -11970,12 +11798,6 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, - "node_modules/jwt-decode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", - "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -13059,12 +12881,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true - }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -13260,12 +13076,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "dev": true - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -15639,30 +15449,6 @@ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, - "node_modules/protobufjs": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", - "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/protocols": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", From d37246716a9d858bb580f3011756d1168fb208e2 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 14 Sep 2023 19:52:15 +0000 Subject: [PATCH 8/8] docs: added entry & handler to CDK sample --- docs/snippets/idempotency/templates/tableCdk.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/snippets/idempotency/templates/tableCdk.ts b/docs/snippets/idempotency/templates/tableCdk.ts index 2e6e4bb21f..8a07d5dc3c 100644 --- a/docs/snippets/idempotency/templates/tableCdk.ts +++ b/docs/snippets/idempotency/templates/tableCdk.ts @@ -19,6 +19,8 @@ export class IdempotencyStack extends Stack { const fnHandler = new NodejsFunction(this, 'helloWorldFunction', { runtime: Runtime.NODEJS_18_X, + handler: 'handler', + entry: 'src/index.ts', environment: { IDEMPOTENCY_TABLE_NAME: table.tableName, },