@leanstacks/lambda-utils 0.2.0-alpha.3 → 0.3.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/clients/dynamodb-client.d.ts +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/docs/API_GATEWAY_RESPONSES.md +451 -0
- package/docs/DYNAMODB_CLIENT.md +214 -0
- package/docs/LOGGING.md +5 -4
- package/docs/README.md +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -49,16 +49,16 @@ export const handler = async (event: any, context: any) => {
|
|
|
49
49
|
### API Response Example
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
|
-
import {
|
|
52
|
+
import { ok, badRequest } from '@leanstacks/lambda-utils';
|
|
53
53
|
|
|
54
|
-
export const handler = async (event:
|
|
54
|
+
export const handler = async (event: APIGatewayProxyEvent) => {
|
|
55
55
|
if (!event.body) {
|
|
56
|
-
return badRequest(
|
|
56
|
+
return badRequest('Body is required');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// Process request
|
|
60
60
|
|
|
61
|
-
return
|
|
61
|
+
return ok({ message: 'Request processed successfully' });
|
|
62
62
|
};
|
|
63
63
|
```
|
|
64
64
|
|
|
@@ -108,10 +108,10 @@ logger.error({ message: 'Operation failed', error: err.message });
|
|
|
108
108
|
Generate properly formatted responses for API Gateway:
|
|
109
109
|
|
|
110
110
|
```typescript
|
|
111
|
-
import {
|
|
111
|
+
import { ok, created, badRequest } from '@leanstacks/lambda-utils';
|
|
112
112
|
|
|
113
|
-
export const handler = async (event:
|
|
114
|
-
return
|
|
113
|
+
export const handler = async (event: APIGatewayProxyEvent) => {
|
|
114
|
+
return ok({
|
|
115
115
|
data: { id: '123', name: 'Example' },
|
|
116
116
|
});
|
|
117
117
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { DynamoDBClient, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { DynamoDBDocumentClient, marshallOptions as MarshallOptions, unmarshallOptions as UnmarshallOptions } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
/**
|
|
4
|
+
* Initializes both the DynamoDB client and DynamoDB Document client with the provided configuration.
|
|
5
|
+
* If the clients are already initialized, this will replace them with new instances.
|
|
6
|
+
*
|
|
7
|
+
* @param config - DynamoDB client configuration
|
|
8
|
+
* @param marshallOptions - Options for marshalling JavaScript objects to DynamoDB AttributeValues
|
|
9
|
+
* @param unmarshallOptions - Options for unmarshalling DynamoDB AttributeValues to JavaScript objects
|
|
10
|
+
* @returns An object containing both the base client and document client
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Initialize with default configuration
|
|
15
|
+
* initializeDynamoDBClients();
|
|
16
|
+
*
|
|
17
|
+
* // Initialize with custom configuration
|
|
18
|
+
* initializeDynamoDBClients(
|
|
19
|
+
* { region: 'us-east-1' },
|
|
20
|
+
* { removeUndefinedValues: true },
|
|
21
|
+
* { wrapNumbers: false }
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const initializeDynamoDBClients: (config?: DynamoDBClientConfig, marshallOptions?: MarshallOptions, unmarshallOptions?: UnmarshallOptions) => {
|
|
26
|
+
client: DynamoDBClient;
|
|
27
|
+
documentClient: DynamoDBDocumentClient;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Returns the singleton DynamoDB client instance.
|
|
31
|
+
* Throws an error if the client has not been initialized.
|
|
32
|
+
*
|
|
33
|
+
* @returns The DynamoDB client instance
|
|
34
|
+
* @throws Error if the client has not been initialized
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const client = getDynamoDBClient();
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare const getDynamoDBClient: () => DynamoDBClient;
|
|
42
|
+
/**
|
|
43
|
+
* Returns the singleton DynamoDB Document client instance.
|
|
44
|
+
* Throws an error if the client has not been initialized.
|
|
45
|
+
*
|
|
46
|
+
* @returns The DynamoDB Document client instance
|
|
47
|
+
* @throws Error if the client has not been initialized
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const docClient = getDynamoDBDocumentClient();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare const getDynamoDBDocumentClient: () => DynamoDBDocumentClient;
|
|
55
|
+
/**
|
|
56
|
+
* Resets both DynamoDB client instances.
|
|
57
|
+
* Useful for testing or when you need to reinitialize the clients with different configurations.
|
|
58
|
+
*/
|
|
59
|
+
export declare const resetDynamoDBClients: () => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { Logger, LoggerConfig, withRequestTracking } from './logging/logger';
|
|
2
2
|
export { createResponse, ok, created, noContent, badRequest, notFound, internalServerError, httpHeaders, Headers, } from './utils/apigateway-response';
|
|
3
|
+
export { getDynamoDBClient, getDynamoDBDocumentClient, initializeDynamoDBClients, resetDynamoDBClients, } from './clients/dynamodb-client';
|
package/dist/index.esm.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import n from"pino";import{lambdaRequestTracker as e,StructuredLogFormatter as t,CloudwatchLogFormatter as o,pinoLambdaDestination as i}from"pino-lambda";import{DynamoDBClient as l}from"@aws-sdk/client-dynamodb";import{DynamoDBDocumentClient as r}from"@aws-sdk/lib-dynamodb";const s=e();class a{constructor(e){this._loggerConfig={enabled:!0,level:"info",format:"json"},this._instance=null,this._createLogger=()=>{const e="json"===this._loggerConfig.format?new t:new o,l=i({formatter:e});return n({enabled:this._loggerConfig.enabled,level:this._loggerConfig.level},l)},e&&(this._loggerConfig={enabled:e.enabled??!0,level:e.level??"info",format:e.format??"json"})}get instance(){return null===this._instance&&(this._instance=this._createLogger()),this._instance}}const m={contentType:n=>({"Content-Type":n}),json:{"Content-Type":"application/json"},cors:(n="*")=>({"Access-Control-Allow-Origin":n})},c=(n,e,t={})=>({statusCode:n,headers:{...t},body:JSON.stringify(e)}),g=(n,e={})=>c(200,n,e),f=(n,e={})=>c(201,n,e),d=(n={})=>c(204,{},n),u=(n="Bad Request",e={})=>c(400,{message:n},e),h=(n="Not Found",e={})=>c(404,{message:n},e),p=(n="Internal Server Error",e={})=>c(500,{message:n},e);let C=null,y=null;const _=(n,e,t)=>{C=new l(n||{});const o={marshallOptions:e||{},unmarshallOptions:t||{}};return y=r.from(C,o),{client:C,documentClient:y}},b=()=>{if(!C)throw new Error("DynamoDB client not initialized. Call initializeDynamoDBClients() first.");return C},w=()=>{if(!y)throw new Error("DynamoDB Document client not initialized. Call initializeDynamoDBClients() first.");return y},D=()=>{C=null,y=null};export{a as Logger,u as badRequest,c as createResponse,f as created,b as getDynamoDBClient,w as getDynamoDBDocumentClient,m as httpHeaders,_ as initializeDynamoDBClients,p as internalServerError,d as noContent,h as notFound,g as ok,D as resetDynamoDBClients,s as withRequestTracking};
|
|
2
2
|
//# sourceMappingURL=index.esm.js.map
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var e=require("pino"),t=require("pino-lambda");const
|
|
1
|
+
"use strict";var e=require("pino"),t=require("pino-lambda"),n=require("@aws-sdk/client-dynamodb"),o=require("@aws-sdk/lib-dynamodb");const r=t.lambdaRequestTracker();const i=(e,t,n={})=>({statusCode:e,headers:{...n},body:JSON.stringify(t)});let s=null,a=null;exports.Logger=class{constructor(n){this._loggerConfig={enabled:!0,level:"info",format:"json"},this._instance=null,this._createLogger=()=>{const n="json"===this._loggerConfig.format?new t.StructuredLogFormatter:new t.CloudwatchLogFormatter,o=t.pinoLambdaDestination({formatter:n});return e({enabled:this._loggerConfig.enabled,level:this._loggerConfig.level},o)},n&&(this._loggerConfig={enabled:n.enabled??!0,level:n.level??"info",format:n.format??"json"})}get instance(){return null===this._instance&&(this._instance=this._createLogger()),this._instance}},exports.badRequest=(e="Bad Request",t={})=>i(400,{message:e},t),exports.createResponse=i,exports.created=(e,t={})=>i(201,e,t),exports.getDynamoDBClient=()=>{if(!s)throw new Error("DynamoDB client not initialized. Call initializeDynamoDBClients() first.");return s},exports.getDynamoDBDocumentClient=()=>{if(!a)throw new Error("DynamoDB Document client not initialized. Call initializeDynamoDBClients() first.");return a},exports.httpHeaders={contentType:e=>({"Content-Type":e}),json:{"Content-Type":"application/json"},cors:(e="*")=>({"Access-Control-Allow-Origin":e})},exports.initializeDynamoDBClients=(e,t,r)=>{s=new n.DynamoDBClient(e||{});const i={marshallOptions:t||{},unmarshallOptions:r||{}};return a=o.DynamoDBDocumentClient.from(s,i),{client:s,documentClient:a}},exports.internalServerError=(e="Internal Server Error",t={})=>i(500,{message:e},t),exports.noContent=(e={})=>i(204,{},e),exports.notFound=(e="Not Found",t={})=>i(404,{message:e},t),exports.ok=(e,t={})=>i(200,e,t),exports.resetDynamoDBClients=()=>{s=null,a=null},exports.withRequestTracking=r;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# API Gateway Responses Guide
|
|
2
|
+
|
|
3
|
+
The Lambda Utilities library provides a set of helper functions for creating properly formatted API Gateway responses. These utilities abstract away the boilerplate of response construction and ensure consistent response formatting across your Lambda functions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
API Gateway responses require a specific structure with a status code, headers, and a JSON-stringified body. The response helpers provided by Lambda Utilities simplify this by:
|
|
8
|
+
|
|
9
|
+
- Providing typed functions for common HTTP status codes
|
|
10
|
+
- Managing automatic JSON serialization
|
|
11
|
+
- Supporting custom headers
|
|
12
|
+
- Ensuring consistency with AWS Lambda proxy integration specifications
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @leanstacks/lambda-utils
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Basic Usage
|
|
21
|
+
|
|
22
|
+
### Creating Responses
|
|
23
|
+
|
|
24
|
+
Import the response helpers from Lambda Utilities:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { ok, created, badRequest, notFound, internalServerError } from '@leanstacks/lambda-utils';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Response Functions
|
|
31
|
+
|
|
32
|
+
#### `ok(body, headers?)`
|
|
33
|
+
|
|
34
|
+
Creates a **200 OK** response.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
export const handler = async (event: any) => {
|
|
38
|
+
const data = { id: '123', name: 'Example' };
|
|
39
|
+
return ok(data);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Response:
|
|
43
|
+
// {
|
|
44
|
+
// statusCode: 200,
|
|
45
|
+
// body: '{"id":"123","name":"Example"}',
|
|
46
|
+
// headers: {}
|
|
47
|
+
// }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### `created(body, headers?)`
|
|
51
|
+
|
|
52
|
+
Creates a **201 Created** response, typically used when a resource is successfully created.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
export const handler = async (event: any) => {
|
|
56
|
+
const newResource = { id: '456', name: 'New Resource' };
|
|
57
|
+
return created(newResource);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Response:
|
|
61
|
+
// {
|
|
62
|
+
// statusCode: 201,
|
|
63
|
+
// body: '{"id":"456","name":"New Resource"}',
|
|
64
|
+
// headers: {}
|
|
65
|
+
// }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### `noContent(headers?)`
|
|
69
|
+
|
|
70
|
+
Creates a **204 No Content** response, used when the request is successful but there's no content to return.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
export const handler = async (event: any) => {
|
|
74
|
+
// Delete operation
|
|
75
|
+
return noContent();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Response:
|
|
79
|
+
// {
|
|
80
|
+
// statusCode: 204,
|
|
81
|
+
// body: '{}',
|
|
82
|
+
// headers: {}
|
|
83
|
+
// }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### `badRequest(message?, headers?)`
|
|
87
|
+
|
|
88
|
+
Creates a **400 Bad Request** error response.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
export const handler = async (event: any) => {
|
|
92
|
+
if (!event.body) {
|
|
93
|
+
return badRequest('Request body is required');
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Response:
|
|
98
|
+
// {
|
|
99
|
+
// statusCode: 400,
|
|
100
|
+
// body: '{"message":"Request body is required"}',
|
|
101
|
+
// headers: {}
|
|
102
|
+
// }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `notFound(message?, headers?)`
|
|
106
|
+
|
|
107
|
+
Creates a **404 Not Found** error response.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
export const handler = async (event: any) => {
|
|
111
|
+
const resource = await getResource(event.pathParameters.id);
|
|
112
|
+
|
|
113
|
+
if (!resource) {
|
|
114
|
+
return notFound(`Resource with id ${event.pathParameters.id} not found`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ok(resource);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Response:
|
|
121
|
+
// {
|
|
122
|
+
// statusCode: 404,
|
|
123
|
+
// body: '{"message":"Resource with id 123 not found"}',
|
|
124
|
+
// headers: {}
|
|
125
|
+
// }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### `internalServerError(message?, headers?)`
|
|
129
|
+
|
|
130
|
+
Creates a **500 Internal Server Error** response.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
export const handler = async (event: any) => {
|
|
134
|
+
try {
|
|
135
|
+
// Process request
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return internalServerError('An unexpected error occurred');
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Response:
|
|
142
|
+
// {
|
|
143
|
+
// statusCode: 500,
|
|
144
|
+
// body: '{"message":"An unexpected error occurred"}',
|
|
145
|
+
// headers: {}
|
|
146
|
+
// }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### `createResponse(statusCode, body, headers?)`
|
|
150
|
+
|
|
151
|
+
Creates a custom response with any status code. Use this for status codes not covered by the helper functions.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { createResponse } from '@leanstacks/lambda-utils';
|
|
155
|
+
|
|
156
|
+
export const handler = async (event: any) => {
|
|
157
|
+
return createResponse(202, { status: 'Accepted' });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Response:
|
|
161
|
+
// {
|
|
162
|
+
// statusCode: 202,
|
|
163
|
+
// body: '{"status":"Accepted"}',
|
|
164
|
+
// headers: {}
|
|
165
|
+
// }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Headers
|
|
169
|
+
|
|
170
|
+
### HTTP Headers Helpers
|
|
171
|
+
|
|
172
|
+
Lambda Utilities provides a `httpHeaders` object with common header builders:
|
|
173
|
+
|
|
174
|
+
#### `httpHeaders.json`
|
|
175
|
+
|
|
176
|
+
Sets the `Content-Type` header to `application/json`.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { ok, httpHeaders } from '@leanstacks/lambda-utils';
|
|
180
|
+
|
|
181
|
+
export const handler = async (event: any) => {
|
|
182
|
+
return ok({ message: 'Success' }, httpHeaders.json);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Response:
|
|
186
|
+
// {
|
|
187
|
+
// statusCode: 200,
|
|
188
|
+
// body: '{"message":"Success"}',
|
|
189
|
+
// headers: { 'Content-Type': 'application/json' }
|
|
190
|
+
// }
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### `httpHeaders.contentType(type)`
|
|
194
|
+
|
|
195
|
+
Sets the `Content-Type` header to a custom MIME type.
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { ok, httpHeaders } from '@leanstacks/lambda-utils';
|
|
199
|
+
|
|
200
|
+
export const handler = async (event: any) => {
|
|
201
|
+
return ok(csvData, httpHeaders.contentType('text/csv'));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Response:
|
|
205
|
+
// {
|
|
206
|
+
// statusCode: 200,
|
|
207
|
+
// body: '...',
|
|
208
|
+
// headers: { 'Content-Type': 'text/csv' }
|
|
209
|
+
// }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### `httpHeaders.cors(origin?)`
|
|
213
|
+
|
|
214
|
+
Sets the `Access-Control-Allow-Origin` header for CORS support. Default is `*`.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { ok, httpHeaders } from '@leanstacks/lambda-utils';
|
|
218
|
+
|
|
219
|
+
export const handler = async (event: any) => {
|
|
220
|
+
return ok({ data: '...' }, httpHeaders.cors('https://example.com'));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Response:
|
|
224
|
+
// {
|
|
225
|
+
// statusCode: 200,
|
|
226
|
+
// body: '{"data":"..."}',
|
|
227
|
+
// headers: { 'Access-Control-Allow-Origin': 'https://example.com' }
|
|
228
|
+
// }
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Custom Headers
|
|
232
|
+
|
|
233
|
+
Combine multiple headers or add custom ones by passing a headers object:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { ok, httpHeaders } from '@leanstacks/lambda-utils';
|
|
237
|
+
|
|
238
|
+
export const handler = async (event: any) => {
|
|
239
|
+
const headers = {
|
|
240
|
+
...httpHeaders.json,
|
|
241
|
+
...httpHeaders.cors(),
|
|
242
|
+
'X-Custom-Header': 'value',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return ok({ message: 'Success' }, headers);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Response:
|
|
249
|
+
// {
|
|
250
|
+
// statusCode: 200,
|
|
251
|
+
// body: '{"message":"Success"}',
|
|
252
|
+
// headers: {
|
|
253
|
+
// 'Content-Type': 'application/json',
|
|
254
|
+
// 'Access-Control-Allow-Origin': '*',
|
|
255
|
+
// 'X-Custom-Header': 'value'
|
|
256
|
+
// }
|
|
257
|
+
// }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Complete Examples
|
|
261
|
+
|
|
262
|
+
### Validation and Error Handling
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { ok, badRequest, internalServerError, httpHeaders } from '@leanstacks/lambda-utils';
|
|
266
|
+
|
|
267
|
+
interface RequestBody {
|
|
268
|
+
email: string;
|
|
269
|
+
name: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export const handler = async (event: any) => {
|
|
273
|
+
try {
|
|
274
|
+
// Validate request
|
|
275
|
+
if (!event.body) {
|
|
276
|
+
return badRequest('Request body is required', httpHeaders.json);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const body: RequestBody = JSON.parse(event.body);
|
|
280
|
+
|
|
281
|
+
if (!body.email || !body.name) {
|
|
282
|
+
return badRequest('Missing required fields: email, name', httpHeaders.json);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Process request
|
|
286
|
+
const result = { id: '123', ...body };
|
|
287
|
+
|
|
288
|
+
return ok(result, httpHeaders.json);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error('Handler error:', error);
|
|
291
|
+
return internalServerError('Failed to process request', httpHeaders.json);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### CRUD Operations
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import {
|
|
300
|
+
ok,
|
|
301
|
+
created,
|
|
302
|
+
noContent,
|
|
303
|
+
badRequest,
|
|
304
|
+
notFound,
|
|
305
|
+
internalServerError,
|
|
306
|
+
httpHeaders,
|
|
307
|
+
} from '@leanstacks/lambda-utils';
|
|
308
|
+
|
|
309
|
+
const headers = httpHeaders.json;
|
|
310
|
+
|
|
311
|
+
export const handlers = {
|
|
312
|
+
// GET /items/{id}
|
|
313
|
+
getItem: async (event: any) => {
|
|
314
|
+
try {
|
|
315
|
+
const item = await findItem(event.pathParameters.id);
|
|
316
|
+
return item ? ok(item, headers) : notFound('Item not found', headers);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return internalServerError('Failed to retrieve item', headers);
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
// POST /items
|
|
323
|
+
createItem: async (event: any) => {
|
|
324
|
+
try {
|
|
325
|
+
if (!event.body) {
|
|
326
|
+
return badRequest('Request body is required', headers);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const newItem = await saveItem(JSON.parse(event.body));
|
|
330
|
+
return created(newItem, headers);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return internalServerError('Failed to create item', headers);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
// DELETE /items/{id}
|
|
337
|
+
deleteItem: async (event: any) => {
|
|
338
|
+
try {
|
|
339
|
+
await removeItem(event.pathParameters.id);
|
|
340
|
+
return noContent(headers);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return internalServerError('Failed to delete item', headers);
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### CORS-Enabled Handler
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { ok, badRequest, httpHeaders } from '@leanstacks/lambda-utils';
|
|
352
|
+
|
|
353
|
+
const corsHeaders = {
|
|
354
|
+
...httpHeaders.json,
|
|
355
|
+
...httpHeaders.cors('https://app.example.com'),
|
|
356
|
+
'X-API-Version': '1.0',
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
export const handler = async (event: any) => {
|
|
360
|
+
// Handle preflight requests
|
|
361
|
+
if (event.requestContext.http.method === 'OPTIONS') {
|
|
362
|
+
return ok({}, corsHeaders);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!event.body) {
|
|
366
|
+
return badRequest('Body is required', corsHeaders);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return ok({ processed: true }, corsHeaders);
|
|
370
|
+
};
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Best Practices
|
|
374
|
+
|
|
375
|
+
1. **Use Consistent Headers** – Define headers once and reuse them across handlers to maintain consistency.
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const defaultHeaders = httpHeaders.json;
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
2. **Provide Meaningful Error Messages** – Include specific error details to help clients understand what went wrong.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
return badRequest(`Missing required field: ${fieldName}`, headers);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
3. **Handle Errors Gracefully** – Use try-catch blocks and return appropriate error responses.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
try {
|
|
391
|
+
// Process
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return internalServerError('Operation failed', headers);
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
4. **Use Appropriate Status Codes** – Choose the correct HTTP status code for each scenario:
|
|
398
|
+
- `200 OK` – Request successful
|
|
399
|
+
- `201 Created` – Resource created
|
|
400
|
+
- `204 No Content` – Request successful, no content
|
|
401
|
+
- `400 Bad Request` – Invalid input
|
|
402
|
+
- `404 Not Found` – Resource not found
|
|
403
|
+
- `500 Internal Server Error` – Unexpected error
|
|
404
|
+
|
|
405
|
+
5. **Log Errors** – Log error details for debugging while returning user-friendly messages.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
catch (error) {
|
|
409
|
+
logger.error({ message: 'Processing failed', error: error.message });
|
|
410
|
+
return internalServerError('Failed to process request', headers);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
6. **Combine with Logging** – Use response helpers with structured logging for complete observability.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import { Logger } from '@leanstacks/lambda-utils';
|
|
418
|
+
const logger = new Logger().instance;
|
|
419
|
+
|
|
420
|
+
export const handler = async (event: any) => {
|
|
421
|
+
logger.info('Request received', { path: event.path });
|
|
422
|
+
return ok({ message: 'Success' }, httpHeaders.json);
|
|
423
|
+
};
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Type Safety
|
|
427
|
+
|
|
428
|
+
All response functions are fully typed with TypeScript. The `body` parameter accepts `unknown`, allowing you to pass any serializable value:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
interface User {
|
|
432
|
+
id: string;
|
|
433
|
+
name: string;
|
|
434
|
+
email: string;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const user: User = { id: '1', name: 'John', email: 'john@example.com' };
|
|
438
|
+
return ok(user); // ✓ Type-safe
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Error functions accept string or number messages:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
return badRequest('Invalid input'); // ✓ String message
|
|
445
|
+
return notFound(404); // ✓ Number message
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Further reading
|
|
449
|
+
|
|
450
|
+
- **[Logging Guide](./LOGGING.md)** – Structured logging for Lambda functions
|
|
451
|
+
- **[Back to the project documentation](README.md)**
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# DynamoDB Client Utilities
|
|
2
|
+
|
|
3
|
+
The DynamoDB client utilities provide reusable singleton instances of `DynamoDBClient` and `DynamoDBDocumentClient` for use in AWS Lambda functions. These utilities enable you to configure clients once and reuse them across invocations, following AWS best practices for Lambda performance optimization.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The utility exports the following functions:
|
|
8
|
+
|
|
9
|
+
- `initializeDynamoDBClients()` - Initialize both DynamoDB clients with optional configuration
|
|
10
|
+
- `getDynamoDBClient()` - Get the singleton DynamoDB client instance
|
|
11
|
+
- `getDynamoDBDocumentClient()` - Get the singleton DynamoDB Document client instance
|
|
12
|
+
- `resetDynamoDBClients()` - Reset both client instances (useful for testing)
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Basic Usage
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { initializeDynamoDBClients, getDynamoDBDocumentClient } from '@leanstacks/lambda-utils';
|
|
20
|
+
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
21
|
+
|
|
22
|
+
// Initialize both clients (typically done once outside the handler)
|
|
23
|
+
initializeDynamoDBClients({ region: 'us-east-1' });
|
|
24
|
+
|
|
25
|
+
export const handler = async (event: any) => {
|
|
26
|
+
// Get the document client (most common use case)
|
|
27
|
+
const docClient = getDynamoDBDocumentClient();
|
|
28
|
+
|
|
29
|
+
// Use the document client
|
|
30
|
+
await docClient.send(
|
|
31
|
+
new PutCommand({
|
|
32
|
+
TableName: 'MyTable',
|
|
33
|
+
Item: { id: '123', name: 'Example' },
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using the Base DynamoDB Client
|
|
40
|
+
|
|
41
|
+
````typescript
|
|
42
|
+
import { initializeDynamoDBClients, getDynamoDBClient } from '@leanstacks/lambda-utils';
|
|
43
|
+
import { PutItemCommand } from '@aws-sdk/client-dynamodb';
|
|
44
|
+
|
|
45
|
+
// Initialize both clients
|
|
46
|
+
initializeDynamoDBClients({ region: 'us-east-1' });
|
|
47
|
+
|
|
48
|
+
export const handler = async (event: any) => {
|
|
49
|
+
// Get the base client for advanced use cases
|
|
50
|
+
const client = getDynamoDBClient();
|
|
51
|
+
|
|
52
|
+
// Use the base client
|
|
53
|
+
await client.send(new PutItemCommand({
|
|
54
|
+
TableName: 'MyTable',
|
|
55
|
+
Item: { id: { S: '123' }, name: { S: 'Example' } }
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
### Advanced Configuration
|
|
59
|
+
|
|
60
|
+
#### Custom DynamoDB Client Configuration
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { initializeDynamoDBClients } from '@leanstacks/lambda-utils';
|
|
64
|
+
|
|
65
|
+
initializeDynamoDBClients({
|
|
66
|
+
region: 'us-west-2',
|
|
67
|
+
endpoint: 'http://localhost:8000', // For local development
|
|
68
|
+
credentials: {
|
|
69
|
+
accessKeyId: 'local',
|
|
70
|
+
secretAccessKey: 'local',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
````
|
|
74
|
+
|
|
75
|
+
#### Custom Marshall/Unmarshall Options
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { initializeDynamoDBClients } from '@leanstacks/lambda-utils';
|
|
79
|
+
|
|
80
|
+
initializeDynamoDBClients(
|
|
81
|
+
{ region: 'us-east-1' },
|
|
82
|
+
{
|
|
83
|
+
// Marshall options
|
|
84
|
+
removeUndefinedValues: true,
|
|
85
|
+
convertEmptyValues: false,
|
|
86
|
+
convertClassInstanceToMap: true,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
// Unmarshall options
|
|
90
|
+
wrapNumbers: false,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Lambda Handler Pattern
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { initializeDynamoDBClients, getDynamoDBDocumentClient } from '@leanstacks/lambda-utils';
|
|
99
|
+
import { GetCommand } from '@aws-sdk/lib-dynamodb';
|
|
100
|
+
|
|
101
|
+
// Initialize clients outside the handler (runs once per cold start)
|
|
102
|
+
initializeDynamoDBClients({ region: process.env.AWS_REGION }, { removeUndefinedValues: true });
|
|
103
|
+
|
|
104
|
+
// Handler function
|
|
105
|
+
export const handler = async (event: any) => {
|
|
106
|
+
const docClient = getDynamoDBDocumentClient();
|
|
107
|
+
|
|
108
|
+
const result = await docClient.send(
|
|
109
|
+
new GetCommand({
|
|
110
|
+
TableName: process.env.TABLE_NAME,
|
|
111
|
+
Key: { id: event.id },
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return result.Item;
|
|
116
|
+
};
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API Reference
|
|
120
|
+
|
|
121
|
+
### `initializeDynamoDBClients(config?, marshallOptions?, unmarshallOptions?): { client: DynamoDBClient; documentClient: DynamoDBDocumentClient }`
|
|
122
|
+
|
|
123
|
+
Initializes both the DynamoDB client and DynamoDB Document client with the provided configuration.
|
|
124
|
+
|
|
125
|
+
**Parameters:**
|
|
126
|
+
|
|
127
|
+
- `config` (optional) - DynamoDB client configuration object
|
|
128
|
+
- `marshallOptions` (optional) - Options for marshalling JavaScript objects to DynamoDB AttributeValues
|
|
129
|
+
- `unmarshallOptions` (optional) - Options for unmarshalling DynamoDB AttributeValues to JavaScript objects
|
|
130
|
+
|
|
131
|
+
**Returns:**
|
|
132
|
+
|
|
133
|
+
- An object containing both `client` (DynamoDBClient) and `documentClient` (DynamoDBDocumentClient)
|
|
134
|
+
|
|
135
|
+
**Notes:**
|
|
136
|
+
|
|
137
|
+
- Creates both clients in a single call
|
|
138
|
+
- If called multiple times, it will replace the existing client instances
|
|
139
|
+
- If no config is provided, uses default AWS SDK configuration
|
|
140
|
+
- Most users will only need the `documentClient` from the return value or via `getDynamoDBDocumentClient()`
|
|
141
|
+
|
|
142
|
+
### `getDynamoDBClient(): DynamoDBClient`
|
|
143
|
+
|
|
144
|
+
Returns the singleton DynamoDB client instance.
|
|
145
|
+
|
|
146
|
+
**Returns:**
|
|
147
|
+
|
|
148
|
+
- The `DynamoDBClient` instance
|
|
149
|
+
|
|
150
|
+
**Throws:**
|
|
151
|
+
|
|
152
|
+
- Error if the client has not been initialized
|
|
153
|
+
|
|
154
|
+
### `getDynamoDBDocumentClient(): DynamoDBDocumentClient`
|
|
155
|
+
|
|
156
|
+
Returns the singleton DynamoDB Document client instance.
|
|
157
|
+
|
|
158
|
+
**Returns:**
|
|
159
|
+
|
|
160
|
+
- The `DynamoDBDocumentClient` instance
|
|
161
|
+
|
|
162
|
+
**Throws:**
|
|
163
|
+
|
|
164
|
+
- Error if the document client has not been initialized
|
|
165
|
+
|
|
166
|
+
### `resetDynamoDBClients(): void`
|
|
167
|
+
|
|
168
|
+
Resets both DynamoDB client instances to null.
|
|
169
|
+
|
|
170
|
+
**Notes:**
|
|
171
|
+
|
|
172
|
+
- Primarily useful for testing scenarios where you need to reinitialize clients with different configurations
|
|
173
|
+
- After calling this, you must reinitialize the clients before using the getter functions
|
|
174
|
+
|
|
175
|
+
## Best Practices
|
|
176
|
+
|
|
177
|
+
1. **Initialize Outside the Handler**: Always initialize clients outside your Lambda handler function to reuse the instance across invocations.
|
|
178
|
+
|
|
179
|
+
2. **Use Environment Variables**: Configure clients using environment variables for flexibility across environments.
|
|
180
|
+
|
|
181
|
+
3. **Error Handling**: Always wrap client operations in try-catch blocks to handle errors gracefully.
|
|
182
|
+
|
|
183
|
+
4. **Testing**: Use `resetDynamoDBClients()` in test setup/teardown to ensure clean test isolation.
|
|
184
|
+
|
|
185
|
+
## Testing Example
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { initializeDynamoDBClients, getDynamoDBDocumentClient, resetDynamoDBClients } from '@leanstacks/lambda-utils';
|
|
189
|
+
|
|
190
|
+
describe('MyLambdaHandler', () => {
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
resetDynamoDBClients();
|
|
193
|
+
initializeDynamoDBClients(
|
|
194
|
+
{ region: 'us-east-1', endpoint: 'http://localhost:8000' },
|
|
195
|
+
{ removeUndefinedValues: true },
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
afterEach(() => {
|
|
200
|
+
resetDynamoDBClients();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should retrieve item from DynamoDB', async () => {
|
|
204
|
+
// Your test code here
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Related Resources
|
|
210
|
+
|
|
211
|
+
- **[AWS SDK for JavaScript v3 - DynamoDB Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/)**
|
|
212
|
+
- **[AWS SDK for JavaScript v3 - DynamoDB Document Client](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-lib-dynamodb/Class/DynamoDBDocumentClient/)**
|
|
213
|
+
- **[AWS Lambda Best Practices](https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html)**
|
|
214
|
+
- **[Back to the project documentation](README.md)**
|
package/docs/LOGGING.md
CHANGED
|
@@ -327,7 +327,8 @@ describe('MyHandler', () => {
|
|
|
327
327
|
|
|
328
328
|
## Further reading
|
|
329
329
|
|
|
330
|
-
- [Pino Documentation](https://getpino.io/)
|
|
331
|
-
- [
|
|
332
|
-
- [
|
|
333
|
-
- [
|
|
330
|
+
- **[Pino Documentation](https://getpino.io/)**
|
|
331
|
+
- **[Pino Lambda Documentation](https://github.com/FormidableLabs/pino-lambda#readme)**
|
|
332
|
+
- **[AWS Lambda Environment and Context](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html)**
|
|
333
|
+
- **[CloudWatch Logs Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html)**
|
|
334
|
+
- **[Back to the project documentation](README.md)**
|
package/docs/README.md
CHANGED
|
@@ -10,8 +10,8 @@ Lambda Utilities is a collection of pre-configured tools and helpers designed to
|
|
|
10
10
|
|
|
11
11
|
- **[Logging Guide](./LOGGING.md)** – Implement structured logging in your Lambda functions with Pino and automatic AWS context enrichment
|
|
12
12
|
- **[API Gateway Responses](./API_GATEWAY_RESPONSES.md)** – Format Lambda responses for API Gateway with standard HTTP status codes and headers
|
|
13
|
+
- **[DynamoDB Client](./DYNAMODB_CLIENT.md)** – Reusable singleton DynamoDB client instances with custom configuration
|
|
13
14
|
- **[Configuration](./CONFIGURATION.md)** – Validate environment variables and configuration with Zod type safety
|
|
14
|
-
- **[AWS Clients](./CLIENTS.md)** – Pre-configured AWS SDK v3 clients optimized for Lambda
|
|
15
15
|
- **[Getting Started](./GETTING_STARTED.md)** – Quick setup and installation instructions
|
|
16
16
|
|
|
17
17
|
## Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leanstacks/lambda-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha.1",
|
|
4
4
|
"description": "A collection of utilities and helper functions designed to streamline the development of AWS Lambda functions using TypeScript.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -63,7 +63,9 @@
|
|
|
63
63
|
"typescript": "5.9.3"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"
|
|
67
|
-
"
|
|
66
|
+
"@aws-sdk/client-dynamodb": "^3.955.0",
|
|
67
|
+
"@aws-sdk/lib-dynamodb": "^3.955.0",
|
|
68
|
+
"pino": "^10.1.0",
|
|
69
|
+
"pino-lambda": "^4.4.1"
|
|
68
70
|
}
|
|
69
71
|
}
|