@meshwatch/backend-core
Version:
Meshwatch backend core services.
1,143 lines (1,111 loc) • 47.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@hapi/boom'), require('lodash'), require('uuid'), require('aws-sdk/clients/dynamodb'), require('http'), require('https'), require('yup'), require('@meshwatch/node-fetch'), require('url'), require('pg'), require('perf_hooks')) :
typeof define === 'function' && define.amd ? define(['exports', '@hapi/boom', 'lodash', 'uuid', 'aws-sdk/clients/dynamodb', 'http', 'https', 'yup', '@meshwatch/node-fetch', 'url', 'pg', 'perf_hooks'], factory) :
(global = global || self, factory(global['@meshwatch/backend-core'] = {}, global.Boom, global.lodash, global.uuid, global.DynamoDB, global.http, global.https, global.Yup, global.nodeFetch, global.url, global.pg, global.perf_hooks));
}(this, function (exports, Boom, lodash, uuid, DynamoDB, http, https, Yup, nodeFetch, url, pg, perf_hooks) { 'use strict';
Boom = Boom && Boom.hasOwnProperty('default') ? Boom['default'] : Boom;
uuid = uuid && uuid.hasOwnProperty('default') ? uuid['default'] : uuid;
DynamoDB = DynamoDB && DynamoDB.hasOwnProperty('default') ? DynamoDB['default'] : DynamoDB;
var https__default = 'default' in https ? https['default'] : https;
nodeFetch = nodeFetch && nodeFetch.hasOwnProperty('default') ? nodeFetch['default'] : nodeFetch;
function validationBoom(errors) {
return newBoom(400, 'Something went wrong while validating your payload', errors);
}
function boomify(error, options = {}) {
let statusCode = options.statusCode;
if (error instanceof ServiceException) {
statusCode = error.httpStatusCode;
}
const boom = Boom.boomify(error, {
...options,
statusCode,
});
const payload = {
...boom.output.payload,
message: error.message || boom.output.payload.message,
};
return fromBoomPayload(payload);
}
function isBoom(body) {
const typedBoom = body;
return typedBoom.error !== undefined;
}
function newBoom(statusCode, message, errors) {
const boom = new Boom(message, {
statusCode,
});
return fromBoomPayload(boom.output.payload, errors);
}
function fromBoomPayload(boomPayload, errors) {
return errors
? {
...boomPayload,
errors,
}
: boomPayload;
}
class ServiceException extends Error {
constructor(message, httpStatusCode, name) {
super(message);
this.name = name;
this.httpStatusCode = httpStatusCode;
this.constructor = ServiceException;
Error.captureStackTrace(this, ServiceException);
Object.setPrototypeOf(this, ServiceException.prototype);
}
}
class DatabaseException extends ServiceException {
constructor(message) {
super(message, 500, 'DatabaseException');
}
}
class NotFoundException extends ServiceException {
constructor(message) {
super(message, 404, 'NotFoundException');
}
}
class ForbiddenException extends ServiceException {
constructor(message) {
super(message, 403, 'ForbiddenException');
}
}
class BadRequestException extends ServiceException {
constructor(message) {
super(message, 400, 'BadRequestException');
}
}
class UnreachableCaseError extends ServiceException {
constructor(val) {
super(`Unreachable case: ${val}`, 500, 'UnreachableCaseError');
}
}
const httpAgent = new http.Agent({
keepAlive: true,
maxSockets: 50,
});
function getDynamoClients(configOptions) {
return {
dynamodb: new DynamoDB(configOptions),
dynamodbDocumentClient: new DynamoDB.DocumentClient(configOptions),
};
}
function getDynamoDBClientConfiguration(config = {}) {
return {
region: process.env.region || 'us-west-2',
endpoint: process.env.endpoint || 'http://localhost:8000',
accessKeyId: process.env.AWS_ACCESS_KEY_ID || 'accessKeyId',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || 'secretAccessKey',
httpOptions: {
agent: httpAgent,
},
...config,
};
}
const config = getDynamoDBClientConfiguration();
{
console.log('Using DynamoDB config: ', config);
}
const { dynamodb, dynamodbDocumentClient } = getDynamoClients(config);
function createTable(dynamodb, params) {
return dynamodb
.createTable(params)
.promise()
.then(resp => {
{
console.log('[DynamoDB]: Created table. Table description JSON:', JSON.stringify(resp, null, 2));
}
return resp;
})
.catch(err => {
{
console.error('[DynamoDB]: Unable to create table. Error JSON:', JSON.stringify(err, null, 2));
}
return err;
});
}
function putDocumentToDynamo(dynamodb, params) {
const { TableName } = params;
// An AttributeValue may not contain an empty string
const Item = lodash.pickBy(params.Item, value => value !== '');
{
console.log('[DynamoDb]: put to: ' + TableName, 'Item: ' + JSON.stringify(Item));
}
return dynamodb
.put({ TableName, Item })
.promise()
.catch(err => {
{
console.log('[DynamoDB] write to: ' + TableName + ' failed.', 'Item: ' + JSON.stringify(Item), err);
}
throw err;
});
}
function queryTable(dynamodb, params) {
const { TableName } = params;
{
console.log('[DynamoDb]: queryTable: ' + TableName, 'QueryInput: ' + JSON.stringify(params));
}
return dynamodb
.query(params)
.promise()
.catch(err => {
{
console.log('[DynamoDb] query to: ' + TableName + ' failed.', 'QueryInput: ' + JSON.stringify(params), err);
}
throw err;
});
}
function deleteItem(dynamodb, params) {
const { TableName } = params;
{
console.log('[DynamoDB]: delete from: ' + TableName, JSON.stringify(params, null, 2));
}
return dynamodb
.delete(params)
.promise()
.catch(err => {
{
console.error('[DynamoDB]: Unable to delete item. Error JSON:', JSON.stringify(err, null, 2));
}
throw err;
});
}
function updateItem(dynamodb, params) {
const { TableName } = params;
{
console.log('[DynamoDB]: update: ' + TableName, JSON.stringify(params, null, 2));
}
return dynamodb
.update(params)
.promise()
.catch(err => {
{
console.error('[DynamoDB]: Unable to update item. Error JSON:', JSON.stringify(err, null, 2));
}
throw err;
});
}
const _MS_PER_DAY = 1000 * 60 * 60 * 24;
class DateUtils {
static getUnixTimestamp(date = new Date()) {
return Math.round(+date / 1000);
}
static dateFromUnixTimestamp(timestamp) {
return new Date(timestamp * 1000);
}
static daysDateDiff(d1, d2) {
const utc1 = Date.UTC(d1.getFullYear(), d1.getMonth(), d1.getDate());
const utc2 = Date.UTC(d2.getFullYear(), d2.getMonth(), d2.getDate());
return Math.floor((utc2 - utc1) / _MS_PER_DAY);
}
}
const MONITOR_TABLE_NAME = 'monitor';
const MONITOR_TABLE_SCHEDULER_GSI = 'schedulerGSI';
const MONITOR_TABLE_SCHEMA = {
TableName: MONITOR_TABLE_NAME,
KeySchema: [
{ AttributeName: 'hashKey', KeyType: 'HASH' },
{ AttributeName: 'sortKey', KeyType: 'RANGE' },
],
AttributeDefinitions: [
{ AttributeName: 'hashKey', AttributeType: 'S' },
{ AttributeName: 'sortKey', AttributeType: 'S' },
{ AttributeName: 'scheduler', AttributeType: 'S' },
],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
GlobalSecondaryIndexes: [
{
IndexName: MONITOR_TABLE_SCHEDULER_GSI,
KeySchema: [
{ AttributeName: 'scheduler', KeyType: 'HASH' },
{ AttributeName: 'sortKey', KeyType: 'RANGE' },
],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
],
};
class DynamoMonitorStorage {
constructor(documentClient = dynamodbDocumentClient, dynamoDB = dynamodb) {
this.createTable = () => {
return createTable(this.dynamoDB, MONITOR_TABLE_SCHEMA);
};
this.bookmarkMonitor = (userId, monitorId, bookmarked) => {
const hashKey = this.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ id: monitorId });
return updateItem(this.documentClient, {
TableName: MONITOR_TABLE_NAME,
Key: {
hashKey,
sortKey,
},
UpdateExpression: `set isBookmarked = :isBookmarked`,
ExpressionAttributeValues: {
':isBookmarked': bookmarked,
},
ReturnValues: 'ALL_NEW',
}).then(resp => this.monitorFromDynamoItem(resp.Attributes));
};
this.getMonitor = (userId, monitorId) => {
const hashKey = this.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ id: monitorId });
return queryTable(this.documentClient, {
TableName: MONITOR_TABLE_NAME,
KeyConditionExpression: `hashKey = :hashKey and sortKey = :sortKey`,
ExpressionAttributeValues: {
':hashKey': hashKey,
':sortKey': sortKey,
},
}).then(resp => {
if (!resp.Items || resp.Items.length === 0) {
throw new NotFoundException(`Could not find monitor id = ${monitorId}`);
}
return this.monitorFromDynamoItem(resp.Items[0]);
});
};
this.deleteMonitor = (userId, monitorId) => {
const hashKey = this.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ id: monitorId });
return deleteItem(this.documentClient, {
TableName: MONITOR_TABLE_NAME,
Key: {
hashKey,
sortKey,
},
});
};
this.searchMonitors = (userId) => {
return queryTable(this.documentClient, {
TableName: MONITOR_TABLE_NAME,
KeyConditionExpression: `hashKey = :hashKey`,
ExpressionAttributeValues: {
':hashKey': this.encodeHashKey({ userId }),
},
}).then(this.mapDynamoRows);
};
this.getMonitorsByScheduler = (scheduler) => {
return queryTable(this.documentClient, {
TableName: MONITOR_TABLE_NAME,
KeyConditionExpression: `scheduler = :scheduler`,
IndexName: MONITOR_TABLE_SCHEDULER_GSI,
ExpressionAttributeValues: {
':scheduler': scheduler,
},
}).then(this.mapDynamoRows);
};
this.createMonitor = (createMonitorPayload) => {
const monitor = { ...createMonitorPayload, id: uuid.v4(), created: new Date() };
return this._putMonitor(monitor);
};
this.updateMonitor = (updateMonitorPayload) => {
return this._putMonitor(updateMonitorPayload);
};
this._putMonitor = (monitor) => {
const Item = this.monitorToDynamoItem(monitor);
return putDocumentToDynamo(this.documentClient, {
TableName: MONITOR_TABLE_NAME,
Item,
}).then(_ => monitor);
};
this.monitorToDynamoItem = (monitor) => {
return {
hashKey: this.encodeHashKey(monitor),
sortKey: this.encodeSortKey(monitor),
name: monitor.name,
body: lodash.get(monitor, 'body'),
scheduler: monitor.scheduler,
apdex: lodash.get(monitor, 'apdex'),
endpoint: monitor.endpoint,
headers: lodash.get(monitor, 'headers'),
isBookmarked: monitor.isBookmarked,
location: monitor.location,
regions: lodash.get(monitor, 'regions'),
type: monitor.type,
created: DateUtils.getUnixTimestamp(monitor.created),
};
};
this.monitorFromDynamoItem = (Item) => {
const { userId } = this.decodeHashKey(Item.hashKey);
const { id } = this.decodeSortKey(Item.sortKey);
return {
userId,
id,
created: DateUtils.dateFromUnixTimestamp(Item.created),
name: Item.name,
scheduler: Item.scheduler,
apdex: Item.apdex,
type: Item.type,
headers: Item.headers,
body: Item.body,
endpoint: Item.endpoint,
isBookmarked: Item.isBookmarked,
location: Item.location,
regions: Item.regions,
};
};
this.mapDynamoRows = (resp) => {
const rows = resp.Items;
return rows.map(this.monitorFromDynamoItem);
};
this.documentClient = documentClient;
this.dynamoDB = dynamoDB;
}
/* hashKey */
encodeHashKey({ userId }) {
return `user#${userId}`;
}
decodeHashKey(hashKey) {
const s = hashKey.split('#');
return { userId: s[1] };
}
/* sortKey */
encodeSortKey({ id }) {
return `id#${id}`;
}
decodeSortKey(sortKey) {
const s = sortKey.split('#');
return { id: s[1] };
}
}
const dynamoMonitorStorage = new DynamoMonitorStorage();
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const isUUIDv4 = (param) => {
return UUID_V4_PATTERN.test(param);
};
const CREATE_MONITOR_BASE_SCHEMA_SHAPE = {
userId: Yup.string()
.test('isUUIDv4', 'uuid/v4 expected but got: `${value}`', value => isUUIDv4(value))
.required('userId is required'),
isBookmarked: Yup.boolean().required('isBookmarked is required'),
scheduler: Yup.string().required('Scheduler is required, so we know how often to monitor your endpoint'),
name: Yup.string().required('Name is required, so you can find your monitors easily'),
endpoint: Yup.string().required('Endpoint is required so we know where to send our requests'),
type: Yup.string()
.oneOf(['latency-check', 'https-check', 'certificate-check'])
.required('Monitor type is required'),
};
const CREATE_MONITOR_SCHEMA = Yup.object()
.shape(CREATE_MONITOR_BASE_SCHEMA_SHAPE)
.typeError(params => `Monitor object expected but got: \`${params.originalValue}\``);
const UPDATE_MONITOR_SCHEMA = Yup.object()
.shape({
...CREATE_MONITOR_BASE_SCHEMA_SHAPE,
created: Yup.date().required('created is required'),
id: Yup.string()
.test('isUUIDv4', 'uuid/v4 expected but got: `${value}`', value => isUUIDv4(value))
.required('id is required'),
})
.typeError(params => `Monitor object expected but got: \`${params.originalValue}\``);
/* Latency check */
const CREATE_LATENCY_CHECK_SCHEMA_SHAPE = {
...CREATE_MONITOR_BASE_SCHEMA_SHAPE,
body: Yup.string(),
headers: Yup.object(),
apdex: Yup.number().required('Apdex value is required so we can better measure performance of your endpoint'),
regions: Yup.array()
.of(Yup.string())
.required('Regions are required so we know from where to monitor your endpoint')
.test('isNonEmpty', 'At least one region should be selected but got: `${value}`', (value) => (value || []).length > 0),
};
const CREATE_LATENCY_MONITOR_SCHEMA = Yup.object()
.shape(CREATE_LATENCY_CHECK_SCHEMA_SHAPE)
.typeError(params => `Monitor object expected but got: \`${params.originalValue}\``);
const UPDATE_LATENCY_MONITOR_SCHEMA = Yup.object()
.shape({
...CREATE_LATENCY_CHECK_SCHEMA_SHAPE,
id: Yup.string()
.test('isUUIDv4', 'uuid/v4 expected but got: `${value}`', value => isUUIDv4(value))
.required('id is required'),
})
.typeError(params => `Monitor object expected but got: \`${params.originalValue}\``);
/* Other */
const COMPLETE_GETTING_STARTED_TASK_SCHEMA = Yup.object().shape({
completedOn: Yup.date().required('created is required'),
userId: Yup.string()
.test('isUUIDv4', 'uuid/v4 expected but got: `${value}`', value => isUUIDv4(value))
.required('userId is required'),
taskName: Yup.string().required('taskName is required'),
});
function isLatencyCheckMonitor(payload) {
return payload.type === 'latency-check';
}
function validateCompleteGettingStartedTaskPayload(payload) {
return validate(COMPLETE_GETTING_STARTED_TASK_SCHEMA, payload);
}
function validateCreateMonitorPayload(payload) {
if (isLatencyCheckMonitor(payload)) {
return validate(CREATE_LATENCY_MONITOR_SCHEMA, payload);
}
return validate(CREATE_MONITOR_SCHEMA, payload);
}
function validateUpdateMonitorPayload(payload) {
if (isLatencyCheckMonitor(payload)) {
return validate(UPDATE_LATENCY_MONITOR_SCHEMA, payload);
}
return validate(UPDATE_MONITOR_SCHEMA, payload);
}
function validate(schema, payload, options = { abortEarly: false, strict: true }) {
if (payload === undefined || typeof payload === 'function') {
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
const errors = { non_field_errors: `Object expected but got: \`${payload}\`` };
return Promise.resolve({
error: true,
errors,
});
}
return schema
.validate(payload, options)
.then(_ => ({
error: false,
}))
.catch((err) => {
const initialValue = {};
return {
error: true,
errors: err.inner.reduce((acc, validationError) => {
const errorPath = validationError.path || 'non_field_errors';
acc[errorPath] = validationError.message;
return acc;
}, initialValue),
};
});
}
class Authorization {
constructor(userId) {
this.canAccessEntity = (entity) => {
return entity.userId === this.userId;
};
this.cannotAccessEntity = (entity) => {
return !this.canAccessEntity(entity);
};
this.userId = userId;
}
}
Authorization.of = (userId) => {
return new Authorization(userId);
};
class BaseService {
constructor() {
this.errorServiceResponse = (error) => {
return this.serviceResponseFromBoom(boomify(error));
};
this.serviceResponseFromBoom = (boom) => {
return {
statusCode: boom.statusCode,
body: boom,
};
};
this.serviceResponse = (body) => {
return { statusCode: 200, body };
};
this.tryExecute = async (f) => {
return f().catch(this.errorServiceResponse);
};
}
}
class MonitoringService extends BaseService {
constructor(monitorStorage = dynamoMonitorStorage) {
super();
this.bookmarkMonitor = async (userId, monitorId) => {
return this.tryExecute(async () => {
let monitor = await this.monitorStorage.getMonitor(userId, monitorId);
if (Authorization.of(userId).cannotAccessEntity(monitor)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
monitor = await this.monitorStorage.bookmarkMonitor(monitor.userId, monitor.id, !monitor.isBookmarked);
return this.serviceResponse(monitor);
});
};
this.getMonitor = async (userId, monitorId) => {
return this.tryExecute(async () => {
const monitor = await this.monitorStorage.getMonitor(userId, monitorId);
if (Authorization.of(userId).cannotAccessEntity(monitor)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
return this.serviceResponse(monitor);
});
};
this.deleteMonitor = async (userId, monitorId) => {
return this.tryExecute(async () => {
const monitor = await this.monitorStorage.getMonitor(userId, monitorId);
if (Authorization.of(userId).cannotAccessEntity(monitor)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
await this.monitorStorage.deleteMonitor(monitor.userId, monitor.id);
return this.serviceResponse(monitor);
});
};
this.getMonitorsByScheduler = async (scheduler) => {
return this.tryExecute(async () => {
const monitors = await this.monitorStorage.getMonitorsByScheduler(scheduler);
return this.serviceResponse(monitors);
});
};
this.getMonitors = async (userId) => {
return this.tryExecute(async () => {
const monitors = await this.monitorStorage.searchMonitors(userId);
return this.serviceResponse(monitors);
});
};
this.updateMonitor = async (userId, updateMonitorPayload) => {
const validationResponse = await validateUpdateMonitorPayload(updateMonitorPayload);
if (validationResponse.error) {
return this.serviceResponseFromBoom(validationBoom(validationResponse.errors));
}
return this.tryExecute(async () => {
if (Authorization.of(userId).cannotAccessEntity(updateMonitorPayload)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
const existingMonitor = await this.monitorStorage.getMonitor(userId, updateMonitorPayload.id);
if (existingMonitor.type !== updateMonitorPayload.type) {
throw new BadRequestException('Monitor type cannot be changes. Please create a new monitor.');
}
const monitor = await this.monitorStorage.updateMonitor({
...updateMonitorPayload,
created: existingMonitor.created,
});
return { statusCode: 200, body: monitor };
});
};
this.createMonitor = async (userId, newMonitorPayload) => {
const validationResponse = await validateCreateMonitorPayload(newMonitorPayload);
if (validationResponse.error) {
return this.serviceResponseFromBoom(validationBoom(validationResponse.errors));
}
return this.tryExecute(async () => {
if (Authorization.of(userId).cannotAccessEntity(newMonitorPayload)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
const monitor = await this.monitorStorage.createMonitor(newMonitorPayload);
return { statusCode: 201, body: monitor };
});
};
this.monitorStorage = monitorStorage;
}
}
const monitoringService = new MonitoringService();
const USER_TABLE_NAME = 'user';
const USER_TABLE_SCHEMA = {
TableName: USER_TABLE_NAME,
KeySchema: [{ AttributeName: 'hashKey', KeyType: 'HASH' }],
AttributeDefinitions: [{ AttributeName: 'hashKey', AttributeType: 'S' }],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
};
const INITIAL_USER_INFO = {
gettingStarted: {
completedTasks: {},
},
};
class DynamoUserStorage {
constructor(documentClient = dynamodbDocumentClient, dynamoDB = dynamodb) {
this.createTable = () => {
return createTable(this.dynamoDB, USER_TABLE_SCHEMA);
};
this.getUserInfo = (userId) => {
const hashKey = this.encodeHashKey({ userId });
return queryTable(this.documentClient, {
TableName: USER_TABLE_NAME,
KeyConditionExpression: `hashKey = :hashKey`,
ExpressionAttributeValues: {
':hashKey': hashKey,
},
}).then(resp => {
if (!resp.Items) {
throw new NotFoundException(`Could not find userInfo for user: ${userId}`);
}
else if (resp.Items.length > 0) {
return this.userInfoFromDynamoRow(resp.Items[0]);
}
{
console.log(`No userInfo found for: ${userId}. Creating initial userInfo: `, INITIAL_USER_INFO);
}
return this.insertUserInfoRow({
hashKey: this.encodeHashKey({ userId }),
...INITIAL_USER_INFO,
});
});
};
this.completeGettingStartedTask = ({ userId, taskName, completedOn, }) => {
const hashKey = this.encodeHashKey({ userId });
return updateItem(this.documentClient, {
TableName: USER_TABLE_NAME,
Key: {
hashKey,
},
UpdateExpression: `set gettingStarted.completedTasks.${taskName} = :created`,
ConditionExpression: `attribute_not_exists(gettingStarted.completedTasks.${taskName})`,
ExpressionAttributeValues: {
':created': DateUtils.getUnixTimestamp(completedOn),
},
ReturnValues: 'UPDATED_NEW',
})
.then(resp => {
if (!resp.Attributes) {
throw new Error(`Could not update completeGettingStartedTask, userId: ${userId}`);
}
const gettingStarted = resp.Attributes.gettingStarted;
const completedTasks = lodash.mapValues(gettingStarted.completedTasks, DateUtils.dateFromUnixTimestamp);
return {
userId,
gettingStarted: {
completedTasks,
},
};
})
.catch(err => {
if (err.name === 'ConditionalCheckFailedException') {
throw new BadRequestException(`Task [${taskName}] is already completed`);
}
else if (err.name === 'ValidationException' &&
err.message ===
'The document path provided in the update expression is invalid for update') {
throw new NotFoundException('Getting started task cannot be completed before userInfo is created');
}
throw err;
});
};
this.insertUserInfoRow = (Item) => {
return putDocumentToDynamo(this.documentClient, {
TableName: USER_TABLE_NAME,
Item,
}).then(_ => this.userInfoFromDynamoRow(Item));
};
this.userInfoFromDynamoRow = (dynamoRow) => {
const { userId } = this.decodeHashKey(dynamoRow.hashKey);
const completedTasks = lodash.mapValues(dynamoRow.gettingStarted.completedTasks, DateUtils.dateFromUnixTimestamp);
return {
userId,
gettingStarted: {
...dynamoRow.gettingStarted,
completedTasks,
},
};
};
this.documentClient = documentClient;
this.dynamoDB = dynamoDB;
}
/* hashKey */
encodeHashKey({ userId }) {
return `user#${userId}`;
}
decodeHashKey(hashKey) {
const s = hashKey.split('#');
return { userId: s[1] };
}
}
const dynamoUserStorage = new DynamoUserStorage();
class UserService extends BaseService {
constructor(userStorage = dynamoUserStorage) {
super();
this.getUserInfo = (userId) => {
return this.tryExecute(async () => {
const userInfo = await this.userStorage.getUserInfo(userId);
return this.serviceResponse(userInfo);
});
};
this.completeGettingStartedTask = async (userId, taskName) => {
const params = {
userId,
taskName,
completedOn: new Date(),
};
const validationResponse = await validateCompleteGettingStartedTaskPayload(params);
if (validationResponse.error) {
return this.serviceResponseFromBoom(validationBoom(validationResponse.errors));
}
return this.tryExecute(async () => {
const userInfo = await this.userStorage.completeGettingStartedTask(params);
return this.serviceResponse(userInfo);
});
};
this.userStorage = userStorage;
}
}
const userService = new UserService();
function timeoutable(f, timeout) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(`Action timed out using timeout of ${timeout}ms.`);
}, timeout);
f()
.then(resp => {
clearTimeout(timeoutId);
resolve(resp);
})
.catch(err => {
clearTimeout(timeoutId);
reject(err);
});
});
}
const CHROME_WINDOWS = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36';
const BASE_HTTP_OPTIONS = {
port: 80,
path: '/',
method: 'GET',
headers: {
'User-Agent': CHROME_WINDOWS,
},
};
class HttpUtils {
static hasRedirect(httpResp) {
return HttpUtils.isRedirectableStatus(httpResp.statusCode);
}
static isRedirectableStatus(statusCode) {
return statusCode !== undefined ? statusCode >= 300 && statusCode < 400 : false;
}
}
HttpUtils.timeoutableGet = (options, timeout) => {
return timeoutable(() => HttpUtils.get(options), timeout);
};
HttpUtils.hasHttpsRedirect = (httpResp) => {
if (HttpUtils.hasRedirect(httpResp)) {
const redirect = httpResp.headers.location;
if (redirect && url.parse(redirect).protocol === 'https:') {
return true;
}
}
return false;
};
HttpUtils.get = (options) => {
if (HttpUtils.isRequestOptionsAsObject(options)) {
options = { ...BASE_HTTP_OPTIONS, ...options };
}
return new Promise((resolve, reject) => {
http.get(options, resp => resolve(resp)).on('error', (err) => {
reject(err);
});
});
};
HttpUtils.isRequestOptionsAsObject = (toBeDetermined) => {
return !lodash.isString(toBeDetermined);
};
const BASE_HTTPS_OPTIONS = {
port: 443,
path: '/',
method: 'GET',
headers: {
'User-Agent': CHROME_WINDOWS,
},
};
class MonitorExecutorService {
constructor(fetch = nodeFetch) {
this.fetch = fetch;
}
executeMonitorCheck(monitor) {
const url$1 = url.parse(monitor.endpoint);
switch (monitor.type) {
case 'latency-check':
return this.latencyCheck({ url: url$1, headers: lodash.get(monitor, 'headers', {}) });
case 'certificate-check':
return this.certificateCheck(url$1);
default:
return this.httpsCheck(url$1);
}
}
latencyCheck({ url, timeout = MonitorExecutorService.DEFAULT_EXECUTOR_TIMEOUT, headers, }) {
return new Promise(async (resolve) => {
const { href } = url;
if (!href) {
return resolve({ httpStatus: 400, error: `Expected url, but got \`${href}\`` });
}
this.fetch(href, { timeout, headers })
.then(resp => {
return resp.blob().then(blob => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const timings = resp.timings;
const blobSize = blob.size;
resolve({
blobSize,
timings,
httpStatus: resp.status,
});
});
})
.catch((err) => {
let errorMessage = err.message;
let httpStatus = 400;
if (err.message.includes('network timeout')) {
errorMessage = `Latency check timed out using timeout of ${timeout}ms.`;
httpStatus = 408; // Connection timed out
}
resolve({
error: errorMessage,
httpStatus,
});
});
});
}
async certificateCheck(url, timeout = MonitorExecutorService.DEFAULT_EXECUTOR_TIMEOUT) {
const hostname = url.protocol ? url.hostname : url.href;
if (!hostname) {
return { hasCertificate: false, error: `Expected hostname, but got \`${hostname}\`` };
}
return new Promise(resolve => {
const timeoutId = setTimeout(() => {
resolve({
hasCertificate: false,
error: `Certificate check timed out using timeout of ${timeout}ms.`,
});
}, timeout);
const options = { ...BASE_HTTPS_OPTIONS, hostname };
const httpsResp = https__default.get(options).on('error', (err) => {
clearTimeout(timeoutId);
const errorMessage = err.message;
resolve({ hasCertificate: false, error: errorMessage });
});
httpsResp.on('socket', (socket) => {
socket.on('secureConnect', () => {
const peerCertificate = socket.getPeerCertificate();
const certificateCheck = this.parseCertificate(peerCertificate);
resolve(certificateCheck);
clearTimeout(timeoutId);
});
});
});
}
async httpsCheck(url, timeout = MonitorExecutorService.DEFAULT_EXECUTOR_TIMEOUT) {
const hostname = url.protocol ? url.hostname : url.href;
if (!hostname) {
return { secure: false, error: `Expected hostname, but got \`${hostname}\`` };
}
return HttpUtils.timeoutableGet({ hostname }, timeout)
.then(resp => {
const secure = HttpUtils.hasHttpsRedirect(resp);
return secure
? { secure }
: {
secure,
error: `Could not find https redirect from ${hostname}, statusMessage: ${resp.statusMessage}`,
};
})
.catch((err) => {
const httpsCheck = { secure: false, error: err.message };
return httpsCheck;
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parseCertificate(certificate) {
const valid_to = new Date(certificate.valid_to);
const expires_in = DateUtils.daysDateDiff(new Date(), valid_to);
const hasCertificate = expires_in !== undefined && expires_in > 0;
return hasCertificate
? {
hasCertificate,
valid_to,
expires_in,
}
: {
hasCertificate,
error: 'Could not find PeerCertificate.',
};
}
}
MonitorExecutorService.DEFAULT_EXECUTOR_TIMEOUT = 10000; // 10 seconds
const monitorExecutorService = new MonitorExecutorService();
const startTimer = () => {
return perf_hooks.performance.now();
};
const poolConfig = {
user: process.env.POSTGRES_USER || 'postgres',
password: process.env.POSTGRES_PASSWORD || 'postgres',
port: parseInt(process.env.POSTGRES_PORT || '5431', 10),
database: process.env.POSTGRES_DATABASE_NAME || 'postgres',
host: process.env.POSTGRES_HOST || 'localhost',
};
{
console.log('Using TimescaleDB config: ', poolConfig);
}
const pgPool = new pg.Pool(poolConfig);
pgPool.on('error', err => {
console.error('[Postgres]: Unexpected error on idle client', err);
});
async function queryPostgres(config, pool = pgPool) {
const t0 = startTimer();
{
console.log('------------------------------------------');
const text = isConfig(config) ? config.text : config;
console.log(`${'[Postgres]: Executing query: '.padEnd(25)} [${text}]`);
if (isConfig(config) && config.values) {
console.log(`${'[Postgres]: Substitution values: '.padEnd(25)} [${config.values}]`);
}
}
return pool
.query(config)
.then(resp => {
{
console.log(`${'[Postgres]: Query took: '.padEnd(25)} [${startTimer() - t0}ms]`);
console.log('------------------------------------------');
}
return resp;
})
.catch((err) => {
const errorMessage = `Something went wrong while executing query: ${err.message}`;
console.error(`[Postgres]: ${errorMessage}`);
throw new DatabaseException(errorMessage);
});
}
function isConfig(param) {
return param.text !== undefined;
}
const CHECKS_SCHEMA = 'checks';
const LATENCY_TABLE_NAME = 'latency';
const CREATE_TABLE_SQL = `CREATE TABLE ${CHECKS_SCHEMA}.${LATENCY_TABLE_NAME} (
time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id UUID NOT NULL,
monitor_id UUID NOT NULL,
region TEXT NOT NULL,
status SMALLINT NOT NULL,
error TEXT NULL,
total_time REAL NULL,
tls_handshake_time REAL NULL,
tcp_connection_time REAL NULL,
dns_lookup_time REAL NULL,
first_byte_time REAL NULL,
blob_size REAL NULL
);`;
const CREATE_HYPERTABLE_SQL = `SELECT create_hypertable('${CHECKS_SCHEMA}.${LATENCY_TABLE_NAME}', 'time');`;
class TimescaleLatencyCheckStorage {
constructor(postgresPool = pgPool) {
this.createTable = () => {
return this.queryPostgres(CREATE_TABLE_SQL).then(_ => {
return this.queryPostgres(CREATE_HYPERTABLE_SQL);
});
};
this.getMonitorChecks = (params) => {
const queryConfig = this.getMonitorChecksQueryConfig(params);
return this.queryPostgres(queryConfig).then(resp => {
const rows = resp.rows;
return rows;
});
};
this.getMonitorChecksQueryConfig = ({ monitorId, pagination: { limit, offset = 0 }, }) => {
return {
name: 'get-monitor-latency-checks',
text: `SELECT * FROM ${CHECKS_SCHEMA}.${LATENCY_TABLE_NAME} ` +
`WHERE ${CHECKS_SCHEMA}.${LATENCY_TABLE_NAME}.monitor_id = $1 ` +
'ORDER BY time DESC LIMIT $2 OFFSET $3',
values: [monitorId, limit, offset],
};
};
this.insertLatencyCheck = (params) => {
const insertLatencyCheckConfig = this.getInsertQueryConfig(params);
return this.queryPostgres(insertLatencyCheckConfig).then(resp => {
const latencyRow = resp.rows[0];
return latencyRow;
});
};
this.postgresPool = postgresPool;
}
queryPostgres(queryConfig) {
return queryPostgres(queryConfig, this.postgresPool);
}
getInsertQueryConfig({ userId, monitorId, region, data, created, }) {
const timings = data.timings || {};
const blobSize = data.blobSize;
const error = data.error;
return {
name: 'insert-latency-check',
text: `INSERT INTO ${CHECKS_SCHEMA}.${LATENCY_TABLE_NAME} ` +
'(time, user_id, monitor_id, region, status, error, total_time, tls_handshake_time, tcp_connection_time, dns_lookup_time, first_byte_time, blob_size) ' +
'VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *',
values: [
created,
userId,
monitorId,
region,
data.httpStatus,
error,
timings.totalTime,
timings.tlsHandshakeTime,
timings.tcpConnectionTime,
timings.dnsLookupTime,
timings.firstByteTime,
blobSize,
],
};
}
}
const timescaleLatencyCheckStorage = new TimescaleLatencyCheckStorage();
class LatencyCheckService extends BaseService {
constructor(latencyCheckStorage = timescaleLatencyCheckStorage, monitorStorage = dynamoMonitorStorage) {
super();
this.getLatencyChecks = (userId, monitorId, pagination = { limit: 20 }) => {
return this.tryExecute(async () => {
const monitor = await this.monitorStorage.getMonitor(userId, monitorId);
if (Authorization.of(userId).cannotAccessEntity(monitor)) {
// Not reachable - getMonitor will throw 404
throw new ForbiddenException('You are not authorized to perform this action');
}
return this.latencyCheckStorage.getMonitorChecks({ monitorId, pagination }).then(resp => {
return this.serviceResponse(resp);
});
});
};
this.latencyCheckStorage = latencyCheckStorage;
this.monitorStorage = monitorStorage;
}
}
const latencyCheckService = new LatencyCheckService();
class TimescaleMonitorCheckStorage {
constructor(postgresPool = pgPool) {
this.createChecksSchema = () => {
return queryPostgres({
name: 'create-schema',
text: `CREATE SCHEMA ${CHECKS_SCHEMA};`,
}, this.postgresPool);
};
this.insertCheckData = (data, executorType) => {
switch (executorType) {
case 'latency-check':
const latencyCheckData = data;
return this.latencyStorage.insertLatencyCheck(latencyCheckData);
case 'certificate-check':
throw new Error('Not implemented');
case 'https-check':
throw new Error('Not implemented');
default:
throw new UnreachableCaseError(executorType);
}
};
this.postgresPool = postgresPool;
this.latencyStorage = new TimescaleLatencyCheckStorage(postgresPool);
}
}
const timescaleMonitorCheckStorage = new TimescaleMonitorCheckStorage();
async function initializeDynamoDBTables(dynamodb$1 = dynamodb) {
await createTable(dynamodb$1, USER_TABLE_SCHEMA);
await createTable(dynamodb$1, MONITOR_TABLE_SCHEMA);
}
async function initiliazeTimescaleDBTables(postgresPool) {
await new TimescaleMonitorCheckStorage(postgresPool).createChecksSchema();
await new TimescaleLatencyCheckStorage(postgresPool).createTable();
}
async function initializeTables(dynamoDB = dynamodb, postgresPool = pgPool) {
await initializeDynamoDBTables(dynamoDB);
await initiliazeTimescaleDBTables(postgresPool);
}
exports.MonitorExecutorService = MonitorExecutorService;
exports.MonitoringService = MonitoringService;
exports.TimescaleMonitorCheckStorage = TimescaleMonitorCheckStorage;
exports.UserService = UserService;
exports.boomify = boomify;
exports.executorService = monitorExecutorService;
exports.initializeTables = initializeTables;
exports.isBoom = isBoom;
exports.monitorCheckStorage = timescaleMonitorCheckStorage;
exports.monitorService = monitoringService;
exports.userService = userService;
}));
//# sourceMappingURL=meshwatchbackend-core.umd.development.js.map