data-api-client 1.0.1 → 1.2.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 +57 -13
- package/index.js +152 -67
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
3
|
[](https://travis-ci.org/jeremydaly/data-api-client)
|
|
4
4
|
[](https://www.npmjs.com/package/data-api-client)
|
|
@@ -24,16 +24,20 @@ const data = require('data-api-client')({
|
|
|
24
24
|
|
|
25
25
|
// Simple SELECT
|
|
26
26
|
let result = await data.query(`SELECT * FROM myTable`)
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// {
|
|
28
|
+
// records: [
|
|
29
|
+
// { id: 1, name: 'Alice', age: null },
|
|
30
|
+
// { id: 2, name: 'Mike', age: 52 },
|
|
31
|
+
// { id: 3, name: 'Carol', age: 50 }
|
|
32
|
+
// ]
|
|
33
|
+
// }
|
|
30
34
|
|
|
31
35
|
// SELECT with named parameters
|
|
32
36
|
let resultParams = await data.query(
|
|
33
37
|
`SELECT * FROM myTable WHERE id = :id`,
|
|
34
38
|
{ id: 2 }
|
|
35
39
|
)
|
|
36
|
-
// [ { id: 2, name: 'Mike', age: 52 } ]
|
|
40
|
+
// { records: [ { id: 2, name: 'Mike', age: 52 } ] }
|
|
37
41
|
|
|
38
42
|
// INSERT with named parameters
|
|
39
43
|
let insert = await data.query(
|
|
@@ -133,11 +137,17 @@ Below is a table containing all of the possible configuration options for the `d
|
|
|
133
137
|
| resourceArn | `string` | The ARN of your Aurora Serverless Cluster. This value is *required*, but can be overridden when querying. | |
|
|
134
138
|
| secretArn | `string` | The ARN of the secret associated with your database credentials. This is *required*, but can be overridden when querying. | |
|
|
135
139
|
| database | `string` | *Optional* default database to use with queries. Can be overridden when querying. | |
|
|
140
|
+
| engine | `mysql` or `pg` | The type of database engine you're connecting to (MySQL or Postgres). | `mysql` |
|
|
136
141
|
| hydrateColumnNames | `boolean` | When `true`, results will be returned as objects with column names as keys. If `false`, results will be returned as an array of values. | `true` |
|
|
137
|
-
| keepAlive | `boolean` |
|
|
138
|
-
| sslEnabled | `boolean` |
|
|
142
|
+
| ~~keepAlive~~ (deprecated) | `boolean` | See [Connection Reuse](#connection-reuse) below. | |
|
|
143
|
+
| ~~sslEnabled~~ (deprecated) | `boolean` | Set this in the `options` | `true` |
|
|
139
144
|
| options | `object` | An *optional* configuration object that is passed directly into the RDSDataService constructor. See [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDSDataService.html#constructor-property) for available options. | `{}` |
|
|
140
|
-
| region | `string` |
|
|
145
|
+
| ~~region~~ (deprecated) | `string` | Set this in the `options` | |
|
|
146
|
+
| formatOptions | `object` | Formatting options to auto parse dates and coerce native JavaScript date objects to MySQL supported date formats. Valid keys are `deserializeDate` and `treatAsLocalDate`. Both accept boolean values. | Both `false` |
|
|
147
|
+
|
|
148
|
+
### Connection Reuse
|
|
149
|
+
It is recommended to enable connection reuse as this dramatically decreases the latency of subsequent calls to the AWS API. This can be done by setting an environment variable
|
|
150
|
+
`AWS_NODEJS_CONNECTION_REUSE_ENABLED=1`. For more information see the [AWS SDK documentation](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html).
|
|
141
151
|
|
|
142
152
|
## How to use this module
|
|
143
153
|
|
|
@@ -227,6 +237,35 @@ SELECT `id`, `name`, `created` FROM `table_123abc` WHERE id > :id LIMIT 10
|
|
|
227
237
|
|
|
228
238
|
You'll notice that we leave the *named parameters* alone. Anything that Data API and the `RDSDataService` Class currently handles, we defer to them.
|
|
229
239
|
|
|
240
|
+
### Type-Casting
|
|
241
|
+
The Aurora Data API can sometimes give you trouble with certain data types, such as uuid, unless you explicitly cast them. While you can certainly do this manually in your SQL string, the Data API Client offers a really easy way to handle this for you.
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
const result = await data.query(
|
|
245
|
+
'INSERT INTO users(id, email, full_name, metadata) VALUES(:id, :email, :fullName, :metadata)',
|
|
246
|
+
[
|
|
247
|
+
{
|
|
248
|
+
name: 'id',
|
|
249
|
+
value: newUserId,
|
|
250
|
+
cast: 'uuid'
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'email',
|
|
254
|
+
value: email
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: 'fullName',
|
|
258
|
+
value: fullName
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'metadata',
|
|
262
|
+
value: JSON.stringify(userMetadata),
|
|
263
|
+
cast: 'jsonb'
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
```
|
|
268
|
+
|
|
230
269
|
### Batch Queries
|
|
231
270
|
The `RDSDataService` Class provides a `batchExecuteStatement` method that allows you to execute a prepared statement multiple times using different parameter sets. This is only allowed for `INSERT`, `UPDATE` and `DELETE` queries, but is much more efficient than issuing multiple `executeStatement` calls. The Data API Client handles the switching for you based on *how* you send in your parameters.
|
|
232
271
|
|
|
@@ -285,7 +324,7 @@ The Data API Client exposes *promisified* versions of the five RDSDataService me
|
|
|
285
324
|
- `executeStatement`
|
|
286
325
|
- `rollbackTransaction`
|
|
287
326
|
|
|
288
|
-
The default configuration information (`resourceArn`, `secretArn`, and `database`) are
|
|
327
|
+
The default configuration information (`resourceArn`, `secretArn`, and `database`) are merged with your supplied parameters, so supplying those values are optional.
|
|
289
328
|
|
|
290
329
|
```javascript
|
|
291
330
|
let result = await data.executeStatement({
|
|
@@ -344,7 +383,7 @@ No worries! The Data API Client gives you the ability to parameterize identifier
|
|
|
344
383
|
This one is a bit frustrating. If you execute a standard `executeStatement`, then it will return a `numberOfRecordsUpdated` field for `UPDATE` and `DELETE` queries. This is handy for knowing if your query succeeded. Unfortunately, a `batchExecuteStatement` does not return this field for you.
|
|
345
384
|
|
|
346
385
|
## Enabling Data API
|
|
347
|
-
In order to use the Data API, you must enable it on your Aurora Serverless Cluster and create a Secret. You also
|
|
386
|
+
In order to use the Data API, you must enable it on your Aurora Serverless Cluster and create a Secret. You also must grant your execution environment a number of permission as outlined in the following sections.
|
|
348
387
|
|
|
349
388
|
### Enable Data API on your Aurora Serverless Cluster
|
|
350
389
|
|
|
@@ -369,7 +408,7 @@ You can then configure your rotation settings, if you want, and then you review
|
|
|
369
408
|
|
|
370
409
|
### Required Permissions
|
|
371
410
|
|
|
372
|
-
In order to use the Data API, your execution environment requires several IAM permissions. Below are the minimum permissions required.
|
|
411
|
+
In order to use the Data API, your execution environment requires several IAM permissions. Below are the minimum permissions required. **Please Note:** The `Resource: "*"` permission for `rds-data` is recommended by AWS (see [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazonrdsdataapi.html#amazonrdsdataapi-resources-for-iam-policies)) because Amazon RDS Data API does not support specifying a resource ARN. The credentials specified in Secrets Manager can be used to restrict access to specific databases.
|
|
373
412
|
|
|
374
413
|
**YAML:**
|
|
375
414
|
```yaml
|
|
@@ -382,7 +421,7 @@ Statement:
|
|
|
382
421
|
- "rds-data:BeginTransaction"
|
|
383
422
|
- "rds-data:RollbackTransaction"
|
|
384
423
|
- "rds-data:CommitTransaction"
|
|
385
|
-
Resource: "
|
|
424
|
+
Resource: "*"
|
|
386
425
|
- Effect: "Allow"
|
|
387
426
|
Action:
|
|
388
427
|
- "secretsmanager:GetSecretValue"
|
|
@@ -402,7 +441,7 @@ Statement:
|
|
|
402
441
|
"rds-data:RollbackTransaction",
|
|
403
442
|
"rds-data:CommitTransaction"
|
|
404
443
|
],
|
|
405
|
-
"Resource": "
|
|
444
|
+
"Resource": "*"
|
|
406
445
|
},
|
|
407
446
|
{
|
|
408
447
|
"Effect": "Allow",
|
|
@@ -412,5 +451,10 @@ Statement:
|
|
|
412
451
|
]
|
|
413
452
|
```
|
|
414
453
|
|
|
454
|
+
## Sponsors
|
|
455
|
+
|
|
456
|
+
[](https://ad.doubleclick.net/ddm/trackclk/N1116303.3950900PODSEARCH.COM/B24770737.285235234;dc_trk_aid=479074825;dc_trk_cid=139488579;dc_lat=;dc_rdid=;tag_for_child_directed_treatment=;tfua=;gdpr=${GDPR};gdpr_consent=${GDPR_CONSENT_755})
|
|
457
|
+
<IMG SRC="https://ad.doubleclick.net/ddm/trackimp/N1116303.3950900PODSEARCH.COM/B24770737.285235234;dc_trk_aid=479074825;dc_trk_cid=139488579;ord=[timestamp];dc_lat=;dc_rdid=;tag_for_child_directed_treatment=;tfua=;gdpr=${GDPR};gdpr_consent=${GDPR_CONSENT_755}?" BORDER="0" HEIGHT="1" WIDTH="1" ALT="Advertisement">
|
|
458
|
+
|
|
415
459
|
## Contributions
|
|
416
460
|
Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/data-api-client/issues) for suggestions and bug reports or create a pull request. You can also contact me on Twitter: [@jeremy_daly](https://twitter.com/jeremy_daly).
|
package/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html
|
|
9
9
|
*
|
|
10
10
|
* @author Jeremy Daly <jeremy@jeremydaly.com>
|
|
11
|
-
* @version 1.
|
|
11
|
+
* @version 1.2.0
|
|
12
12
|
* @license MIT
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -31,21 +31,6 @@ const supportedTypes = [
|
|
|
31
31
|
'structValue'
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
-
/**********************************************************************/
|
|
35
|
-
/** Enable HTTP Keep-Alive per https://vimeo.com/287511222 **/
|
|
36
|
-
/** This dramatically increases the speed of subsequent HTTP calls **/
|
|
37
|
-
/**********************************************************************/
|
|
38
|
-
|
|
39
|
-
const https = require('https')
|
|
40
|
-
|
|
41
|
-
const sslAgent = new https.Agent({
|
|
42
|
-
keepAlive: true,
|
|
43
|
-
maxSockets: 50, // same as aws-sdk
|
|
44
|
-
rejectUnauthorized: true // same as aws-sdk
|
|
45
|
-
})
|
|
46
|
-
sslAgent.setMaxListeners(0) // same as aws-sdk
|
|
47
|
-
|
|
48
|
-
|
|
49
34
|
/********************************************************************/
|
|
50
35
|
/** PRIVATE METHODS **/
|
|
51
36
|
/********************************************************************/
|
|
@@ -75,7 +60,7 @@ const parseDatabase = (config,args) =>
|
|
|
75
60
|
: typeof args[0].database === 'string' ? args[0].database
|
|
76
61
|
: args[0].database ? error('\'database\' must be a string.')
|
|
77
62
|
: config.database ? config.database
|
|
78
|
-
: error('No \'database\' provided.')
|
|
63
|
+
: undefined // removed for #47 - error('No \'database\' provided.')
|
|
79
64
|
|
|
80
65
|
// Parse the supplied hydrateColumnNames command, or default to config
|
|
81
66
|
const parseHydrate = (config,args) =>
|
|
@@ -83,6 +68,19 @@ const parseHydrate = (config,args) =>
|
|
|
83
68
|
: args[0].hydrateColumnNames ? error('\'hydrateColumnNames\' must be a boolean.')
|
|
84
69
|
: config.hydrateColumnNames
|
|
85
70
|
|
|
71
|
+
// Parse the supplied format options, or default to config
|
|
72
|
+
const parseFormatOptions = (config,args) =>
|
|
73
|
+
typeof args[0].formatOptions === 'object' ? {
|
|
74
|
+
deserializeDate: typeof args[0].formatOptions.deserializeDate === 'boolean' ? args[0].formatOptions.deserializeDate
|
|
75
|
+
: args[0].formatOptions.deserializeDate ? error('\'formatOptions.deserializeDate\' must be a boolean.')
|
|
76
|
+
: config.formatOptions.deserializeDate,
|
|
77
|
+
treatAsLocalDate: typeof args[0].formatOptions.treatAsLocalDate == 'boolean' ? args[0].formatOptions.treatAsLocalDate
|
|
78
|
+
: args[0].formatOptions.treatAsLocalDate ? error('\'formatOptions.treatAsLocalDate\' must be a boolean.')
|
|
79
|
+
: config.formatOptions.treatAsLocalDate
|
|
80
|
+
}
|
|
81
|
+
: args[0].formatOptions ? error('\'formatOptions\' must be an object.')
|
|
82
|
+
: config.formatOptions
|
|
83
|
+
|
|
86
84
|
// Prepare method params w/ supplied inputs if an object is passed
|
|
87
85
|
const prepareParams = ({ secretArn,resourceArn },args) => {
|
|
88
86
|
return Object.assign(
|
|
@@ -106,27 +104,38 @@ const pick = (obj,values) => Object.keys(obj).reduce((acc,x) =>
|
|
|
106
104
|
const flatten = arr => arr.reduce((acc,x) => acc.concat(x),[])
|
|
107
105
|
|
|
108
106
|
// Normize parameters so that they are all in standard format
|
|
109
|
-
const normalizeParams = params => params.reduce((acc,p) =>
|
|
107
|
+
const normalizeParams = params => params.reduce((acc, p) =>
|
|
110
108
|
Array.isArray(p) ? acc.concat([normalizeParams(p)])
|
|
111
|
-
:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
: (
|
|
110
|
+
(Object.keys(p).length === 2 && p.name && typeof p.value !== 'undefined') ||
|
|
111
|
+
(Object.keys(p).length === 3 && p.name && typeof p.value !== 'undefined' && p.cast)
|
|
112
|
+
) ? acc.concat(p)
|
|
113
|
+
: acc.concat(splitParams(p))
|
|
114
|
+
, []) // end reduce
|
|
115
115
|
|
|
116
116
|
// Prepare parameters
|
|
117
|
-
const processParams = (sql,sqlParams,params,row=0) => {
|
|
117
|
+
const processParams = (engine,sql,sqlParams,params,formatOptions,row=0) => {
|
|
118
118
|
return {
|
|
119
119
|
processedParams: params.reduce((acc,p) => {
|
|
120
120
|
if (Array.isArray(p)) {
|
|
121
|
-
|
|
121
|
+
const result = processParams(engine,sql,sqlParams,p,formatOptions,row)
|
|
122
122
|
if (row === 0) { sql = result.escapedSql; row++ }
|
|
123
123
|
return acc.concat([result.processedParams])
|
|
124
124
|
} else if (sqlParams[p.name]) {
|
|
125
125
|
if (sqlParams[p.name].type === 'n_ph') {
|
|
126
|
-
|
|
126
|
+
if (p.cast) {
|
|
127
|
+
const regex = new RegExp(':' + p.name + '\\b', 'g')
|
|
128
|
+
sql = sql.replace(
|
|
129
|
+
regex,
|
|
130
|
+
engine === 'pg'
|
|
131
|
+
? `:${p.name}::${p.cast}`
|
|
132
|
+
: `CAST(:${p.name} AS ${p.cast})`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
acc.push(formatParam(p.name,p.value,formatOptions))
|
|
127
136
|
} else if (row === 0) {
|
|
128
|
-
|
|
129
|
-
sql = sql.replace(regex,sqlString.escapeId(p.value))
|
|
137
|
+
const regex = new RegExp('::' + p.name + '\\b', 'g')
|
|
138
|
+
sql = sql.replace(regex, sqlString.escapeId(p.value))
|
|
130
139
|
}
|
|
131
140
|
return acc
|
|
132
141
|
} else {
|
|
@@ -138,7 +147,7 @@ const processParams = (sql,sqlParams,params,row=0) => {
|
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
// Converts parameter to the name/value format
|
|
141
|
-
const formatParam = (n,v) => formatType(n,v,getType(v))
|
|
150
|
+
const formatParam = (n,v,formatOptions) => formatType(n,v,getType(v),getTypeHint(v),formatOptions)
|
|
142
151
|
|
|
143
152
|
// Converts object params into name/value format
|
|
144
153
|
const splitParams = p => Object.keys(p).reduce((arr,x) =>
|
|
@@ -174,6 +183,7 @@ const getType = val =>
|
|
|
174
183
|
: typeof val === 'number' && parseInt(val) === val ? 'longValue'
|
|
175
184
|
: typeof val === 'number' && parseFloat(val) === val ? 'doubleValue'
|
|
176
185
|
: val === null ? 'isNull'
|
|
186
|
+
: isDate(val) ? 'stringValue'
|
|
177
187
|
: Buffer.isBuffer(val) ? 'blobValue'
|
|
178
188
|
// : Array.isArray(val) ? 'arrayValue' This doesn't work yet
|
|
179
189
|
// TODO: there is a 'structValue' now for postgres
|
|
@@ -182,20 +192,56 @@ const getType = val =>
|
|
|
182
192
|
&& supportedTypes.includes(Object.keys(val)[0]) ? null
|
|
183
193
|
: undefined
|
|
184
194
|
|
|
195
|
+
// Hint to specify the underlying object type for data type mapping
|
|
196
|
+
const getTypeHint = val =>
|
|
197
|
+
isDate(val) ? 'TIMESTAMP' : undefined
|
|
198
|
+
|
|
199
|
+
const isDate = val =>
|
|
200
|
+
val instanceof Date
|
|
201
|
+
|
|
185
202
|
// Creates a standard Data API parameter using the supplied inputs
|
|
186
|
-
const formatType = (name,value,type) => {
|
|
203
|
+
const formatType = (name,value,type,typeHint,formatOptions) => {
|
|
187
204
|
return Object.assign(
|
|
188
|
-
{ name },
|
|
205
|
+
typeHint != null ? { name, typeHint } : { name },
|
|
189
206
|
type === null ? { value }
|
|
190
207
|
: {
|
|
191
208
|
value: {
|
|
192
209
|
[type ? type : error(`'${name}' is an invalid type`)]
|
|
193
|
-
: type === 'isNull' ? true
|
|
210
|
+
: type === 'isNull' ? true
|
|
211
|
+
: isDate(value) ? formatToTimeStamp(value, formatOptions && formatOptions.treatAsLocalDate)
|
|
212
|
+
: value
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
)
|
|
197
216
|
} // end formatType
|
|
198
217
|
|
|
218
|
+
// Formats the (UTC) date to the AWS accepted YYYY-MM-DD HH:MM:SS[.FFF] format
|
|
219
|
+
// See https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_SqlParameter.html
|
|
220
|
+
const formatToTimeStamp = (date, treatAsLocalDate) => {
|
|
221
|
+
const pad = (val,num=2) => '0'.repeat(num-(val + '').length) + val
|
|
222
|
+
|
|
223
|
+
const year = treatAsLocalDate ? date.getFullYear() : date.getUTCFullYear()
|
|
224
|
+
const month = (treatAsLocalDate ? date.getMonth() : date.getUTCMonth()) + 1 // Convert to human month
|
|
225
|
+
const day = treatAsLocalDate ? date.getDate() : date.getUTCDate()
|
|
226
|
+
|
|
227
|
+
const hours = treatAsLocalDate ? date.getHours() : date.getUTCHours()
|
|
228
|
+
const minutes = treatAsLocalDate ? date.getMinutes() : date.getUTCMinutes()
|
|
229
|
+
const seconds = treatAsLocalDate ? date.getSeconds() : date.getUTCSeconds()
|
|
230
|
+
const ms = treatAsLocalDate ? date.getMilliseconds() : date.getUTCMilliseconds()
|
|
231
|
+
|
|
232
|
+
const fraction = ms <= 0 ? '' : `.${pad(ms,3)}`
|
|
233
|
+
|
|
234
|
+
return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}${fraction}`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Converts the string value to a Date object.
|
|
238
|
+
// If standard TIMESTAMP format (YYYY-MM-DD[ HH:MM:SS[.FFF]]) without TZ + treatAsLocalDate=false then assume UTC Date
|
|
239
|
+
// In all other cases convert value to datetime as-is (also values with TZ info)
|
|
240
|
+
const formatFromTimeStamp = (value,treatAsLocalDate) =>
|
|
241
|
+
!treatAsLocalDate && /^\d{4}-\d{2}-\d{2}(\s\d{2}:\d{2}:\d{2}(\.\d{3})?)?$/.test(value) ?
|
|
242
|
+
new Date(value + 'Z') :
|
|
243
|
+
new Date(value)
|
|
244
|
+
|
|
199
245
|
// Formats the results of a query response
|
|
200
246
|
const formatResults = (
|
|
201
247
|
{ // destructure results
|
|
@@ -206,27 +252,27 @@ const formatResults = (
|
|
|
206
252
|
updateResults // ONLY on batchExecuteStatement
|
|
207
253
|
},
|
|
208
254
|
hydrate,
|
|
209
|
-
includeMeta
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
includeMeta,
|
|
256
|
+
formatOptions
|
|
257
|
+
) => Object.assign(
|
|
258
|
+
includeMeta ? { columnMetadata } : {},
|
|
259
|
+
numberOfRecordsUpdated !== undefined && !records ? { numberOfRecordsUpdated } : {},
|
|
260
|
+
records ? {
|
|
261
|
+
records: formatRecords(records, columnMetadata, hydrate, formatOptions)
|
|
262
|
+
} : {},
|
|
263
|
+
updateResults ? { updateResults: formatUpdateResults(updateResults) } : {},
|
|
264
|
+
generatedFields && generatedFields.length > 0 ?
|
|
265
|
+
{ insertId: generatedFields[0].longValue } : {}
|
|
266
|
+
)
|
|
221
267
|
|
|
222
268
|
// Processes records and either extracts Typed Values into an array, or
|
|
223
269
|
// object with named column labels
|
|
224
|
-
const formatRecords = (recs,columns) => {
|
|
270
|
+
const formatRecords = (recs,columns,hydrate,formatOptions) => {
|
|
225
271
|
|
|
226
272
|
// Create map for efficient value parsing
|
|
227
273
|
let fmap = recs && recs[0] ? recs[0].map((x,i) => {
|
|
228
274
|
return Object.assign({},
|
|
229
|
-
columns ? { label: columns[i].label } : {} ) // add column
|
|
275
|
+
columns ? { label: columns[i].label, typeName: columns[i].typeName } : {} ) // add column label and typeName
|
|
230
276
|
}) : {}
|
|
231
277
|
|
|
232
278
|
// Map over all the records (rows)
|
|
@@ -237,15 +283,16 @@ const formatRecords = (recs,columns) => {
|
|
|
237
283
|
|
|
238
284
|
// If the field is null, always return null
|
|
239
285
|
if (field.isNull === true) {
|
|
240
|
-
return
|
|
286
|
+
return hydrate ? // object if hydrate, else array
|
|
241
287
|
Object.assign(acc,{ [fmap[i].label]: null })
|
|
242
288
|
: acc.concat(null)
|
|
243
289
|
|
|
244
290
|
// If the field is mapped, return the mapped field
|
|
245
291
|
} else if (fmap[i] && fmap[i].field) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
292
|
+
const value = formatRecordValue(field[fmap[i].field],fmap[i].typeName,formatOptions)
|
|
293
|
+
return hydrate ? // object if hydrate, else array
|
|
294
|
+
Object.assign(acc,{ [fmap[i].label]: value })
|
|
295
|
+
: acc.concat(value)
|
|
249
296
|
|
|
250
297
|
// Else discover the field type
|
|
251
298
|
} else {
|
|
@@ -258,15 +305,22 @@ const formatRecords = (recs,columns) => {
|
|
|
258
305
|
})
|
|
259
306
|
|
|
260
307
|
// Return the mapped field (this should NEVER be null)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
308
|
+
const value = formatRecordValue(field[fmap[i].field],fmap[i].typeName,formatOptions)
|
|
309
|
+
return hydrate ? // object if hydrate, else array
|
|
310
|
+
Object.assign(acc,{ [fmap[i].label]: value })
|
|
311
|
+
: acc.concat(value)
|
|
264
312
|
}
|
|
265
313
|
|
|
266
|
-
},
|
|
314
|
+
}, hydrate ? {} : []) // init object if hydrate, else init array
|
|
267
315
|
}) : [] // empty record set returns an array
|
|
268
316
|
} // end formatRecords
|
|
269
317
|
|
|
318
|
+
// Format record value based on its value, the database column's typeName and the formatting options
|
|
319
|
+
const formatRecordValue = (value,typeName,formatOptions) => formatOptions && formatOptions.deserializeDate &&
|
|
320
|
+
['DATE', 'DATETIME', 'TIMESTAMP', 'TIMESTAMP WITH TIME ZONE'].includes(typeName)
|
|
321
|
+
? formatFromTimeStamp(value,(formatOptions && formatOptions.treatAsLocalDate) || typeName === 'TIMESTAMP WITH TIME ZONE')
|
|
322
|
+
: value
|
|
323
|
+
|
|
270
324
|
// Format updateResults and extract insertIds
|
|
271
325
|
const formatUpdateResults = res => res.map(x => {
|
|
272
326
|
return x.generatedFields && x.generatedFields.length > 0 ?
|
|
@@ -286,7 +340,6 @@ const mergeConfig = (initialConfig,args) =>
|
|
|
286
340
|
|
|
287
341
|
// Query function (use standard form for `this` context)
|
|
288
342
|
const query = async function(config,..._args) {
|
|
289
|
-
|
|
290
343
|
// Flatten array if nested arrays (fixes #30)
|
|
291
344
|
const args = Array.isArray(_args[0]) ? flatten(_args) : _args
|
|
292
345
|
|
|
@@ -297,15 +350,18 @@ const query = async function(config,..._args) {
|
|
|
297
350
|
// Parse hydration setting
|
|
298
351
|
const hydrateColumnNames = parseHydrate(config,args)
|
|
299
352
|
|
|
353
|
+
// Parse data format settings
|
|
354
|
+
const formatOptions = parseFormatOptions(config,args)
|
|
355
|
+
|
|
300
356
|
// Parse and normalize parameters
|
|
301
357
|
const parameters = normalizeParams(parseParams(args))
|
|
302
358
|
|
|
303
359
|
// Process parameters and escape necessary SQL
|
|
304
|
-
const { processedParams,escapedSql } = processParams(sql,sqlParams,parameters)
|
|
360
|
+
const { processedParams,escapedSql } = processParams(config.engine,sql,sqlParams,parameters,formatOptions)
|
|
305
361
|
|
|
306
362
|
// Determine if this is a batch request
|
|
307
363
|
const isBatch = processedParams.length > 0
|
|
308
|
-
&& Array.isArray(processedParams[0])
|
|
364
|
+
&& Array.isArray(processedParams[0])
|
|
309
365
|
|
|
310
366
|
// Create/format the parameters
|
|
311
367
|
const params = Object.assign(
|
|
@@ -324,7 +380,7 @@ const query = async function(config,..._args) {
|
|
|
324
380
|
config.transactionId ? { transactionId: config.transactionId } : {}
|
|
325
381
|
) // end params
|
|
326
382
|
|
|
327
|
-
try { // attempt to run the query
|
|
383
|
+
try { // attempt to run the query
|
|
328
384
|
|
|
329
385
|
// Capture the result for debugging
|
|
330
386
|
let result = await (isBatch ? config.RDS.batchExecuteStatement(params).promise()
|
|
@@ -334,7 +390,8 @@ const query = async function(config,..._args) {
|
|
|
334
390
|
return formatResults(
|
|
335
391
|
result,
|
|
336
392
|
hydrateColumnNames,
|
|
337
|
-
args[0].includeResultMetadata === true
|
|
393
|
+
args[0].includeResultMetadata === true,
|
|
394
|
+
formatOptions
|
|
338
395
|
)
|
|
339
396
|
|
|
340
397
|
} catch(e) {
|
|
@@ -370,6 +427,7 @@ const transaction = (config,_args) => {
|
|
|
370
427
|
{
|
|
371
428
|
database: parseDatabase(config,args), // add database
|
|
372
429
|
hydrateColumnNames: parseHydrate(config,args), // add hydrate
|
|
430
|
+
formatOptions: parseFormatOptions(config,args), // add formatOptions
|
|
373
431
|
RDS: config.RDS // reference the RDSDataService instance
|
|
374
432
|
}
|
|
375
433
|
)
|
|
@@ -429,32 +487,49 @@ const commit = async (config,queries,rollback) => {
|
|
|
429
487
|
/********************************************************************/
|
|
430
488
|
|
|
431
489
|
// Export main function
|
|
432
|
-
|
|
490
|
+
/**
|
|
491
|
+
* Create a Data API client instance
|
|
492
|
+
* @param {object} params
|
|
493
|
+
* @param {'mysql'|'pg'} [params.engine=mysql] The type of database (MySQL or Postgres)
|
|
494
|
+
* @param {string} params.resourceArn The ARN of your Aurora Serverless Cluster
|
|
495
|
+
* @param {string} params.secretArn The ARN of the secret associated with your
|
|
496
|
+
* database credentials
|
|
497
|
+
* @param {string} [params.database] The name of the database
|
|
498
|
+
* @param {boolean} [params.hydrateColumnNames=true] Return objects with column
|
|
499
|
+
* names as keys
|
|
500
|
+
* @param {object} [params.options={}] Configuration object passed directly
|
|
501
|
+
* into RDSDataService
|
|
502
|
+
* @param {object} [params.formatOptions] Date-related formatting options
|
|
503
|
+
* @param {boolean} [params.formatOptions.deserializeDate=false]
|
|
504
|
+
* @param {boolean} [params.formatOptions.treatAsLocalDate=false]
|
|
505
|
+
* @param {boolean} [params.keepAlive] DEPRECATED
|
|
506
|
+
* @param {boolean} [params.sslEnabled=true] DEPRECATED
|
|
507
|
+
* @param {string} [params.region] DEPRECATED
|
|
508
|
+
*
|
|
509
|
+
*/
|
|
510
|
+
const init = params => {
|
|
433
511
|
|
|
434
512
|
// Set the options for the RDSDataService
|
|
435
513
|
const options = typeof params.options === 'object' ? params.options
|
|
436
514
|
: params.options !== undefined ? error('\'options\' must be an object')
|
|
437
515
|
: {}
|
|
438
516
|
|
|
439
|
-
// Update the default AWS http agent with our new sslAgent
|
|
440
|
-
if (typeof params.keepAlive === 'boolean' ? params.keepAlive : true) {
|
|
441
|
-
AWS.config.update({ httpOptions: { agent: sslAgent } })
|
|
442
|
-
}
|
|
443
|
-
|
|
444
517
|
// Update the AWS http agent with the region
|
|
445
518
|
if (typeof params.region === 'string') {
|
|
446
|
-
|
|
519
|
+
options.region = params.region
|
|
447
520
|
}
|
|
448
521
|
|
|
449
522
|
// Disable ssl if wanted for local development
|
|
450
523
|
if (params.sslEnabled === false) {
|
|
451
|
-
// AWS.config.update({ sslEnabled: false })
|
|
452
524
|
options.sslEnabled = false
|
|
453
525
|
}
|
|
454
526
|
|
|
455
|
-
|
|
456
527
|
// Set the configuration for this instance
|
|
457
528
|
const config = {
|
|
529
|
+
// Require engine
|
|
530
|
+
engine: typeof params.engine === 'string' ?
|
|
531
|
+
params.engine
|
|
532
|
+
: 'mysql',
|
|
458
533
|
|
|
459
534
|
// Require secretArn
|
|
460
535
|
secretArn: typeof params.secretArn === 'string' ?
|
|
@@ -482,6 +557,14 @@ module.exports = (params) => {
|
|
|
482
557
|
typeof params.hydrateColumnNames === 'boolean' ?
|
|
483
558
|
params.hydrateColumnNames : true,
|
|
484
559
|
|
|
560
|
+
// Value formatting options. For date the deserialization is enabled and (re)stored as UTC
|
|
561
|
+
formatOptions: {
|
|
562
|
+
deserializeDate:
|
|
563
|
+
typeof params.formatOptions === 'object' && params.formatOptions.deserializeDate === false ? false : true,
|
|
564
|
+
treatAsLocalDate:
|
|
565
|
+
typeof params.formatOptions === 'object' && params.formatOptions.treatAsLocalDate
|
|
566
|
+
},
|
|
567
|
+
|
|
485
568
|
// TODO: Put this in a separate module for testing?
|
|
486
569
|
// Create an instance of RDSDataService
|
|
487
570
|
RDS: new AWS.RDSDataService(options)
|
|
@@ -519,3 +602,5 @@ module.exports = (params) => {
|
|
|
519
602
|
}
|
|
520
603
|
|
|
521
604
|
} // end exports
|
|
605
|
+
|
|
606
|
+
module.exports = init
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-api-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A lightweight wrapper that simplifies working with the Amazon Aurora Serverless Data API",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,17 +25,15 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/jeremydaly/data-api-client#readme",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"aws-sdk": "^2.
|
|
29
|
-
"eslint": "^6.
|
|
30
|
-
"jest": "^
|
|
31
|
-
"rewire": "^
|
|
28
|
+
"aws-sdk": "^2.811.0",
|
|
29
|
+
"eslint": "^6.8.0",
|
|
30
|
+
"jest": "^27.5.1",
|
|
31
|
+
"rewire": "^6.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"sqlstring": "^2.3.
|
|
34
|
+
"sqlstring": "^2.3.2"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"LICENSE",
|
|
38
|
-
"README.md",
|
|
39
37
|
"index.js"
|
|
40
38
|
]
|
|
41
39
|
}
|