data-api-client 1.2.1 → 1.3.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 +122 -93
  2. package/index.js +290 -271
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  ![Aurora Serverless Data API Client](https://user-images.githubusercontent.com/2053544/79285017-44053500-7e8a-11ea-8515-998ccf9c2d2e.png)
2
2
 
3
- [![Build Status](https://travis-ci.org/jeremydaly/data-api-client.svg?branch=master)](https://travis-ci.org/jeremydaly/data-api-client)
4
3
  [![npm](https://img.shields.io/npm/v/data-api-client.svg)](https://www.npmjs.com/package/data-api-client)
5
4
  [![npm](https://img.shields.io/npm/l/data-api-client.svg)](https://www.npmjs.com/package/data-api-client)
6
5
 
6
+ > #### Project Update: October 7, 2024
7
+ >
8
+ > With the recent announcement that Amazon Aurora MySQL-Compatible Edition now supports a redesigned [RDS Data API for Aurora Serverless v2 and Aurora provisioned database instances](https://aws.amazon.com/about-aws/whats-new/2024/09/amazon-aurora-mysql-rds-data-api/), there have been several requests to add support to this project. The new RDS Data API also supports [Amazon Aurora PostgreSQL-Compatible Edition](https://aws.amazon.com/about-aws/whats-new/2023/12/amazon-aurora-postgresql-rds-data-api/) (more detail [here](https://aws.amazon.com/blogs/database/introducing-the-data-api-for-amazon-aurora-serverless-v2-and-amazon-aurora-provisioned-clusters/)).
9
+ >
10
+ > Star and watch the project for the 2.0 branch updates.
11
+
7
12
  The **Data API Client** is a lightweight wrapper that simplifies working with the Amazon Aurora Serverless Data API by abstracting away the notion of field values. This abstraction annotates native JavaScript types supplied as input parameters, as well as converts annotated response data to native JavaScript types. It's basically a [DocumentClient](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html) for the Data API. It also promisifies the `AWS.RDSDataService` client to make working with `async/await` or Promise chains easier AND dramatically simplifies **transactions**.
8
13
 
9
14
  For more information about the Aurora Serverless Data API, you can review the [official documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html) or read [Aurora Serverless Data API: An (updated) First Look](https://www.jeremydaly.com/aurora-serverless-data-api-a-first-look/) for some more insights on performance.
@@ -33,34 +38,26 @@ let result = await data.query(`SELECT * FROM myTable`)
33
38
  // }
34
39
 
35
40
  // SELECT with named parameters
36
- let resultParams = await data.query(
37
- `SELECT * FROM myTable WHERE id = :id`,
38
- { id: 2 }
39
- )
41
+ let resultParams = await data.query(`SELECT * FROM myTable WHERE id = :id`, { id: 2 })
40
42
  // { records: [ { id: 2, name: 'Mike', age: 52 } ] }
41
43
 
42
44
  // INSERT with named parameters
43
- let insert = await data.query(
44
- `INSERT INTO myTable (name,age,has_curls) VALUES(:name,:age,:curls)`,
45
- { name: 'Greg', age: 18, curls: false }
46
- )
45
+ let insert = await data.query(`INSERT INTO myTable (name,age,has_curls) VALUES(:name,:age,:curls)`, {
46
+ name: 'Greg',
47
+ age: 18,
48
+ curls: false
49
+ })
47
50
 
48
51
  // BATCH INSERT with named parameters
49
- let batchInsert = await data.query(
50
- `INSERT INTO myTable (name,age,has_curls) VALUES(:name,:age,:curls)`,
51
- [
52
- [{ name: 'Marcia', age: 17, curls: false }],
53
- [{ name: 'Peter', age: 15, curls: false }],
54
- [{ name: 'Jan', age: 15, curls: false }],
55
- [{ name: 'Cindy', age: 12, curls: true }],
56
- [{ name: 'Bobby', age: 12, curls: false }]
57
- ]
58
- )
52
+ let batchInsert = await data.query(`INSERT INTO myTable (name,age,has_curls) VALUES(:name,:age,:curls)`, [
53
+ [{ name: 'Marcia', age: 17, curls: false }],
54
+ [{ name: 'Peter', age: 15, curls: false }],
55
+ [{ name: 'Jan', age: 15, curls: false }],
56
+ [{ name: 'Cindy', age: 12, curls: true }],
57
+ [{ name: 'Bobby', age: 12, curls: false }]
58
+ ])
59
59
  // Update with named parameters
60
- let update = await data.query(
61
- `UPDATE myTable SET age = :age WHERE id = :id`,
62
- { age: 13, id: 5 }
63
- )
60
+ let update = await data.query(`UPDATE myTable SET age = :age WHERE id = :id`, { age: 13, id: 5 })
64
61
 
65
62
  // Delete with named parameters
66
63
  let remove = await data.query(
@@ -73,14 +70,12 @@ let custom = data.query({
73
70
  sql: `SELECT * FROM myOtherTable WHERE id = :id AND active = :isActive`,
74
71
  continueAfterTimeout: true,
75
72
  database: 'myOtherDatabase',
76
- parameters: [
77
- { id: 123},
78
- { name: 'isActive', value: { booleanValue: true } }
79
- ]
73
+ parameters: [{ id: 123 }, { name: 'isActive', value: { booleanValue: true } }]
80
74
  })
81
75
  ```
82
76
 
83
77
  ## Why do I need this?
78
+
84
79
  The [Data API](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html) requires you to specify data types when passing in parameters. The basic `INSERT` example above would look like this using the native `AWS.RDSDataService` class:
85
80
 
86
81
  ```javascript
@@ -119,9 +114,11 @@ Specifying all of those data types in the parameters is a bit clunky. In additio
119
114
  "booleanValue": false
120
115
  }
121
116
  ```
117
+
122
118
  Not only are there no column names, but you have to pull the value from the data type field. Lots of extra work that the **Data API Client** handles automatically for you. 😀
123
119
 
124
120
  ## Installation and Setup
121
+
125
122
  ```
126
123
  npm i data-api-client
127
124
  ```
@@ -132,20 +129,22 @@ For more information on enabling Data API, see [Enabling Data API](#enabling-dat
132
129
 
133
130
  Below is a table containing all of the possible configuration options for the `data-api-client`. Additional details are provided throughout the documentation.
134
131
 
135
- | Property | Type | Description | Default |
136
- | -------- | ---- | ----------- | ------- |
137
- | resourceArn | `string` | The ARN of your Aurora Serverless Cluster. This value is *required*, but can be overridden when querying. | |
138
- | secretArn | `string` | The ARN of the secret associated with your database credentials. This is *required*, but can be overridden when querying. | |
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` |
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` |
142
- | ~~keepAlive~~ (deprecated) | `boolean` | See [Connection Reuse](#connection-reuse) below. | |
143
- | ~~sslEnabled~~ (deprecated) | `boolean` | Set this in the `options` | `true` |
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. | `{}` |
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` |
132
+ | Property | Type | Description | Default |
133
+ | --------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
134
+ | AWS | `AWS` | A custom `aws-sdk` instance | |
135
+ | resourceArn | `string` | The ARN of your Aurora Serverless Cluster. This value is _required_, but can be overridden when querying. | |
136
+ | secretArn | `string` | The ARN of the secret associated with your database credentials. This is _required_, but can be overridden when querying. | |
137
+ | database | `string` | _Optional_ default database to use with queries. Can be overridden when querying. | |
138
+ | engine | `mysql` or `pg` | The type of database engine you're connecting to (MySQL or Postgres). | `mysql` |
139
+ | 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` |
140
+ | ~~keepAlive~~ (deprecated) | `boolean` | See [Connection Reuse](#connection-reuse) below. | |
141
+ | ~~sslEnabled~~ (deprecated) | `boolean` | Set this in the `options` | `true` |
142
+ | 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. | `{}` |
143
+ | ~~region~~ (deprecated) | `string` | Set this in the `options` | |
144
+ | 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
145
 
148
146
  ### Connection Reuse
147
+
149
148
  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
149
  `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).
151
150
 
@@ -165,6 +164,7 @@ const data = require('data-api-client')({
165
164
  ```
166
165
 
167
166
  ### Running a query
167
+
168
168
  Once initialized, running a query is super simple. Use the `query()` method and pass in your SQL statement:
169
169
 
170
170
  ```javascript
@@ -172,8 +172,9 @@ let result = await data.query(`SELECT * FROM myTable`)
172
172
  ```
173
173
 
174
174
  By default, this will return your rows as an array of objects with column names as property names:
175
+
175
176
  ```javascript
176
- [
177
+ ;[
177
178
  { id: 1, name: 'Alice', age: null },
178
179
  { id: 2, name: 'Mike', age: 52 },
179
180
  { id: 3, name: 'Carol', age: 50 }
@@ -183,7 +184,8 @@ By default, this will return your rows as an array of objects with column names
183
184
  To query with parameters, you can use named parameters in your SQL, and then provider an object containing your parameters as the second argument to the `query()` method:
184
185
 
185
186
  ```javascript
186
- let result = await data.query(`
187
+ let result = await data.query(
188
+ `
187
189
  SELECT * FROM myTable WHERE id = :id AND created > :createDate`,
188
190
  { id: 2, createDate: '2019-06-01' }
189
191
  )
@@ -192,14 +194,12 @@ let result = await data.query(`
192
194
  The Data API Client will automatically convert your parameters into the correct Data API parameter format using native JavaScript types. If you prefer to use the clunky format, or you need more control over the data type, you can just pass in the `RDSDataService` format:
193
195
 
194
196
  ```javascript
195
- let result = await data.query(
196
- `SELECT * FROM myTable WHERE id = :id AND created > :createDate`,
197
- [ // An array of objects is totally cool, too. We'll merge them for you.
198
- { id: 2 },
199
- // Data API Client just passes this straight on through
200
- { name: 'createDate', value: { blobValue: new Buffer('2019-06-01') } }
201
- ]
202
- )
197
+ let result = await data.query(`SELECT * FROM myTable WHERE id = :id AND created > :createDate`, [
198
+ // An array of objects is totally cool, too. We'll merge them for you.
199
+ { id: 2 },
200
+ // Data API Client just passes this straight on through
201
+ { name: 'createDate', value: { blobValue: new Buffer('2019-06-01') } }
202
+ ])
203
203
  ```
204
204
 
205
205
  If you want even more control, you can pass in an `object` as the first parameter. This will allow you to add additional configuration options and override defaults as well.
@@ -217,27 +217,26 @@ let result = await data.query({
217
217
  }
218
218
  ```
219
219
 
220
- Sometimes you might want to have *dynamic identifiers* in your SQL statements. Unfortunately, the `RDSDataService` doesn't do this, but the **Data API Client** does! We're using the [sqlstring](https://github.com/mysqljs/sqlstring) module under the hood, so as long as [NO_BACKSLASH_ESCAPES](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_backslash_escapes) SQL mode is disabled (which is the default state for Aurora Serverless), you're good to go. Use a double colon (`::`) prefix to create *named identifiers* and you can do cool things like this:
220
+ Sometimes you might want to have _dynamic identifiers_ in your SQL statements. Unfortunately, the `RDSDataService` doesn't do this, but the **Data API Client** does! We're using the [sqlstring](https://github.com/mysqljs/sqlstring) module under the hood, so as long as [NO_BACKSLASH_ESCAPES](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_backslash_escapes) SQL mode is disabled (which is the default state for Aurora Serverless), you're good to go. Use a double colon (`::`) prefix to create _named identifiers_ and you can do cool things like this:
221
221
 
222
222
  ```javascript
223
- let result = await data.query(
224
- `SELECT ::fields FROM ::table WHERE id > :id`,
225
- {
226
- fields: ['id','name','created'],
227
- table: 'table_' + someScaryUserInput, // someScaryUserInput = 123abc
228
- id: 1
229
- }
230
- )
223
+ let result = await data.query(`SELECT ::fields FROM ::table WHERE id > :id`, {
224
+ fields: ['id', 'name', 'created'],
225
+ table: 'table_' + someScaryUserInput, // someScaryUserInput = 123abc
226
+ id: 1
227
+ })
231
228
  ```
232
229
 
233
230
  Which will produce a query like this:
231
+
234
232
  ```sql
235
233
  SELECT `id`, `name`, `created` FROM `table_123abc` WHERE id > :id LIMIT 10
236
234
  ```
237
235
 
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.
236
+ You'll notice that we leave the _named parameters_ alone. Anything that Data API and the `RDSDataService` Class currently handles, we defer to them.
239
237
 
240
238
  ### Type-Casting
239
+
241
240
  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
241
 
243
242
  ```javascript
@@ -267,28 +266,28 @@ const result = await data.query(
267
266
  ```
268
267
 
269
268
  ### Batch Queries
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.
269
+
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.
271
271
 
272
272
  To issue a batch query, use the `query()` method (either by passing an object or using the two arity form), and provide multiple parameter sets as nested arrays. For example, if you wanted to update multiple records at once, your query might look like this:
273
273
 
274
274
  ```javascript
275
- let result = await data.query(
276
- `UPDATE myTable SET name = :newName WHERE id = :id`,
277
- [
278
- [ { id: 1, newName: 'Alice Franklin' } ],
279
- [ { id: 7, newName: 'Jan Glass' } ]
280
- ]
281
- )
275
+ let result = await data.query(`UPDATE myTable SET name = :newName WHERE id = :id`, [
276
+ [{ id: 1, newName: 'Alice Franklin' }],
277
+ [{ id: 7, newName: 'Jan Glass' }]
278
+ ])
282
279
  ```
283
280
 
284
- You can also use *named identifiers* in batch queries, which will update and escape your SQL statement. **ONLY** parameters from the first parameter set will be used to update the query. Subsequent parameter sets will only update *named parameters* supported by the Data API.
281
+ You can also use _named identifiers_ in batch queries, which will update and escape your SQL statement. **ONLY** parameters from the first parameter set will be used to update the query. Subsequent parameter sets will only update _named parameters_ supported by the Data API.
285
282
 
286
283
  Whenever a batch query is executed, it returns an `updateResults` field. Other than for `INSERT` statements, however, there is no useful feedback provided by this field.
287
284
 
288
285
  ### Retrieving Insert IDs
286
+
289
287
  The Data API returns a `generatedFields` array that contains the value of auto-incrementing primary keys. If this value is returned, the Data API Client will parse this and return it as the `insertId`. This also works for batch queries as well.
290
288
 
291
289
  ## Transaction Support
290
+
292
291
  Transaction support in the Data API Client has been dramatically simplified. Start a new transaction using the `transaction()` method, and then chain queries using the `query()` method. The `query()` method supports all standard query options. Alternatively, you can specify a function as the only argument in a `query()` method call and return the arguments as an array of values. The function receives two arguments, the result of the last query executed, and an array containing all the previous query results. This is useful if you need values from a previous query as part of your transaction.
293
292
 
294
293
  You can specify an optional `rollback()` method in the chain. This will receive the `error` object and the `transactionStatus` object, allowing you to add additional logging or perform some other action. Call the `commit()` method when you are ready to execute the queries.
@@ -304,10 +303,13 @@ let results = await mysql.transaction()
304
303
  With a function to get the `insertId` from the previous query:
305
304
 
306
305
  ```javascript
307
- let results = await mysql.transaction()
306
+ let results = await mysql
307
+ .transaction()
308
308
  .query('INSERT INTO myTable (name) VALUES(:name)', { name: 'Tiger' })
309
- .query((r) => [ 'UPDATE myTable SET age = :age WHERE id = :id', { age: 4, id: r.insertId } ])
310
- .rollback((e,status) => { /* do something with the error */ }) // optional
309
+ .query((r) => ['UPDATE myTable SET age = :age WHERE id = :id', { age: 4, id: r.insertId }])
310
+ .rollback((e, status) => {
311
+ /* do something with the error */
312
+ }) // optional
311
313
  .commit() // execute the queries
312
314
  ```
313
315
 
@@ -317,7 +319,8 @@ By default, the `transaction()` method will use the `resourceArn`, `secretArn` a
317
319
 
318
320
  ### Using native methods directly
319
321
 
320
- The Data API Client exposes *promisified* versions of the five RDSDataService methods. These are:
322
+ The Data API Client exposes _promisified_ versions of the five RDSDataService methods. These are:
323
+
321
324
  - `batchExecuteStatement`
322
325
  - `beginTransaction`
323
326
  - `commitTransaction`
@@ -336,10 +339,36 @@ let result = await data.executeStatement({
336
339
  )
337
340
  ```
338
341
 
342
+ ## Custom AWS instance
343
+
344
+ `data-api-client` allows for introducing a custom `AWS` as a parameter. This parameter is optional. If not present - `data-api-client` will fall back to the default `AWS` instance that comes with the library.
345
+
346
+ ```javascript
347
+ // Instantiate data-api-client with a custom AWS instance
348
+ const data = require('data-api-client')({
349
+ AWS: customAWS,
350
+ ...
351
+ })
352
+ ```
353
+
354
+ Custom AWS parameter allows to introduce, e.g. tracing Data API calls through X-Ray with:
355
+
356
+ ```javascript
357
+ const AWSXRay = require('aws-xray-sdk')
358
+ const AWS = AWSXRay.captureAWS(require('aws-sdk'))
359
+
360
+ const data = require('data-api-client')({
361
+ AWS: AWS,
362
+ ...
363
+ })
364
+ ```
365
+
339
366
  ## Data API Limitations / Wonkiness
340
- The first GA release of the Data API has *a lot* of promise, unfortunately, there are still quite a few things that make it a bit wonky and may require you to implement some workarounds. I've outlined some of my findings below.
367
+
368
+ The first GA release of the Data API has _a lot_ of promise, unfortunately, there are still quite a few things that make it a bit wonky and may require you to implement some workarounds. I've outlined some of my findings below.
341
369
 
342
370
  ### You can't send in an array of values
371
+
343
372
  The GitHub repo for RDSDataService mentions something about `arrayValues`, but I've been unable to get arrays (including TypedArrays and Buffers) to be used for parameters with `IN` clauses. For example, the following query will **NOT** work:
344
373
 
345
374
  ```javascript
@@ -357,9 +386,11 @@ let result = await data.executeStatement({
357
386
  I'm using `blobValue` because it's the only generic value field. You could send it in as a string, but then it only uses the first value. Hopefully they will add an `arrayValues` or something similar to support this in the future.
358
387
 
359
388
  ### ~~Named parameters MUST be sent in order~~
360
- ~~Read that again if you need to. So parameters have to be **BOTH** named and *in order*, otherwise the query **may** fail. I stress **may**, because if you send in two fields of compatible type in the wrong order, the query will work, just with your values flipped. 🤦🏻‍♂️ Watch out for this one.~~ 👈This was fixed!
389
+
390
+ ~~Read that again if you need to. So parameters have to be **BOTH** named and _in order_, otherwise the query **may** fail. I stress **may**, because if you send in two fields of compatible type in the wrong order, the query will work, just with your values flipped. 🤦🏻‍♂️ Watch out for this one.~~ 👈This was fixed!
361
391
 
362
392
  ### You can't parameterize identifiers
393
+
363
394
  If you want to use dynamic column or field names, there is no way to do it automatically with the Data API. The `mysql` package, for example, lets you use `??` to dynamically insert escaped identifiers. Something like the example below is currently not possible.
364
395
 
365
396
  ```javascript
@@ -378,18 +409,19 @@ let result = await data.executeStatement({
378
409
 
379
410
  No worries! The Data API Client gives you the ability to parameterize identifiers and auto escape them. Just use a double colon (`::`) to prefix your named identifiers.
380
411
 
381
-
382
412
  ### Batch statements do not give you updated record counts
413
+
383
414
  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.
384
415
 
385
416
  ## Enabling Data API
417
+
386
418
  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.
387
419
 
388
420
  ### Enable Data API on your Aurora Serverless Cluster
389
421
 
390
422
  ![Enable Data API in Network & Security settings of your cluster](https://user-images.githubusercontent.com/2053544/58768968-79ee4300-8570-11e9-9266-1433182e0db2.png)
391
423
 
392
- You need to modify your Aurora Serverless cluster by clicking “ACTIONS” and then “Modify Cluster”. Just check the Data API box in the *Network & Security* section and you’re good to go. Remember that your Aurora Serverless cluster still runs in a VPC, even though you don’t need to run your Lambdas in a VPC to access it via the Data API.
424
+ You need to modify your Aurora Serverless cluster by clicking “ACTIONS” and then “Modify Cluster”. Just check the Data API box in the _Network & Security_ section and you’re good to go. Remember that your Aurora Serverless cluster still runs in a VPC, even though you don’t need to run your Lambdas in a VPC to access it via the Data API.
393
425
 
394
426
  ### Set up a secret in the Secrets Manager
395
427
 
@@ -397,7 +429,6 @@ Next you need to set up a secret in the Secrets Manager. This is actually quite
397
429
 
398
430
  ![Enter database credentials and select database to access](https://user-images.githubusercontent.com/2053544/58768974-912d3080-8570-11e9-8878-636dfb742b00.png)
399
431
 
400
-
401
432
  Next we give it a name, this is important, because this will be part of the arn when we set up permissions later. You can give it a description as well so you don’t forget what this secret is about when you look at it in a few weeks.
402
433
 
403
434
  ![Give your secret a name and add a description](https://user-images.githubusercontent.com/2053544/58768984-a7d38780-8570-11e9-8b21-199db5548c73.png)
@@ -411,24 +442,26 @@ You can then configure your rotation settings, if you want, and then you review
411
442
  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.
412
443
 
413
444
  **YAML:**
445
+
414
446
  ```yaml
415
447
  Statement:
416
- - Effect: "Allow"
448
+ - Effect: 'Allow'
417
449
  Action:
418
- - "rds-data:ExecuteSql"
419
- - "rds-data:ExecuteStatement"
420
- - "rds-data:BatchExecuteStatement"
421
- - "rds-data:BeginTransaction"
422
- - "rds-data:RollbackTransaction"
423
- - "rds-data:CommitTransaction"
424
- Resource: "*"
425
- - Effect: "Allow"
450
+ - 'rds-data:ExecuteSql'
451
+ - 'rds-data:ExecuteStatement'
452
+ - 'rds-data:BatchExecuteStatement'
453
+ - 'rds-data:BeginTransaction'
454
+ - 'rds-data:RollbackTransaction'
455
+ - 'rds-data:CommitTransaction'
456
+ Resource: '*'
457
+ - Effect: 'Allow'
426
458
  Action:
427
- - "secretsmanager:GetSecretValue"
428
- Resource: "arn:aws:secretsmanager:{REGION}:{ACCOUNT-ID}:secret:{PATH-TO-SECRET}/*"
459
+ - 'secretsmanager:GetSecretValue'
460
+ Resource: 'arn:aws:secretsmanager:{REGION}:{ACCOUNT-ID}:secret:{PATH-TO-SECRET}/*'
429
461
  ```
430
462
 
431
463
  **JSON:**
464
+
432
465
  ```javascript
433
466
  "Statement" : [
434
467
  {
@@ -451,10 +484,6 @@ Statement:
451
484
  ]
452
485
  ```
453
486
 
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
-
459
487
  ## Contributions
488
+
460
489
  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
@@ -36,103 +36,125 @@ const supportedTypes = [
36
36
  /********************************************************************/
37
37
 
38
38
  // Simple error function
39
- const error = (...err) => { throw Error(...err) }
39
+ const error = (...err) => {
40
+ throw Error(...err)
41
+ }
40
42
 
41
43
  // Parse SQL statement from provided arguments
42
- const parseSQL = args =>
43
- typeof args[0] === 'string' ? args[0]
44
- : typeof args[0] === 'object' && typeof args[0].sql === 'string' ? args[0].sql
45
- : error('No \'sql\' statement provided.')
44
+ const parseSQL = (args) =>
45
+ typeof args[0] === 'string'
46
+ ? args[0]
47
+ : typeof args[0] === 'object' && typeof args[0].sql === 'string'
48
+ ? args[0].sql
49
+ : error(`No 'sql' statement provided.`)
46
50
 
47
51
  // Parse the parameters from provided arguments
48
- const parseParams = args =>
49
- Array.isArray(args[0].parameters) ? args[0].parameters
50
- : typeof args[0].parameters === 'object' ? [args[0].parameters]
51
- : Array.isArray(args[1]) ? args[1]
52
- : typeof args[1] === 'object' ? [args[1]]
53
- : args[0].parameters ? error('\'parameters\' must be an object or array')
54
- : args[1] ? error('Parameters must be an object or array')
55
- : []
52
+ const parseParams = (args) =>
53
+ Array.isArray(args[0].parameters)
54
+ ? args[0].parameters
55
+ : typeof args[0].parameters === 'object'
56
+ ? [args[0].parameters]
57
+ : Array.isArray(args[1])
58
+ ? args[1]
59
+ : typeof args[1] === 'object'
60
+ ? [args[1]]
61
+ : args[0].parameters
62
+ ? error(`'parameters' must be an object or array`)
63
+ : args[1]
64
+ ? error('Parameters must be an object or array')
65
+ : []
56
66
 
57
67
  // Parse the supplied database, or default to config
58
- const parseDatabase = (config,args) =>
59
- config.transactionId ? config.database
60
- : typeof args[0].database === 'string' ? args[0].database
61
- : args[0].database ? error('\'database\' must be a string.')
62
- : config.database ? config.database
63
- : undefined // removed for #47 - error('No \'database\' provided.')
68
+ const parseDatabase = (config, args) =>
69
+ config.transactionId
70
+ ? config.database
71
+ : typeof args[0].database === 'string'
72
+ ? args[0].database
73
+ : args[0].database
74
+ ? error(`'database' must be a string.`)
75
+ : config.database
76
+ ? config.database
77
+ : undefined // removed for #47 - error('No \'database\' provided.')
64
78
 
65
79
  // Parse the supplied hydrateColumnNames command, or default to config
66
- const parseHydrate = (config,args) =>
67
- typeof args[0].hydrateColumnNames === 'boolean' ? args[0].hydrateColumnNames
68
- : args[0].hydrateColumnNames ? error('\'hydrateColumnNames\' must be a boolean.')
69
- : config.hydrateColumnNames
80
+ const parseHydrate = (config, args) =>
81
+ typeof args[0].hydrateColumnNames === 'boolean'
82
+ ? args[0].hydrateColumnNames
83
+ : args[0].hydrateColumnNames
84
+ ? error(`'hydrateColumnNames' must be a boolean.`)
85
+ : config.hydrateColumnNames
70
86
 
71
87
  // 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
88
+ const parseFormatOptions = (config, args) =>
89
+ typeof args[0].formatOptions === 'object'
90
+ ? {
91
+ deserializeDate:
92
+ typeof args[0].formatOptions.deserializeDate === 'boolean'
93
+ ? args[0].formatOptions.deserializeDate
94
+ : args[0].formatOptions.deserializeDate
95
+ ? error(`'formatOptions.deserializeDate' must be a boolean.`)
96
+ : config.formatOptions.deserializeDate,
97
+ treatAsLocalDate:
98
+ typeof args[0].formatOptions.treatAsLocalDate == 'boolean'
99
+ ? args[0].formatOptions.treatAsLocalDate
100
+ : args[0].formatOptions.treatAsLocalDate
101
+ ? error(`'formatOptions.treatAsLocalDate' must be a boolean.`)
102
+ : config.formatOptions.treatAsLocalDate
103
+ }
104
+ : args[0].formatOptions
105
+ ? error(`'formatOptions' must be an object.`)
106
+ : config.formatOptions
83
107
 
84
108
  // Prepare method params w/ supplied inputs if an object is passed
85
- const prepareParams = ({ secretArn,resourceArn },args) => {
109
+ const prepareParams = ({ secretArn, resourceArn }, args) => {
86
110
  return Object.assign(
87
- { secretArn,resourceArn }, // return Arns
88
- typeof args[0] === 'object' ?
89
- omit(args[0],['hydrateColumnNames','parameters']) : {} // merge any inputs
111
+ { secretArn, resourceArn }, // return Arns
112
+ typeof args[0] === 'object' ? omit(args[0], ['hydrateColumnNames', 'parameters']) : {} // merge any inputs
90
113
  )
91
114
  }
92
115
 
93
116
  // Utility function for removing certain keys from an object
94
- const omit = (obj,values) => Object.keys(obj).reduce((acc,x) =>
95
- values.includes(x) ? acc : Object.assign(acc,{ [x]: obj[x] })
96
- ,{})
117
+ const omit = (obj, values) =>
118
+ Object.keys(obj).reduce((acc, x) => (values.includes(x) ? acc : Object.assign(acc, { [x]: obj[x] })), {})
97
119
 
98
120
  // Utility function for picking certain keys from an object
99
- const pick = (obj,values) => Object.keys(obj).reduce((acc,x) =>
100
- values.includes(x) ? Object.assign(acc,{ [x]: obj[x] }) : acc
101
- ,{})
121
+ const pick = (obj, values) =>
122
+ Object.keys(obj).reduce((acc, x) => (values.includes(x) ? Object.assign(acc, { [x]: obj[x] }) : acc), {})
102
123
 
103
124
  // Utility function for flattening arrays
104
- const flatten = arr => arr.reduce((acc,x) => acc.concat(x),[])
125
+ const flatten = (arr) => arr.reduce((acc, x) => acc.concat(x), [])
105
126
 
106
127
  // Normize parameters so that they are all in standard format
107
- const normalizeParams = params => params.reduce((acc, p) =>
108
- Array.isArray(p) ? acc.concat([normalizeParams(p)])
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
128
+ const normalizeParams = (params) =>
129
+ params.reduce(
130
+ (acc, p) =>
131
+ Array.isArray(p)
132
+ ? acc.concat([normalizeParams(p)])
133
+ : (Object.keys(p).length === 2 && p.name && typeof p.value !== 'undefined') ||
134
+ (Object.keys(p).length === 3 && p.name && typeof p.value !== 'undefined' && p.cast)
135
+ ? acc.concat(p)
136
+ : acc.concat(splitParams(p)),
137
+ []
138
+ ) // end reduce
115
139
 
116
140
  // Prepare parameters
117
- const processParams = (engine,sql,sqlParams,params,formatOptions,row=0) => {
141
+ const processParams = (engine, sql, sqlParams, params, formatOptions, row = 0) => {
118
142
  return {
119
- processedParams: params.reduce((acc,p) => {
143
+ processedParams: params.reduce((acc, p) => {
120
144
  if (Array.isArray(p)) {
121
- const result = processParams(engine,sql,sqlParams,p,formatOptions,row)
122
- if (row === 0) { sql = result.escapedSql; row++ }
145
+ const result = processParams(engine, sql, sqlParams, p, formatOptions, row)
146
+ if (row === 0) {
147
+ sql = result.escapedSql
148
+ row++
149
+ }
123
150
  return acc.concat([result.processedParams])
124
151
  } else if (sqlParams[p.name]) {
125
152
  if (sqlParams[p.name].type === 'n_ph') {
126
153
  if (p.cast) {
127
154
  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
- )
155
+ sql = sql.replace(regex, engine === 'pg' ? `:${p.name}::${p.cast}` : `CAST(:${p.name} AS ${p.cast})`)
134
156
  }
135
- acc.push(formatParam(p.name,p.value,formatOptions))
157
+ acc.push(formatParam(p.name, p.value, formatOptions))
136
158
  } else if (row === 0) {
137
159
  const regex = new RegExp('::' + p.name + '\\b', 'g')
138
160
  sql = sql.replace(regex, sqlString.escapeId(p.value))
@@ -141,84 +163,91 @@ const processParams = (engine,sql,sqlParams,params,formatOptions,row=0) => {
141
163
  } else {
142
164
  return acc
143
165
  }
144
- },[]),
166
+ }, []),
145
167
  escapedSql: sql
146
168
  }
147
169
  }
148
170
 
149
171
  // Converts parameter to the name/value format
150
- const formatParam = (n,v,formatOptions) => formatType(n,v,getType(v),getTypeHint(v),formatOptions)
172
+ const formatParam = (n, v, formatOptions) => formatType(n, v, getType(v), getTypeHint(v), formatOptions)
151
173
 
152
174
  // Converts object params into name/value format
153
- const splitParams = p => Object.keys(p).reduce((arr,x) =>
154
- arr.concat({ name: x, value: p[x] }),[])
175
+ const splitParams = (p) => Object.keys(p).reduce((arr, x) => arr.concat({ name: x, value: p[x] }), [])
155
176
 
156
177
  // Get all the sql parameters and assign them types
157
- const getSqlParams = sql => {
178
+ const getSqlParams = (sql) => {
158
179
  // TODO: probably need to remove comments from the sql
159
180
  // TODO: placeholders?
160
181
  // sql.match(/\:{1,2}\w+|\?+/g).map((p,i) => {
161
- return (sql.match(/:{1,2}\w+/g) || []).map((p) => {
162
- // TODO: future support for placeholder parsing?
163
- // return p === '??' ? { type: 'id' } // identifier
164
- // : p === '?' ? { type: 'ph', label: '__d'+i } // placeholder
165
- return p.startsWith('::') ? { type: 'n_id', label: p.substr(2) } // named id
166
- : { type: 'n_ph', label: p.substr(1) } // named placeholder
167
- }).reduce((acc,x) => {
168
- return Object.assign(acc,
169
- {
182
+ return (sql.match(/:{1,2}\w+/g) || [])
183
+ .map((p) => {
184
+ // TODO: future support for placeholder parsing?
185
+ // return p === '??' ? { type: 'id' } // identifier
186
+ // : p === '?' ? { type: 'ph', label: '__d'+i } // placeholder
187
+ return p.startsWith('::')
188
+ ? { type: 'n_id', label: p.substr(2) } // named id
189
+ : { type: 'n_ph', label: p.substr(1) } // named placeholder
190
+ })
191
+ .reduce((acc, x) => {
192
+ return Object.assign(acc, {
170
193
  [x.label]: {
171
194
  type: x.type
172
195
  }
173
- }
174
- )
175
- },{}) // end reduce
196
+ })
197
+ }, {}) // end reduce
176
198
  }
177
199
 
178
200
  // Gets the value type and returns the correct value field name
179
201
  // TODO: Support more types as the are released
180
- const getType = val =>
181
- typeof val === 'string' ? 'stringValue'
182
- : typeof val === 'boolean' ? 'booleanValue'
183
- : typeof val === 'number' && parseInt(val) === val ? 'longValue'
184
- : typeof val === 'number' && parseFloat(val) === val ? 'doubleValue'
185
- : val === null ? 'isNull'
186
- : isDate(val) ? 'stringValue'
187
- : Buffer.isBuffer(val) ? 'blobValue'
188
- // : Array.isArray(val) ? 'arrayValue' This doesn't work yet
189
- // TODO: there is a 'structValue' now for postgres
190
- : typeof val === 'object'
191
- && Object.keys(val).length === 1
192
- && supportedTypes.includes(Object.keys(val)[0]) ? null
193
- : undefined
202
+ const getType = (val) =>
203
+ typeof val === 'string'
204
+ ? 'stringValue'
205
+ : typeof val === 'boolean'
206
+ ? 'booleanValue'
207
+ : typeof val === 'number' && parseInt(val) === val
208
+ ? 'longValue'
209
+ : typeof val === 'number' && parseFloat(val) === val
210
+ ? 'doubleValue'
211
+ : val === null
212
+ ? 'isNull'
213
+ : isDate(val)
214
+ ? 'stringValue'
215
+ : Buffer.isBuffer(val)
216
+ ? 'blobValue'
217
+ : // : Array.isArray(val) ? 'arrayValue' This doesn't work yet
218
+ // TODO: there is a 'structValue' now for postgres
219
+ typeof val === 'object' && Object.keys(val).length === 1 && supportedTypes.includes(Object.keys(val)[0])
220
+ ? null
221
+ : undefined
194
222
 
195
223
  // Hint to specify the underlying object type for data type mapping
196
- const getTypeHint = val =>
197
- isDate(val) ? 'TIMESTAMP' : undefined
224
+ const getTypeHint = (val) => (isDate(val) ? 'TIMESTAMP' : undefined)
198
225
 
199
- const isDate = val =>
200
- val instanceof Date
226
+ const isDate = (val) => val instanceof Date
201
227
 
202
228
  // Creates a standard Data API parameter using the supplied inputs
203
- const formatType = (name,value,type,typeHint,formatOptions) => {
229
+ const formatType = (name, value, type, typeHint, formatOptions) => {
204
230
  return Object.assign(
205
231
  typeHint != null ? { name, typeHint } : { name },
206
- type === null ? { value }
207
- : {
208
- value: {
209
- [type ? type : error(`'${name}' is an invalid type`)]
210
- : type === 'isNull' ? true
211
- : isDate(value) ? formatToTimeStamp(value, formatOptions && formatOptions.treatAsLocalDate)
212
- : value
213
- }
214
- }
232
+ type === null
233
+ ? { value }
234
+ : {
235
+ value: {
236
+ [type ? type : error(`'${name}' is an invalid type`)]:
237
+ type === 'isNull'
238
+ ? true
239
+ : isDate(value)
240
+ ? formatToTimeStamp(value, formatOptions && formatOptions.treatAsLocalDate)
241
+ : value
242
+ }
243
+ }
215
244
  )
216
245
  } // end formatType
217
246
 
218
247
  // Formats the (UTC) date to the AWS accepted YYYY-MM-DD HH:MM:SS[.FFF] format
219
248
  // See https://docs.aws.amazon.com/rdsdataservice/latest/APIReference/API_SqlParameter.html
220
249
  const formatToTimeStamp = (date, treatAsLocalDate) => {
221
- const pad = (val,num=2) => '0'.repeat(num-(val + '').length) + val
250
+ const pad = (val, num = 2) => '0'.repeat(num - (val + '').length) + val
222
251
 
223
252
  const year = treatAsLocalDate ? date.getFullYear() : date.getUTCFullYear()
224
253
  const month = (treatAsLocalDate ? date.getMonth() : date.getUTCMonth()) + 1 // Convert to human month
@@ -229,7 +258,7 @@ const formatToTimeStamp = (date, treatAsLocalDate) => {
229
258
  const seconds = treatAsLocalDate ? date.getSeconds() : date.getUTCSeconds()
230
259
  const ms = treatAsLocalDate ? date.getMilliseconds() : date.getUTCMilliseconds()
231
260
 
232
- const fraction = ms <= 0 ? '' : `.${pad(ms,3)}`
261
+ const fraction = ms <= 0 ? '' : `.${pad(ms, 3)}`
233
262
 
234
263
  return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}${fraction}`
235
264
  }
@@ -237,14 +266,15 @@ const formatToTimeStamp = (date, treatAsLocalDate) => {
237
266
  // Converts the string value to a Date object.
238
267
  // If standard TIMESTAMP format (YYYY-MM-DD[ HH:MM:SS[.FFF]]) without TZ + treatAsLocalDate=false then assume UTC Date
239
268
  // 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)
269
+ const formatFromTimeStamp = (value, treatAsLocalDate) =>
270
+ !treatAsLocalDate && /^\d{4}-\d{2}-\d{2}(\s\d{2}:\d{2}:\d{2}(\.\d+)?)?$/.test(value)
271
+ ? new Date(value + 'Z')
272
+ : new Date(value)
244
273
 
245
274
  // Formats the results of a query response
246
275
  const formatResults = (
247
- { // destructure results
276
+ {
277
+ // destructure results
248
278
  columnMetadata, // ONLY when hydrate or includeResultMetadata is true
249
279
  numberOfRecordsUpdated, // ONLY for executeStatement method
250
280
  records, // ONLY for executeStatement method
@@ -254,92 +284,104 @@ const formatResults = (
254
284
  hydrate,
255
285
  includeMeta,
256
286
  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
- )
287
+ ) =>
288
+ Object.assign(
289
+ includeMeta ? { columnMetadata } : {},
290
+ numberOfRecordsUpdated !== undefined && !records ? { numberOfRecordsUpdated } : {},
291
+ records
292
+ ? {
293
+ records: formatRecords(records, columnMetadata, hydrate, formatOptions)
294
+ }
295
+ : {},
296
+ updateResults ? { updateResults: formatUpdateResults(updateResults) } : {},
297
+ generatedFields && generatedFields.length > 0 ? { insertId: generatedFields[0].longValue } : {}
298
+ )
267
299
 
268
300
  // Processes records and either extracts Typed Values into an array, or
269
301
  // object with named column labels
270
- const formatRecords = (recs,columns,hydrate,formatOptions) => {
271
-
302
+ const formatRecords = (recs, columns, hydrate, formatOptions) => {
272
303
  // Create map for efficient value parsing
273
- let fmap = recs && recs[0] ? recs[0].map((x,i) => {
274
- return Object.assign({},
275
- columns ? { label: columns[i].label, typeName: columns[i].typeName } : {} ) // add column label and typeName
276
- }) : {}
277
-
278
- // Map over all the records (rows)
279
- return recs ? recs.map(rec => {
280
-
281
- // Reduce each field in the record (row)
282
- return rec.reduce((acc,field,i) => {
283
-
284
- // If the field is null, always return null
285
- if (field.isNull === true) {
286
- return hydrate ? // object if hydrate, else array
287
- Object.assign(acc,{ [fmap[i].label]: null })
288
- : acc.concat(null)
289
-
290
- // If the field is mapped, return the mapped field
291
- } else if (fmap[i] && 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)
296
-
297
- // Else discover the field type
298
- } else {
299
-
300
- // Look for non-null fields
301
- Object.keys(field).map(type => {
302
- if (type !== 'isNull' && field[type] !== null) {
303
- fmap[i]['field'] = type
304
- }
304
+ let fmap =
305
+ recs && recs[0]
306
+ ? recs[0].map((x, i) => {
307
+ return Object.assign({}, columns ? { label: columns[i].label, typeName: columns[i].typeName } : {}) // add column label and typeName
305
308
  })
309
+ : {}
306
310
 
307
- // Return the mapped field (this should NEVER be null)
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)
312
- }
313
-
314
- }, hydrate ? {} : []) // init object if hydrate, else init array
315
- }) : [] // empty record set returns an array
311
+ // Map over all the records (rows)
312
+ return recs
313
+ ? recs.map((rec) => {
314
+ // Reduce each field in the record (row)
315
+ return rec.reduce(
316
+ (acc, field, i) => {
317
+ // If the field is null, always return null
318
+ if (field.isNull === true) {
319
+ return hydrate // object if hydrate, else array
320
+ ? Object.assign(acc, { [fmap[i].label]: null })
321
+ : acc.concat(null)
322
+
323
+ // If the field is mapped, return the mapped field
324
+ } else if (fmap[i] && fmap[i].field) {
325
+ const value = formatRecordValue(field[fmap[i].field], fmap[i].typeName, formatOptions)
326
+ return hydrate // object if hydrate, else array
327
+ ? Object.assign(acc, { [fmap[i].label]: value })
328
+ : acc.concat(value)
329
+
330
+ // Else discover the field type
331
+ } else {
332
+ // Look for non-null fields
333
+ Object.keys(field).map((type) => {
334
+ if (type !== 'isNull' && field[type] !== null) {
335
+ fmap[i]['field'] = type
336
+ }
337
+ })
338
+
339
+ // Return the mapped field (this should NEVER be null)
340
+ const value = formatRecordValue(field[fmap[i].field], fmap[i].typeName, formatOptions)
341
+ return hydrate // object if hydrate, else array
342
+ ? Object.assign(acc, { [fmap[i].label]: value })
343
+ : acc.concat(value)
344
+ }
345
+ },
346
+ hydrate ? {} : []
347
+ ) // init object if hydrate, else init array
348
+ })
349
+ : [] // empty record set returns an array
316
350
  } // end formatRecords
317
351
 
318
352
  // 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
353
+ const formatRecordValue = (value, typeName, formatOptions) => {
354
+ if (
355
+ formatOptions &&
356
+ formatOptions.deserializeDate &&
357
+ ['DATE', 'DATETIME', 'TIMESTAMP', 'TIMESTAMPTZ', 'TIMESTAMP WITH TIME ZONE'].includes(typeName.toUpperCase())
358
+ ) {
359
+ return formatFromTimeStamp(
360
+ value,
361
+ (formatOptions && formatOptions.treatAsLocalDate) || typeName === 'TIMESTAMP WITH TIME ZONE'
362
+ )
363
+ } else if (typeName === 'JSON') {
364
+ return JSON.parse(value)
365
+ } else {
366
+ return value
367
+ }
368
+ }
323
369
 
324
370
  // Format updateResults and extract insertIds
325
- const formatUpdateResults = res => res.map(x => {
326
- return x.generatedFields && x.generatedFields.length > 0 ?
327
- { insertId: x.generatedFields[0].longValue } : {}
328
- })
329
-
371
+ const formatUpdateResults = (res) =>
372
+ res.map((x) => {
373
+ return x.generatedFields && x.generatedFields.length > 0 ? { insertId: x.generatedFields[0].longValue } : {}
374
+ })
330
375
 
331
376
  // Merge configuration data with supplied arguments
332
- const mergeConfig = (initialConfig,args) =>
333
- Object.assign(initialConfig,args)
334
-
335
-
377
+ const mergeConfig = (initialConfig, args) => Object.assign(initialConfig, args)
336
378
 
337
379
  /********************************************************************/
338
380
  /** QUERY MANAGEMENT **/
339
381
  /********************************************************************/
340
382
 
341
383
  // Query function (use standard form for `this` context)
342
- const query = async function(config,..._args) {
384
+ const query = async function (config, ..._args) {
343
385
  // Flatten array if nested arrays (fixes #30)
344
386
  const args = Array.isArray(_args[0]) ? flatten(_args) : _args
345
387
 
@@ -348,92 +390,80 @@ const query = async function(config,..._args) {
348
390
  const sqlParams = getSqlParams(sql)
349
391
 
350
392
  // Parse hydration setting
351
- const hydrateColumnNames = parseHydrate(config,args)
393
+ const hydrateColumnNames = parseHydrate(config, args)
352
394
 
353
395
  // Parse data format settings
354
- const formatOptions = parseFormatOptions(config,args)
396
+ const formatOptions = parseFormatOptions(config, args)
355
397
 
356
398
  // Parse and normalize parameters
357
399
  const parameters = normalizeParams(parseParams(args))
358
400
 
359
401
  // Process parameters and escape necessary SQL
360
- const { processedParams,escapedSql } = processParams(config.engine,sql,sqlParams,parameters,formatOptions)
402
+ const { processedParams, escapedSql } = processParams(config.engine, sql, sqlParams, parameters, formatOptions)
361
403
 
362
404
  // Determine if this is a batch request
363
- const isBatch = processedParams.length > 0
364
- && Array.isArray(processedParams[0])
405
+ const isBatch = processedParams.length > 0 && Array.isArray(processedParams[0])
365
406
 
366
407
  // Create/format the parameters
367
408
  const params = Object.assign(
368
- prepareParams(config,args),
409
+ prepareParams(config, args),
369
410
  {
370
- database: parseDatabase(config,args), // add database
411
+ database: parseDatabase(config, args), // add database
371
412
  sql: escapedSql // add escaped sql statement
372
413
  },
373
414
  // Only include parameters if they exist
374
- processedParams.length > 0 ?
375
- // Batch statements require parameterSets instead of parameters
376
- { [isBatch ? 'parameterSets' : 'parameters']: processedParams } : {},
415
+ processedParams.length > 0
416
+ ? // Batch statements require parameterSets instead of parameters
417
+ { [isBatch ? 'parameterSets' : 'parameters']: processedParams }
418
+ : {},
377
419
  // Force meta data if set and not a batch
378
420
  hydrateColumnNames && !isBatch ? { includeResultMetadata: true } : {},
379
421
  // If a transactionId is passed, overwrite any manual input
380
422
  config.transactionId ? { transactionId: config.transactionId } : {}
381
423
  ) // end params
382
424
 
383
- try { // attempt to run the query
425
+ try {
426
+ // attempt to run the query
384
427
 
385
428
  // Capture the result for debugging
386
- let result = await (isBatch ? config.RDS.batchExecuteStatement(params).promise()
429
+ let result = await (isBatch
430
+ ? config.RDS.batchExecuteStatement(params).promise()
387
431
  : config.RDS.executeStatement(params).promise())
388
432
 
389
433
  // Format and return the results
390
- return formatResults(
391
- result,
392
- hydrateColumnNames,
393
- args[0].includeResultMetadata === true,
394
- formatOptions
395
- )
396
-
397
- } catch(e) {
398
-
434
+ return formatResults(result, hydrateColumnNames, args[0].includeResultMetadata === true, formatOptions)
435
+ } catch (e) {
399
436
  if (this && this.rollback) {
400
437
  let rollback = await config.RDS.rollbackTransaction(
401
- pick(params,['resourceArn','secretArn','transactionId'])
438
+ pick(params, ['resourceArn', 'secretArn', 'transactionId'])
402
439
  ).promise()
403
440
 
404
- this.rollback(e,rollback)
441
+ this.rollback(e, rollback)
405
442
  }
406
443
  // Throw the error
407
444
  throw e
408
445
  }
409
-
410
446
  } // end query
411
447
 
412
-
413
-
414
448
  /********************************************************************/
415
449
  /** TRANSACTION MANAGEMENT **/
416
450
  /********************************************************************/
417
451
 
418
452
  // Init a transaction object and return methods
419
- const transaction = (config,_args) => {
420
-
453
+ const transaction = (config, _args) => {
421
454
  let args = typeof _args === 'object' ? [_args] : [{}]
422
455
  let queries = [] // keep track of queries
423
456
  let rollback = () => {} // default rollback event
424
457
 
425
- const txConfig = Object.assign(
426
- prepareParams(config,args),
427
- {
428
- database: parseDatabase(config,args), // add database
429
- hydrateColumnNames: parseHydrate(config,args), // add hydrate
430
- formatOptions: parseFormatOptions(config,args), // add formatOptions
431
- RDS: config.RDS // reference the RDSDataService instance
432
- }
433
- )
458
+ const txConfig = Object.assign(prepareParams(config, args), {
459
+ database: parseDatabase(config, args), // add database
460
+ hydrateColumnNames: parseHydrate(config, args), // add hydrate
461
+ formatOptions: parseFormatOptions(config, args), // add formatOptions
462
+ RDS: config.RDS // reference the RDSDataService instance
463
+ })
434
464
 
435
465
  return {
436
- query: function(...args) {
466
+ query: function (...args) {
437
467
  if (typeof args[0] === 'function') {
438
468
  queries.push(args[0])
439
469
  } else {
@@ -441,22 +471,25 @@ const transaction = (config,_args) => {
441
471
  }
442
472
  return this
443
473
  },
444
- rollback: function(fn) {
445
- if (typeof fn === 'function') { rollback = fn }
474
+ rollback: function (fn) {
475
+ if (typeof fn === 'function') {
476
+ rollback = fn
477
+ }
446
478
  return this
447
479
  },
448
- commit: async function() { return await commit(txConfig,queries,rollback) }
480
+ commit: async function () {
481
+ return await commit(txConfig, queries, rollback)
482
+ }
449
483
  }
450
484
  }
451
485
 
452
486
  // Commit transaction by running queries
453
- const commit = async (config,queries,rollback) => {
454
-
487
+ const commit = async (config, queries, rollback) => {
455
488
  let results = [] // keep track of results
456
489
 
457
490
  // Start a transaction
458
491
  const { transactionId } = await config.RDS.beginTransaction(
459
- pick(config,['resourceArn','secretArn','database'])
492
+ pick(config, ['resourceArn', 'secretArn', 'database'])
460
493
  ).promise()
461
494
 
462
495
  // Add transactionId to the config
@@ -465,18 +498,18 @@ const commit = async (config,queries,rollback) => {
465
498
  // Loop through queries
466
499
  for (let i = 0; i < queries.length; i++) {
467
500
  // Execute the queries, pass the rollback as context
468
- let result = await query.apply({rollback},[config,queries[i](results[results.length-1],results)])
501
+ let result = await query.apply({ rollback }, [config, queries[i](results[results.length - 1], results)])
469
502
  // Add the result to the main results accumulator
470
503
  results.push(result)
471
504
  }
472
505
 
473
506
  // Commit our transaction
474
507
  const { transactionStatus } = await txConfig.RDS.commitTransaction(
475
- pick(config,['resourceArn','secretArn','transactionId'])
508
+ pick(config, ['resourceArn', 'secretArn', 'transactionId'])
476
509
  ).promise()
477
510
 
478
511
  // Add the transaction status to the results
479
- results.push({transactionStatus})
512
+ results.push({ transactionStatus })
480
513
 
481
514
  // Return the results
482
515
  return results
@@ -507,12 +540,14 @@ const commit = async (config,queries,rollback) => {
507
540
  * @param {string} [params.region] DEPRECATED
508
541
  *
509
542
  */
510
- const init = params => {
511
-
543
+ const init = (params) => {
512
544
  // Set the options for the RDSDataService
513
- const options = typeof params.options === 'object' ? params.options
514
- : params.options !== undefined ? error('\'options\' must be an object')
515
- : {}
545
+ const options =
546
+ typeof params.options === 'object'
547
+ ? params.options
548
+ : params.options !== undefined
549
+ ? error(`'options' must be an object`)
550
+ : {}
516
551
 
517
552
  // Update the AWS http agent with the region
518
553
  if (typeof params.region === 'string') {
@@ -527,25 +562,22 @@ const init = params => {
527
562
  // Set the configuration for this instance
528
563
  const config = {
529
564
  // Require engine
530
- engine: typeof params.engine === 'string' ?
531
- params.engine
532
- : 'mysql',
565
+ engine: typeof params.engine === 'string' ? params.engine : 'mysql',
533
566
 
534
567
  // Require secretArn
535
- secretArn: typeof params.secretArn === 'string' ?
536
- params.secretArn
537
- : error('\'secretArn\' string value required'),
568
+ secretArn: typeof params.secretArn === 'string' ? params.secretArn : error(`'secretArn' string value required`),
538
569
 
539
570
  // Require resourceArn
540
- resourceArn: typeof params.resourceArn === 'string' ?
541
- params.resourceArn
542
- : error('\'resourceArn\' string value required'),
571
+ resourceArn:
572
+ typeof params.resourceArn === 'string' ? params.resourceArn : error(`'resourceArn' string value required`),
543
573
 
544
574
  // Load optional database
545
- database: typeof params.database === 'string' ?
546
- params.database
547
- : params.database !== undefined ? error('\'database\' must be a string')
548
- : undefined,
575
+ database:
576
+ typeof params.database === 'string'
577
+ ? params.database
578
+ : params.database !== undefined
579
+ ? error(`'database' must be a string`)
580
+ : undefined,
549
581
 
550
582
  // Load optional schema DISABLED for now since this isn't used with MySQL
551
583
  // schema: typeof params.schema === 'string' ? params.schema
@@ -553,54 +585,41 @@ const init = params => {
553
585
  // : undefined,
554
586
 
555
587
  // Set hydrateColumnNames (default to true)
556
- hydrateColumnNames:
557
- typeof params.hydrateColumnNames === 'boolean' ?
558
- params.hydrateColumnNames : true,
588
+ hydrateColumnNames: typeof params.hydrateColumnNames === 'boolean' ? params.hydrateColumnNames : true,
559
589
 
560
590
  // Value formatting options. For date the deserialization is enabled and (re)stored as UTC
561
591
  formatOptions: {
562
592
  deserializeDate:
563
593
  typeof params.formatOptions === 'object' && params.formatOptions.deserializeDate === false ? false : true,
564
- treatAsLocalDate:
565
- typeof params.formatOptions === 'object' && params.formatOptions.treatAsLocalDate
594
+ treatAsLocalDate: typeof params.formatOptions === 'object' && params.formatOptions.treatAsLocalDate
566
595
  },
567
596
 
568
597
  // TODO: Put this in a separate module for testing?
569
598
  // Create an instance of RDSDataService
570
- RDS: new AWS.RDSDataService(options)
571
-
599
+ RDS: params.AWS ? new params.AWS.RDSDataService(options) : new AWS.RDSDataService(options)
572
600
  } // end config
573
601
 
574
602
  // Return public methods
575
603
  return {
576
604
  // Query method, pass config and parameters
577
- query: (...x) => query(config,...x),
605
+ query: (...x) => query(config, ...x),
578
606
  // Transaction method, pass config and parameters
579
- transaction: (x) => transaction(config,x),
607
+ transaction: (x) => transaction(config, x),
580
608
 
581
609
  // Export promisified versions of the RDSDataService methods
582
610
  batchExecuteStatement: (args) =>
583
611
  config.RDS.batchExecuteStatement(
584
- mergeConfig(pick(config,['resourceArn','secretArn','database']),args)
612
+ mergeConfig(pick(config, ['resourceArn', 'secretArn', 'database']), args)
585
613
  ).promise(),
586
614
  beginTransaction: (args) =>
587
- config.RDS.beginTransaction(
588
- mergeConfig(pick(config,['resourceArn','secretArn','database']),args)
589
- ).promise(),
615
+ config.RDS.beginTransaction(mergeConfig(pick(config, ['resourceArn', 'secretArn', 'database']), args)).promise(),
590
616
  commitTransaction: (args) =>
591
- config.RDS.commitTransaction(
592
- mergeConfig(pick(config,['resourceArn','secretArn']),args)
593
- ).promise(),
617
+ config.RDS.commitTransaction(mergeConfig(pick(config, ['resourceArn', 'secretArn']), args)).promise(),
594
618
  executeStatement: (args) =>
595
- config.RDS.executeStatement(
596
- mergeConfig(pick(config,['resourceArn','secretArn','database']),args)
597
- ).promise(),
619
+ config.RDS.executeStatement(mergeConfig(pick(config, ['resourceArn', 'secretArn', 'database']), args)).promise(),
598
620
  rollbackTransaction: (args) =>
599
- config.RDS.rollbackTransaction(
600
- mergeConfig(pick(config,['resourceArn','secretArn']),args)
601
- ).promise()
621
+ config.RDS.rollbackTransaction(mergeConfig(pick(config, ['resourceArn', 'secretArn']), args)).promise()
602
622
  }
603
-
604
623
  } // end exports
605
624
 
606
625
  module.exports = init
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-api-client",
3
- "version": "1.2.1",
3
+ "version": "1.3.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": {
@@ -26,8 +26,10 @@
26
26
  "homepage": "https://github.com/jeremydaly/data-api-client#readme",
27
27
  "devDependencies": {
28
28
  "aws-sdk": "^2.811.0",
29
- "eslint": "^6.8.0",
29
+ "eslint": "^8.12.0",
30
+ "eslint-config-prettier": "^8.5.0",
30
31
  "jest": "^27.5.1",
32
+ "prettier": "^2.6.2",
31
33
  "rewire": "^6.0.0"
32
34
  },
33
35
  "dependencies": {