UNPKG

@meshwatch/backend-core

Version:

Meshwatch backend core services.

1,229 lines (1,198 loc) 85.6 kB
'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