data-api-client 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/index.js +290 -271
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -134,6 +134,7 @@ Below is a table containing all of the possible configuration options for the `d
|
|
|
134
134
|
|
|
135
135
|
| Property | Type | Description | Default |
|
|
136
136
|
| -------- | ---- | ----------- | ------- |
|
|
137
|
+
| AWS | `AWS` | A custom `aws-sdk` instance | |
|
|
137
138
|
| resourceArn | `string` | The ARN of your Aurora Serverless Cluster. This value is *required*, but can be overridden when querying. | |
|
|
138
139
|
| secretArn | `string` | The ARN of the secret associated with your database credentials. This is *required*, but can be overridden when querying. | |
|
|
139
140
|
| database | `string` | *Optional* default database to use with queries. Can be overridden when querying. | |
|
|
@@ -336,6 +337,30 @@ let result = await data.executeStatement({
|
|
|
336
337
|
)
|
|
337
338
|
```
|
|
338
339
|
|
|
340
|
+
## Custom AWS instance
|
|
341
|
+
|
|
342
|
+
`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.
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
// Instantiate data-api-client with a custom AWS instance
|
|
346
|
+
const data = require('data-api-client')({
|
|
347
|
+
AWS: customAWS,
|
|
348
|
+
...
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Custom AWS parameter allows to introduce, e.g. tracing Data API calls through X-Ray with:
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
const AWSXRay = require('aws-xray-sdk')
|
|
356
|
+
const AWS = AWSXRay.captureAWS(require('aws-sdk'))
|
|
357
|
+
|
|
358
|
+
const data = require('data-api-client')({
|
|
359
|
+
AWS: AWS,
|
|
360
|
+
...
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
339
364
|
## Data API Limitations / Wonkiness
|
|
340
365
|
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
366
|
|
package/index.js
CHANGED
|
@@ -36,103 +36,125 @@ const supportedTypes = [
|
|
|
36
36
|
/********************************************************************/
|
|
37
37
|
|
|
38
38
|
// Simple error function
|
|
39
|
-
const 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'
|
|
44
|
-
|
|
45
|
-
|
|
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)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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'
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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) {
|
|
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) || [])
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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'
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
-
) =>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
records
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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 =
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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) =>
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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 =>
|
|
326
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
425
|
+
try {
|
|
426
|
+
// attempt to run the query
|
|
384
427
|
|
|
385
428
|
// Capture the result for debugging
|
|
386
|
-
let result = await (isBatch
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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') {
|
|
474
|
+
rollback: function (fn) {
|
|
475
|
+
if (typeof fn === 'function') {
|
|
476
|
+
rollback = fn
|
|
477
|
+
}
|
|
446
478
|
return this
|
|
447
479
|
},
|
|
448
|
-
commit: async function() {
|
|
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 =
|
|
514
|
-
|
|
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:
|
|
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:
|
|
546
|
-
params.database
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.3.0",
|
|
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": "^
|
|
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": {
|