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.
Files changed (3) hide show
  1. package/README.md +57 -13
  2. package/index.js +152 -67
  3. package/package.json +6 -8
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Aurora Serverless Data API Client
1
+ ![Aurora Serverless Data API Client](https://user-images.githubusercontent.com/2053544/79285017-44053500-7e8a-11ea-8515-998ccf9c2d2e.png)
2
2
 
3
3
  [![Build Status](https://travis-ci.org/jeremydaly/data-api-client.svg?branch=master)](https://travis-ci.org/jeremydaly/data-api-client)
4
4
  [![npm](https://img.shields.io/npm/v/data-api-client.svg)](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
- // [ { id: 1, name: 'Alice', age: null },
28
- // { id: 2, name: 'Mike', age: 52 },
29
- // { id: 3, name: 'Carol', age: 50 } ]
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` | Enables HTTP Keep-Alive for calls to the AWS SDK. This dramatically decreases the latency of subsequent calls. | `true` |
138
- | sslEnabled | `boolean` | *Optional* Enables SSL HTTP endpoint. Can be disable for local development. | `true` |
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` | *Optional* AWS region to use. | `aws-sdk default` |
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 merge with your supplied parameters, so supplying those values are optional.
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 musst grant your execution environment a number of permission as outlined in the following sections.
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: "arn:aws:rds:{REGION}:{ACCOUNT-ID}:cluster:{YOUR-CLUSTER-NAME}"
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": "arn:aws:rds:{REGION}:{ACCOUNT-ID}:cluster:{YOUR-CLUSTER-NAME}"
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
+ [![New Relic](https://user-images.githubusercontent.com/2053544/96728664-55238700-1382-11eb-93cb-82fe7cb5e043.png)](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.0.0-beta
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
- : Object.keys(p).length === 2 && p.name && p.value ? acc.concat(p)
112
- : acc.concat(splitParams(p))
113
- ,[]) // end reduce
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
- let result = processParams(sql,sqlParams,p,row)
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
- acc.push(formatParam(p.name,p.value))
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
- let regex = new RegExp('::' + p.name + '\\b','g')
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 : value
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
- Object.assign(
212
- includeMeta ? { columnMetadata } : {},
213
- numberOfRecordsUpdated !== undefined && !records ? { numberOfRecordsUpdated } : {},
214
- records ? {
215
- records: formatRecords(records, hydrate ? columnMetadata : false)
216
- } : {},
217
- updateResults ? { updateResults: formatUpdateResults(updateResults) } : {},
218
- generatedFields && generatedFields.length > 0 ?
219
- { insertId: generatedFields[0].longValue } : {}
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 labels
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 columns ? // object if hydrate, else array
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
- return columns ? // object if hydrate, else array
247
- Object.assign(acc,{ [fmap[i].label]: field[fmap[i].field] })
248
- : acc.concat(field[fmap[i].field])
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
- return columns ? // object if hydrate, else array
262
- Object.assign(acc,{ [fmap[i].label]: field[fmap[i].field] })
263
- : acc.concat(field[fmap[i].field])
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
- }, columns ? {} : []) // init object if hydrate, else init array
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]) ? true : false
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 ? true : false
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
- module.exports = (params) => {
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
- AWS.config.update({ region: params.region })
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.0.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.588.0",
29
- "eslint": "^6.7.2",
30
- "jest": "^25.0.0",
31
- "rewire": "^4.0.1"
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.1"
34
+ "sqlstring": "^2.3.2"
35
35
  },
36
36
  "files": [
37
- "LICENSE",
38
- "README.md",
39
37
  "index.js"
40
38
  ]
41
39
  }