@meshwatch/backend-core
Version:
Meshwatch backend core services.
1,229 lines (1,198 loc) • 85.6 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var logging = require('@meshwatch/logging');
var all = require('aws-sdk/clients/all');
var http = require('http');
var https = require('https');
var https__default = _interopDefault(https);
var Boom = _interopDefault(require('@hapi/boom'));
var lodash = require('lodash');
var Yup = require('yup');
var dns = require('dns');
var url = require('url');
var uuid = _interopDefault(require('uuid'));
var nodeFetch = _interopDefault(require('@meshwatch/node-fetch'));
var pg = require('pg');
var perf_hooks = require('perf_hooks');
// is set in production and with serveless-offline plugin
const IN_LAMBDA = !!(process.env.LAMBDA_TASK_ROOT || process.env.AWS_EXECUTION_ENV);
// is set when using serverless invoke local
const IS_SLS_LOCAL = (process.env.IS_LOCAL || 'false') === 'true';
// is set when using serveless-offline plugin
const IS_SLS_OFFLINE = (process.env.IS_OFFLINE || 'false') === 'true';
const IS_PRODUCTION_LAMBDA = IN_LAMBDA && !IS_SLS_LOCAL && !IS_SLS_OFFLINE;
const AWS_DYNAMO_REGION_PATTERN = /^https:\/\/dynamodb\.((us(-gov)?|ap|ca|cn|eu|sa)-(central|(north|south)?(east|west)?)-\d).amazonaws\.com$/;
function isInCloudEnvironment(endpoint) {
if (!endpoint) {
return true;
}
return AWS_DYNAMO_REGION_PATTERN.test(endpoint);
}
function awsClientConfiguration(config) {
const { AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = process.env;
let region = AWS_REGION;
let accessKeyId = undefined;
let secretAccessKey = undefined;
if (!IS_PRODUCTION_LAMBDA) {
region = region || 'us-east-1';
accessKeyId = AWS_ACCESS_KEY_ID || 'accessKeyId';
secretAccessKey = AWS_SECRET_ACCESS_KEY || 'secretAccessKey';
}
// keepAlive improves performance
const agentOptions = {
keepAlive: true,
maxSockets: 50,
};
const dynamoHttpAgent = isInCloudEnvironment(config.endpoint)
? new https.Agent({ ...agentOptions, rejectUnauthorized: true })
: new http.Agent(agentOptions);
return {
region,
accessKeyId,
secretAccessKey,
httpOptions: {
agent: dynamoHttpAgent,
},
...config,
};
}
function dynamoClientConfiguration(config = {}) {
const { AWS_DYNAMODB_ENDPOINT } = process.env;
let dynamoEndpoint = config.endpoint || AWS_DYNAMODB_ENDPOINT;
if (!IS_PRODUCTION_LAMBDA && !dynamoEndpoint) {
dynamoEndpoint = 'http://localhost:8000';
}
return awsClientConfiguration({ ...config, endpoint: dynamoEndpoint });
}
function sqsClientConfiguration(config = {}) {
const { AWS_SQS_ENDPOINT } = process.env;
let sqsEndpoint = config.endpoint || AWS_SQS_ENDPOINT;
if (!IS_PRODUCTION_LAMBDA && !sqsEndpoint) {
sqsEndpoint = 'http://localhost:9324';
}
return awsClientConfiguration({ ...config, endpoint: sqsEndpoint });
}
class DynamoDBTableClient {
constructor(tableName, dynamoDbClient) {
this.tableName = tableName;
this.dynamoDbClient = dynamoDbClient;
}
createTable(params) {
return this.dynamoDbClient.createTable({ ...params, TableName: this.tableName });
}
putDocumentToDynamo(params) {
return this.dynamoDbClient.putDocumentToDynamo({ ...params, TableName: this.tableName });
}
queryTable(params) {
return this.dynamoDbClient.queryTable({ ...params, TableName: this.tableName });
}
scanTable(params) {
return this.dynamoDbClient.scanTable({ ...params, TableName: this.tableName });
}
deleteItem(params) {
return this.dynamoDbClient.deleteItem({ ...params, TableName: this.tableName });
}
updateItem(params) {
return this.dynamoDbClient.updateItem({ ...params, TableName: this.tableName });
}
}
class DynamoDBClient {
constructor(config) {
this.createTable = (createTableParams) => {
return this.dynamodb
.createTable(createTableParams)
.promise()
.then(createTableResponse => {
{
logging.logger.debug('[DynamoDB]: Created table', { createTableResponse });
}
return createTableResponse;
})
.catch(err => {
logging.logger.error('[DynamoDB]: Unable to create table', err, { createTableParams });
throw err;
});
};
this.transactWriteItems = (transactWriteParams) => {
{
logging.logger.debug('[DynamoDb]: transactWriteParams', transactWriteParams);
}
return this.dynamodbDocumentClient
.transactWrite(transactWriteParams)
.promise()
.catch(err => {
logging.logger.error(`[DynamoDB] transactWriteItems failed`, err, {
transactWriteParams,
});
throw err;
});
};
this.putDocumentToDynamo = (putItemParams) => {
const { TableName } = putItemParams;
{
logging.logger.debug(`[DynamoDb]: putting Item to table [${TableName}]`, { putItemParams });
}
return this.dynamodbDocumentClient
.put(putItemParams)
.promise()
.catch(err => {
logging.logger.error(`[DynamoDB] put to table [${TableName}] failed`, err, { putItemParams });
throw err;
});
};
this.queryTable = (queryTableParams) => {
const { TableName } = queryTableParams;
{
logging.logger.debug(`[DynamoDb]: querying table [${TableName}]`, { queryTableParams });
}
return this.dynamodbDocumentClient
.query(queryTableParams)
.promise()
.catch(err => {
logging.logger.error(`[DynamoDb] query to table [${TableName}] failed`, err, { queryTableParams });
throw err;
});
};
this.scanTable = (scanTableParams) => {
const { TableName } = scanTableParams;
{
logging.logger.debug(`[DynamoDb]: scanTable: ${TableName}`, scanTableParams);
}
return this.dynamodbDocumentClient
.scan(scanTableParams)
.promise()
.catch(err => {
logging.logger.error(`[DynamoDb] scan to table [${TableName}] failed`, err, { scanTableParams });
throw err;
});
};
this.deleteItem = (deleteItemParams) => {
const { TableName } = deleteItemParams;
{
logging.logger.debug(`[DynamoDB]: deleting item from table [${TableName}]`, deleteItemParams);
}
return this.dynamodbDocumentClient
.delete(deleteItemParams)
.promise()
.catch(err => {
logging.logger.error('[DynamoDB]: Unable to delete item', err, { deleteItemParams });
throw err;
});
};
this.updateItem = (updateItemParams) => {
const { TableName } = updateItemParams;
{
logging.logger.debug(`[DynamoDB]: updating item in table [${TableName}]`, updateItemParams);
}
return this.dynamodbDocumentClient
.update(updateItemParams)
.promise()
.catch(err => {
logging.logger.error('[DynamoDB]: Unable to update item', err, { updateItemParams });
throw err;
});
};
const dynamoConfig = dynamoClientConfiguration(config);
this.dynamodb = new all.DynamoDB(dynamoConfig);
this.dynamodbDocumentClient = new all.DynamoDB.DocumentClient({
...dynamoConfig,
convertEmptyValues: true,
});
}
}
const defaultClientConfig = dynamoClientConfiguration();
logging.logger.debug('[DynamoDB]: Default client configuration', { defaultClientConfig });
const DEFAULT_DYNAMO_DB_CLIENT = new DynamoDBClient(defaultClientConfig);
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 formErrorsBoom = body;
return formErrorsBoom.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');
}
}
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(error => this.errorServiceResponse(error));
};
}
}
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 roundToMiliseconds(date = new Date()) {
return this.dateFromUnixTimestamp(this.getUnixTimestamp(date));
}
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);
}
static addMinutes(minutes, date = new Date()) {
const result = new Date(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
}
static subtractMinutes(minutes, date = new Date()) {
return this.addMinutes(-minutes, date);
}
static addDays(days, date = new Date()) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
}
DateUtils.toDatetime = (date) => {
return date
.toISOString()
.slice(0, 19)
.replace('T', ' ');
};
const { MONITORING_TABLE_NAME = 'monitoring' } = process.env;
const MONITORING_TABLE_SCHEDULER_GSI = 'schedulerGSI';
const MONITORING_TABLE_SCHEMA = {
TableName: MONITORING_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: MONITORING_TABLE_SCHEDULER_GSI,
KeySchema: [
{ AttributeName: 'scheduler', KeyType: 'HASH' },
{ AttributeName: 'sortKey', KeyType: 'RANGE' },
],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
],
};
class DynamoMonitoringDatasource {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
this.createTable = () => {
return this.dynamoClient.createTable(MONITORING_TABLE_SCHEMA);
};
this.alertFromDynamoItem = (Item) => {
const { userId } = DynamoMonitoringDatasource.decodeHashKey(Item.hashKey);
const { monitorId, alertId } = this.decodeAlertSortKey(Item.sortKey);
return {
userId,
id: alertId,
monitorId,
created: DateUtils.dateFromUnixTimestamp(Item.created),
type: Item.type,
scheduler: Item.scheduler,
action: Item.action,
actionValue: Item.actionValue,
operation: Item.operation,
value: Item.value,
windowDuration: Item.windowDuration,
};
};
this.monitorFromDynamoItem = (Item) => {
const { userId } = DynamoMonitoringDatasource.decodeHashKey(Item.hashKey);
const { monitorId } = this.decodeMonitorSortKey(Item.sortKey);
const monitor = {
userId,
id: monitorId,
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,
};
// Strip undefined fields to reduce ammount of network transfer
return lodash.pickBy(monitor, value => value !== undefined);
};
this.dynamoClient = new DynamoDBTableClient(MONITORING_TABLE_SCHEMA.TableName, dynamoClient);
this.dynamoRawClient = dynamoClient;
this.tableName = MONITORING_TABLE_SCHEMA.TableName;
}
/* hashKey */
static encodeHashKey({ userId }) {
return `user#${userId}`;
}
static decodeHashKey(hashKey) {
const s = hashKey.split('#');
return { userId: s[1] };
}
/* alert */
decodeAlertSortKey(sortKey) {
const s = sortKey.split('#');
return { monitorId: s[1], alertId: s[2] };
}
/* monitor */
decodeMonitorSortKey(sortKey) {
const s = sortKey.split('#');
return { monitorId: s[1] };
}
}
const INITIAL_USER_INFO = {
gettingStarted: {
completedTasks: {},
},
};
const USER_INFO_SORT_KEY = 'userInfo';
const completeGettingStartedTaskUpdate = (userId, completedOn, taskName) => {
return {
Key: {
hashKey: DynamoMonitoringDatasource.encodeHashKey({ userId }),
sortKey: USER_INFO_SORT_KEY,
},
TableName: MONITORING_TABLE_SCHEMA.TableName,
UpdateExpression: `set gettingStarted.completedTasks.${taskName} = :created`,
ConditionExpression: `attribute_not_exists(gettingStarted.completedTasks.${taskName})`,
ExpressionAttributeValues: {
':created': DateUtils.getUnixTimestamp(completedOn),
},
};
};
class DynamoUserDatasource {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
this.createTable = () => {
return this.dynamoClient.createTable(MONITORING_TABLE_SCHEMA);
};
this.getUserInfo = (userId) => {
const hashKey = DynamoMonitoringDatasource.encodeHashKey({ userId });
return this.dynamoClient
.queryTable({
KeyConditionExpression: `hashKey = :hashKey and sortKey = :sortKey`,
ExpressionAttributeValues: {
':hashKey': hashKey,
':sortKey': USER_INFO_SORT_KEY,
},
})
.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]);
}
{
logging.logger.debug(`No userInfo found for: ${userId}. Creating initial userInfo: `, INITIAL_USER_INFO);
}
return this.insertUserInfoRow({
hashKey: DynamoMonitoringDatasource.encodeHashKey({ userId }),
sortKey: USER_INFO_SORT_KEY,
...INITIAL_USER_INFO,
});
});
};
this.completeGettingStartedTask = (params) => {
const { taskName, completedOn, userId } = params;
return this.dynamoClient
.updateItem({
...completeGettingStartedTaskUpdate(userId, completedOn, taskName),
ReturnValues: 'UPDATED_NEW',
})
.then(resp => {
if (!resp.Attributes) {
throw new Error(`Could not update completeGettingStartedTask, userId: ${userId}`);
}
const attributes = resp.Attributes;
return this.userInfoFromDynamoRow({
hashKey: DynamoMonitoringDatasource.encodeHashKey({ userId }),
sortKey: USER_INFO_SORT_KEY,
...attributes,
});
})
.catch(err => {
logging.logger.error('Could not update completeGettingStartedTask', err, { params });
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 this.dynamoClient
.putDocumentToDynamo({ Item })
.then(_ => this.userInfoFromDynamoRow(Item));
};
this.userInfoFromDynamoRow = (dynamoRow) => {
const { userId } = DynamoMonitoringDatasource.decodeHashKey(dynamoRow.hashKey);
const completedTasks = lodash.mapValues(dynamoRow.gettingStarted.completedTasks, DateUtils.dateFromUnixTimestamp);
return {
userId,
gettingStarted: {
...dynamoRow.gettingStarted,
completedTasks,
},
};
};
this.dynamoClient = new DynamoDBTableClient(MONITORING_TABLE_SCHEMA.TableName, dynamoClient);
}
}
const dynamoUserDatasource = new DynamoUserDatasource();
const typeErrorHandler = (name, params) => {
return `${name} object expected but got: \`${params.originalValue}\``;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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) => {
let keyPath = validationError.path;
if (keyPath === undefined) {
keyPath = 'non_field_errors';
}
acc[keyPath] = validationError.message;
return acc;
}, initialValue),
};
});
}
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;
class StringUtils {
}
StringUtils.isUUIDv4 = (param) => {
return UUID_V4_PATTERN.test(param);
};
const uuidSchema = (fieldName = 'id') => {
return Yup.string()
.test('isUUIDv4', 'uuid/v4 expected but got: `${value}`', value => StringUtils.isUUIDv4(value))
.required(`${fieldName} is required`);
};
const uuidIdSchema = uuidSchema();
const userIdSchema = uuidSchema('userId');
const COMPLETE_GETTING_STARTED_TASK_SCHEMA = Yup.object().shape({
userId: userIdSchema,
completedOn: Yup.date().required('created is required'),
taskName: Yup.string().required('taskName is required'),
});
function validateCompleteGettingStartedTaskPayload(payload) {
return validate(COMPLETE_GETTING_STARTED_TASK_SCHEMA, payload);
}
class UserService extends BaseService {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
super();
this.getUserInfo = (userId) => {
return this.tryExecute(async () => {
const userInfo = await this.datasource.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.datasource.completeGettingStartedTask(params);
return this.serviceResponse(userInfo);
});
};
this.datasource = new DynamoUserDatasource(dynamoClient);
}
}
const userService = new UserService();
class DnsService extends BaseService {
constructor() {
super(...arguments);
this.cnameLookup = (endpoint) => {
const url$1 = url.parse(endpoint);
const hostname = (url$1.protocol ? url$1.hostname : url$1.href) || '';
return this.tryExecute(() => new Promise((resolve, reject) => {
dns.resolveCname(hostname, (error, addresses) => {
if (error) {
reject(new Error(error.message));
return;
}
resolve(this.serviceResponse(addresses));
});
}));
};
}
}
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);
};
const { STATUS_PAGE_TABLE_NAME = 'status-page' } = process.env;
const STATUS_PAGE_TABLE_SEARCH_GSI = 'searchGSI';
const STATUS_PAGE_TABLE_CUSTOM_DOMAIN_GSI = 'customDomainGSI';
const STATUS_PAGE_TABLE_SCHEMA = {
TableName: STATUS_PAGE_TABLE_NAME,
AttributeDefinitions: [
{ AttributeName: 'id', AttributeType: 'S' },
{ AttributeName: 'userId', AttributeType: 'S' },
{ AttributeName: 'customDomain', AttributeType: 'S' },
{ AttributeName: 'created', AttributeType: 'N' },
],
// getStatusPage
KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
GlobalSecondaryIndexes: [
{
IndexName: STATUS_PAGE_TABLE_SEARCH_GSI,
// findStatusPages
KeySchema: [
{ AttributeName: 'userId', KeyType: 'HASH' },
{ AttributeName: 'created', KeyType: 'RANGE' },
],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
{
IndexName: STATUS_PAGE_TABLE_CUSTOM_DOMAIN_GSI,
// lookupStatusPage
KeySchema: [
{ AttributeName: 'customDomain', KeyType: 'HASH' },
{ AttributeName: 'created', KeyType: 'RANGE' },
],
Projection: {
ProjectionType: 'ALL',
},
ProvisionedThroughput: {
ReadCapacityUnits: 1,
WriteCapacityUnits: 1,
},
},
],
};
class DynamoStatusPageDatasource {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
this.createTable = () => {
return this.dynamoClient.createTable(STATUS_PAGE_TABLE_SCHEMA);
};
this.updateStatusPage = (params) => {
return this.putStatusPage(params);
};
this.createStatusPage = (params) => {
const statusPage = {
id: uuid.v4(),
created: new Date(),
...params,
};
return this.putStatusPage(statusPage);
};
this.putStatusPage = (statusPage) => {
const Item = this.statusPageToDynamoItem(statusPage);
return this.dynamoClient.putDocumentToDynamo({ Item }).then(_ => statusPage);
};
this.findStatusPages = (userId) => {
const userIdHashKey = this.encodeUserId(userId);
return this.dynamoClient
.queryTable({
IndexName: STATUS_PAGE_TABLE_SEARCH_GSI,
KeyConditionExpression: `userId = :userId`,
ExpressionAttributeValues: {
':userId': userIdHashKey,
},
})
.then(resp => {
const dynamoItems = resp.Items;
return dynamoItems.map(this.statusPageFromDynamoItem);
});
};
this.deleteStatusPage = (id) => {
return this.dynamoClient.deleteItem({ Key: { id: this.encodeId(id) } });
};
this.lookupStatusPage = (customDomain) => {
return this.dynamoClient
.queryTable({
IndexName: STATUS_PAGE_TABLE_CUSTOM_DOMAIN_GSI,
KeyConditionExpression: `customDomain = :customDomain`,
ExpressionAttributeValues: {
':customDomain': customDomain,
},
})
.then(resp => {
if (!resp.Items || resp.Items.length === 0) {
throw new NotFoundException(`Could not find statusPage: customDomain = ${customDomain}`);
}
const dynamoItem = resp.Items[0];
return this.statusPageFromDynamoItem(dynamoItem);
});
};
this.getStatusPage = (id) => {
return this.dynamoClient
.queryTable({
KeyConditionExpression: `id = :id`,
ExpressionAttributeValues: {
':id': this.encodeId(id),
},
})
.then(resp => {
if (!resp.Items || resp.Items.length === 0) {
throw new NotFoundException(`Could not find statusPage: id = ${id}`);
}
const dynamoItem = resp.Items[0];
return this.statusPageFromDynamoItem(dynamoItem);
});
};
this.statusPageToDynamoItem = (statusPage) => {
const userId = this.encodeUserId(statusPage.userId);
const id = this.encodeId(statusPage.id);
return {
userId,
id,
monitors: statusPage.monitors,
created: DateUtils.getUnixTimestamp(statusPage.created),
includeAllMonitors: statusPage.includeAllMonitors,
niceName: statusPage.niceName,
logo: statusPage.logo,
customDomain: statusPage.customDomain,
customCss: statusPage.customCss,
customHtmlFooter: statusPage.customHtmlFooter,
customHtmlHeader: statusPage.customHtmlHeader,
};
};
this.statusPageFromDynamoItem = (Item) => {
const userId = this.decodeHashKey(Item.userId);
const id = this.decodeHashKey(Item.id);
return {
id,
userId,
monitors: Item.monitors,
created: DateUtils.dateFromUnixTimestamp(Item.created),
includeAllMonitors: Item.includeAllMonitors,
niceName: Item.niceName,
logo: Item.logo,
customDomain: Item.customDomain,
customCss: Item.customCss,
customHtmlFooter: Item.customHtmlFooter,
customHtmlHeader: Item.customHtmlHeader,
};
};
this.dynamoClient = new DynamoDBTableClient(STATUS_PAGE_TABLE_SCHEMA.TableName, dynamoClient);
}
encodeId(statusPageId) {
return `id#${statusPageId}`;
}
encodeUserId(userId) {
return `userId#${userId}`;
}
decodeHashKey(hashKey) {
const s = hashKey.split('#');
return s[1];
}
}
const dynamoStatusPageDatasource = new DynamoStatusPageDatasource();
const CREATE_STATUS_PAGE_SCHEMA_SHAPE = {
userId: userIdSchema,
niceName: Yup.string().required('niceName is required'),
includeAllMonitors: Yup.boolean().required('includeAllMonitors is required'),
monitors: Yup.array().of(Yup.string()),
customDomain: Yup.string(),
logo: Yup.string(),
};
const CREATE_STATUS_PAGE_SCHEMA = Yup.object().shape(CREATE_STATUS_PAGE_SCHEMA_SHAPE);
const UPDATE_STATUS_PAGE_SCHEMA_SHAPE = {
...CREATE_STATUS_PAGE_SCHEMA_SHAPE,
id: uuidIdSchema,
};
const UPDATE_STATUS_PAGE_SCHEMA = Yup.object().shape(UPDATE_STATUS_PAGE_SCHEMA_SHAPE);
function validateCreateStatusPagePayload(payload) {
return validate(CREATE_STATUS_PAGE_SCHEMA, payload);
}
function validateUpdateStatusPagePayload(payload) {
return validate(UPDATE_STATUS_PAGE_SCHEMA, payload);
}
class StatusPageService extends BaseService {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
super();
this.getStatusPage = async (id) => {
return this.tryExecute(async () => {
const statusPage = await this.datasource.getStatusPage(id);
return this.serviceResponse(statusPage);
});
};
this.lookupStatusPage = async (customDomain) => {
return this.tryExecute(async () => {
const statusPage = await this.datasource.lookupStatusPage(customDomain);
return this.serviceResponse(statusPage);
});
};
this.updateStatusPage = async (userId, updateStatusPagePayload) => {
const validationResponse = await validateUpdateStatusPagePayload(updateStatusPagePayload);
if (validationResponse.error) {
return this.serviceResponseFromBoom(validationBoom(validationResponse.errors));
}
return this.tryExecute(async () => {
const existingStatusPage = await this.datasource.getStatusPage(updateStatusPagePayload.id);
if (Authorization.of(userId).cannotAccessEntity(existingStatusPage)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
const updatePayload = {
...updateStatusPagePayload,
created: existingStatusPage.created,
};
const updatedStatusPage = await this.datasource.updateStatusPage(updatePayload);
return this.serviceResponse(updatedStatusPage);
});
};
this.deleteStatusPage = async (userId, id) => {
return this.tryExecute(async () => {
const statusPage = await this.datasource.getStatusPage(id);
if (Authorization.of(userId).cannotAccessEntity(statusPage)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
await this.datasource.deleteStatusPage(statusPage.id);
return this.serviceResponse(statusPage);
});
};
this.createStatusPage = async (userId, params) => {
const validationResponse = await validateCreateStatusPagePayload(params);
if (validationResponse.error) {
return this.serviceResponseFromBoom(validationBoom(validationResponse.errors));
}
return this.tryExecute(async () => {
if (Authorization.of(userId).cannotAccessEntity(params)) {
throw new ForbiddenException('You are not authorized to perform this action');
}
const statusPage = await this.datasource.createStatusPage(params);
return { statusCode: 201, body: statusPage };
});
};
this.findStatusPages = async (userId) => {
return this.tryExecute(async () => {
const statusPages = await this.datasource.findStatusPages(userId);
return { statusCode: 200, body: statusPages };
});
};
this.datasource = new DynamoStatusPageDatasource(dynamoClient);
}
}
const statusPageService = new StatusPageService();
class DynamoMonitorDatasource extends DynamoMonitoringDatasource {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
super(dynamoClient);
this.bookmarkMonitor = (userId, monitorId, bookmarked) => {
const hashKey = DynamoMonitoringDatasource.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ monitorId });
return this.dynamoClient
.updateItem({
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 = DynamoMonitoringDatasource.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ monitorId });
return this.dynamoClient
.queryTable({
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 = DynamoMonitoringDatasource.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ monitorId });
return this.dynamoClient.deleteItem({
Key: {
hashKey,
sortKey,
},
});
};
this.searchMonitors = (userId) => {
return this.dynamoClient
.queryTable({
KeyConditionExpression: `hashKey = :hashKey and begins_with(sortKey, :sortKey)`,
ExpressionAttributeValues: {
':hashKey': DynamoMonitoringDatasource.encodeHashKey({ userId }),
':sortKey': 'monitor',
},
})
.then(this.mapDynamoRows);
};
this.getMonitorsByScheduler = (scheduler) => {
return this.dynamoClient
.queryTable({
KeyConditionExpression: `scheduler = :scheduler and begins_with(sortKey, :sortKey)`,
IndexName: MONITORING_TABLE_SCHEDULER_GSI,
ExpressionAttributeValues: {
':scheduler': scheduler,
':sortKey': 'monitor',
},
})
.then(this.mapDynamoRows);
};
this.createMonitor = async (payload) => {
const taskName = 'createMonitor';
const monitor = { ...payload, id: uuid.v4(), created: new Date() };
const Item = this.monitorToDynamoItem(monitor);
const TransactItems = [{ Put: { Item, TableName: this.tableName } }];
const userInfo = await this.userDatasource.getUserInfo(monitor.userId);
if (!userInfo.gettingStarted.completedTasks[taskName]) {
const completeTaskUpdateItemInput = completeGettingStartedTaskUpdate(monitor.userId, monitor.created, taskName);
TransactItems.push({ Update: completeTaskUpdateItemInput });
}
return this.dynamoRawClient.transactWriteItems({ TransactItems }).then(_ => monitor);
};
this.updateMonitor = (updateMonitorPayload) => {
return this._putMonitor(updateMonitorPayload);
};
this._putMonitor = (monitor) => {
const Item = this.monitorToDynamoItem(monitor);
return this.dynamoClient.putDocumentToDynamo({ Item }).then(_ => monitor);
};
this.monitorToDynamoItem = (monitor) => {
return {
hashKey: DynamoMonitorDatasource.encodeHashKey(monitor),
sortKey: this.encodeSortKey({ monitorId: monitor.id }),
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.mapDynamoRows = (resp) => {
const rows = resp.Items;
return rows.map(this.monitorFromDynamoItem);
};
this.userDatasource = new DynamoUserDatasource(dynamoClient);
}
/* sortKey */
encodeSortKey({ monitorId }) {
return `monitor#${monitorId}`;
}
}
const dynamoMonitorDatasource = new DynamoMonitorDatasource();
const executorTypeErrorHandler = (params) => {
return typeErrorHandler('Executor', params);
};
const EXECUTE_MONITOR_SCHEMA_OBJECT = {
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 EXECUTE_MONITOR_SCHEMA = Yup.object()
.shape(EXECUTE_MONITOR_SCHEMA_OBJECT)
.typeError(executorTypeErrorHandler);
function validateExecuteMonitorPayload(payload) {
return validate(EXECUTE_MONITOR_SCHEMA, payload);
}
const monitorTypeErrorHandler = (params) => {
return typeErrorHandler('Monitor', params);
};
/* Monitors */
const CREATE_MONITOR_BASE_SCHEMA_SHAPE = {
...EXECUTE_MONITOR_SCHEMA_OBJECT,
userId: userIdSchema,
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'),
};
const UPDATE_MONITOR_BASE_SCHEMA_SHAPE = {
id: uuidIdSchema,
};
const CREATE_MONITOR_SCHEMA = Yup.object()
.shape(CREATE_MONITOR_BASE_SCHEMA_SHAPE)
.typeError(monitorTypeErrorHandler);
const UPDATE_MONITOR_SCHEMA = Yup.object()
.shape({
...CREATE_MONITOR_BASE_SCHEMA_SHAPE,
...UPDATE_MONITOR_BASE_SCHEMA_SHAPE,
})
.typeError(monitorTypeErrorHandler);
/* 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(monitorTypeErrorHandler);
const UPDATE_LATENCY_MONITOR_SCHEMA = Yup.object()
.shape({
...CREATE_LATENCY_CHECK_SCHEMA_SHAPE,
...UPDATE_MONITOR_BASE_SCHEMA_SHAPE,
})
.typeError(monitorTypeErrorHandler);
function isLatencyCheckMonitor(payload) {
return payload.type === 'latency-check';
}
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);
}
class MonitorsService extends BaseService {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
super();
this.bookmarkMonitor = async (userId, monitorId) => {
return this.tryExecute(async () => {
let monitor = await this.datasource.getMonitor(userId, monitorId);
monitor = await this.datasource.bookmarkMonitor(monitor.userId, monitor.id, !monitor.isBookmarked);
return this.serviceResponse(monitor);
});
};
this.getMonitor = async (userId, monitorId) => {
return this.tryExecute(async () => {
const monitor = await this.datasource.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.datasource.getMonitor(userId, monitorId);
await this.datasource.deleteMonitor(monitor.userId, monitor.id);
return this.serviceResponse(monitor);
});
};
this.getMonitorsByScheduler = async (scheduler) => {
return this.tryExecute(async () => {
const monitors = await this.datasource.getMonitorsByScheduler(scheduler);
return this.serviceResponse(monitors);
});
};
this.getMonitors = async (userId) => {
return this.tryExecute(async () => {
const monitors = await this.datasource.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.datasource.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.datasource.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.datasource.createMonitor(newMonitorPayload);
return { statusCode: 201, body: monitor };
});
};
this.datasource = new DynamoMonitorDatasource(dynamoClient);
}
}
const monitorsService = new MonitorsService();
class DynamoAlertDatasource extends DynamoMonitoringDatasource {
constructor(dynamoClient = DEFAULT_DYNAMO_DB_CLIENT) {
super(dynamoClient);
this.createAlert = async (payload) => {
const taskName = 'createAlert';
const alert = { ...payload, id: uuid.v4(), created: new Date() };
const Item = this.alertToDynamoItem(alert);
const TransactItems = [{ Put: { Item, TableName: this.tableName } }];
const userInfo = await this.userDatasource.getUserInfo(alert.userId);
if (!userInfo.gettingStarted.completedTasks[taskName]) {
const completeTaskUpdateItemInput = completeGettingStartedTaskUpdate(alert.userId, alert.created, taskName);
TransactItems.push({ Update: completeTaskUpdateItemInput });
}
return this.dynamoRawClient.transactWriteItems({ TransactItems }).then(_ => alert);
};
this.deleteAlert = (userId, monitorId, alertId) => {
const hashKey = DynamoMonitoringDatasource.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ monitorId, alertId });
return this.dynamoClient.deleteItem({
Key: {
hashKey,
sortKey,
},
});
};
this.getAlert = (userId, monitorId, alertId) => {
const hashKey = DynamoMonitoringDatasource.encodeHashKey({ userId });
const sortKey = this.encodeSortKey({ mo