yandex-cloud-functions-router
Version:
Node router for Yandex Cloud Functions
185 lines (184 loc) • 10.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.httpRouter = void 0;
const routerError_1 = require("../models/routerError");
const log_1 = require("../helpers/log");
const appendCorsHeadersToMainResponse_1 = require("./http/cors/appendCorsHeadersToMainResponse");
const handleCorsPreflight_1 = require("./http/cors/handleCorsPreflight");
const matchObjectPattern_1 = require("../helpers/matchObjectPattern");
const resolveCorsOptions_1 = require("./http/cors/resolveCorsOptions");
const validateHttpMethod = (httpMethod, event) => {
const result = httpMethod
? httpMethod.map((m) => m.trim().toLowerCase()).indexOf(event.httpMethod.trim().toLocaleLowerCase()) !== -1
: true;
log_1.debug(event.requestContext.requestId, `Validating HTTP method: ${result ? 'valid' : 'invalid'}`, {
allowed: httpMethod !== null && httpMethod !== void 0 ? httpMethod : '',
actual: event.httpMethod
});
return result;
};
const validatePath = (path, event) => {
const result = path ? path.map((p) => p.trim().toLowerCase()).indexOf(event.path.trim().toLocaleLowerCase()) !== -1 : true;
log_1.debug(event.requestContext.requestId, `Validating path: ${result ? 'valid' : 'invalid'}`, {
allowed: path !== null && path !== void 0 ? path : '',
actual: event.path
});
return result;
};
const validateParams = (params, event) => {
if (params) {
const eventParams = Object.entries(event.queryStringParameters).reduce((result, [key, value]) => (Object.assign(Object.assign({}, result), { [key.trim().toLowerCase()]: value })), {});
const handlerParams = Object.entries(params).reduce((result, [key, value]) => (Object.assign(Object.assign({}, result), { [key.trim().toLowerCase()]: value })), {});
log_1.debug(event.requestContext.requestId, 'Validating HTTP params', { request: eventParams, route: handlerParams });
const validationErrorFound = Object.entries(handlerParams).some(([name, { type, value, pattern }]) => {
var _a, _b;
const eventParamValue = eventParams[name];
if (type === 'exact') {
log_1.debug(event.requestContext.requestId, `Validating ${name} param (type - exact): ${!(value === eventParamValue) ? 'invalid' : 'valid'}`, { request: eventParamValue, route: value !== null && value !== void 0 ? value : '' });
return !(value === eventParamValue);
}
else if (type === 'substring') {
if (value) {
log_1.debug(event.requestContext.requestId, `Validating ${name} param (type - substring): ${!(eventParamValue.indexOf(value) !== -1) ? 'invalid' : 'valid'}`, { request: eventParamValue, route: value !== null && value !== void 0 ? value : '' });
return !(eventParamValue.indexOf(value) !== -1);
}
else {
// throw validation error on empty value
log_1.debug(event.requestContext.requestId, `Validating ${name} param (type - substring): invalid`, {
request: eventParamValue,
route: '(empty value)'
});
return true;
}
}
else if (type === 'regexp') {
log_1.debug(event.requestContext.requestId, `Validating ${name} param (type - regexp): ${!((_a = pattern === null || pattern === void 0 ? void 0 : pattern.test(eventParamValue)) !== null && _a !== void 0 ? _a : false) ? 'invalid' : 'valid'}`, { request: eventParamValue, route: value !== null && value !== void 0 ? value : '' });
return !((_b = pattern === null || pattern === void 0 ? void 0 : pattern.test(eventParamValue)) !== null && _b !== void 0 ? _b : false);
}
else {
log_1.debug(event.requestContext.requestId, `Validating ${name} param: unknown validation type - ${type}`, {
request: eventParamValue,
route: value !== null && value !== void 0 ? value : ''
});
throw new routerError_1.HttpParamNotSupportedTypeRouteError(`Not supported type: ${type}`);
}
});
return !validationErrorFound;
}
else {
return true;
}
};
const validateBodyPattern = (pattern, event) => {
var _a;
if (pattern) {
log_1.debug(event.requestContext.requestId, 'Validating HTTP body', { pattern });
if (pattern.json) {
try {
const contentTypeMatched = String(((_a = Object.entries(event.headers).find(([name]) => name.trim().toLowerCase() === 'content-type')) !== null && _a !== void 0 ? _a : [])[1] || '')
.toLowerCase()
.indexOf('application/json') !== -1;
log_1.debug(event.requestContext.requestId, 'Validating HTTP body type (JSON)', { contentTypeMatched });
if (contentTypeMatched) {
const bodyObject = JSON.parse(event.body);
const result = matchObjectPattern_1.matchObjectPattern(bodyObject, pattern.json);
log_1.debug(event.requestContext.requestId, 'Validating HTTP body', { bodyObject, result });
return result;
}
else {
return false;
}
}
catch (e) {
log_1.debug(event.requestContext.requestId, 'Validating HTTP body failed with error', { error: e });
if (e instanceof SyntaxError) {
return false;
}
else {
throw e;
}
}
}
else {
return false;
}
}
else {
return true;
}
};
const validateWithValidators = (validators, event, context) => {
var _a;
try {
return validators ? validators.every((validator) => validator(event, context)) : true;
}
catch (e) {
log_1.log('WARN', context.requestId, `Validator failed with error: ${((_a = e === null || e === void 0 ? void 0 : e.toString()) !== null && _a !== void 0 ? _a : 'unknown error').replace(/[\r\n]+/g, '')}`, {});
return false;
}
};
const unwrapBase64Body = (event) => {
var _a;
if (event.isBase64Encoded) {
log_1.debug(event.requestContext.requestId, 'HTTP request - unwrapping base64 body', {});
const body = Buffer.from((_a = event.body) !== null && _a !== void 0 ? _a : '', 'base64').toString('utf-8');
return Object.assign(Object.assign({}, event), { body, isBase64Encoded: false });
}
else {
return event;
}
};
const httpRouter = (routes, event, context, options) => __awaiter(void 0, void 0, void 0, function* () {
const corsOptions = resolveCorsOptions_1.resolveCorsOptions(options === null || options === void 0 ? void 0 : options.cors);
log_1.debug(context.requestId, 'HTTP request processing started', { CORS: corsOptions });
for (const { httpMethod, path, params, body, validators, decodeBase64Body, handler } of routes) {
const matched = validateHttpMethod(httpMethod, event) &&
validateParams(params, event) &&
validateBodyPattern(body, event) &&
validatePath(path, event);
log_1.debug(context.requestId, 'HTTP request matching completed', {
matched,
event,
httpMethod: httpMethod !== null && httpMethod !== void 0 ? httpMethod : '',
path: path !== null && path !== void 0 ? path : '',
params: params !== null && params !== void 0 ? params : '',
body: body !== null && body !== void 0 ? body : ''
});
if (matched) {
const unwrappedEvent = decodeBase64Body ? unwrapBase64Body(event) : event;
const validatorsPassed = validateWithValidators(validators, unwrappedEvent, context);
log_1.debug(context.requestId, 'HTTP request validating completed', { validatorsPassed });
if (validatorsPassed) {
const handlerResult = handler(unwrappedEvent, context);
const result = handlerResult instanceof Promise ? yield handlerResult : handlerResult;
log_1.debug(context.requestId, 'HTTP processed', {});
return appendCorsHeadersToMainResponse_1.appendCorsHeadersToMainResponse(event, result, corsOptions);
}
else {
log_1.log('WARN', context.requestId, 'Invalid request', {});
throw new routerError_1.InvalidRequestError('Invalid request.');
}
}
}
const preflightResponse = handleCorsPreflight_1.handleCorsPreflight(event, corsOptions);
if (preflightResponse) {
log_1.debug(context.requestId, 'HTTP CORS preflight request processed', { preflightResponse });
log_1.log('INFO', context.requestId, 'CORS preflight request handled ', {});
return preflightResponse;
}
else {
log_1.debug(context.requestId, 'HTTP CORS preflight request skipped', {});
}
log_1.log('WARN', context.requestId, 'There is no matched route', {});
throw new routerError_1.NoMatchedRouteError('There is no matched route.');
});
exports.httpRouter = httpRouter;