data-api-client
Advanced tools
Comparing version 1.0.0-beta to 1.0.0
654
index.js
@@ -22,2 +22,14 @@ 'use strict' | ||
// Supported value types in the Data API | ||
const supportedTypes = [ | ||
'arrayValue', | ||
'blobValue', | ||
'booleanValue', | ||
'doubleValue', | ||
'isNull', | ||
'longValue', | ||
'stringValue', | ||
'structValue' | ||
] | ||
/**********************************************************************/ | ||
@@ -28,10 +40,10 @@ /** Enable HTTP Keep-Alive per https://vimeo.com/287511222 **/ | ||
const https = require('https') | ||
const https = require('https') | ||
const sslAgent = new https.Agent({ | ||
keepAlive: true, | ||
maxSockets: 50, // same as aws-sdk | ||
rejectUnauthorized: true // same as aws-sdk | ||
}) | ||
sslAgent.setMaxListeners(0) // same as aws-sdk | ||
const sslAgent = new https.Agent({ | ||
keepAlive: true, | ||
maxSockets: 50, // same as aws-sdk | ||
rejectUnauthorized: true // same as aws-sdk | ||
}) | ||
sslAgent.setMaxListeners(0) // same as aws-sdk | ||
@@ -43,232 +55,238 @@ | ||
// Simple error function | ||
const error = (...err) => { throw Error(...err) } | ||
// Simple error function | ||
const error = (...err) => { throw Error(...err) } | ||
// Parse SQL statement from provided arguments | ||
const parseSQL = args => | ||
typeof args[0] === 'string' ? args[0] | ||
: typeof args[0] === 'object' && typeof args[0].sql === 'string' ? args[0].sql | ||
: error(`No 'sql' statement provided.`) | ||
// Parse SQL statement from provided arguments | ||
const parseSQL = args => | ||
typeof args[0] === 'string' ? args[0] | ||
: typeof args[0] === 'object' && typeof args[0].sql === 'string' ? args[0].sql | ||
: error('No \'sql\' statement provided.') | ||
// Parse the parameters from provided arguments | ||
const parseParams = args => | ||
Array.isArray(args[0].parameters) ? args[0].parameters | ||
: typeof args[0].parameters === 'object' ? [args[0].parameters] | ||
: Array.isArray(args[1]) ? args[1] | ||
: typeof args[1] === 'object' ? [args[1]] | ||
: args[0].parameters ? error(`'parameters' must be an object or array`) | ||
: args[1] ? error(`Parameters must be an object or array`) | ||
: [] | ||
// Parse the parameters from provided arguments | ||
const parseParams = args => | ||
Array.isArray(args[0].parameters) ? args[0].parameters | ||
: typeof args[0].parameters === 'object' ? [args[0].parameters] | ||
: Array.isArray(args[1]) ? args[1] | ||
: typeof args[1] === 'object' ? [args[1]] | ||
: args[0].parameters ? error('\'parameters\' must be an object or array') | ||
: args[1] ? error('Parameters must be an object or array') | ||
: [] | ||
// Parse the supplied database, or default to config | ||
const parseDatabase = (config,args) => | ||
config.transactionId ? config.database | ||
: typeof args[0].database === 'string' ? args[0].database | ||
: args[0].database ? error(`'database' must be a string.`) | ||
: config.database ? config.database | ||
: error(`No 'database' provided.`) | ||
// Parse the supplied database, or default to config | ||
const parseDatabase = (config,args) => | ||
config.transactionId ? config.database | ||
: typeof args[0].database === 'string' ? args[0].database | ||
: args[0].database ? error('\'database\' must be a string.') | ||
: config.database ? config.database | ||
: error('No \'database\' provided.') | ||
// Parse the supplied hydrateColumnNames command, or default to config | ||
const parseHydrate = (config,args) => | ||
typeof args[0].hydrateColumnNames === 'boolean' ? args[0].hydrateColumnNames | ||
: args[0].hydrateColumnNames ? error(`'hydrateColumnNames' must be a boolean.`) | ||
: config.hydrateColumnNames | ||
// Parse the supplied hydrateColumnNames command, or default to config | ||
const parseHydrate = (config,args) => | ||
typeof args[0].hydrateColumnNames === 'boolean' ? args[0].hydrateColumnNames | ||
: args[0].hydrateColumnNames ? error('\'hydrateColumnNames\' must be a boolean.') | ||
: config.hydrateColumnNames | ||
// Prepare method params w/ supplied inputs if an object is passed | ||
const prepareParams = ({ secretArn,resourceArn },args) => { | ||
return Object.assign( | ||
{ secretArn,resourceArn }, // return Arns | ||
typeof args[0] === 'object' ? | ||
omit(args[0],['hydrateColumnNames','parameters']) : {} // merge any inputs | ||
) | ||
} | ||
// Prepare method params w/ supplied inputs if an object is passed | ||
const prepareParams = ({ secretArn,resourceArn },args) => { | ||
return Object.assign( | ||
{ secretArn,resourceArn }, // return Arns | ||
typeof args[0] === 'object' ? | ||
omit(args[0],['hydrateColumnNames','parameters']) : {} // merge any inputs | ||
) | ||
} | ||
// Utility function for removing certain keys from an object | ||
const omit = (obj,values) => Object.keys(obj).reduce((acc,x) => | ||
values.includes(x) ? acc : Object.assign(acc,{ [x]: obj[x] }) | ||
// Utility function for removing certain keys from an object | ||
const omit = (obj,values) => Object.keys(obj).reduce((acc,x) => | ||
values.includes(x) ? acc : Object.assign(acc,{ [x]: obj[x] }) | ||
,{}) | ||
// Utility function for picking certain keys from an object | ||
const pick = (obj,values) => Object.keys(obj).reduce((acc,x) => | ||
values.includes(x) ? Object.assign(acc,{ [x]: obj[x] }) : acc | ||
// Utility function for picking certain keys from an object | ||
const pick = (obj,values) => Object.keys(obj).reduce((acc,x) => | ||
values.includes(x) ? Object.assign(acc,{ [x]: obj[x] }) : acc | ||
,{}) | ||
// Utility function for flattening arrays | ||
const flatten = arr => arr.reduce((acc,x) => acc.concat(x),[]) | ||
// Utility function for flattening arrays - deprecated | ||
// const flatten = arr => arr.reduce((acc,x) => acc.concat(x),[]) | ||
// Normize parameters so that they are all in standard format | ||
const normalizeParams = params => params.reduce((acc,p) => | ||
Array.isArray(p) ? acc.concat([normalizeParams(p)]) | ||
: Object.keys(p).length === 2 && p.name && p.value ? acc.concat(p) | ||
: acc.concat(splitParams(p)) | ||
// Normize parameters so that they are all in standard format | ||
const normalizeParams = params => params.reduce((acc,p) => | ||
Array.isArray(p) ? acc.concat([normalizeParams(p)]) | ||
: Object.keys(p).length === 2 && p.name && p.value ? acc.concat(p) | ||
: acc.concat(splitParams(p)) | ||
,[]) // end reduce | ||
// Annotate parameters with correct types | ||
const annotateParams = params => params.reduce((acc,p) => | ||
Array.isArray(p) ? acc.concat([annotateParams(p)]) | ||
: Object.keys(p).length === 2 && p.name && p.value ? acc.concat(p) | ||
: acc.concat( | ||
formatParam(Object.keys(p)[0],Object.values(p)[0]) | ||
) | ||
,[]) // end reduce | ||
// // Annotate parameters with correct types | ||
// const annotateParams = params => params.reduce((acc,p) => | ||
// Array.isArray(p) ? acc.concat([annotateParams(p)]) | ||
// : Object.keys(p).length === 2 && p.name && p.value ? acc.concat(p) | ||
// : acc.concat( | ||
// formatParam(Object.keys(p)[0],Object.values(p)[0]) | ||
// ) | ||
// ,[]) // end reduce | ||
// Prepare parameters | ||
const processParams = (sql,sqlParams,params,row=0) => { | ||
return { | ||
processedParams: params.reduce((acc,p,i) => { | ||
if (Array.isArray(p)) { | ||
let result = processParams(sql,sqlParams,p,row) | ||
if (row === 0) { sql = result.escapedSql; row++ } | ||
return acc.concat([result.processedParams]) | ||
} else if (sqlParams[p.name]) { | ||
if (sqlParams[p.name].type === 'n_ph') { | ||
acc[sqlParams[p.name].index] = formatParam(p.name,p.value) | ||
} else if (row === 0) { | ||
let regex = new RegExp('::' + p.name + '\\b','g') | ||
sql = sql.replace(regex,sqlString.escapeId(p.value)) | ||
} | ||
return acc | ||
} else { | ||
return acc | ||
// Prepare parameters | ||
const processParams = (sql,sqlParams,params,row=0) => { | ||
return { | ||
processedParams: params.reduce((acc,p) => { | ||
if (Array.isArray(p)) { | ||
let result = processParams(sql,sqlParams,p,row) | ||
if (row === 0) { sql = result.escapedSql; row++ } | ||
return acc.concat([result.processedParams]) | ||
} else if (sqlParams[p.name]) { | ||
if (sqlParams[p.name].type === 'n_ph') { | ||
acc.push(formatParam(p.name,p.value)) | ||
} else if (row === 0) { | ||
let regex = new RegExp('::' + p.name + '\\b','g') | ||
sql = sql.replace(regex,sqlString.escapeId(p.value)) | ||
} | ||
},[]), | ||
escapedSql: sql | ||
} | ||
return acc | ||
} else { | ||
return acc | ||
} | ||
},[]), | ||
escapedSql: sql | ||
} | ||
} | ||
// Converts parameter to the name/value format | ||
const formatParam = (n,v) => formatType(n,v,getType(v)) | ||
// Converts parameter to the name/value format | ||
const formatParam = (n,v) => formatType(n,v,getType(v)) | ||
const splitParams = p => Object.keys(p).reduce((arr,x) => | ||
arr.concat({ name: x, value: p[x] }),[]) | ||
// Converts object params into name/value format | ||
const splitParams = p => Object.keys(p).reduce((arr,x) => | ||
arr.concat({ name: x, value: p[x] }),[]) | ||
// This appears to be a bug, so hopefully it will go away soon, but named | ||
// parameters will *not* work if they are out of order! :facepalm: | ||
const getSqlParams = sql => { | ||
let p = 0 // position index for named parameters | ||
// TODO: probably need to remove comments from the sql | ||
// TODO: placeholders? | ||
// sql.match(/\:{1,2}\w+|\?+/g).map((p,i) => { | ||
return (sql.match(/\:{1,2}\w+/g) || []).map((p,i) => { | ||
return p === '??' ? { type: 'id' } // identifier | ||
: p === '?' ? { type: 'ph', label: '__d'+i } // placeholder | ||
: p.startsWith('::') ? { type: 'n_id', label: p.substr(2) } // named id | ||
: { type: 'n_ph', label: p.substr(1) } // named placeholder | ||
}).reduce((acc,x,i) => { | ||
return Object.assign(acc, | ||
{ | ||
[x.label]: { | ||
type: x.type, index: x.type === 'n_ph' ? p++ : undefined | ||
} | ||
// Get all the sql parameters and assign them types | ||
const getSqlParams = sql => { | ||
// TODO: probably need to remove comments from the sql | ||
// TODO: placeholders? | ||
// sql.match(/\:{1,2}\w+|\?+/g).map((p,i) => { | ||
return (sql.match(/:{1,2}\w+/g) || []).map((p) => { | ||
// TODO: future support for placeholder parsing? | ||
// return p === '??' ? { type: 'id' } // identifier | ||
// : p === '?' ? { type: 'ph', label: '__d'+i } // placeholder | ||
return p.startsWith('::') ? { type: 'n_id', label: p.substr(2) } // named id | ||
: { type: 'n_ph', label: p.substr(1) } // named placeholder | ||
}).reduce((acc,x) => { | ||
return Object.assign(acc, | ||
{ | ||
[x.label]: { | ||
type: x.type | ||
} | ||
) | ||
},{}) // end reduce | ||
} | ||
} | ||
) | ||
},{}) // end reduce | ||
} | ||
// Gets the value type and returns the correct value field name | ||
// TODO: Support more types as the are released | ||
const getType = val => | ||
typeof val === 'string' ? 'stringValue' | ||
: typeof val === 'boolean' ? 'booleanValue' | ||
: typeof val === 'number' && parseInt(val) === val ? 'longValue' | ||
: typeof val === 'number' && parseFloat(val) === val ? 'doubleValue' | ||
: val === null ? 'isNull' | ||
: Buffer.isBuffer(val) ? 'blobValue' | ||
// : Array.isArray(val) ? 'arrayValue' This doesn't work yet | ||
: undefined | ||
// Gets the value type and returns the correct value field name | ||
// TODO: Support more types as the are released | ||
const getType = val => | ||
typeof val === 'string' ? 'stringValue' | ||
: typeof val === 'boolean' ? 'booleanValue' | ||
: typeof val === 'number' && parseInt(val) === val ? 'longValue' | ||
: typeof val === 'number' && parseFloat(val) === val ? 'doubleValue' | ||
: val === null ? 'isNull' | ||
: Buffer.isBuffer(val) ? 'blobValue' | ||
// : Array.isArray(val) ? 'arrayValue' This doesn't work yet | ||
// TODO: there is a 'structValue' now for postgres | ||
: typeof val === 'object' | ||
&& Object.keys(val).length === 1 | ||
&& supportedTypes.includes(Object.keys(val)[0]) ? null | ||
: undefined | ||
// Creates a standard Data API parameter using the supplied inputs | ||
const formatType = (name,value,type) => { | ||
return { | ||
name, | ||
// Creates a standard Data API parameter using the supplied inputs | ||
const formatType = (name,value,type) => { | ||
return Object.assign( | ||
{ name }, | ||
type === null ? { value } | ||
: { | ||
value: { | ||
[type ? type : error(`'${name}'' is an invalid type`)] : | ||
type === 'isNull' ? true : value | ||
[type ? type : error(`'${name}' is an invalid type`)] | ||
: type === 'isNull' ? true : value | ||
} | ||
} | ||
} // end formatType | ||
) | ||
} // end formatType | ||
// Formats the results of a query response | ||
// TODO: Support generatedFields (use case insertId) | ||
const formatResults = ( | ||
{ // destructure results | ||
columnMetadata, // ONLY when hydrate or includeResultMetadata is true | ||
numberOfRecordsUpdated, // ONLY for executeStatement method | ||
records, // ONLY for executeStatement method | ||
generatedFields, // ONLY for INSERTS | ||
updateResults // ONLY on batchExecuteStatement | ||
}, | ||
hydrate, | ||
includeMeta | ||
) => | ||
Object.assign( | ||
includeMeta ? { columnMetadata } : {}, | ||
numberOfRecordsUpdated !== undefined ? { numberOfRecordsUpdated } : {}, | ||
records ? { | ||
records: formatRecords(records, hydrate ? columnMetadata : false) | ||
} : {}, | ||
updateResults ? { updateResults: formatUpdateResults(updateResults) } : {}, | ||
generatedFields && generatedFields.length > 0 ? | ||
{ insertId: generatedFields[0].longValue } : {} | ||
) | ||
// Formats the results of a query response | ||
const formatResults = ( | ||
{ // destructure results | ||
columnMetadata, // ONLY when hydrate or includeResultMetadata is true | ||
numberOfRecordsUpdated, // ONLY for executeStatement method | ||
records, // ONLY for executeStatement method | ||
generatedFields, // ONLY for INSERTS | ||
updateResults // ONLY on batchExecuteStatement | ||
}, | ||
hydrate, | ||
includeMeta | ||
) => | ||
Object.assign( | ||
includeMeta ? { columnMetadata } : {}, | ||
numberOfRecordsUpdated !== undefined && !records ? { numberOfRecordsUpdated } : {}, | ||
records ? { | ||
records: formatRecords(records, hydrate ? columnMetadata : false) | ||
} : {}, | ||
updateResults ? { updateResults: formatUpdateResults(updateResults) } : {}, | ||
generatedFields && generatedFields.length > 0 ? | ||
{ insertId: generatedFields[0].longValue } : {} | ||
) | ||
// Processes records and either extracts Typed Values into an array, or | ||
// object with named column labels | ||
const formatRecords = (recs,columns) => { | ||
// Processes records and either extracts Typed Values into an array, or | ||
// object with named column labels | ||
const formatRecords = (recs,columns) => { | ||
// Create map for efficient value parsing | ||
let fmap = recs && recs[0] ? recs[0].map((x,i) => { | ||
return Object.assign({}, | ||
columns ? { label: columns[i].label } : {} ) // add column labels | ||
}) : {} | ||
// Create map for efficient value parsing | ||
let fmap = recs && recs[0] ? recs[0].map((x,i) => { | ||
return Object.assign({}, | ||
columns ? { label: columns[i].label } : {} ) // add column labels | ||
}) : {} | ||
// Map over all the records (rows) | ||
return recs ? recs.map(rec => { | ||
// Map over all the records (rows) | ||
return recs ? recs.map(rec => { | ||
// Reduce each field in the record (row) | ||
return rec.reduce((acc,field,i) => { | ||
// Reduce each field in the record (row) | ||
return rec.reduce((acc,field,i) => { | ||
// If the field is null, always return null | ||
if (field.isNull === true) { | ||
return columns ? // object if hydrate, else array | ||
Object.assign(acc,{ [fmap[i].label]: null }) | ||
: acc.concat(null) | ||
// If the field is null, always return null | ||
if (field.isNull === true) { | ||
return columns ? // object if hydrate, else array | ||
Object.assign(acc,{ [fmap[i].label]: null }) | ||
: acc.concat(null) | ||
// If the field is mapped, return the mapped field | ||
} else if (fmap[i] && fmap[i].field) { | ||
return columns ? // object if hydrate, else array | ||
Object.assign(acc,{ [fmap[i].label]: field[fmap[i].field] }) | ||
: acc.concat(field[fmap[i].field]) | ||
// If the field is mapped, return the mapped field | ||
} else if (fmap[i] && fmap[i].field) { | ||
return columns ? // object if hydrate, else array | ||
Object.assign(acc,{ [fmap[i].label]: field[fmap[i].field] }) | ||
: acc.concat(field[fmap[i].field]) | ||
// Else discover the field type | ||
} else { | ||
// Else discover the field type | ||
} else { | ||
// Look for non-null fields | ||
Object.keys(field).map(type => { | ||
if (type !== 'isNull' && field[type] !== null) { | ||
fmap[i]['field'] = type | ||
} | ||
}) | ||
// Look for non-null fields | ||
Object.keys(field).map(type => { | ||
if (type !== 'isNull' && field[type] !== null) { | ||
fmap[i]['field'] = type | ||
} | ||
}) | ||
// Return the mapped field (this should NEVER be null) | ||
return columns ? // object if hydrate, else array | ||
Object.assign(acc,{ [fmap[i].label]: field[fmap[i].field] }) | ||
: acc.concat(field[fmap[i].field]) | ||
} | ||
// Return the mapped field (this should NEVER be null) | ||
return columns ? // object if hydrate, else array | ||
Object.assign(acc,{ [fmap[i].label]: field[fmap[i].field] }) | ||
: acc.concat(field[fmap[i].field]) | ||
} | ||
}, columns ? {} : []) // init object if hydrate, else init array | ||
}) : [] // empty record set returns an array | ||
} // end formatRecords | ||
}, columns ? {} : []) // init object if hydrate, else init array | ||
}) : [] // empty record set returns an array | ||
} // end formatRecords | ||
// Format updateResults and extract insertIds | ||
const formatUpdateResults = res => res.map(x => { | ||
return x.generatedFields && x.generatedFields.length > 0 ? | ||
{ insertId: x.generatedFields[0].longValue } : {} | ||
}) | ||
// Format updateResults and extract insertIds | ||
const formatUpdateResults = res => res.map(x => { | ||
return x.generatedFields && x.generatedFields.length > 0 ? | ||
{ insertId: x.generatedFields[0].longValue } : {} | ||
}) | ||
// Merge configuration data with supplied arguments | ||
const mergeConfig = ({secretArn,resourceArn,database},args) => | ||
Object.assign({secretArn,resourceArn,database},args) | ||
// Merge configuration data with supplied arguments | ||
const mergeConfig = (initialConfig,args) => | ||
Object.assign(initialConfig,args) | ||
@@ -281,70 +299,71 @@ | ||
// Query function (use standard form for `this` context) | ||
const query = async function(config,..._args) { | ||
// Query function (use standard form for `this` context) | ||
const query = async function(config,...args) { | ||
// Flatten passed in args to single depth array | ||
const args = flatten(_args) | ||
// Deprecated this since it was collapsing batches | ||
// const args = flatten(_args) | ||
// Parse and process sql | ||
const sql = parseSQL(args) | ||
const sqlParams = getSqlParams(sql) | ||
// Parse and process sql | ||
const sql = parseSQL(args) | ||
const sqlParams = getSqlParams(sql) | ||
// Parse hydration setting | ||
const hydrateColumnNames = parseHydrate(config,args) | ||
// Parse hydration setting | ||
const hydrateColumnNames = parseHydrate(config,args) | ||
// Parse and normalize parameters | ||
const parameters = normalizeParams(parseParams(args)) | ||
// Parse and normalize parameters | ||
const parameters = normalizeParams(parseParams(args)) | ||
// Process parameters and escape necessary SQL | ||
const { processedParams,escapedSql } = processParams(sql,sqlParams,parameters) | ||
// Process parameters and escape necessary SQL | ||
const { processedParams,escapedSql } = processParams(sql,sqlParams,parameters) | ||
// Determine if this is a batch request | ||
const isBatch = processedParams.length > 0 | ||
&& Array.isArray(processedParams[0]) ? true : false | ||
// Determine if this is a batch request | ||
const isBatch = processedParams.length > 0 | ||
&& Array.isArray(processedParams[0]) ? true : false | ||
const params = Object.assign( | ||
prepareParams(config,args), | ||
{ | ||
database: parseDatabase(config,args), // add database | ||
sql: escapedSql // add escaped sql statement | ||
}, | ||
// Only include parameters if they exist | ||
processedParams.length > 0 ? | ||
// Batch statements require parameterSets instead of parameters | ||
{ [isBatch ? 'parameterSets' : 'parameters']: processedParams } : {}, | ||
// Force meta data if set and not a batch | ||
hydrateColumnNames && !isBatch ? { includeResultMetadata: true } : {}, | ||
// If a transactionId is passed, overwrite any manual input | ||
config.transactionId ? { transactionId: config.transactionId } : {} | ||
) // end params | ||
// Create/format the parameters | ||
const params = Object.assign( | ||
prepareParams(config,args), | ||
{ | ||
database: parseDatabase(config,args), // add database | ||
sql: escapedSql // add escaped sql statement | ||
}, | ||
// Only include parameters if they exist | ||
processedParams.length > 0 ? | ||
// Batch statements require parameterSets instead of parameters | ||
{ [isBatch ? 'parameterSets' : 'parameters']: processedParams } : {}, | ||
// Force meta data if set and not a batch | ||
hydrateColumnNames && !isBatch ? { includeResultMetadata: true } : {}, | ||
// If a transactionId is passed, overwrite any manual input | ||
config.transactionId ? { transactionId: config.transactionId } : {} | ||
) // end params | ||
try { // attempt to run the query | ||
try { // attempt to run the query | ||
// Capture the result for debugging | ||
let result = await (isBatch ? config.RDS.batchExecuteStatement(params).promise() | ||
: config.RDS.executeStatement(params).promise()) | ||
// Capture the result for debugging | ||
let result = await (isBatch ? config.RDS.batchExecuteStatement(params).promise() | ||
: config.RDS.executeStatement(params).promise()) | ||
// console.log(result) | ||
// FOR DEBUGGING: console.log(JSON.stringify(result,null,2)) | ||
// Format and return the results | ||
return formatResults( | ||
result, | ||
hydrateColumnNames, | ||
args[0].includeResultMetadata === true ? true : false | ||
) | ||
// Format and return the results | ||
return formatResults( | ||
result, | ||
hydrateColumnNames, | ||
args[0].includeResultMetadata === true ? true : false | ||
) | ||
} catch(e) { | ||
} catch(e) { | ||
if (this && this.rollback) { | ||
let rollback = await config.RDS.rollbackTransaction( | ||
pick(params,['resourceArn','secretArn','transactionId']) | ||
).promise() | ||
if (this && this.rollback) { | ||
let rollback = await config.RDS.rollbackTransaction( | ||
pick(params,['resourceArn','secretArn','transactionId']) | ||
).promise() | ||
this.rollback(e,rollback) | ||
} | ||
// Throw the error | ||
throw e | ||
this.rollback(e,rollback) | ||
} | ||
// Throw the error | ||
throw e | ||
} | ||
} // end query | ||
} // end query | ||
@@ -357,67 +376,67 @@ | ||
// Init a transaction object and return methods | ||
const transaction = (config,_args) => { | ||
// Init a transaction object and return methods | ||
const transaction = (config,_args) => { | ||
let args = typeof _args === 'object' ? [_args] : [{}] | ||
let queries = [] // keep track of queries | ||
let rollback = () => {} // default rollback event | ||
let args = typeof _args === 'object' ? [_args] : [{}] | ||
let queries = [] // keep track of queries | ||
let rollback = () => {} // default rollback event | ||
const txConfig = Object.assign( | ||
prepareParams(config,args), | ||
{ | ||
database: parseDatabase(config,args), // add database | ||
hydrateColumnNames: parseHydrate(config,args), // add hydrate | ||
RDS: config.RDS // reference the RDSDataService instance | ||
const txConfig = Object.assign( | ||
prepareParams(config,args), | ||
{ | ||
database: parseDatabase(config,args), // add database | ||
hydrateColumnNames: parseHydrate(config,args), // add hydrate | ||
RDS: config.RDS // reference the RDSDataService instance | ||
} | ||
) | ||
return { | ||
query: function(...args) { | ||
if (typeof args[0] === 'function') { | ||
queries.push(args[0]) | ||
} else { | ||
queries.push(() => [...args]) | ||
} | ||
) | ||
return { | ||
query: function(...args) { | ||
if (typeof args[0] === 'function') { | ||
queries.push(args[0]) | ||
} else { | ||
queries.push(() => [...args]) | ||
} | ||
return this | ||
}, | ||
rollback: function(fn) { | ||
if (typeof fn === 'function') { rollback = fn } | ||
return this | ||
}, | ||
commit: async function() { return await commit(txConfig,queries,rollback) } | ||
} | ||
return this | ||
}, | ||
rollback: function(fn) { | ||
if (typeof fn === 'function') { rollback = fn } | ||
return this | ||
}, | ||
commit: async function() { return await commit(txConfig,queries,rollback) } | ||
} | ||
} | ||
// Commit transaction by running queries | ||
const commit = async (config,queries,rollback) => { | ||
// Commit transaction by running queries | ||
const commit = async (config,queries,rollback) => { | ||
let results = [] // keep track of results | ||
let results = [] // keep track of results | ||
// Start a transaction | ||
const { transactionId } = await config.RDS.beginTransaction( | ||
pick(config,['resourceArn','secretArn','database']) | ||
).promise() | ||
// Start a transaction | ||
const { transactionId } = await config.RDS.beginTransaction( | ||
pick(config,['resourceArn','secretArn','database']) | ||
).promise() | ||
// Add transactionId to the config | ||
let txConfig = Object.assign(config, { transactionId }) | ||
// Add transactionId to the config | ||
let txConfig = Object.assign(config, { transactionId }) | ||
// Loop through queries | ||
for (let i = 0; i < queries.length; i++) { | ||
// Execute the queries, pass the rollback as context | ||
let result = await query.apply({rollback},[config,queries[i](results[results.length-1],results)]) | ||
// Add the result to the main results accumulator | ||
results.push(result) | ||
} | ||
// Loop through queries | ||
for (let i = 0; i < queries.length; i++) { | ||
// Execute the queries, pass the rollback as context | ||
let result = await query.apply({rollback},[config,queries[i](results[results.length-1],results)]) | ||
// Add the result to the main results accumulator | ||
results.push(result) | ||
} | ||
// Commit our transaction | ||
const { transactionStatus } = await txConfig.RDS.commitTransaction( | ||
pick(config,['resourceArn','secretArn','transactionId']) | ||
).promise() | ||
// Commit our transaction | ||
const { transactionStatus } = await txConfig.RDS.commitTransaction( | ||
pick(config,['resourceArn','secretArn','transactionId']) | ||
).promise() | ||
// Add the transaction status to the results | ||
results.push({transactionStatus}) | ||
// Add the transaction status to the results | ||
results.push({transactionStatus}) | ||
// Return the results | ||
return results | ||
} | ||
// Return the results | ||
return results | ||
} | ||
@@ -433,10 +452,22 @@ /********************************************************************/ | ||
const options = typeof params.options === 'object' ? params.options | ||
: params.options !== undefined ? error(`'options' must be an object`) | ||
: params.options !== undefined ? error('\'options\' must be an object') | ||
: {} | ||
// Update the default AWS http agent with our new sslAgent | ||
if (typeof params.keepAlive !== false) { | ||
if (typeof params.keepAlive === 'boolean' ? params.keepAlive : true) { | ||
AWS.config.update({ httpOptions: { agent: sslAgent } }) | ||
} | ||
// Update the AWS http agent with the region | ||
if (typeof params.region === 'string') { | ||
AWS.config.update({ region: params.region }) | ||
} | ||
// Disable ssl if wanted for local development | ||
if (params.sslEnabled === false) { | ||
// AWS.config.update({ sslEnabled: false }) | ||
options.sslEnabled = false | ||
} | ||
// Set the configuration for this instance | ||
@@ -446,12 +477,15 @@ const config = { | ||
// Require secretArn | ||
secretArn: typeof params.secretArn === 'string' ? params.secretArn | ||
: error(`'secretArn' string value required`), | ||
secretArn: typeof params.secretArn === 'string' ? | ||
params.secretArn | ||
: error('\'secretArn\' string value required'), | ||
// Require resourceArn | ||
resourceArn: typeof params.resourceArn === 'string' ? params.resourceArn | ||
: error(`'resourceArn' string value required`), | ||
resourceArn: typeof params.resourceArn === 'string' ? | ||
params.resourceArn | ||
: error('\'resourceArn\' string value required'), | ||
// Load optional database | ||
database: typeof params.database === 'string' ? params.database | ||
: params.database !== undefined ? error(`'database' must be a string`) | ||
database: typeof params.database === 'string' ? | ||
params.database | ||
: params.database !== undefined ? error('\'database\' must be a string') | ||
: undefined, | ||
@@ -458,0 +492,0 @@ |
{ | ||
"name": "data-api-client", | ||
"version": "1.0.0-beta", | ||
"version": "1.0.0", | ||
"description": "A lightweight wrapper that simplifies working with the Amazon Aurora Serverless Data API", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "jest", | ||
"lint": "eslint ." | ||
}, | ||
@@ -26,7 +27,15 @@ "repository": { | ||
"devDependencies": { | ||
"aws-sdk": "^2.466.0" | ||
"aws-sdk": "^2.466.0", | ||
"eslint": "^6.6.0", | ||
"jest": "^25.0.0", | ||
"rewire": "^4.0.1" | ||
}, | ||
"dependencies": { | ||
"sqlstring": "^2.3.1" | ||
} | ||
}, | ||
"files": [ | ||
"LICENSE", | ||
"README.md", | ||
"index.js" | ||
] | ||
} |
@@ -6,4 +6,2 @@ # Aurora Serverless Data API Client | ||
**THIS PROJECT IS IN BETA. I WOULD LOVE YOUR FEEDBACK! PLEASE FEEL FREE TO CONTACT ME ON TWITTER [@jeremy_daly](https://twitter.com/jeremy_daly)** | ||
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**. | ||
@@ -50,10 +48,9 @@ | ||
[ | ||
{ name: 'Marcia', age: 17, curls: false }, | ||
{ name: 'Peter', age: 15, curls: false }, | ||
{ name: 'Jan', age: 15, curls: false }, | ||
{ name: 'Cindy', age: 12, curls: true }, | ||
{ name: 'Bobby', age: 12, curls: false } | ||
[{ name: 'Marcia', age: 17, curls: false }], | ||
[{ name: 'Peter', age: 15, curls: false }], | ||
[{ name: 'Jan', age: 15, curls: false }], | ||
[{ name: 'Cindy', age: 12, curls: true }], | ||
[{ name: 'Bobby', age: 12, curls: false }] | ||
] | ||
) | ||
// Update with named parameters | ||
@@ -106,41 +103,19 @@ let update = await data.query( | ||
Specifying all of those data types in the parameters is a bit clunky, plus every query requires you to pass in the `secretArn`, `resourceArn`, `database`, and any other method parameters you might need. | ||
Specifying all of those data types in the parameters is a bit clunky. In addition to requiring types for parameters, it also returns each field as an object with its value assigned to a key that represents its data type, like this: | ||
In addition to requiring types for parameters, it also returns each field as an object containing all possible data types, like this: | ||
```javascript | ||
{ // id field | ||
"blobValue": null, | ||
"booleanValue": null, | ||
"doubleValue": null, | ||
"isNull": null, | ||
"longValue": 9, | ||
"stringValue": null | ||
"longValue": 9 | ||
}, | ||
{ // name field | ||
"blobValue": null, | ||
"booleanValue": null, | ||
"doubleValue": null, | ||
"isNull": null, | ||
"longValue": null, | ||
"stringValue": "Cousin Oliver" | ||
}, | ||
{ // age field | ||
"blobValue": null, | ||
"booleanValue": null, | ||
"doubleValue": null, | ||
"isNull": null, | ||
"longValue": 10, | ||
"stringValue": null | ||
"longValue": 10 | ||
}, | ||
{ // has_curls field | ||
"blobValue": null, | ||
"booleanValue": false, | ||
"doubleValue": null, | ||
"isNull": null, | ||
"longValue": null, | ||
"stringValue": null | ||
"booleanValue": false | ||
} | ||
``` | ||
Not only are there no column names, but you have to remove all `null` fields and pull the value from the remaining data type. Lots of extra work that the **Data API Client** handles automatically for you. 😀 | ||
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. 😀 | ||
@@ -165,5 +140,6 @@ ## Installation and Setup | ||
| keepAlive | `boolean` | Enables HTTP Keep-Alive for calls to the AWS SDK. This dramatically decreases the latency of subsequent calls. | `true` | | ||
| sslEnabled | `boolean` | *Optional* Enables SSL HTTP endpoint. Can be disable for local development. | `true` | | ||
| 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. | `{}` | | ||
| region | `string` | *Optional* AWS region to use. | `aws-sdk default` | | ||
## How to use this module | ||
@@ -328,3 +304,3 @@ | ||
## Data API Limitations / Wonkiness | ||
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 outline some of my findings below. | ||
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. | ||
@@ -348,4 +324,4 @@ ### You can't send in an array of values | ||
### Named parameters MUST be sent in order | ||
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. | ||
### ~~Named parameters MUST be sent in order~~ | ||
~~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! | ||
@@ -444,4 +420,3 @@ ### You can't parameterize identifiers | ||
## Contributions | ||
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. | ||
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). |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
439
0
1
41401
4
416