@aws-lite/client
Advanced tools
Comparing version 0.12.2 to 0.13.0
{ | ||
"name": "@aws-lite/client", | ||
"version": "0.12.2", | ||
"version": "0.13.0", | ||
"description": "A simple, fast, extensible AWS client", | ||
@@ -19,5 +19,5 @@ "homepage": "https://github.com/architect/aws-lite", | ||
"test:precommit": "cross-env PRECOMMIT=true npm run gen && npm run lint", | ||
"test:integration": "cross-env tape 'test/integration/**/*-test.js' | tap-spec", | ||
"test:unit": "cross-env tape 'test/unit/**/*-test.js' | tap-spec", | ||
"test:live": "cross-env tape 'test/live/**/*-test.js' | tap-spec", | ||
"test:integration": "cross-env tape 'test/integration/**/*-test.js' | tap-arc", | ||
"test:unit": "cross-env tape 'test/unit/**/*-test.js' | tap-arc", | ||
"test:live": "cross-env tape 'test/live/**/*-test.js' | tap-arc", | ||
"coverage": "nyc --reporter=lcov --reporter=text npm run test:unit", | ||
@@ -53,3 +53,3 @@ "vendor": "scripts/vendor/vendor.sh" | ||
"simple-git-hooks": "^2.9.0", | ||
"tap-spec": "^5.0.0", | ||
"tap-arc": "^1.2.2", | ||
"tape": "^5.7.0" | ||
@@ -61,3 +61,8 @@ }, | ||
"workspaces": [ | ||
"plugins/apigateway", | ||
"plugins/apigatewaymanagementapi", | ||
"plugins/apigatewayv2", | ||
"plugins/cloudformation", | ||
"plugins/cloudfront", | ||
"plugins/cloudwatch-logs", | ||
"plugins/dynamodb", | ||
@@ -64,0 +69,0 @@ "plugins/lambda", |
@@ -195,3 +195,7 @@ <h1><a href="https://aws-lite.org"><code>aws-lite</code></a></h1> | ||
<!-- plugins_start --> | ||
- [API Gateway V2](https://www.npmjs.com/package/@aws-lite/apigatewayv2) | ||
- [API Gateway WebSocket Management API](https://www.npmjs.com/package/@aws-lite/apigatewaymanagementapi) | ||
- [CloudFormation](https://www.npmjs.com/package/@aws-lite/cloudformation) | ||
- [CloudFront](https://www.npmjs.com/package/@aws-lite/cloudfront) | ||
- [CloudWatch Logs](https://www.npmjs.com/package/@aws-lite/cloudwatch-logs) | ||
- [DynamoDB](https://www.npmjs.com/package/@aws-lite/dynamodb) | ||
@@ -198,0 +202,0 @@ - [Lambda](https://www.npmjs.com/package/@aws-lite/lambda) |
@@ -1,7 +0,5 @@ | ||
let { readdir } = require('fs/promises') | ||
let { join } = require('path') | ||
let { services } = require('./services') | ||
let request = require('./request') | ||
let { validateInput } = require('./validate') | ||
let { awsjson } = require('./lib') | ||
let { awsjson, exists } = require('./lib') | ||
let errorHandler = require('./error') | ||
@@ -54,2 +52,4 @@ let aws | ||
if (autoloadPlugins) { | ||
let { readdir } = require('fs/promises') | ||
let { join } = require('path') | ||
let awsLite = '@aws-lite' | ||
@@ -59,12 +59,14 @@ let nodeModulesDir | ||
catch { nodeModulesDir = join(process.cwd(), 'node_modules') } // Likely just aws-lite tests | ||
let mods = await readdir(nodeModulesDir) | ||
// Find first-party plugins | ||
if (mods.includes(awsLite)) { | ||
let knownPlugins = await readdir(join(nodeModulesDir, awsLite)) | ||
let filtered = knownPlugins.filter(p => !ignored.includes(p) && !p.endsWith('-types')).map(p => `@aws-lite/${p}`) | ||
plugins.push(...filtered) | ||
if (await exists(nodeModulesDir)) { | ||
let mods = await readdir(nodeModulesDir) | ||
// Find first-party plugins | ||
if (mods.includes(awsLite)) { | ||
let knownPlugins = await readdir(join(nodeModulesDir, awsLite)) | ||
let filtered = knownPlugins.filter(p => !ignored.includes(p) && !p.endsWith('-types')).map(p => `@aws-lite/${p}`) | ||
plugins.push(...filtered) | ||
} | ||
// Find correctly namespaced 3rd-party plugins | ||
let findPlugins = mod => mod.startsWith('aws-lite-plugin-') && plugins.push(mod) | ||
mods.forEach(findPlugins) | ||
} | ||
// Find correctly namespaced 3rd-party plugins | ||
let findPlugins = mod => mod.startsWith('aws-lite-plugin-') && plugins.push(mod) | ||
mods.forEach(findPlugins) | ||
} | ||
@@ -78,3 +80,2 @@ | ||
try { | ||
// eslint-disable-next-line | ||
plugin = require(pluginName) | ||
@@ -115,3 +116,2 @@ } | ||
if (!aws) { | ||
// eslint-disable-next-line | ||
aws = require('./_vendor/aws') | ||
@@ -199,3 +199,3 @@ } | ||
let updatedError | ||
if (method.error && !(input instanceof Error)) { | ||
if (method.error && !(err instanceof Error)) { | ||
try { | ||
@@ -202,0 +202,0 @@ updatedError = await method.error(err, { ...pluginUtils, region: selectedRegion }) |
@@ -22,7 +22,12 @@ module.exports = function errorHandler (input) { | ||
// The most common error response from AWS services | ||
// Note: many services return a `Code` property; this generally represents the coded error name | ||
// (jic, also look for `code` and `__type`) | ||
if (error && typeof error === 'object') { | ||
Object.entries(error).forEach(([ name, value ]) => { | ||
if (name.toLowerCase() === 'message') err.message = value | ||
else err[name] = value | ||
Object.entries(error).forEach(([ n, value ]) => { | ||
const name = n.toLowerCase() | ||
/**/ if (name === 'message') err[name] = value | ||
else if (name === 'code') err[name] = err.name = value | ||
else err[n] = value | ||
}) | ||
if (err.__type && !err.code) err.code = err.name = err.__type | ||
} | ||
@@ -29,0 +34,0 @@ // Less common: sometimes strings (of XML), possibly without a content-type |
@@ -1,2 +0,2 @@ | ||
let aws, ini | ||
let aws, ini, xml | ||
@@ -60,2 +60,12 @@ // AWS-flavored JSON stuff | ||
function tidyQuery (obj) { | ||
let qs = require('querystring') | ||
let tidied = {} | ||
Object.entries(obj).forEach(([ k, v ]) => { | ||
// Who knows, maybe there's an API service that uses boolean query string params | ||
if (v || v === false) tidied[k] = v | ||
}) | ||
if (Object.keys(tidied).length) return qs.stringify(tidied) | ||
} | ||
// Probably this is going to need some refactoring in Arc 11 | ||
@@ -74,2 +84,52 @@ // Certainly it is not reliable in !Arc local Lambda emulation | ||
module.exports = { awsjson, exists, loadAwsConfig, readConfig, useAWS } | ||
// XML stuff | ||
let textNodeName = '#text' | ||
/* istanbul ignore next */ | ||
function instantiateXml () { | ||
if (xml) return | ||
// Only require the vendor if + when it's actually needed | ||
let vendor = require('./_vendor/xml') | ||
// The following was pulled directly from AWS's implementations of `fast-xml-parser` in SDKv3 | ||
xml = { | ||
parser: new vendor.XMLParser({ | ||
attributeNamePrefix: '', | ||
htmlEntities: true, | ||
ignoreAttributes: false, | ||
ignoreDeclaration: true, | ||
parseTagValue: false, | ||
trimValues: false, | ||
tagValueProcessor: (_, val) => (val.trim() === '' && val.includes('\n') ? '' : undefined), | ||
}), | ||
builder: new vendor.XMLBuilder(), | ||
} | ||
xml.parser.addEntity('#xD', '\r') | ||
xml.parser.addEntity('#10', '\n') | ||
xml.parser.getValueFromTextNode = vendor.getValueFromTextNode | ||
} | ||
function buildXml (obj) { | ||
instantiateXml() | ||
return xml.builder.build(obj) | ||
} | ||
function parseXml (body) { | ||
instantiateXml() | ||
let parsed = xml.parser.parse(body) | ||
let key = Object.keys(parsed)[0] | ||
let payloadToReturn = parsed[key] | ||
/* istanbul ignore next */ // TODO remove + test | ||
if (payloadToReturn[textNodeName]) { | ||
payloadToReturn[key] = payloadToReturn[textNodeName] | ||
delete payloadToReturn[textNodeName] | ||
} | ||
return xml.parser.getValueFromTextNode(payloadToReturn) | ||
} | ||
module.exports = { | ||
awsjson, | ||
exists, | ||
loadAwsConfig, | ||
readConfig, | ||
tidyQuery, | ||
useAWS, | ||
buildXml, | ||
parseXml, | ||
} |
@@ -1,8 +0,4 @@ | ||
let qs = require('querystring') | ||
let { Readable } = require('stream') | ||
let aws4 = require('aws4') | ||
let { globalServices, semiGlobalServices } = require('./services') | ||
let { is } = require('./validate') | ||
let { awsjson, useAWS } = require('./lib') | ||
let xml | ||
let { awsjson, buildXml, parseXml, tidyQuery, useAWS } = require('./lib') | ||
@@ -17,3 +13,2 @@ /* istanbul ignore next */ | ||
let XMLContentType = ct => ct.match(XMLregex) | ||
let textNodeName = '#text' | ||
@@ -37,31 +32,2 @@ // HTTP client agent cache to prevent generating new agents for every request | ||
} | ||
/* istanbul ignore next */ | ||
function instantiateXml () { | ||
// eslint-disable-next-line | ||
let vendor = require('./_vendor/xml') | ||
// The following was pulled directly from AWS's implementations of `fast-xml-parser` in SDKv3 | ||
xml = new vendor.XMLParser({ | ||
attributeNamePrefix: '', | ||
htmlEntities: true, | ||
ignoreAttributes: false, | ||
ignoreDeclaration: true, | ||
parseTagValue: false, | ||
trimValues: false, | ||
tagValueProcessor: (_, val) => (val.trim() === '' && val.includes('\n') ? '' : undefined), | ||
}) | ||
xml.addEntity('#xD', '\r') | ||
xml.addEntity('#10', '\n') | ||
xml.getValueFromTextNode = vendor.getValueFromTextNode | ||
} | ||
function parseXml (body) { | ||
let parsed = xml.parse(body) | ||
let key = Object.keys(parsed)[0] | ||
let payloadToReturn = parsed[key] | ||
/* istanbul ignore next */ // TODO remove + test | ||
if (payloadToReturn[textNodeName]) { | ||
payloadToReturn[key] = payloadToReturn[textNodeName] | ||
delete payloadToReturn[textNodeName] | ||
} | ||
return xml.getValueFromTextNode(payloadToReturn) | ||
} | ||
@@ -105,7 +71,11 @@ module.exports = async function _request (params, creds, region, config, metadata) { | ||
if (params.query) { | ||
let { is } = require('./validate') | ||
if (!is.object(params.query)) { | ||
throw ReferenceError('Query property must be an object') | ||
} | ||
// Expect aws4 to handle RFC 3986 encoding when appending the query string to the passed path | ||
params.path += '?' + qs.stringify(params.query) | ||
let query = tidyQuery(params.query) | ||
if (query) { | ||
// Expect aws4 to handle RFC 3986 encoding when appending the query string to the passed path | ||
params.path += '?' + query | ||
} | ||
} | ||
@@ -122,3 +92,3 @@ | ||
let isBuffer = body instanceof Buffer | ||
let isStream = body instanceof Readable | ||
let isStream = (body?.on && body?._read && body?._readableState) | ||
@@ -130,15 +100,20 @@ // Detecting objects leaves open the possibility of some weird valid JSON (like just a null), deal with it if / when we need to I guess | ||
// A variety of services use AWS JSON; we'll make it easier via a header or passed param | ||
// Allow for manual encoding by passing a header while setting awsjson to false | ||
let awsjsonEncode = params.awsjson || | ||
(AwsJSONContentType(contentType) && params.awsjson !== false) | ||
if (awsjsonEncode) { | ||
// Backfill content-type header yet again | ||
if (!AwsJSONContentType(contentType)) { | ||
contentType = 'application/x-amz-json-1.0' | ||
if (XMLContentType(contentType)) { | ||
params.body = buildXml(body) | ||
} | ||
else { | ||
// A variety of services use AWS JSON; we'll make it easier via a header or passed param | ||
// Allow for manual encoding by passing a header while setting awsjson to false | ||
let awsjsonEncode = params.awsjson || | ||
(AwsJSONContentType(contentType) && params.awsjson !== false) | ||
if (awsjsonEncode) { | ||
// Backfill content-type header yet again | ||
if (!AwsJSONContentType(contentType)) { | ||
contentType = 'application/x-amz-json-1.0' | ||
} | ||
body = awsjson.marshall(body, params.awsjson) | ||
} | ||
body = awsjson.marshall(body, params.awsjson) | ||
// Final JSON encoding | ||
params.body = JSON.stringify(body) | ||
} | ||
// Final JSON encoding | ||
params.body = JSON.stringify(body) | ||
} | ||
@@ -186,3 +161,3 @@ // Everything besides streams pass through for signing | ||
let isHTTPS = options.host.includes('.amazonaws.com') || options.protocol === 'https:' | ||
/* istanbul ignore next */ // eslint-disable-next-line | ||
/* istanbul ignore next */ | ||
let http = isHTTPS ? require('https') : require('http') | ||
@@ -220,3 +195,5 @@ | ||
let body = Buffer.concat(data), payload, rawString | ||
let contentType = config.responseContentType || headers['content-type'] || headers['Content-Type'] || '' | ||
let contentType = config.responseContentType || | ||
headers['content-type'] || | ||
headers['Content-Type'] || '' | ||
if (body.length && (JSONContentType(contentType) || AwsJSONContentType(contentType))) { | ||
@@ -237,6 +214,5 @@ payload = JSON.parse(body) | ||
if (body.length && XMLContentType(contentType)) { | ||
// Only require the vendor if it's actually needed | ||
payload = parseXml(body) | ||
/* istanbul ignore next */ | ||
if (!xml) instantiateXml() | ||
payload = parseXml(body) | ||
if (payload.xmlns) delete payload.xmlns | ||
@@ -254,5 +230,2 @@ /* istanbul ignore next */ | ||
try { | ||
// Only require the vendor if it's actually needed | ||
/* istanbul ignore next */ | ||
if (!xml) instantiateXml() | ||
payload = parseXml(body) | ||
@@ -321,2 +294,4 @@ } | ||
let { type, cursor, token, accumulator } = params.paginator | ||
let nestedAccumulator = accumulator.split('.').length > 1 | ||
if (!cursor || typeof cursor !== 'string') { | ||
@@ -340,2 +315,3 @@ throw ReferenceError(`aws-lite paginator requires a cursor property name (string)`) | ||
let items = [] | ||
let statusCode, headers | ||
async function get () { | ||
@@ -352,10 +328,38 @@ let result = await request( | ||
} | ||
if (!result.payload[accumulator]) { | ||
let accumulated = nestedAccumulator | ||
? accumulator.split('.').reduce((parent, child) => parent?.[child], result.payload) | ||
: result.payload[accumulator] | ||
if (!accumulated) { | ||
throw ReferenceError(`Pagination error: response accumulator property '${accumulator}' not found`) | ||
} | ||
if (!Array.isArray(result.payload[accumulator])) { | ||
throw ReferenceError(`Pagination error: response accumulator property '${accumulator}' must be an array`) | ||
// Best effort handling of properties that sometimes are / are not arrays, courtesy of XML | ||
// This can perhaps backfire in a few different ways, so hold onto your butts | ||
if (accumulated && !Array.isArray(accumulated)) { | ||
accumulated = [ accumulated ] | ||
} | ||
items.push(...result.payload[accumulator]) | ||
// Update statusCode and headers for response hooks | ||
statusCode = result.statusCode | ||
headers = result.headers | ||
// Exit if we're out of results | ||
if (!accumulated.length) { | ||
return | ||
} | ||
// Some services will just keep re-sending the final page with the final token | ||
// Exit here to prevent infinite loops if cursors match | ||
if (result.payload[token] && (type === 'payload' || !type) && | ||
result.payload[token] === params.payload[cursor]) { | ||
return | ||
} | ||
if (result.payload[token] && (type === 'query') && | ||
result.payload[token] === params.query[cursor]) { | ||
return | ||
} | ||
items.push(...accumulated) | ||
if (result.payload[token]) { | ||
@@ -375,3 +379,13 @@ if (type === 'payload' || !type) { | ||
await get() | ||
return { payload: { [accumulator]: items } } | ||
if (nestedAccumulator) { | ||
return { statusCode, headers, payload: reNestAccumulated(accumulator, items) } | ||
} | ||
return { statusCode, headers, payload: { [accumulator]: items } } | ||
} | ||
/* istanbul ignore next */ | ||
function reNestAccumulated (acc, items) { | ||
acc = Array.isArray(acc) ? acc : acc.split('.') | ||
if (!acc.length) return items | ||
return { [acc.shift()]: reNestAccumulated(acc, items) } | ||
} |
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
117542
2059
210