@ttoss/lambda-postgres-query 0.3.30 → 0.5.0
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 +33 -12
- package/dist/cloudformation/index.d.cts +6 -2
- package/dist/cloudformation/index.d.ts +6 -2
- package/dist/cloudformation/index.js +167 -10
- package/dist/esm/cloudformation/index.js +162 -9
- package/dist/esm/index.js +0 -2
- package/dist/index.d.cts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -28,27 +28,45 @@ export default template;
|
|
|
28
28
|
|
|
29
29
|
### Lambda Handler
|
|
30
30
|
|
|
31
|
-
Create a handler file that exports
|
|
31
|
+
Create a handler file that exports both Lambda functions:
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
-
export { handler } from '@ttoss/lambda-postgres-query';
|
|
34
|
+
export { handler, readOnlyHandler } from '@ttoss/lambda-postgres-query';
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
### Environment Variables
|
|
38
38
|
|
|
39
|
-
Configure the following environment variables:
|
|
39
|
+
Configure the following environment variables for the general-purpose Lambda:
|
|
40
40
|
|
|
41
41
|
```env
|
|
42
42
|
DATABASE_NAME=your_database_name
|
|
43
43
|
DATABASE_USERNAME=your_username
|
|
44
44
|
DATABASE_PASSWORD=your_password
|
|
45
45
|
DATABASE_HOST=your_database_host
|
|
46
|
-
DATABASE_HOST_READ_ONLY=your_read_only_host # Optional
|
|
47
46
|
DATABASE_PORT=5432
|
|
48
47
|
SECURITY_GROUP_IDS=sg-xxxxx,sg-yyyyy
|
|
49
48
|
SUBNET_IDS=subnet-xxxxx,subnet-yyyyy
|
|
50
49
|
```
|
|
51
50
|
|
|
51
|
+
For the dedicated read-only Lambda (`readOnlyHandler`), define at least one `*_READ_ONLY` variable. Any read-only variable that is not set falls back to the corresponding main variable. For example, to point only the host to a read replica:
|
|
52
|
+
|
|
53
|
+
```env
|
|
54
|
+
DATABASE_HOST_READ_ONLY=your_read_only_host
|
|
55
|
+
# DATABASE_NAME, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_PORT are reused automatically
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To use a fully isolated read-only database user:
|
|
59
|
+
|
|
60
|
+
```env
|
|
61
|
+
DATABASE_NAME_READ_ONLY=your_read_only_database_name
|
|
62
|
+
DATABASE_USERNAME_READ_ONLY=your_read_only_username
|
|
63
|
+
DATABASE_PASSWORD_READ_ONLY=your_read_only_password
|
|
64
|
+
DATABASE_HOST_READ_ONLY=your_read_only_host
|
|
65
|
+
DATABASE_PORT_READ_ONLY=5432
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Security note:** If none of the `*_READ_ONLY` variables are set, the read-only Lambda throws an error at invocation time.
|
|
69
|
+
|
|
52
70
|
### Deployment
|
|
53
71
|
|
|
54
72
|
Add a deploy script to your `package.json`:
|
|
@@ -96,12 +114,6 @@ const result = await query({
|
|
|
96
114
|
values: [userId],
|
|
97
115
|
});
|
|
98
116
|
|
|
99
|
-
// Use read-only connection
|
|
100
|
-
const result = await query({
|
|
101
|
-
text: 'SELECT * FROM users',
|
|
102
|
-
readOnly: true, // Defaults to true
|
|
103
|
-
});
|
|
104
|
-
|
|
105
117
|
// Disable automatic camelCase conversion
|
|
106
118
|
const result = await query({
|
|
107
119
|
text: 'SELECT * FROM users',
|
|
@@ -115,6 +127,12 @@ const result = await query({
|
|
|
115
127
|
});
|
|
116
128
|
```
|
|
117
129
|
|
|
130
|
+
## Security: Isolating Read-Only Access
|
|
131
|
+
|
|
132
|
+
Deploying a dedicated `readOnlyHandler` Lambda lets you enforce the principle of least privilege at the AWS IAM level. Services that only need to read data (dashboards, reports, public APIs) receive IAM permissions to invoke **only** the read-only Lambda — they have no way to invoke the general-purpose Lambda and cannot perform write operations, regardless of the SQL they send.
|
|
133
|
+
|
|
134
|
+
This means a compromised or misconfigured service can never corrupt or delete data; it is limited to SELECT queries enforced both by the database (`BEGIN READ ONLY` transaction) and by the IAM boundary.
|
|
135
|
+
|
|
118
136
|
## API Reference
|
|
119
137
|
|
|
120
138
|
### `createLambdaPostgresQueryTemplate(options?)`
|
|
@@ -141,7 +159,6 @@ Accepts either a SQL string or an options object extending [`QueryConfig`](https
|
|
|
141
159
|
|
|
142
160
|
- `text` (string): SQL query text
|
|
143
161
|
- `values` (array, optional): Query parameter values
|
|
144
|
-
- `readOnly` (boolean, optional): Use read-only database host if available. Default: `true`
|
|
145
162
|
- `lambdaPostgresQueryFunction` (string, optional): Name of the query Lambda function. Default: `LAMBDA_POSTGRES_QUERY_FUNCTION` environment variable
|
|
146
163
|
- `camelCaseKeys` (boolean, optional): Convert snake_case column names to camelCase. Default: `true`
|
|
147
164
|
|
|
@@ -151,4 +168,8 @@ A [`QueryResult`](https://node-postgres.com/apis/result) object with transformed
|
|
|
151
168
|
|
|
152
169
|
### `handler`
|
|
153
170
|
|
|
154
|
-
AWS Lambda handler function for processing database queries within the VPC.
|
|
171
|
+
AWS Lambda handler function for processing database queries within the VPC. Uses `DATABASE_NAME`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_HOST`, and `DATABASE_PORT`.
|
|
172
|
+
|
|
173
|
+
### `readOnlyHandler`
|
|
174
|
+
|
|
175
|
+
AWS Lambda handler function for processing **read-only** database queries within the VPC. Enforces read-only access via a PostgreSQL `BEGIN READ ONLY` transaction. Uses dedicated `*_READ_ONLY` environment variables where defined, falling back to the main variables for any that are not set. Throws an error at invocation time if none of the `*_READ_ONLY` variables are defined.
|
|
@@ -5,14 +5,18 @@ import { QueryParams } from '../index.cjs';
|
|
|
5
5
|
import 'pg';
|
|
6
6
|
|
|
7
7
|
declare const HANDLER_DEFAULT = "handler.handler";
|
|
8
|
+
declare const HANDLER_READ_ONLY_DEFAULT = "handler.readOnlyHandler";
|
|
8
9
|
declare const MEMORY_SIZE_DEFAULT = 128;
|
|
9
10
|
declare const TIMEOUT_DEFAULT = 30;
|
|
10
|
-
declare const createLambdaPostgresQueryTemplate: ({ handler, memorySize, timeout, }?: {
|
|
11
|
+
declare const createLambdaPostgresQueryTemplate: ({ handler, readOnlyHandler, memorySize, timeout, }?: {
|
|
11
12
|
handler?: string;
|
|
13
|
+
readOnlyHandler?: string;
|
|
12
14
|
memorySize?: number;
|
|
13
15
|
timeout?: number;
|
|
14
16
|
}) => CloudFormationTemplate;
|
|
15
17
|
|
|
16
18
|
declare const handler: Handler<QueryParams>;
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
declare const readOnlyHandler: Handler<QueryParams>;
|
|
21
|
+
|
|
22
|
+
export { HANDLER_DEFAULT, HANDLER_READ_ONLY_DEFAULT, MEMORY_SIZE_DEFAULT, TIMEOUT_DEFAULT, createLambdaPostgresQueryTemplate, handler, readOnlyHandler };
|
|
@@ -5,14 +5,18 @@ import { QueryParams } from '../index.js';
|
|
|
5
5
|
import 'pg';
|
|
6
6
|
|
|
7
7
|
declare const HANDLER_DEFAULT = "handler.handler";
|
|
8
|
+
declare const HANDLER_READ_ONLY_DEFAULT = "handler.readOnlyHandler";
|
|
8
9
|
declare const MEMORY_SIZE_DEFAULT = 128;
|
|
9
10
|
declare const TIMEOUT_DEFAULT = 30;
|
|
10
|
-
declare const createLambdaPostgresQueryTemplate: ({ handler, memorySize, timeout, }?: {
|
|
11
|
+
declare const createLambdaPostgresQueryTemplate: ({ handler, readOnlyHandler, memorySize, timeout, }?: {
|
|
11
12
|
handler?: string;
|
|
13
|
+
readOnlyHandler?: string;
|
|
12
14
|
memorySize?: number;
|
|
13
15
|
timeout?: number;
|
|
14
16
|
}) => CloudFormationTemplate;
|
|
15
17
|
|
|
16
18
|
declare const handler: Handler<QueryParams>;
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
declare const readOnlyHandler: Handler<QueryParams>;
|
|
21
|
+
|
|
22
|
+
export { HANDLER_DEFAULT, HANDLER_READ_ONLY_DEFAULT, MEMORY_SIZE_DEFAULT, TIMEOUT_DEFAULT, createLambdaPostgresQueryTemplate, handler, readOnlyHandler };
|
|
@@ -32,19 +32,23 @@ var __toCommonJS = mod => __copyProps(__defProp({}, "__esModule", {
|
|
|
32
32
|
var cloudformation_exports = {};
|
|
33
33
|
__export(cloudformation_exports, {
|
|
34
34
|
HANDLER_DEFAULT: () => HANDLER_DEFAULT,
|
|
35
|
+
HANDLER_READ_ONLY_DEFAULT: () => HANDLER_READ_ONLY_DEFAULT,
|
|
35
36
|
MEMORY_SIZE_DEFAULT: () => MEMORY_SIZE_DEFAULT,
|
|
36
37
|
TIMEOUT_DEFAULT: () => TIMEOUT_DEFAULT,
|
|
37
38
|
createLambdaPostgresQueryTemplate: () => createLambdaPostgresQueryTemplate,
|
|
38
|
-
handler: () => handler
|
|
39
|
+
handler: () => handler,
|
|
40
|
+
readOnlyHandler: () => readOnlyHandler
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(cloudformation_exports);
|
|
41
43
|
|
|
42
44
|
// src/cloudformation/createLambdaPostgresQueryTemplate.ts
|
|
43
45
|
var HANDLER_DEFAULT = "handler.handler";
|
|
46
|
+
var HANDLER_READ_ONLY_DEFAULT = "handler.readOnlyHandler";
|
|
44
47
|
var MEMORY_SIZE_DEFAULT = 128;
|
|
45
48
|
var TIMEOUT_DEFAULT = 30;
|
|
46
49
|
var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
47
50
|
handler: handler2 = HANDLER_DEFAULT,
|
|
51
|
+
readOnlyHandler: readOnlyHandler2 = HANDLER_READ_ONLY_DEFAULT,
|
|
48
52
|
memorySize = 128,
|
|
49
53
|
timeout = 30
|
|
50
54
|
} = {}) => {
|
|
@@ -70,13 +74,32 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
70
74
|
},
|
|
71
75
|
DatabasePassword: {
|
|
72
76
|
Type: "String",
|
|
73
|
-
Description: "Database password."
|
|
77
|
+
Description: "Database password.",
|
|
78
|
+
NoEcho: true
|
|
74
79
|
},
|
|
75
80
|
DatabasePort: {
|
|
76
81
|
Type: "String",
|
|
77
82
|
Default: "5432",
|
|
78
83
|
Description: "Database port."
|
|
79
84
|
},
|
|
85
|
+
DatabaseNameReadOnly: {
|
|
86
|
+
Type: "String",
|
|
87
|
+
Description: "Database name for read-only access."
|
|
88
|
+
},
|
|
89
|
+
DatabaseUsernameReadOnly: {
|
|
90
|
+
Type: "String",
|
|
91
|
+
Description: "Database username for read-only access."
|
|
92
|
+
},
|
|
93
|
+
DatabasePasswordReadOnly: {
|
|
94
|
+
Type: "String",
|
|
95
|
+
Description: "Database password for read-only access.",
|
|
96
|
+
NoEcho: true
|
|
97
|
+
},
|
|
98
|
+
DatabasePortReadOnly: {
|
|
99
|
+
Type: "String",
|
|
100
|
+
Default: "5432",
|
|
101
|
+
Description: "Database port for read-only access."
|
|
102
|
+
},
|
|
80
103
|
LambdaS3Bucket: {
|
|
81
104
|
Type: "String",
|
|
82
105
|
Description: "The S3 bucket where the Lambda code is stored."
|
|
@@ -135,15 +158,12 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
135
158
|
Role: {
|
|
136
159
|
"Fn::GetAtt": ["LambdaQueryExecutionRole", "Arn"]
|
|
137
160
|
},
|
|
138
|
-
Runtime: "
|
|
161
|
+
Runtime: "nodejs24.x",
|
|
139
162
|
Environment: {
|
|
140
163
|
Variables: {
|
|
141
164
|
DATABASE_HOST: {
|
|
142
165
|
Ref: "DatabaseHost"
|
|
143
166
|
},
|
|
144
|
-
DATABASE_HOST_READ_ONLY: {
|
|
145
|
-
Ref: "DatabaseHostReadOnly"
|
|
146
|
-
},
|
|
147
167
|
DATABASE_NAME: {
|
|
148
168
|
Ref: "DatabaseName"
|
|
149
169
|
},
|
|
@@ -177,7 +197,84 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
177
197
|
Ref: "LambdaQueryFunction"
|
|
178
198
|
}]]
|
|
179
199
|
},
|
|
180
|
-
RetentionInDays:
|
|
200
|
+
RetentionInDays: 3
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
LambdaReadOnlyQueryFunction: {
|
|
204
|
+
Type: "AWS::Lambda::Function",
|
|
205
|
+
Properties: {
|
|
206
|
+
Code: {
|
|
207
|
+
S3Bucket: {
|
|
208
|
+
Ref: "LambdaS3Bucket"
|
|
209
|
+
},
|
|
210
|
+
S3Key: {
|
|
211
|
+
Ref: "LambdaS3Key"
|
|
212
|
+
},
|
|
213
|
+
S3ObjectVersion: {
|
|
214
|
+
Ref: "LambdaS3ObjectVersion"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
MemorySize: memorySize,
|
|
218
|
+
Timeout: timeout,
|
|
219
|
+
Handler: readOnlyHandler2,
|
|
220
|
+
Role: {
|
|
221
|
+
"Fn::GetAtt": ["LambdaQueryExecutionRole", "Arn"]
|
|
222
|
+
},
|
|
223
|
+
Runtime: "nodejs24.x",
|
|
224
|
+
Environment: {
|
|
225
|
+
Variables: {
|
|
226
|
+
DATABASE_HOST: {
|
|
227
|
+
Ref: "DatabaseHost"
|
|
228
|
+
},
|
|
229
|
+
DATABASE_NAME: {
|
|
230
|
+
Ref: "DatabaseName"
|
|
231
|
+
},
|
|
232
|
+
DATABASE_USERNAME: {
|
|
233
|
+
Ref: "DatabaseUsername"
|
|
234
|
+
},
|
|
235
|
+
DATABASE_PASSWORD: {
|
|
236
|
+
Ref: "DatabasePassword"
|
|
237
|
+
},
|
|
238
|
+
DATABASE_PORT: {
|
|
239
|
+
Ref: "DatabasePort"
|
|
240
|
+
},
|
|
241
|
+
DATABASE_HOST_READ_ONLY: {
|
|
242
|
+
Ref: "DatabaseHostReadOnly"
|
|
243
|
+
},
|
|
244
|
+
DATABASE_NAME_READ_ONLY: {
|
|
245
|
+
Ref: "DatabaseNameReadOnly"
|
|
246
|
+
},
|
|
247
|
+
DATABASE_USERNAME_READ_ONLY: {
|
|
248
|
+
Ref: "DatabaseUsernameReadOnly"
|
|
249
|
+
},
|
|
250
|
+
DATABASE_PASSWORD_READ_ONLY: {
|
|
251
|
+
Ref: "DatabasePasswordReadOnly"
|
|
252
|
+
},
|
|
253
|
+
DATABASE_PORT_READ_ONLY: {
|
|
254
|
+
Ref: "DatabasePortReadOnly"
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
VpcConfig: {
|
|
259
|
+
SecurityGroupIds: {
|
|
260
|
+
Ref: "SecurityGroupIds"
|
|
261
|
+
},
|
|
262
|
+
SubnetIds: {
|
|
263
|
+
Ref: "SubnetIds"
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
LambdaReadOnlyQueryFunctionLogs: {
|
|
269
|
+
Type: "AWS::Logs::LogGroup",
|
|
270
|
+
DependsOn: "LambdaReadOnlyQueryFunction",
|
|
271
|
+
Properties: {
|
|
272
|
+
LogGroupName: {
|
|
273
|
+
"Fn::Join": ["", ["/aws/lambda/", {
|
|
274
|
+
Ref: "LambdaReadOnlyQueryFunction"
|
|
275
|
+
}]]
|
|
276
|
+
},
|
|
277
|
+
RetentionInDays: 3
|
|
181
278
|
}
|
|
182
279
|
}
|
|
183
280
|
},
|
|
@@ -193,6 +290,18 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
193
290
|
Value: {
|
|
194
291
|
"Fn::GetAtt": ["LambdaQueryFunction", "Arn"]
|
|
195
292
|
}
|
|
293
|
+
},
|
|
294
|
+
LambdaPostgresReadOnlyQueryFunction: {
|
|
295
|
+
Description: "Lambda function to query PostgreSQL (read-only).",
|
|
296
|
+
Value: {
|
|
297
|
+
Ref: "LambdaReadOnlyQueryFunction"
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
LambdaPostgresReadOnlyQueryFunctionArn: {
|
|
301
|
+
Description: "Lambda function to query PostgreSQL (read-only) ARN.",
|
|
302
|
+
Value: {
|
|
303
|
+
"Fn::GetAtt": ["LambdaReadOnlyQueryFunction", "Arn"]
|
|
304
|
+
}
|
|
196
305
|
}
|
|
197
306
|
}
|
|
198
307
|
};
|
|
@@ -204,7 +313,6 @@ var database = process.env.DATABASE_NAME;
|
|
|
204
313
|
var username = process.env.DATABASE_USERNAME;
|
|
205
314
|
var password = process.env.DATABASE_PASSWORD;
|
|
206
315
|
var host = process.env.DATABASE_HOST;
|
|
207
|
-
var hostReadOnly = process.env.DATABASE_HOST_READ_ONLY;
|
|
208
316
|
var port = process.env.DATABASE_PORT;
|
|
209
317
|
var handler = /* @__PURE__ */__name(async event => {
|
|
210
318
|
try {
|
|
@@ -212,7 +320,7 @@ var handler = /* @__PURE__ */__name(async event => {
|
|
|
212
320
|
database,
|
|
213
321
|
user: username,
|
|
214
322
|
password,
|
|
215
|
-
host
|
|
323
|
+
host,
|
|
216
324
|
port: Number(port)
|
|
217
325
|
});
|
|
218
326
|
await client.connect();
|
|
@@ -230,11 +338,60 @@ var handler = /* @__PURE__ */__name(async event => {
|
|
|
230
338
|
throw error;
|
|
231
339
|
}
|
|
232
340
|
}, "handler");
|
|
341
|
+
|
|
342
|
+
// src/cloudformation/lambdaReadOnlyQueryHandler.ts
|
|
343
|
+
var import_pg2 = require("pg");
|
|
344
|
+
var databaseReadOnly = process.env.DATABASE_NAME_READ_ONLY;
|
|
345
|
+
var usernameReadOnly = process.env.DATABASE_USERNAME_READ_ONLY;
|
|
346
|
+
var passwordReadOnly = process.env.DATABASE_PASSWORD_READ_ONLY;
|
|
347
|
+
var hostReadOnly = process.env.DATABASE_HOST_READ_ONLY;
|
|
348
|
+
var portReadOnly = process.env.DATABASE_PORT_READ_ONLY;
|
|
349
|
+
var readOnlyHandler = /* @__PURE__ */__name(async event => {
|
|
350
|
+
if (!databaseReadOnly && !usernameReadOnly && !passwordReadOnly && !hostReadOnly && !portReadOnly) {
|
|
351
|
+
throw new Error("At least one read-only override must be defined (DATABASE_NAME_READ_ONLY, DATABASE_USERNAME_READ_ONLY, DATABASE_PASSWORD_READ_ONLY, DATABASE_HOST_READ_ONLY, DATABASE_PORT_READ_ONLY). Unset overrides fall back to the corresponding non-read-only env vars.");
|
|
352
|
+
}
|
|
353
|
+
const database2 = databaseReadOnly || process.env.DATABASE_NAME;
|
|
354
|
+
const username2 = usernameReadOnly || process.env.DATABASE_USERNAME;
|
|
355
|
+
const password2 = passwordReadOnly || process.env.DATABASE_PASSWORD;
|
|
356
|
+
const host2 = hostReadOnly || process.env.DATABASE_HOST;
|
|
357
|
+
const port2 = portReadOnly || process.env.DATABASE_PORT;
|
|
358
|
+
try {
|
|
359
|
+
const client = new import_pg2.Client({
|
|
360
|
+
database: database2,
|
|
361
|
+
user: username2,
|
|
362
|
+
password: password2,
|
|
363
|
+
host: host2,
|
|
364
|
+
port: Number(port2)
|
|
365
|
+
});
|
|
366
|
+
await client.connect();
|
|
367
|
+
try {
|
|
368
|
+
await client.query("BEGIN READ ONLY");
|
|
369
|
+
try {
|
|
370
|
+
const res = await client.query(event);
|
|
371
|
+
await client.query("COMMIT");
|
|
372
|
+
return res;
|
|
373
|
+
} catch (queryError) {
|
|
374
|
+
await client.query("ROLLBACK");
|
|
375
|
+
throw queryError;
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
await client.end();
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error("Error running read-only query", {
|
|
382
|
+
error,
|
|
383
|
+
event
|
|
384
|
+
});
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}, "readOnlyHandler");
|
|
233
388
|
// Annotate the CommonJS export names for ESM import in node:
|
|
234
389
|
0 && (module.exports = {
|
|
235
390
|
HANDLER_DEFAULT,
|
|
391
|
+
HANDLER_READ_ONLY_DEFAULT,
|
|
236
392
|
MEMORY_SIZE_DEFAULT,
|
|
237
393
|
TIMEOUT_DEFAULT,
|
|
238
394
|
createLambdaPostgresQueryTemplate,
|
|
239
|
-
handler
|
|
395
|
+
handler,
|
|
396
|
+
readOnlyHandler
|
|
240
397
|
});
|
|
@@ -3,10 +3,12 @@ import { __name } from "../chunk-V4MHYKRI.js";
|
|
|
3
3
|
|
|
4
4
|
// src/cloudformation/createLambdaPostgresQueryTemplate.ts
|
|
5
5
|
var HANDLER_DEFAULT = "handler.handler";
|
|
6
|
+
var HANDLER_READ_ONLY_DEFAULT = "handler.readOnlyHandler";
|
|
6
7
|
var MEMORY_SIZE_DEFAULT = 128;
|
|
7
8
|
var TIMEOUT_DEFAULT = 30;
|
|
8
9
|
var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
9
10
|
handler: handler2 = HANDLER_DEFAULT,
|
|
11
|
+
readOnlyHandler: readOnlyHandler2 = HANDLER_READ_ONLY_DEFAULT,
|
|
10
12
|
memorySize = 128,
|
|
11
13
|
timeout = 30
|
|
12
14
|
} = {}) => {
|
|
@@ -32,13 +34,32 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
32
34
|
},
|
|
33
35
|
DatabasePassword: {
|
|
34
36
|
Type: "String",
|
|
35
|
-
Description: "Database password."
|
|
37
|
+
Description: "Database password.",
|
|
38
|
+
NoEcho: true
|
|
36
39
|
},
|
|
37
40
|
DatabasePort: {
|
|
38
41
|
Type: "String",
|
|
39
42
|
Default: "5432",
|
|
40
43
|
Description: "Database port."
|
|
41
44
|
},
|
|
45
|
+
DatabaseNameReadOnly: {
|
|
46
|
+
Type: "String",
|
|
47
|
+
Description: "Database name for read-only access."
|
|
48
|
+
},
|
|
49
|
+
DatabaseUsernameReadOnly: {
|
|
50
|
+
Type: "String",
|
|
51
|
+
Description: "Database username for read-only access."
|
|
52
|
+
},
|
|
53
|
+
DatabasePasswordReadOnly: {
|
|
54
|
+
Type: "String",
|
|
55
|
+
Description: "Database password for read-only access.",
|
|
56
|
+
NoEcho: true
|
|
57
|
+
},
|
|
58
|
+
DatabasePortReadOnly: {
|
|
59
|
+
Type: "String",
|
|
60
|
+
Default: "5432",
|
|
61
|
+
Description: "Database port for read-only access."
|
|
62
|
+
},
|
|
42
63
|
LambdaS3Bucket: {
|
|
43
64
|
Type: "String",
|
|
44
65
|
Description: "The S3 bucket where the Lambda code is stored."
|
|
@@ -97,15 +118,12 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
97
118
|
Role: {
|
|
98
119
|
"Fn::GetAtt": ["LambdaQueryExecutionRole", "Arn"]
|
|
99
120
|
},
|
|
100
|
-
Runtime: "
|
|
121
|
+
Runtime: "nodejs24.x",
|
|
101
122
|
Environment: {
|
|
102
123
|
Variables: {
|
|
103
124
|
DATABASE_HOST: {
|
|
104
125
|
Ref: "DatabaseHost"
|
|
105
126
|
},
|
|
106
|
-
DATABASE_HOST_READ_ONLY: {
|
|
107
|
-
Ref: "DatabaseHostReadOnly"
|
|
108
|
-
},
|
|
109
127
|
DATABASE_NAME: {
|
|
110
128
|
Ref: "DatabaseName"
|
|
111
129
|
},
|
|
@@ -139,7 +157,84 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
139
157
|
Ref: "LambdaQueryFunction"
|
|
140
158
|
}]]
|
|
141
159
|
},
|
|
142
|
-
RetentionInDays:
|
|
160
|
+
RetentionInDays: 3
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
LambdaReadOnlyQueryFunction: {
|
|
164
|
+
Type: "AWS::Lambda::Function",
|
|
165
|
+
Properties: {
|
|
166
|
+
Code: {
|
|
167
|
+
S3Bucket: {
|
|
168
|
+
Ref: "LambdaS3Bucket"
|
|
169
|
+
},
|
|
170
|
+
S3Key: {
|
|
171
|
+
Ref: "LambdaS3Key"
|
|
172
|
+
},
|
|
173
|
+
S3ObjectVersion: {
|
|
174
|
+
Ref: "LambdaS3ObjectVersion"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
MemorySize: memorySize,
|
|
178
|
+
Timeout: timeout,
|
|
179
|
+
Handler: readOnlyHandler2,
|
|
180
|
+
Role: {
|
|
181
|
+
"Fn::GetAtt": ["LambdaQueryExecutionRole", "Arn"]
|
|
182
|
+
},
|
|
183
|
+
Runtime: "nodejs24.x",
|
|
184
|
+
Environment: {
|
|
185
|
+
Variables: {
|
|
186
|
+
DATABASE_HOST: {
|
|
187
|
+
Ref: "DatabaseHost"
|
|
188
|
+
},
|
|
189
|
+
DATABASE_NAME: {
|
|
190
|
+
Ref: "DatabaseName"
|
|
191
|
+
},
|
|
192
|
+
DATABASE_USERNAME: {
|
|
193
|
+
Ref: "DatabaseUsername"
|
|
194
|
+
},
|
|
195
|
+
DATABASE_PASSWORD: {
|
|
196
|
+
Ref: "DatabasePassword"
|
|
197
|
+
},
|
|
198
|
+
DATABASE_PORT: {
|
|
199
|
+
Ref: "DatabasePort"
|
|
200
|
+
},
|
|
201
|
+
DATABASE_HOST_READ_ONLY: {
|
|
202
|
+
Ref: "DatabaseHostReadOnly"
|
|
203
|
+
},
|
|
204
|
+
DATABASE_NAME_READ_ONLY: {
|
|
205
|
+
Ref: "DatabaseNameReadOnly"
|
|
206
|
+
},
|
|
207
|
+
DATABASE_USERNAME_READ_ONLY: {
|
|
208
|
+
Ref: "DatabaseUsernameReadOnly"
|
|
209
|
+
},
|
|
210
|
+
DATABASE_PASSWORD_READ_ONLY: {
|
|
211
|
+
Ref: "DatabasePasswordReadOnly"
|
|
212
|
+
},
|
|
213
|
+
DATABASE_PORT_READ_ONLY: {
|
|
214
|
+
Ref: "DatabasePortReadOnly"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
VpcConfig: {
|
|
219
|
+
SecurityGroupIds: {
|
|
220
|
+
Ref: "SecurityGroupIds"
|
|
221
|
+
},
|
|
222
|
+
SubnetIds: {
|
|
223
|
+
Ref: "SubnetIds"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
LambdaReadOnlyQueryFunctionLogs: {
|
|
229
|
+
Type: "AWS::Logs::LogGroup",
|
|
230
|
+
DependsOn: "LambdaReadOnlyQueryFunction",
|
|
231
|
+
Properties: {
|
|
232
|
+
LogGroupName: {
|
|
233
|
+
"Fn::Join": ["", ["/aws/lambda/", {
|
|
234
|
+
Ref: "LambdaReadOnlyQueryFunction"
|
|
235
|
+
}]]
|
|
236
|
+
},
|
|
237
|
+
RetentionInDays: 3
|
|
143
238
|
}
|
|
144
239
|
}
|
|
145
240
|
},
|
|
@@ -155,6 +250,18 @@ var createLambdaPostgresQueryTemplate = /* @__PURE__ */__name(({
|
|
|
155
250
|
Value: {
|
|
156
251
|
"Fn::GetAtt": ["LambdaQueryFunction", "Arn"]
|
|
157
252
|
}
|
|
253
|
+
},
|
|
254
|
+
LambdaPostgresReadOnlyQueryFunction: {
|
|
255
|
+
Description: "Lambda function to query PostgreSQL (read-only).",
|
|
256
|
+
Value: {
|
|
257
|
+
Ref: "LambdaReadOnlyQueryFunction"
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
LambdaPostgresReadOnlyQueryFunctionArn: {
|
|
261
|
+
Description: "Lambda function to query PostgreSQL (read-only) ARN.",
|
|
262
|
+
Value: {
|
|
263
|
+
"Fn::GetAtt": ["LambdaReadOnlyQueryFunction", "Arn"]
|
|
264
|
+
}
|
|
158
265
|
}
|
|
159
266
|
}
|
|
160
267
|
};
|
|
@@ -166,7 +273,6 @@ var database = process.env.DATABASE_NAME;
|
|
|
166
273
|
var username = process.env.DATABASE_USERNAME;
|
|
167
274
|
var password = process.env.DATABASE_PASSWORD;
|
|
168
275
|
var host = process.env.DATABASE_HOST;
|
|
169
|
-
var hostReadOnly = process.env.DATABASE_HOST_READ_ONLY;
|
|
170
276
|
var port = process.env.DATABASE_PORT;
|
|
171
277
|
var handler = /* @__PURE__ */__name(async event => {
|
|
172
278
|
try {
|
|
@@ -174,7 +280,7 @@ var handler = /* @__PURE__ */__name(async event => {
|
|
|
174
280
|
database,
|
|
175
281
|
user: username,
|
|
176
282
|
password,
|
|
177
|
-
host
|
|
283
|
+
host,
|
|
178
284
|
port: Number(port)
|
|
179
285
|
});
|
|
180
286
|
await client.connect();
|
|
@@ -192,4 +298,51 @@ var handler = /* @__PURE__ */__name(async event => {
|
|
|
192
298
|
throw error;
|
|
193
299
|
}
|
|
194
300
|
}, "handler");
|
|
195
|
-
|
|
301
|
+
|
|
302
|
+
// src/cloudformation/lambdaReadOnlyQueryHandler.ts
|
|
303
|
+
import { Client as Client2 } from "pg";
|
|
304
|
+
var databaseReadOnly = process.env.DATABASE_NAME_READ_ONLY;
|
|
305
|
+
var usernameReadOnly = process.env.DATABASE_USERNAME_READ_ONLY;
|
|
306
|
+
var passwordReadOnly = process.env.DATABASE_PASSWORD_READ_ONLY;
|
|
307
|
+
var hostReadOnly = process.env.DATABASE_HOST_READ_ONLY;
|
|
308
|
+
var portReadOnly = process.env.DATABASE_PORT_READ_ONLY;
|
|
309
|
+
var readOnlyHandler = /* @__PURE__ */__name(async event => {
|
|
310
|
+
if (!databaseReadOnly && !usernameReadOnly && !passwordReadOnly && !hostReadOnly && !portReadOnly) {
|
|
311
|
+
throw new Error("At least one read-only override must be defined (DATABASE_NAME_READ_ONLY, DATABASE_USERNAME_READ_ONLY, DATABASE_PASSWORD_READ_ONLY, DATABASE_HOST_READ_ONLY, DATABASE_PORT_READ_ONLY). Unset overrides fall back to the corresponding non-read-only env vars.");
|
|
312
|
+
}
|
|
313
|
+
const database2 = databaseReadOnly || process.env.DATABASE_NAME;
|
|
314
|
+
const username2 = usernameReadOnly || process.env.DATABASE_USERNAME;
|
|
315
|
+
const password2 = passwordReadOnly || process.env.DATABASE_PASSWORD;
|
|
316
|
+
const host2 = hostReadOnly || process.env.DATABASE_HOST;
|
|
317
|
+
const port2 = portReadOnly || process.env.DATABASE_PORT;
|
|
318
|
+
try {
|
|
319
|
+
const client = new Client2({
|
|
320
|
+
database: database2,
|
|
321
|
+
user: username2,
|
|
322
|
+
password: password2,
|
|
323
|
+
host: host2,
|
|
324
|
+
port: Number(port2)
|
|
325
|
+
});
|
|
326
|
+
await client.connect();
|
|
327
|
+
try {
|
|
328
|
+
await client.query("BEGIN READ ONLY");
|
|
329
|
+
try {
|
|
330
|
+
const res = await client.query(event);
|
|
331
|
+
await client.query("COMMIT");
|
|
332
|
+
return res;
|
|
333
|
+
} catch (queryError) {
|
|
334
|
+
await client.query("ROLLBACK");
|
|
335
|
+
throw queryError;
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
await client.end();
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error("Error running read-only query", {
|
|
342
|
+
error,
|
|
343
|
+
event
|
|
344
|
+
});
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}, "readOnlyHandler");
|
|
348
|
+
export { HANDLER_DEFAULT, HANDLER_READ_ONLY_DEFAULT, MEMORY_SIZE_DEFAULT, TIMEOUT_DEFAULT, createLambdaPostgresQueryTemplate, handler, readOnlyHandler };
|
package/dist/esm/index.js
CHANGED
|
@@ -9,7 +9,6 @@ var textDecoder = new TextDecoder("utf-8");
|
|
|
9
9
|
var query = /* @__PURE__ */__name(async params => {
|
|
10
10
|
try {
|
|
11
11
|
const {
|
|
12
|
-
readOnly = true,
|
|
13
12
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
14
13
|
lambdaPostgresQueryFunction = process.env.LAMBDA_POSTGRES_QUERY_FUNCTION,
|
|
15
14
|
camelCaseKeys = true,
|
|
@@ -20,7 +19,6 @@ var query = /* @__PURE__ */__name(async params => {
|
|
|
20
19
|
const input = {
|
|
21
20
|
FunctionName: lambdaPostgresQueryFunction,
|
|
22
21
|
Payload: JSON.stringify({
|
|
23
|
-
readOnly,
|
|
24
22
|
...pgParams
|
|
25
23
|
})
|
|
26
24
|
};
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -54,7 +54,6 @@ var textDecoder = new TextDecoder("utf-8");
|
|
|
54
54
|
var query = /* @__PURE__ */__name(async params => {
|
|
55
55
|
try {
|
|
56
56
|
const {
|
|
57
|
-
readOnly = true,
|
|
58
57
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
59
58
|
lambdaPostgresQueryFunction = process.env.LAMBDA_POSTGRES_QUERY_FUNCTION,
|
|
60
59
|
camelCaseKeys = true,
|
|
@@ -65,7 +64,6 @@ var query = /* @__PURE__ */__name(async params => {
|
|
|
65
64
|
const input = {
|
|
66
65
|
FunctionName: lambdaPostgresQueryFunction,
|
|
67
66
|
Payload: JSON.stringify({
|
|
68
|
-
readOnly,
|
|
69
67
|
...pgParams
|
|
70
68
|
})
|
|
71
69
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/lambda-postgres-query",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Create a Lambda function that queries a PostgreSQL database.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ttoss",
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
"dist"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@aws-sdk/client-lambda": "^3.
|
|
32
|
+
"@aws-sdk/client-lambda": "^3.1025.0",
|
|
33
33
|
"camelcase-keys": "^7.0.2",
|
|
34
|
-
"pg": "^8.
|
|
34
|
+
"pg": "^8.20.0",
|
|
35
35
|
"@ttoss/cloudformation": "^0.12.10"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/jest": "^30.0.0",
|
|
39
|
-
"@types/pg": "^8.
|
|
39
|
+
"@types/pg": "^8.20.0",
|
|
40
40
|
"aws-sdk-client-mock": "^4.1.0",
|
|
41
41
|
"jest": "^30.3.0",
|
|
42
42
|
"tsup": "^8.5.1",
|