@middy/util
Version:
🛵 The stylish Node.js middleware engine for AWS Lambda (util package)
303 lines (276 loc) • 8.21 kB
JavaScript
export const createPrefetchClient = (options) => {
const { awsClientOptions } = options;
const client = new options.AwsClient(awsClientOptions);
// AWS XRay
if (options.awsClientCapture) {
if (options.disablePrefetch) {
return options.awsClientCapture(client);
}
console.warn("Unable to apply X-Ray outside of handler invocation scope.");
}
return client;
};
export const createClient = async (options, request) => {
let awsClientCredentials = {};
// Role Credentials
if (options.awsClientAssumeRole) {
if (!request) {
throw new Error("Request required when assuming role", {
cause: { package: "@middy/util" },
});
}
awsClientCredentials = await getInternal(
{ credentials: options.awsClientAssumeRole },
request,
);
}
awsClientCredentials = {
...awsClientCredentials,
...options.awsClientOptions,
};
return createPrefetchClient({
...options,
awsClientOptions: awsClientCredentials,
});
};
export const canPrefetch = (options = {}) => {
return !options.awsClientAssumeRole && !options.disablePrefetch;
};
// Internal Context
export const getInternal = async (variables, request) => {
if (!variables || !request) return {};
let keys = [];
let values = [];
if (variables === true) {
keys = values = Object.keys(request.internal);
} else if (typeof variables === "string") {
keys = values = [variables];
} else if (Array.isArray(variables)) {
keys = values = variables;
} else if (typeof variables === "object") {
keys = Object.keys(variables);
values = Object.values(variables);
}
const promises = [];
for (const internalKey of values) {
// 'internal.key.sub_value' -> { [key]: internal.key.sub_value }
const pathOptionKey = internalKey.split(".");
const rootOptionKey = pathOptionKey.shift();
let valuePromise = request.internal[rootOptionKey];
if (!isPromise(valuePromise)) {
valuePromise = Promise.resolve(valuePromise);
}
promises.push(
valuePromise.then((value) =>
pathOptionKey.reduce((p, c) => p?.[c], value),
),
);
}
// ensure promise has resolved by the time it's needed
// If one of the promises throws it will bubble up to @middy/core
values = await Promise.allSettled(promises);
const errors = values
.filter((res) => res.status === "rejected")
.map((res) => res.reason);
if (errors.length) {
throw new Error("Failed to resolve internal values", {
cause: { package: "@middy/util", data: errors },
});
}
const obj = {};
for (let i = keys.length; i--; ) {
obj[sanitizeKey(keys[i])] = values[i].value;
}
return obj;
};
const isPromise = (promise) => typeof promise?.then === "function";
const sanitizeKeyPrefixLeadingNumber = /^([0-9])/;
const sanitizeKeyRemoveDisallowedChar = /[^a-zA-Z0-9]+/g;
export const sanitizeKey = (key) => {
return key
.replace(sanitizeKeyPrefixLeadingNumber, "_$1")
.replace(sanitizeKeyRemoveDisallowedChar, "_");
};
// fetch Cache
const cache = {}; // key: { value:{fetchKey:Promise}, expiry }
export const processCache = (
options,
middlewareFetch = () => undefined,
request = {},
) => {
let { cacheKey, cacheKeyExpiry, cacheExpiry } = options;
cacheExpiry = cacheKeyExpiry?.[cacheKey] ?? cacheExpiry;
const now = Date.now();
if (cacheExpiry) {
const cached = getCache(cacheKey);
const unexpired = cached.expiry && (cacheExpiry < 0 || cached.expiry > now);
if (unexpired) {
if (cached.modified) {
const value = middlewareFetch(request, cached.value);
Object.assign(cached.value, value);
cache[cacheKey] = { value: cached.value, expiry: cached.expiry };
return cache[cacheKey];
}
return { ...cached, cache: true };
}
}
const value = middlewareFetch(request);
// secrets-manager can override to unix timestamp
const expiry = cacheExpiry > 86400000 ? cacheExpiry : now + cacheExpiry;
const duration = cacheExpiry > 86400000 ? cacheExpiry - now : cacheExpiry;
if (cacheExpiry) {
const refresh =
duration > 0
? setTimeout(
() => processCache(options, middlewareFetch, request),
duration,
)
: undefined;
cache[cacheKey] = { value, expiry, refresh };
}
return { value, expiry };
};
export const catchInvalidSignatureException = (e, client, command) => {
if (e.__type === "InvalidSignatureException") {
return client.send(command);
}
throw e;
};
export const getCache = (key) => {
if (!cache[key]) return {};
return cache[key];
};
// Used to remove parts of a cache
export const modifyCache = (cacheKey, value) => {
if (!cache[cacheKey]) return;
clearTimeout(cache[cacheKey]?.refresh);
cache[cacheKey] = { ...cache[cacheKey], value, modified: true };
};
export const clearCache = (inputKeys = null) => {
let keys = inputKeys;
keys ??= Object.keys(cache);
if (!Array.isArray(keys)) {
keys = [keys];
}
for (const cacheKey of keys) {
clearTimeout(cache[cacheKey]?.refresh);
cache[cacheKey] = undefined;
}
};
export const jsonSafeParse = (text, reviver) => {
if (typeof text !== "string") return text;
const firstChar = text[0];
if (firstChar !== "{" && firstChar !== "[" && firstChar !== '"') return text;
try {
return JSON.parse(text, reviver);
} catch (_e) {}
return text;
};
export const jsonSafeStringify = (value, replacer, space) => {
try {
return JSON.stringify(value, replacer, space);
} catch (_e) {}
return value;
};
export const normalizeHttpResponse = (request) => {
let { response } = request;
if (typeof response === "undefined") {
response = {};
} else if (
typeof response?.statusCode === "undefined" &&
typeof response?.body === "undefined" &&
typeof response?.headers === "undefined"
) {
response = { statusCode: 200, body: response };
}
response.statusCode ??= 500;
response.headers ??= {};
request.response = response;
return response;
};
const createErrorRegexp = /[^a-zA-Z]/g;
export class HttpError extends Error {
constructor(code, optionalMessage, optionalOptions = {}) {
let message = optionalMessage;
let options = optionalOptions;
if (message && typeof message !== "string") {
options = message;
message = undefined;
}
message ??= httpErrorCodes[code];
super(message, options);
const name = httpErrorCodes[code].replace(createErrorRegexp, "");
this.name = name.substr(-5) !== "Error" ? `${name}Error` : name;
this.status = this.statusCode = code; // setting `status` for backwards compatibility w/ `http-errors`
this.expose = options.expose ?? code < 500;
}
}
export const createError = (code, message, properties = {}) => {
return new HttpError(code, message, properties);
};
const httpErrorCodes = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
103: "Early Hints",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
208: "Already Reported",
226: "IM Used",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
306: "(Unused)",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Payload Too Large",
414: "URI Too Long",
415: "Unsupported Media Type",
416: "Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
421: "Misdirected Request",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Unordered Collection",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
451: "Unavailable For Legal Reasons",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
508: "Loop Detected",
509: "Bandwidth Limit Exceeded",
510: "Not Extended",
511: "Network Authentication Required",
};