@allthings/cloud-toolkit
Version:
Standardizes the setup of aws, datadog and other things
1,111 lines (1,072 loc) • 38.5 kB
JavaScript
import * as querystring from 'querystring';
import * as zlib from 'zlib';
import * as crypto from 'crypto';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, UpdateCommand, ScanCommand, QueryCommand, PutCommand, GetCommand, DeleteCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb';
import Bottleneck from 'bottleneck';
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
import { SSMClient, GetParametersCommand, GetParameterCommand } from '@aws-sdk/client-ssm';
import * as aws$1 from '@aws-sdk/client-ses';
import { createTransport } from 'nodemailer';
import { SQSClient, SendMessageCommand, GetQueueUrlCommand, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs';
import { format, createLogger, transports } from 'winston';
export { Logger } from 'winston';
const defaultErrorHandler = (request, response, error) => {
const { requestId } = request.requestContext;
const errorName = error.name && typeof error.name === 'string'
? error.name.replace(/([A-Z])([A-Z])([a-z])|([a-z])([A-Z])/g, '$1$4 $2$3$5')
: 'Error';
const errorMessage = error.message || '';
const statusCode = error.statusCode ? ` ${error.statusCode}` : '';
return error.name === 'ClientError' ||
(error.statusCode >= 400 && error.statusCode < 500)
? response.respondTo({
html: `<html><body><strong>${errorName}${statusCode}</strong>: ${errorMessage}<br/>Request ID: ${requestId}</body></html>`,
json: { error: error.name, message: errorMessage, requestId },
}, error.statusCode || 400)
: response.respondTo({
html: `<html><body>An internal server error occurred.<br/>Request ID: ${requestId}</body></html>`,
json: {
error: 'Internal server error',
message: 'An internal server error occurred',
requestId,
},
}, 500);
};
class ClientError extends Error {
name = 'ClientError';
statusCode = 400;
constructor(message, name, statusCode = 400) {
super(String(message));
this.name = name || this.name;
this.statusCode = statusCode;
}
}
class ServerError extends Error {
name = 'ServerError';
statusCode = 500;
constructor(message, name, statusCode = 500) {
super(String(message));
this.name = name || this.name;
this.statusCode = statusCode;
}
}
const applyMiddleware = async (middlewareList, initialData, ...arguments_) => middlewareList.reduce(async (applied, middleware) => middleware(await applied, ...arguments_), Promise.resolve(initialData));
const decodeBase64 = (encoded) => {
const bodyBuffer = Buffer.from(encoded, 'base64');
return bodyBuffer.toString('utf8');
};
const decodeBase64Body = (request) => request.isBase64Encoded && typeof request.body === 'string'
? {
...request,
body: decodeBase64(request.body),
}
: request;
const parseCookiePair = (pair) => {
const indexOfEqualCharacter = pair.indexOf('=');
const looksLikeKeyValue = indexOfEqualCharacter !== -1;
const key = looksLikeKeyValue &&
pair.slice(0, Math.max(0, indexOfEqualCharacter)).trim();
const value = looksLikeKeyValue &&
pair.substr(indexOfEqualCharacter + 1, pair.length).trim();
const cleanValue = value && '"' === value[0] ? value.slice(1, -1) : value;
return key ? { [key]: cleanValue ? decodeURIComponent(cleanValue) : '' } : {};
};
const parseCookieHeader = (cookieHeader) => {
const pairs = cookieHeader.split(/; */);
return pairs.reduce((parsedCookies, pair) => ({ ...parsedCookies, ...parseCookiePair(pair) }), {});
};
const cookies = (request) => ({
...request,
cookies: parseCookieHeader(request.headers?.cookie || ''),
});
const setHostname = (request) => ({
...request,
hostname: request.headers?.host || undefined,
});
const parseJson = (stringified) => {
try {
return JSON.parse(stringified);
}
catch {
return {};
}
};
const parseJsonBody = (request) => {
const { headers, body } = request;
return headers['content-type'] &&
headers['content-type'].split(';').shift() === 'application/json' &&
typeof body === 'string'
? { ...request, body: parseJson(body) }
: request;
};
const state = new Map([['invocationCount', 0]]);
const meta = (request) => {
const invocationCount = Number(state.get('invocationCount'));
return {
...request,
meta: {
...request.meta,
coldStart: !invocationCount,
invocationCount: state
.set('invocationCount', invocationCount + 1)
.get('invocationCount'),
},
};
};
const normalizeHeaders = ({ headers = {}, ...request }) => ({
...request,
headers: Object.keys(headers).reduce((normalizedHeaders, key) => {
const normalizedHeaderKey = key.toLowerCase();
return request.provider === 'aws' &&
Reflect.has(normalizedHeaders, normalizedHeaderKey) &&
normalizedHeaderKey === 'content-type' &&
headers[key] === 'application/json'
? normalizedHeaders
: {
...normalizedHeaders,
[normalizedHeaderKey]: headers[key],
};
}, {}),
});
const normalizeProgrammingModel = (request) => ({
...request,
method: request.httpMethod ??
request.requestContext?.http?.method ??
'',
path: request.path ??
request.requestContext?.http?.path ??
'',
provider: 'aws',
query: request.queryStringParameters || {},
source: request.requestContext ? 'api-gateway' : request.source,
});
const addTimestamp = (request) => ({
...request,
timestamp: Date.now(),
});
const parseUrlEncodedBody = (request) => {
const { headers, body } = request;
return headers['content-type'] &&
headers['content-type'].split(';').shift() ===
'application/x-www-form-urlencoded' &&
typeof body === 'string'
? { ...request, body: querystring.parse(body) }
: request;
};
var makeRequest = async (event, context, options = {}) => applyMiddleware([
addTimestamp,
normalizeProgrammingModel,
normalizeHeaders,
decodeBase64Body,
cookies,
parseUrlEncodedBody,
parseJsonBody,
setHostname,
meta,
...(options.requestMiddleware || []),
], Object.freeze({
...event,
context,
}), options);
const setHeader = (response, responseData, key, value) => {
Object.assign(responseData.headers, { [key]: value });
return response;
};
const makeResponseObject = (responseData, body = '', statusCode = 200, { headers = {}, ...options } = {}, contentType) => Object.freeze({
...responseData,
body: body || '',
headers: {
...responseData?.headers,
'content-type': contentType,
...headers,
},
statusCode,
...options,
});
const html = (responseData, _, body, statusCode, options) => makeResponseObject(responseData, body, statusCode, options, 'text/html');
const stringifyIfNotStringifiedJson = (data) => {
const maybeTrimmedDataString = typeof data === 'string' ? data.trim() : data;
const isStringifiedJson = typeof data === 'string' &&
['{', '"'].includes(maybeTrimmedDataString[0]) &&
['}', '"'].includes(maybeTrimmedDataString.at(-1));
return isStringifiedJson ? maybeTrimmedDataString : JSON.stringify(data);
};
const json = (responseData, _, body, statusCode, options) => makeResponseObject(responseData, stringifyIfNotStringifiedJson(body), statusCode, options, 'application/json');
const COMPRESSIBLE = [
'application/activity+json',
'application/alto-costmap+json',
'application/alto-costmapfilter+json',
'application/alto-directory+json',
'application/alto-endpointcost+json',
'application/alto-endpointcostparams+json',
'application/alto-endpointprop+json',
'application/alto-endpointpropparams+json',
'application/alto-error+json',
'application/alto-networkmap+json',
'application/alto-networkmapfilter+json',
'application/atom+xml',
'application/calendar+json',
'application/coap-group+json',
'application/csvm+json',
'application/dart',
'application/dicom+json',
'application/ecmascript',
'application/fhir+json',
'application/fido.trusted-apps+json',
'application/geo+json',
'application/javascript',
'application/jf2feed+json',
'application/jose+json',
'application/jrd+json',
'application/json',
'application/json-patch+json',
'application/jsonml+json',
'application/jwk+json',
'application/jwk-set+json',
'application/ld+json',
'application/manifest+json',
'application/merge-patch+json',
'application/mud+json',
'application/postscript',
'application/ppsp-tracker+json',
'application/problem+json',
'application/raml+yaml',
'application/rdap+json',
'application/rdf+xml',
'application/reputon+json',
'application/rss+xml',
'application/rtf',
'application/scim+json',
'application/soap+xml',
'application/tar',
'application/vcard+json',
'application/vnd.amadeus+json',
'application/vnd.api+json',
'application/vnd.apothekende.reservation+json',
'application/vnd.avalon+json',
'application/vnd.bbf.usp.msg+json',
'application/vnd.bekitzur-stech+json',
'application/vnd.capasystems-pg+json',
'application/vnd.collection+json',
'application/vnd.collection.doc+json',
'application/vnd.collection.next+json',
'application/vnd.coreos.ignition+json',
'application/vnd.dart',
'application/vnd.datapackage+json',
'application/vnd.dataresource+json',
'application/vnd.document+json',
'application/vnd.drive+json',
'application/vnd.geo+json',
'application/vnd.google-earth.kml+xml',
'application/vnd.hal+json',
'application/vnd.hc+json',
'application/vnd.heroku+json',
'application/vnd.hyper+json',
'application/vnd.hyper-item+json',
'application/vnd.hyperdrive+json',
'application/vnd.ims.lis.v2.result+json',
'application/vnd.ims.lti.v2.toolconsumerprofile+json',
'application/vnd.ims.lti.v2.toolproxy+json',
'application/vnd.ims.lti.v2.toolproxy.id+json',
'application/vnd.ims.lti.v2.toolsettings+json',
'application/vnd.ims.lti.v2.toolsettings.simple+json',
'application/vnd.las.las+json',
'application/vnd.mason+json',
'application/vnd.micro+json',
'application/vnd.miele+json',
'application/vnd.mozilla.xul+xml',
'application/vnd.ms-fontobject',
'application/vnd.ms-opentype',
'application/vnd.nearst.inv+json',
'application/vnd.oftn.l10n+json',
'application/vnd.oma.lwm2m+json',
'application/vnd.oracle.resource+json',
'application/vnd.pagerduty+json',
'application/vnd.restful+json',
'application/vnd.siren+json',
'application/vnd.sun.wadl+xml',
'application/vnd.tableschema+json',
'application/vnd.vel+json',
'application/vnd.xacml+json',
'application/voucher-cms+json',
'application/wasm',
'application/webpush-options+json',
'application/x-httpd-php',
'application/x-javascript',
'application/x-ns-proxy-autoconfig',
'application/x-sh',
'application/x-tar',
'application/x-virtualbox-hdd',
'application/x-virtualbox-ova',
'application/x-virtualbox-ovf',
'application/x-virtualbox-vbox',
'application/x-virtualbox-vdi',
'application/x-virtualbox-vhd',
'application/x-virtualbox-vmdk',
'application/x-web-app-manifest+json',
'application/x-www-form-urlencoded',
'application/xhtml+xml',
'application/xml',
'application/xml-dtd',
'application/xop+xml',
'application/yang-data+json',
'application/yang-patch+json',
'font/otf',
'image/bmp',
'image/svg+xml',
'image/vnd.adobe.photoshop',
'image/x-icon',
'image/x-ms-bmp',
'message/imdn+xml',
'message/rfc822',
'model/gltf+json',
'model/gltf-binary',
'model/x3d+xml',
'text/cache-manifest',
'text/calender',
'text/cmd',
'text/css',
'text/csv',
'text/html',
'text/javascript',
'text/jsx',
'text/markdown',
'text/n3',
'text/plain',
'text/richtext',
'text/rtf',
'text/tab-separated-values',
'text/uri-list',
'text/vcard',
'text/vtt',
'text/x-gwt-rpc',
'text/x-jquery-tmpl',
'text/x-markdown',
'text/x-org',
'text/x-processing',
'text/x-suse-ymp',
'text/xml',
'x-shader/x-fragment',
'x-shader/x-vertex',
];
const ZLIB_DEFAULT_OPTIONS = {
level: 5,
};
const acceptEncoding = (accepts, encodings) => accepts.find((accept) => encodings.some((encoding) => accept === encoding));
const getBestAcceptEncoding = (request) => {
const { headers } = request;
const acceptEncodings = (headers['accept-encoding'] || '').split(/, */);
const encoding = acceptEncoding(acceptEncodings, [
'gzip',
'deflate',
'identity',
]);
return encoding === 'deflate' && acceptEncoding(acceptEncodings, ['gzip'])
? acceptEncoding(acceptEncodings, ['gzip', 'identity'])
: encoding;
};
const compressResponse = (response, encoding) => {
const { body, headers, ...rest } = response;
const compressed = encoding === 'gzip'
? zlib.gzipSync(body, ZLIB_DEFAULT_OPTIONS)
: zlib.deflateSync(body, ZLIB_DEFAULT_OPTIONS);
return {
...rest,
body: compressed.toString('base64'),
headers: {
...headers,
'content-encoding': encoding,
'content-length': compressed.byteLength,
},
isBase64Encoded: true,
};
};
const compress = (response, request) => {
const { body, headers } = response;
const encoding = getBestAcceptEncoding(request);
const weShouldCompress = COMPRESSIBLE.includes(headers['content-type']) &&
encoding !== undefined &&
encoding !== 'identity' &&
body &&
body.length > 256;
return weShouldCompress && encoding
? compressResponse(response, encoding)
: response;
};
const contentLengthHeader = (response) => {
const { body, headers, ...rest } = response;
return {
...rest,
body,
headers: {
...headers,
'content-length': headers['content-length'] || Buffer.byteLength(body),
},
};
};
const DEFAULT_POLICIES = {
'default-src': "'self' https: 'unsafe-inline'",
'img-src': '* data: blob:',
};
const cspHeaders = (response, _, options) => {
const { headers = {}, ...rest } = response;
const cspPolicies = {
...DEFAULT_POLICIES,
...options.cspPolicies,
};
const cspPolicy = Object.keys(cspPolicies)
.map((policy) => `${policy} ${cspPolicies[policy]}`)
.join(';');
return {
...rest,
headers: {
...headers,
'content-security-policy': cspPolicy,
'referrer-policy': 'strict-origin-when-cross-origin',
'x-content-type-options': 'nosniff',
'x-frame-options': {
"'none'": 'DENY',
"'self'": 'SAMEORIGIN',
}[cspPolicies['frame-ancestors']] || undefined,
'x-xss-protection': '1; mode=block',
},
};
};
const enforcedHeaders = (response, _, options) => ({
...response,
headers: {
...response.headers,
...(options.headers ?? {}),
},
});
const EMPTY_ENTITY_TAG = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
const entitytag = (entity) => {
const hash = crypto
.createHash('sha1')
.update(entity, 'utf8')
.digest('base64')
.slice(0, 27);
const length = typeof entity === 'string'
? Buffer.byteLength(entity, 'utf8')
: entity.length;
return `"${length.toString(16)}-${hash}"`;
};
const etag = (entity) => entity.length === 0 ? EMPTY_ENTITY_TAG : entitytag(entity);
const etagHeader = (response) => ({
...response,
headers: {
...response.headers,
etag: etag(response.body),
},
});
const aws = (request) => {
const { headers: requestHeaders, requestContext, context } = request;
return {
apig: {
requestId: requestContext.requestId,
resourcePath: requestContext.resourcePath ??
'',
},
awsAccountId: requestContext.accountId,
cloudfront: {
country: requestHeaders['cloudfront-viewer-country'],
desktop: requestHeaders['cloudfront-is-desktop-viewer'] === 'true',
mobile: requestHeaders['cloudfront-is-mobile-viewer'] === 'true',
smarttv: requestHeaders['cloudfront-is-smarttv-viewer'] === 'true',
tablet: requestHeaders['cloudfront-is-tablet-viewer'] === 'true',
},
deploymentStage: requestContext.stage,
lambda: {
functionName: context.functionName,
memoryLimit: Number(context.memoryLimitInMB),
remainingExecutionTime: context.getRemainingTimeInMillis?.(),
requestId: context.awsRequestId,
},
sourceIp: requestContext.identity
?.sourceIp ??
requestContext?.http?.sourceIp,
};
};
const logger$1 = (request, response) => {
const { source, headers: requestHeaders, meta, requestContext, provider, hostname, body, timestamp, path, query, } = request;
const error = response.statusCode >= 500 && response.statusCode < 600;
const logEntry = {
dateTime: new Date().toISOString(),
eventSource: source,
hostname,
httpMethod: requestContext.httpMethod,
httpProtocol: requestHeaders.via?.split(' ')[0],
httpReferrer: requestHeaders.referer,
httpUserAgent: requestHeaders['user-agent'],
requestContentLength: (body && body.length > 0) || 0,
requestTime: Date.now() - timestamp,
requestUri: path +
(query && Object.keys(query).length > 0
? `?${querystring.stringify(query)}`
: ''),
responseContentLength: response.headers['content-length'],
statusCode: response.statusCode,
xForwardedHost: requestHeaders['x-forwarded-host'],
...meta,
...(provider === 'aws' && aws(request)),
};
return ((error ? process.stderr : process.stdout).write(JSON.stringify(logEntry)) &&
logEntry);
};
const logResponse = (response, request, options) => (options.enableLogger &&
(typeof options.logger === 'function'
? options.logger(request, response)
: logger$1(request, response)) &&
response) ||
response;
const redirect = (responseData, _, location, statusCode = 302, options = {}) => makeResponseObject(responseData, '', statusCode, {
...options,
headers: {
...options.headers,
location,
},
});
const getBestMatchedFormat = (formats, accept) => {
const fallback = formats.default || 'html';
const acceptFormats = accept &&
typeof accept === 'string' &&
accept
.split(';')[0]
.split(',')
.map((type) => {
const mimeParts = type.split('/');
return mimeParts.at(-1);
});
return ((acceptFormats &&
acceptFormats.find((type) => Reflect.has(formats, type))) ||
fallback);
};
const getBestMatchedResponseHelper = (format) => {
switch (format) {
case 'html':
return html;
case 'json':
return json;
}
throw new TypeError(`"${format}" is not a valid Alagarr respondTo() format.`);
};
const getBestMatchedFormatBody = (formats, format) => {
switch (format) {
case 'html':
return formats.html;
case 'json':
return formats.json;
}
throw new TypeError(`"${format}" is not a valid Alagarr respondTo() format.`);
};
const respondTo = (responseData, request, formats, statusCode, options) => {
const { headers: { accept }, } = request;
const format = getBestMatchedFormat(formats, accept);
const responseHelper = getBestMatchedResponseHelper(format);
const body = getBestMatchedFormatBody(formats, format);
return responseHelper(responseData, request, body, statusCode, options);
};
const text = (responseData, _, body, statusCode, options) => makeResponseObject(responseData, body, statusCode, options, 'text/plain');
const middlewareMap = {
enableCompression: compress,
enableContentLength: contentLengthHeader,
enableCspHeaders: cspHeaders,
enableETagHeader: etagHeader,
enableEnforcedHeaders: enforcedHeaders,
};
var makeResponse = async (request, callback, options) => {
const responseData = makeResponseObject();
const responseWithHelpers = [
text,
html,
json,
redirect,
respondTo,
].reduce((methods, method) => ({
...methods,
[method.name]: async (...arguments_) => callback(null, await applyMiddleware(Object.keys(middlewareMap).reduce((middlewareList, middleware) => options[middleware]
? [...middlewareList, middlewareMap[middleware]]
: middlewareList, [
...(options.responseMiddleware || []),
...(options.enableLogger ? [logResponse] : []),
]), method(responseData, request, ...arguments_), request, options)),
}), { raw: callback });
const response = [setHeader].reduce((responseMethods, method) => ({
...responseMethods,
[method.name]: (...arguments_) => method(response, responseData, ...arguments_),
}), responseWithHelpers);
return response;
};
const DEFAULT_OPTIONS = {
cspPolicies: [],
enableCompression: true,
enableContentLength: true,
enableCspHeaders: true,
enableETagHeader: true,
enableEnforcedHeaders: true,
enableLogger: true,
enableStrictTransportSecurity: true,
errorHandler: defaultErrorHandler,
requestMiddleware: [],
responseMiddleware: [],
};
const noopHandler = (_, _1) => {
throw new ServerError('Misconfiguration in Alagarr setup. No handler function was provided.');
};
const alagarr = (handler = noopHandler, options = DEFAULT_OPTIONS) => async (event, context, callback) => {
const mergedOptions = {
...DEFAULT_OPTIONS,
...options,
};
const request = await makeRequest(event, context, mergedOptions);
const response = await makeResponse(request, callback, mergedOptions);
try {
return await handler(request, response, context);
}
catch (handlerError) {
const errorHandler = typeof mergedOptions.errorHandler === 'function'
? mergedOptions.errorHandler
: defaultErrorHandler;
try {
return await errorHandler(request, response, handlerError);
}
catch (error) {
console.error('There was an error in the error handler provided to Alagaar', error, errorHandler.toString());
return defaultErrorHandler(request, response, error);
}
}
};
const DEFAULT_AWS_CONFIGURATION = {
region: 'eu-central-2',
};
const splitEvery = (every, list) => Array.from({ length: Math.ceil(list.length / every) }).map((_, count) => list.slice(count * every, count * every + every));
const until = async (predicate, transformer, initialValue, iterationCount = 0) => {
const transformed = await transformer(initialValue, iterationCount);
return (await predicate(transformed, iterationCount))
? transformed
: until(predicate, transformer, transformed, iterationCount + 1);
};
const DEFAULT_BATCH_ITEMS_MAX_RETRIES = 50;
let queue;
const makeKey = (key) => {
if (typeof key === 'string') {
return { id: key };
}
return key;
};
const deleteItem = async (config, documentClient, table, key, options = {}) => {
try {
const { ReturnValues = 'NONE' } = options;
const command = new DeleteCommand({
Key: makeKey(key),
ReturnValues,
TableName: config.tableNamePrefix + table,
});
const result = await queue.schedule(() => documentClient.send(command));
return result.Attributes;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const getItem = async (config, documentClient, table, key, options = {}) => {
try {
const { ConsistentRead = false } = options;
const command = new GetCommand({
ConsistentRead,
Key: makeKey(key),
TableName: config.tableNamePrefix + table,
});
const result = await queue.schedule(() => documentClient.send(command));
return result.Item;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const putItem = async (config, documentClient, table, Item) => {
try {
const { expires, ...data } = Item;
const command = new PutCommand({
Item: expires === null ? data : Item,
TableName: config.tableNamePrefix + table,
});
return (await queue.schedule(() => documentClient.send(command))) && Item;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const updateItem = async (config, documentClient, table, key, options = {}) => {
try {
const command = new UpdateCommand({
Key: makeKey(key),
...options,
ReturnValues: 'ALL_NEW',
TableName: config.tableNamePrefix + table,
});
const result = await queue.schedule(async () => documentClient.send(command));
return result.Attributes;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const hasNoUnprocessedItems = ({ UnprocessedItems, }) => !UnprocessedItems || Object.keys(UnprocessedItems).length === 0;
const makeBatchWriteRequestWithExponentialBackoff = (documentClient, table, batchWriteMaxRetries) => async (batchItems, retries) => {
if (retries > batchWriteMaxRetries) {
throw new Error('Reached maximum number of DynamoDB BatchWrite request retries.');
}
const command = new BatchWriteCommand({
RequestItems: batchItems.UnprocessedItems ?? {
[table]: batchItems.map((Item) => ({
PutRequest: { Item },
})),
},
ReturnConsumedCapacity: 'TOTAL',
ReturnItemCollectionMetrics: 'SIZE',
});
return documentClient.send(command);
};
const batchWriteItems = async (config, documentClient, table, items) => {
try {
const batches = splitEvery(25, items);
const result = await Promise.all(batches.map(async (batch) => queue.schedule(async () => until(hasNoUnprocessedItems, makeBatchWriteRequestWithExponentialBackoff(documentClient, config.tableNamePrefix + table, config.batchWriteMaxRetries ?? DEFAULT_BATCH_ITEMS_MAX_RETRIES), batch))));
return result && items;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const scanTable = async (config, documentClient, table, options = {}) => {
try {
const { ConsistentRead = false } = options;
const command = new ScanCommand({
...options,
ConsistentRead,
TableName: config.tableNamePrefix + table,
});
const result = await queue.schedule(() => documentClient.send(command));
return result;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const queryTable = async (config, documentClient, table, options = {}) => {
try {
const { ConsistentRead = false } = options;
const command = new QueryCommand({
...options,
ConsistentRead,
TableName: config.tableNamePrefix + table,
});
const result = await queue.schedule(() => documentClient.send(command));
return result;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const dynamodb = (userConfig) => {
const { clientConfig, ...config } = {
...DEFAULT_AWS_CONFIGURATION,
...userConfig,
};
const ddbClient = new DynamoDBClient(config.dockerConfig
? {
credentials: {
accessKeyId: 'foobarkey',
secretAccessKey: 'foobarsecret',
},
endpoint: config.dockerConfig.endpoint,
region: 'local',
retryMode: 'standard',
}
: { retryMode: 'standard', ...clientConfig });
const documentClient = DynamoDBDocumentClient.from(ddbClient);
queue = new Bottleneck({
maxConcurrent: config.queueConfig?.queueConcurrency ?? 25,
minTime: config.queueConfig?.queueDelay ?? undefined,
});
return {
batchWriteItems: (table, items) => batchWriteItems(config, documentClient, table, items),
deleteItem: (table, key, options) => deleteItem(config, documentClient, table, key, options),
getItem: (table, key, options) => getItem(config, documentClient, table, key, options),
putItem: (table, item) => putItem(config, documentClient, table, item),
queryTable: (table, options) => queryTable(config, documentClient, table, options),
scanTable: (table, options) => scanTable(config, documentClient, table, options),
updateItem: (table, key, options) => updateItem(config, documentClient, table, key, options),
};
};
const invokeLambda = async (lambdaClient, config, invocationType, payload) => {
try {
const invokeCommand = new InvokeCommand({
FunctionName: config.functionName,
InvocationType: invocationType,
Payload: Buffer.from(JSON.stringify(payload)),
});
const result = await lambdaClient.send(invokeCommand);
if (config.logger) {
config.logger.info(JSON.stringify(result));
}
return true;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return false;
}
};
const lambda = (userConfig) => {
const { clientConfig, ...config } = {
...DEFAULT_AWS_CONFIGURATION,
...userConfig,
};
const lambdaClient = new LambdaClient(clientConfig);
return {
invokeLambda: (invocationType, payload) => invokeLambda(lambdaClient, config, invocationType, payload),
};
};
const DEFAULT_CLIENT_CONFIG$1 = {
clientConfig: DEFAULT_AWS_CONFIGURATION,
onlyDefault: false,
prefix: process.env.SSM_PARAM_PATH,
};
const getSecrets = async (ssm, config, parameters_, defaultValues) => {
if (config.onlyDefault) {
return defaultValues;
}
try {
const command = new GetParametersCommand({
Names: parameters_.map((parameter) => `${config.prefix ?? ''}${parameter}`),
WithDecryption: true,
});
const parameters = await ssm.send(command);
return (parameters?.Parameters?.map((parameter) => {
const defaultIfNoParameter = !parameter.Value && parameter.Name
? defaultValues.find((defaultValue) => defaultValue.name === parameter.Name)
: defaultValues[0];
return {
name: parameter.Name ?? defaultIfNoParameter?.name ?? '',
value: parameter.Value ?? defaultIfNoParameter?.value ?? '',
};
}) ?? defaultValues);
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return defaultValues;
}
};
const getSecret = async (ssm, config, parameter_, defaultValue) => {
if (config.onlyDefault) {
return defaultValue;
}
try {
const parameter = await ssm.send(new GetParameterCommand({
Name: `${config.prefix ?? ''}${parameter_}`,
WithDecryption: true,
}));
return parameter?.Parameter?.Value ?? defaultValue;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return defaultValue;
}
};
const parameterStore = (userConfig) => {
const { clientConfig, ...config } = {
...DEFAULT_CLIENT_CONFIG$1,
...userConfig,
};
const ssm = new SSMClient(clientConfig);
return {
getSecret: (parameter, defaultValue = '') => getSecret(ssm, config, parameter, defaultValue),
getSecrets: (parameters, defaultValues = []) => getSecrets(ssm, config, parameters, defaultValues),
};
};
const DEFAULT_CLIENT_CONFIG = {
clientConfig: { region: 'eu-west-1' },
};
const sendMail = async (config, transport, options, from, responseCallback) => {
try {
if (!options.html && !options.text) {
if (config.logger) {
config.logger.error('The option text or html needs to be set.');
}
return false;
}
if (!options.to || !options.subject) {
if (config.logger) {
config.logger.error('The fields to and subject have to be set.');
}
return false;
}
const transportResponse = await transport.sendMail({
from,
...options,
});
if (responseCallback) {
await responseCallback(transportResponse);
}
if (!transportResponse) {
if (config.logger) {
config.logger.error('Sending email failed!');
}
return false;
}
return true;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return false;
}
};
const ses = (fromEmailAddress, userConfig) => {
const { clientConfig, ...config } = {
...DEFAULT_CLIENT_CONFIG,
...userConfig,
};
const sesClient = new aws$1.SESClient(clientConfig);
const transport = createTransport(userConfig.customSMTPConfig ?? {
SES: { aws: aws$1, ses: sesClient },
});
return {
sendMail: (options, responseCallback) => sendMail(config, transport, options, fromEmailAddress, responseCallback),
};
};
const deleteQueueItem = async (sqsClient, config, queueUrl, receiptHandle) => {
try {
const deleteMessageCommand = new DeleteMessageCommand({
QueueUrl: queueUrl,
ReceiptHandle: receiptHandle,
});
const result = await sqsClient.send(deleteMessageCommand);
return result;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const getQueueItems = async (sqsClient, config, queueUrl, maxNumberOfMessages = 1) => {
try {
const receiveMessageCommand = new ReceiveMessageCommand({
MaxNumberOfMessages: maxNumberOfMessages,
QueueUrl: queueUrl,
});
const result = await sqsClient.send(receiveMessageCommand);
return result;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const getQueueUrl = async (sqsClient, config, queueName) => {
try {
const getUrlCommand = new GetQueueUrlCommand({ QueueName: queueName });
const result = await sqsClient.send(getUrlCommand);
return result.QueueUrl;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const sendMessage = async (sqsClient, config, queueUrl, message, fifoSettings = {}, messageAttributes, messageDelay = 0) => {
try {
const sendMessageCommand = new SendMessageCommand({
...fifoSettings,
...(messageAttributes ? { MessageAttributes: messageAttributes } : {}),
DelaySeconds: messageDelay,
MessageBody: message,
QueueUrl: queueUrl,
});
const result = await sqsClient.send(sendMessageCommand);
return result.MessageId;
}
catch (error) {
if (config.logger) {
config.logger.error(error);
}
return undefined;
}
};
const sqs = (userConfig) => {
const { clientConfig, ...config } = {
...DEFAULT_AWS_CONFIGURATION,
...userConfig,
};
const sqsClient = new SQSClient(clientConfig);
return {
deleteQueueItem: (queueUrl, receiptHandle) => deleteQueueItem(sqsClient, config, queueUrl, receiptHandle),
getQueueItems: (queueUrl, maxNumberOfMessages) => getQueueItems(sqsClient, config, queueUrl, maxNumberOfMessages),
getQueueUrl: (queueName) => getQueueUrl(sqsClient, config, queueName),
sendMessage: (queueUrl, message, fifoSettings, messageAttributes, messageDelay) => sendMessage(sqsClient, config, queueUrl, message, fifoSettings, messageAttributes, messageDelay),
};
};
const logger = (config) => {
const loggerConfig = {
defaultMeta: {
...config.defaultLogInformation,
deploymentStage: config.deploymentStage,
service: config.service,
},
format: format.combine(format.timestamp({
format: 'DD-MM-YYYY HH:mm:ss',
}), format.json()),
};
if (config.datadogApiKey) {
const httpOptions = {
host: 'http-intake.logs.datadoghq.eu',
path: `/api/v2/logs?dd-api-key=${config.datadogApiKey}&ddsource=nodejs&service=${config.service}`,
port: 443,
ssl: true,
};
return createLogger({
...loggerConfig,
transports: [new transports.Http(httpOptions)],
});
}
return createLogger({
...loggerConfig,
transports: [new transports.Console({ forceConsole: true })],
});
};
export { ClientError, DEFAULT_CLIENT_CONFIG$1 as DEFAULT_PARAMETER_STORE_CLIENT_CONFIG, DEFAULT_CLIENT_CONFIG as DEFAULT_SES_CLIENT_CONFIG, ServerError, alagarr, dynamodb, lambda, logger, parameterStore, ses, sqs };