From 38a6ba54c86a27aec96164fd47e9319682297d15 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 23 Dec 2025 12:34:49 -0500 Subject: [PATCH 1/2] Add SQS client wrapper and tests --- package-lock.json | 84 ++++++++++ package.json | 1 + src/clients/sqs-client.test.ts | 293 +++++++++++++++++++++++++++++++++ src/clients/sqs-client.ts | 109 ++++++++++++ src/index.ts | 7 + 5 files changed, 494 insertions(+) create mode 100644 src/clients/sqs-client.test.ts create mode 100644 src/clients/sqs-client.ts diff --git a/package-lock.json b/package-lock.json index dd22833..c0c2f4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/client-dynamodb": "3.957.0", "@aws-sdk/client-lambda": "3.957.0", "@aws-sdk/client-sns": "3.957.0", + "@aws-sdk/client-sqs": "3.957.0", "@aws-sdk/lib-dynamodb": "3.957.0", "pino": "10.1.0", "pino-lambda": "4.4.1", @@ -339,6 +340,58 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.957.0.tgz", + "integrity": "sha512-V5hoymq0nnS7eAoMedj5p4YSyAbxkS4bzC8xl1tKEOmKIXDA0+uO1Ink5QIgUVbB7DEsLbdrgHE9G77DqwvcQA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.957.0", + "@aws-sdk/credential-provider-node": "3.957.0", + "@aws-sdk/middleware-host-header": "3.957.0", + "@aws-sdk/middleware-logger": "3.957.0", + "@aws-sdk/middleware-recursion-detection": "3.957.0", + "@aws-sdk/middleware-sdk-sqs": "3.957.0", + "@aws-sdk/middleware-user-agent": "3.957.0", + "@aws-sdk/region-config-resolver": "3.957.0", + "@aws-sdk/types": "3.957.0", + "@aws-sdk/util-endpoints": "3.957.0", + "@aws-sdk/util-user-agent-browser": "3.957.0", + "@aws-sdk/util-user-agent-node": "3.957.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/md5-js": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { "version": "3.957.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.957.0.tgz", @@ -685,6 +738,23 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.957.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.957.0.tgz", + "integrity": "sha512-3A1V2oSV/NzWukwDBwnf/ng+n+8zU32jRml0lbYiP9PzBgc6D6Y4Z/RCbPp7g+PO8XrCRrZg6QKspO3cLpGnOw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.957.0", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.957.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz", @@ -3056,6 +3126,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/md5-js": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.7.tgz", + "integrity": "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", diff --git a/package.json b/package.json index ee0b9f3..6cb0318 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@aws-sdk/client-dynamodb": "3.957.0", "@aws-sdk/client-lambda": "3.957.0", "@aws-sdk/client-sns": "3.957.0", + "@aws-sdk/client-sqs": "3.957.0", "@aws-sdk/lib-dynamodb": "3.957.0", "pino": "10.1.0", "pino-lambda": "4.4.1", diff --git a/src/clients/sqs-client.test.ts b/src/clients/sqs-client.test.ts new file mode 100644 index 0000000..c698115 --- /dev/null +++ b/src/clients/sqs-client.test.ts @@ -0,0 +1,293 @@ +import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; +import { mockClient } from 'aws-sdk-client-mock'; + +import { getSQSClient, initializeSQSClient, SQSMessageAttributes, sendToQueue, resetSQSClient } from './sqs-client'; + +// Create a mock for SQSClient +const sqsMock = mockClient(SQSClient); + +describe('sqs-client', () => { + beforeEach(() => { + // Reset the client and mock before each test + resetSQSClient(); + sqsMock.reset(); + }); + + afterEach(() => { + // Clean up after each test + resetSQSClient(); + sqsMock.reset(); + }); + + describe('initializeSQSClient', () => { + it('should create client with default config', () => { + // Arrange + // Act + const result = initializeSQSClient(); + + // Assert + expect(result).toBeInstanceOf(SQSClient); + }); + + it('should create client with custom config', () => { + // Arrange + const config = { region: 'us-west-2' }; + + // Act + const result = initializeSQSClient(config); + + // Assert + expect(result).toBeInstanceOf(SQSClient); + }); + + it('should replace existing client when called multiple times', () => { + // Arrange + const result1 = initializeSQSClient({ region: 'us-east-1' }); + + // Act + const result2 = initializeSQSClient({ region: 'us-west-2' }); + + // Assert + expect(result1).toBeInstanceOf(SQSClient); + expect(result2).toBeInstanceOf(SQSClient); + expect(result2).not.toBe(result1); + }); + }); + + describe('getSQSClient', () => { + it('should return the initialized client', () => { + // Arrange + const result = initializeSQSClient(); + + // Act + const client = getSQSClient(); + + // Assert + expect(client).toBe(result); + }); + + it('should create client with default config if not initialized', () => { + // Arrange + // Act + const client = getSQSClient(); + + // Assert + expect(client).toBeInstanceOf(SQSClient); + }); + + it('should return same instance on multiple calls', () => { + // Arrange + initializeSQSClient(); + + // Act + const client1 = getSQSClient(); + const client2 = getSQSClient(); + + // Assert + expect(client1).toBe(client2); + }); + + it('should return same instance when auto-initialized', () => { + // Arrange + // Act + const client1 = getSQSClient(); + const client2 = getSQSClient(); + + // Assert + expect(client1).toBe(client2); + }); + }); + + describe('resetSQSClient', () => { + it('should reset the client', () => { + // Arrange + const client1 = initializeSQSClient(); + + // Act + resetSQSClient(); + const client2 = getSQSClient(); + + // Assert + expect(client2).not.toBe(client1); + }); + + it('should allow reinitialization after reset', () => { + // Arrange + initializeSQSClient({ region: 'us-east-1' }); + resetSQSClient(); + + // Act + const result = initializeSQSClient({ region: 'us-west-2' }); + + // Assert + expect(result).toBeInstanceOf(SQSClient); + expect(getSQSClient()).toBe(result); + }); + }); + + describe('sendToQueue', () => { + const queueUrl = 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue'; + const message = { orderId: '12345', status: 'completed' }; + const messageId = 'test-message-id-123'; + + beforeEach(() => { + // Mock successful send + sqsMock.on(SendMessageCommand).resolves({ + MessageId: messageId, + }); + }); + + it('should send message to queue', async () => { + // Arrange + initializeSQSClient(); + + // Act + const result = await sendToQueue(queueUrl, message); + + // Assert + expect(result).toBe(messageId); + expect(sqsMock.calls()).toHaveLength(1); + }); + + it('should send message with attributes', async () => { + // Arrange + initializeSQSClient(); + const attributes: SQSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, + attempts: { + DataType: 'Number', + StringValue: '1', + }, + }; + + // Act + const result = await sendToQueue(queueUrl, message, attributes); + + // Assert + expect(result).toBe(messageId); + expect(sqsMock.calls()).toHaveLength(1); + const call = sqsMock.call(0); + expect(call.args[0].input).toEqual({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(message), + MessageAttributes: attributes, + }); + }); + + it('should send message with binary data attributes', async () => { + // Arrange + initializeSQSClient(); + const binaryData = new Uint8Array([1, 2, 3, 4, 5]); + const attributes: SQSMessageAttributes = { + imageData: { + DataType: 'Binary', + BinaryValue: binaryData, + }, + priority: { + DataType: 'String', + StringValue: 'normal', + }, + }; + + // Act + const result = await sendToQueue(queueUrl, message, attributes); + + // Assert + expect(result).toBe(messageId); + expect(sqsMock.calls()).toHaveLength(1); + const call = sqsMock.call(0); + expect(call.args[0].input).toEqual({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(message), + MessageAttributes: attributes, + }); + }); + + it('should initialize client if not already initialized', async () => { + // Arrange + // Do not initialize the client + + // Act + const result = await sendToQueue(queueUrl, message); + + // Assert + expect(result).toBe(messageId); + expect(sqsMock.calls()).toHaveLength(1); + }); + + it('should use singleton client instance', async () => { + // Arrange + initializeSQSClient(); + + // Act + await sendToQueue(queueUrl, message); + await sendToQueue(queueUrl, { orderId: '67890' }); + + // Assert + expect(sqsMock.calls()).toHaveLength(2); + }); + + it('should convert message to JSON string', async () => { + // Arrange + initializeSQSClient(); + const complexMessage = { + orderId: '12345', + items: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ], + metadata: { + source: 'web', + timestamp: '2025-12-23T12:00:00Z', + }, + }; + + // Act + await sendToQueue(queueUrl, complexMessage); + + // Assert + const call = sqsMock.call(0); + const input = call.args[0].input as { MessageBody?: string }; + expect(input.MessageBody).toBe(JSON.stringify(complexMessage)); + }); + + it('should return empty string if MessageId is undefined', async () => { + // Arrange + initializeSQSClient(); + sqsMock.reset(); + sqsMock.on(SendMessageCommand).resolves({ + MessageId: undefined, + }); + + // Act + const result = await sendToQueue(queueUrl, message); + + // Assert + expect(result).toBe(''); + }); + + it('should throw error if send fails', async () => { + // Arrange + initializeSQSClient(); + const error = new Error('Failed to send message'); + sqsMock.reset(); + sqsMock.on(SendMessageCommand).rejects(error); + + // Act & Assert + await expect(sendToQueue(queueUrl, message)).rejects.toThrow('Failed to send message'); + }); + + it('should throw error if SQS service is unavailable', async () => { + // Arrange + initializeSQSClient(); + sqsMock.reset(); + sqsMock.on(SendMessageCommand).rejects(new Error('Service Unavailable')); + + // Act & Assert + await expect(sendToQueue(queueUrl, message)).rejects.toThrow('Service Unavailable'); + }); + }); +}); diff --git a/src/clients/sqs-client.ts b/src/clients/sqs-client.ts new file mode 100644 index 0000000..15f21a8 --- /dev/null +++ b/src/clients/sqs-client.ts @@ -0,0 +1,109 @@ +import { SendMessageCommand, SQSClient, SQSClientConfig } from '@aws-sdk/client-sqs'; + +/** + * Interface for SQS message attributes. + * Supports the AWS SQS data types: String, Number, and Binary. + */ +export interface SQSMessageAttributes { + [key: string]: { + DataType: 'String' | 'Number' | 'Binary'; + StringValue?: string; + BinaryValue?: Uint8Array; + }; +} + +/** + * Singleton instance of SQS client + */ +let sqsClient: SQSClient | null = null; + +/** + * Initializes the SQS client with the provided configuration. + * If the client is already initialized, this will replace it with a new instance. + * + * @param config - SQS client configuration + * @returns The SQS client instance + * + * @example + * ```typescript + * // Initialize with default configuration + * initializeSQSClient(); + * + * // Initialize with custom configuration + * initializeSQSClient({ region: 'us-east-1' }); + * ``` + */ +export const initializeSQSClient = (config?: SQSClientConfig): SQSClient => { + sqsClient = new SQSClient(config || {}); + return sqsClient; +}; + +/** + * Returns the singleton SQS client instance. + * If the client has not been initialized, creates one with default configuration. + * + * @returns The SQS client instance + * + * @example + * ```typescript + * const client = getSQSClient(); + * ``` + */ +export const getSQSClient = (): SQSClient => { + if (!sqsClient) { + sqsClient = new SQSClient({}); + } + return sqsClient; +}; + +/** + * Resets the SQS client instance. + * Useful for testing or when you need to reinitialize the client with a different configuration. + */ +export const resetSQSClient = (): void => { + sqsClient = null; +}; + +/** + * Sends a message to an SQS queue. + * + * @param queueUrl - The URL of the SQS queue to send to + * @param message - The message content (will be converted to JSON string) + * @param attributes - Optional message attributes for filtering + * @returns Promise that resolves to the message ID + * @throws Error if the SQS send operation fails + * + * @example + * ```typescript + * const messageId = await sendToQueue( + * 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue', + * { orderId: '12345', status: 'completed' }, + * { + * priority: { + * DataType: 'String', + * StringValue: 'high' + * }, + * attempts: { + * DataType: 'Number', + * StringValue: '1' + * } + * } + * ); + * ``` + */ +export const sendToQueue = async ( + queueUrl: string, + message: Record, + attributes?: SQSMessageAttributes, +): Promise => { + const client = getSQSClient(); + + const command = new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(message), + MessageAttributes: attributes, + }); + + const response = await client.send(command); + return response.MessageId ?? ''; +}; diff --git a/src/index.ts b/src/index.ts index df1e9ef..0e87a24 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,4 +30,11 @@ export { invokeLambdaSync, resetLambdaClient, } from './clients/lambda-client'; +export { + getSQSClient, + initializeSQSClient, + SQSMessageAttributes, + sendToQueue, + resetSQSClient, +} from './clients/sqs-client'; export { createConfigManager, ConfigManager } from './validation/config'; From 6fc56e583f6a8ae092b0d83a21f398542c0df6a3 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 23 Dec 2025 13:22:37 -0500 Subject: [PATCH 2/2] Add SQS client utilities and update documentation --- README.md | 64 ++++++--- docs/README.md | 5 +- docs/SQS_CLIENT.md | 352 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 20 deletions(-) create mode 100644 docs/SQS_CLIENT.md diff --git a/README.md b/README.md index 3acc784..e89441f 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ export const handler = async (event: APIGatewayProxyEvent) => { - **📝 Structured Logging** – Pino logger pre-configured for Lambda with automatic AWS request context enrichment - **📤 API Response Helpers** – Standard response formatting for API Gateway with proper HTTP status codes - **⚙️ Configuration Validation** – Environment variable validation with Zod schema support -- **🔌 AWS SDK Clients** – Pre-configured AWS SDK v3 clients including DynamoDB, SNS, and Lambda with singleton patterns +- **🔌 AWS SDK Clients** – Pre-configured AWS SDK v3 clients including DynamoDB, Lambda, SNS, and SQS with singleton patterns - **🔒 Full TypeScript Support** – Complete type definitions and IDE autocomplete - **⚡ Lambda Optimized** – Designed for performance in serverless environments @@ -108,8 +108,9 @@ Comprehensive guides and examples are available in the `docs` directory: | **[Logging Guide](./docs/LOGGING.md)** | Configure and use structured logging with automatic AWS Lambda context | | **[API Gateway Responses](./docs/API_GATEWAY_RESPONSES.md)** | Format responses for API Gateway with standard HTTP patterns | | **[DynamoDB Client](./docs/DYNAMODB_CLIENT.md)** | Use pre-configured DynamoDB clients with singleton pattern | -| **[SNS Client](./docs/SNS_CLIENT.md)** | Publish messages to SNS topics with message attributes | | **[Lambda Client](./docs/LAMBDA_CLIENT.md)** | Invoke other Lambda functions synchronously or asynchronously | +| **[SNS Client](./docs/SNS_CLIENT.md)** | Publish messages to SNS topics with message attributes | +| **[SQS Client](./docs/SQS_CLIENT.md)** | Send messages to SQS queues with message attributes | ## Usage @@ -197,6 +198,32 @@ export const handler = async (event: any, context: any) => { **→ See [DynamoDB Client Guide](./docs/DYNAMODB_CLIENT.md) for detailed configuration and examples** +#### Lambda Client + +Invoke other Lambda functions synchronously or asynchronously: + +```typescript +import { invokeLambdaSync, invokeLambdaAsync } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + // Synchronous invocation - wait for response + const response = await invokeLambdaSync('my-function-name', { + key: 'value', + data: { nested: true }, + }); + + // Asynchronous invocation - fire and forget + await invokeLambdaAsync('my-async-function', { + eventType: 'process', + data: [1, 2, 3], + }); + + return { statusCode: 200, body: JSON.stringify(response) }; +}; +``` + +**→ See [Lambda Client Guide](./docs/LAMBDA_CLIENT.md) for detailed configuration and examples** + #### SNS Client Publish messages to SNS topics with optional message attributes: @@ -224,35 +251,36 @@ export const handler = async (event: any) => { **→ See [SNS Client Guide](./docs/SNS_CLIENT.md) for detailed configuration and examples** -#### Lambda Client +#### SQS Client -Invoke other Lambda functions synchronously or asynchronously: +Send messages to SQS queues with optional message attributes: ```typescript -import { invokeLambdaSync, invokeLambdaAsync } from '@leanstacks/lambda-utils'; +import { sendToQueue, SQSMessageAttributes } from '@leanstacks/lambda-utils'; export const handler = async (event: any) => { - // Synchronous invocation - wait for response - const response = await invokeLambdaSync('my-function-name', { - key: 'value', - data: { nested: true }, - }); + const attributes: SQSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, + }; - // Asynchronous invocation - fire and forget - await invokeLambdaAsync('my-async-function', { - eventType: 'process', - data: [1, 2, 3], - }); + const messageId = await sendToQueue( + 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue', + { orderId: '12345', status: 'completed' }, + attributes, + ); - return { statusCode: 200, body: JSON.stringify(response) }; + return { statusCode: 200, body: JSON.stringify({ messageId }) }; }; ``` -**→ See [Lambda Client Guide](./docs/LAMBDA_CLIENT.md) for detailed configuration and examples** +**→ See [SQS Client Guide](./docs/SQS_CLIENT.md) for detailed configuration and examples** ## Examples -Example Lambda functions using Lambda Utilities are available in the repository: +To see an example Lambda microservice using the Lambda Utilities, see the LeanStacks [`lambda-starter`](https://github.com/leanstacks/lambda-starter) :octocat: GitHub repository. - API Gateway with logging and response formatting - Configuration validation and DynamoDB integration diff --git a/docs/README.md b/docs/README.md index 81f229b..56ecb1e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,15 +12,16 @@ Lambda Utilities is a collection of pre-configured tools and helpers designed to - **[Logging Guide](./LOGGING.md)** – Implement structured logging in your Lambda functions with Pino and automatic AWS context enrichment - **[API Gateway Responses](./API_GATEWAY_RESPONSES.md)** – Format Lambda responses for API Gateway with standard HTTP status codes and headers - **[DynamoDB Client](./DYNAMODB_CLIENT.md)** – Reusable singleton DynamoDB client instances with custom configuration -- **[SNS Client](./SNS_CLIENT.md)** – Reusable singleton SNS client for publishing messages to topics with message attributes - **[Lambda Client](./LAMBDA_CLIENT.md)** – Reusable singleton Lambda client for invoking other Lambda functions +- **[SNS Client](./SNS_CLIENT.md)** – Reusable singleton SNS client for publishing messages to topics with message attributes +- **[SQS Client](./SQS_CLIENT.md)** – Reusable singleton SQS client for sending messages to queues with message attributes ## Features - 📝 **Structured Logging** – Pino logger pre-configured for Lambda with automatic request context - 📤 **API Response Helpers** – Standard response formatting for API Gateway integration - ⚙️ **Configuration Validation** – Environment variable validation with Zod schema support -- 🔌 **AWS Clients** – Pre-configured AWS SDK v3 clients for DynamoDB, SNS, and Lambda +- 🔌 **AWS Clients** – Pre-configured AWS SDK v3 clients for DynamoDB, SNS, SQS, and Lambda - 🔒 **Type Safe** – Full TypeScript support with comprehensive type definitions ## Support diff --git a/docs/SQS_CLIENT.md b/docs/SQS_CLIENT.md new file mode 100644 index 0000000..d3cbdfd --- /dev/null +++ b/docs/SQS_CLIENT.md @@ -0,0 +1,352 @@ +# SQS Client Utilities + +The SQS client utilities provide a reusable singleton instance of `SQSClient` for use in AWS Lambda functions. These utilities enable you to configure the client once and reuse it across invocations, following AWS best practices for Lambda performance optimization. + +## Overview + +The utility exports the following functions: + +- `initializeSQSClient()` - Initialize the SQS client with optional configuration +- `getSQSClient()` - Get the singleton SQS client instance +- `sendToQueue()` - Send a message to an SQS queue +- `resetSQSClient()` - Reset the client instance (useful for testing) + +## Usage + +### Basic Usage + +```typescript +import { sendToQueue } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + // Send a message to an SQS queue + const messageId = await sendToQueue('https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue', { + orderId: '12345', + status: 'completed', + }); + + return { + statusCode: 200, + body: JSON.stringify({ messageId }), + }; +}; +``` + +### Sending with Message Attributes + +Message attributes enable SQS subscribers to filter messages based on metadata: + +```typescript +import { sendToQueue, SQSMessageAttributes } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + const attributes: SQSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, + attempts: { + DataType: 'Number', + StringValue: '1', + }, + }; + + const messageId = await sendToQueue( + 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue', + { orderId: '12345', status: 'completed' }, + attributes, + ); + + return { statusCode: 200, body: JSON.stringify({ messageId }) }; +}; +``` + +### Using Binary Data in Message Attributes + +SQS supports binary data in message attributes: + +```typescript +import { sendToQueue, SQSMessageAttributes } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + const binaryData = new Uint8Array([1, 2, 3, 4, 5]); + const attributes: SQSMessageAttributes = { + imageData: { + DataType: 'Binary', + BinaryValue: binaryData, + }, + }; + + const messageId = await sendToQueue( + 'https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue', + { imageId: '12345' }, + attributes, + ); + + return { statusCode: 200, body: JSON.stringify({ messageId }) }; +}; +``` + +### Using the SQS Client Directly + +For advanced use cases, you can access the underlying SQS client: + +```typescript +import { getSQSClient } from '@leanstacks/lambda-utils'; +import { ListQueuesCommand } from '@aws-sdk/client-sqs'; + +export const handler = async (event: any) => { + const client = getSQSClient(); + + const response = await client.send(new ListQueuesCommand({})); + + return { + statusCode: 200, + body: JSON.stringify(response.QueueUrls), + }; +}; +``` + +### Advanced Configuration + +#### Custom SQS Client Configuration + +```typescript +import { initializeSQSClient } from '@leanstacks/lambda-utils'; + +// Initialize client with custom configuration (typically done once outside the handler) +initializeSQSClient({ + region: 'us-west-2', + endpoint: 'http://localhost:9324', // For local development with LocalStack +}); + +export const handler = async (event: any) => { + // Client is now initialized and ready to use + // Use sendToQueue or getSQSClient as needed +}; +``` + +### Lambda Handler Pattern + +```typescript +import { initializeSQSClient, sendToQueue } from '@leanstacks/lambda-utils'; + +// Initialize client outside the handler (runs once per cold start) +initializeSQSClient({ region: process.env.AWS_REGION }); + +export const handler = async (event: any) => { + const messageId = await sendToQueue(process.env.QUEUE_URL!, { + timestamp: new Date().toISOString(), + data: event, + }); + + return { + statusCode: 200, + body: JSON.stringify({ messageId }), + }; +}; +``` + +## API Reference + +### `initializeSQSClient(config?): SQSClient` + +Initializes the SQS client with the provided configuration. + +**Parameters:** + +- `config` (optional) - SQS client configuration object + +**Returns:** + +- The `SQSClient` instance + +**Notes:** + +- If called multiple times, it will replace the existing client instance +- If no config is provided, uses default AWS SDK configuration + +### `getSQSClient(): SQSClient` + +Returns the singleton SQS client instance. + +**Returns:** + +- The `SQSClient` instance + +**Notes:** + +- If the client has not been initialized, creates one with default configuration +- Automatically initializes the client on first use if not already initialized + +### `sendToQueue(queueUrl, message, attributes?): Promise` + +Sends a message to an SQS queue. + +**Parameters:** + +- `queueUrl` (string) - The URL of the SQS queue to send to +- `message` (Record) - The message content (will be converted to JSON string) +- `attributes` (optional) - Message attributes for filtering + +**Returns:** + +- Promise that resolves to the message ID (string) + +**Throws:** + +- Error if the SQS send operation fails + +**Notes:** + +- Automatically initializes the SQS client if not already initialized +- Message is automatically serialized to JSON +- Returns empty string if MessageId is not provided in the response + +### `resetSQSClient(): void` + +Resets the SQS client instance to null. + +**Notes:** + +- Primarily useful for testing scenarios where you need to reinitialize the client with different configurations +- After calling this, the client will be automatically re-initialized on next use + +### `SQSMessageAttributes` + +Type definition for SQS message attributes. + +**Interface:** + +```typescript +interface SQSMessageAttributes { + [key: string]: { + DataType: 'String' | 'Number' | 'Binary'; + StringValue?: string; + BinaryValue?: Uint8Array; + }; +} +``` + +**Supported Data Types:** + +- `String` - UTF-8 encoded string values +- `Number` - Numeric values (stored as strings) +- `Binary` - Binary data + +**Note:** Unlike SNS, SQS does not support array data types (`String.Array`). + +## Best Practices + +1. **Initialize Outside the Handler**: Always initialize the client outside your Lambda handler function to reuse the instance across invocations. + +2. **Use Environment Variables**: Configure the client using environment variables for flexibility across environments. + +3. **Error Handling**: Always wrap send operations in try-catch blocks to handle errors gracefully. + +4. **Message Attributes**: Use message attributes for message filtering and routing rather than including filter criteria in the message body. + +5. **Testing**: Use `resetSQSClient()` in test setup/teardown to ensure clean test isolation. + +## Message Attribute Examples + +### String Attribute + +```typescript +const attributes: SQSMessageAttributes = { + priority: { + DataType: 'String', + StringValue: 'high', + }, +}; +``` + +### Number Attribute + +```typescript +const attributes: SQSMessageAttributes = { + temperature: { + DataType: 'Number', + StringValue: '72.5', + }, +}; +``` + +### Binary Attribute + +```typescript +const attributes: SQSMessageAttributes = { + thumbnail: { + DataType: 'Binary', + BinaryValue: new Uint8Array([1, 2, 3, 4]), + }, +}; +``` + +## Testing Example + +```typescript +import { initializeSQSClient, sendToQueue, resetSQSClient } from '@leanstacks/lambda-utils'; + +describe('MyLambdaHandler', () => { + beforeEach(() => { + resetSQSClient(); + initializeSQSClient({ + region: 'us-east-1', + endpoint: 'http://localhost:9324', // LocalStack + }); + }); + + afterEach(() => { + resetSQSClient(); + }); + + it('should send message to SQS queue', async () => { + const messageId = await sendToQueue('https://sqs.us-east-1.amazonaws.com/123456789012/TestQueue', { + test: 'data', + }); + + expect(messageId).toBeTruthy(); + }); +}); +``` + +## Error Handling Example + +```typescript +import { sendToQueue } from '@leanstacks/lambda-utils'; + +export const handler = async (event: any) => { + try { + const messageId = await sendToQueue( + process.env.QUEUE_URL!, + { orderId: event.orderId, status: 'processing' }, + { + priority: { + DataType: 'String', + StringValue: 'high', + }, + }, + ); + + return { + statusCode: 200, + body: JSON.stringify({ success: true, messageId }), + }; + } catch (error) { + console.error('Failed to send message to SQS', error); + + return { + statusCode: 500, + body: JSON.stringify({ success: false, error: 'Failed to send message' }), + }; + } +}; +``` + +## Related Resources + +- **[AWS SDK for JavaScript v3 - SQS Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-sqs/)** +- **[Amazon SQS Developer Guide](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/)** +- **[AWS Lambda Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html)** +- **[Back to the project documentation](README.md)**