UNPKG

@meshwatch/backend-core

Version:

Meshwatch backend core services.

3 lines (2 loc) 20.2 kB
"use strict";function e(e){return e&&"object"==typeof e&&"default"in e?e.default:e}var t=e(require("@hapi/boom")),r=require("lodash"),o=e(require("uuid")),s=e(require("aws-sdk/clients/dynamodb")),n=require("http"),i=require("https"),a=e(i),c=require("yup"),u=e(require("@meshwatch/node-fetch")),h=require("url"),d=require("pg"),m=require("perf_hooks");function p(e){return function(e,r,o){const s=new t(r,{statusCode:e});return l(s.output.payload,o)}(400,"Something went wrong while validating your payload",e)}function y(e,r={}){let o=r.statusCode;e instanceof g&&(o=e.httpStatusCode);const s=t.boomify(e,{...r,statusCode:o}),n={...s.output.payload,message:e.message||s.output.payload.message};return l(n)}function l(e,t){return t?{...e,errors:t}:e}class g extends Error{constructor(e,t,r){super(e),this.name=r,this.httpStatusCode=t,this.constructor=g,Error.captureStackTrace(this,g),Object.setPrototypeOf(this,g.prototype)}}class f extends g{constructor(e){super(e,500,"DatabaseException")}}class b extends g{constructor(e){super(e,404,"NotFoundException")}}class T extends g{constructor(e){super(e,403,"ForbiddenException")}}class w extends g{constructor(e){super(e,400,"BadRequestException")}}class E extends g{constructor(e){super(`Unreachable case: ${e}`,500,"UnreachableCaseError")}}const S=new i.Agent({keepAlive:!0,maxSockets:50,rejectUnauthorized:!0});const C=function(e={}){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:S},...e}}(),{dynamodb:k,dynamodbDocumentClient:x}=(I=C,{dynamodb:new s(I),dynamodbDocumentClient:new s.DocumentClient(I)});var I;function A(e,t){return e.createTable(t).promise().then(e=>e).catch(e=>e)}function K(e,t){const{TableName:o}=t,s=r.pickBy(t.Item,e=>""!==e);return e.put({TableName:o,Item:s}).promise().catch(e=>{throw e})}function U(e,t){return e.query(t).promise().catch(e=>{throw e})}function R(e,t){return e.update(t).promise().catch(e=>{throw e})}const v=864e5;class M{static getUnixTimestamp(e=new Date){return Math.round(+e/1e3)}static dateFromUnixTimestamp(e){return new Date(1e3*e)}static daysDateDiff(e,t){const r=Date.UTC(e.getFullYear(),e.getMonth(),e.getDate()),o=Date.UTC(t.getFullYear(),t.getMonth(),t.getDate());return Math.floor((o-r)/v)}}const N="monitor",_="schedulerGSI",$={TableName:N,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:_,KeySchema:[{AttributeName:"scheduler",KeyType:"HASH"},{AttributeName:"sortKey",KeyType:"RANGE"}],Projection:{ProjectionType:"ALL"},ProvisionedThroughput:{ReadCapacityUnits:1,WriteCapacityUnits:1}}]};const D=new class{constructor(e=x,t=k){this.createTable=(()=>A(this.dynamoDB,$)),this.bookmarkMonitor=((e,t,r)=>{const o=this.encodeHashKey({userId:e}),s=this.encodeSortKey({id:t});return R(this.documentClient,{TableName:N,Key:{hashKey:o,sortKey:s},UpdateExpression:"set isBookmarked = :isBookmarked",ExpressionAttributeValues:{":isBookmarked":r},ReturnValues:"ALL_NEW"}).then(e=>this.monitorFromDynamoItem(e.Attributes))}),this.getMonitor=((e,t)=>{const r=this.encodeHashKey({userId:e}),o=this.encodeSortKey({id:t});return U(this.documentClient,{TableName:N,KeyConditionExpression:"hashKey = :hashKey and sortKey = :sortKey",ExpressionAttributeValues:{":hashKey":r,":sortKey":o}}).then(e=>{if(!e.Items||0===e.Items.length)throw new b(`Could not find monitor id = ${t}`);return this.monitorFromDynamoItem(e.Items[0])})}),this.deleteMonitor=((e,t)=>{const r=this.encodeHashKey({userId:e}),o=this.encodeSortKey({id:t});return function(e,t){return e.delete(t).promise().catch(e=>{throw e})}(this.documentClient,{TableName:N,Key:{hashKey:r,sortKey:o}})}),this.searchMonitors=(e=>U(this.documentClient,{TableName:N,KeyConditionExpression:"hashKey = :hashKey",ExpressionAttributeValues:{":hashKey":this.encodeHashKey({userId:e})}}).then(this.mapDynamoRows)),this.getMonitorsByScheduler=(e=>U(this.documentClient,{TableName:N,KeyConditionExpression:"scheduler = :scheduler",IndexName:_,ExpressionAttributeValues:{":scheduler":e}}).then(this.mapDynamoRows)),this.createMonitor=(e=>{const t={...e,id:o.v4(),created:new Date};return this._putMonitor(t)}),this.updateMonitor=(e=>this._putMonitor(e)),this._putMonitor=(e=>{const t=this.monitorToDynamoItem(e);return K(this.documentClient,{TableName:N,Item:t}).then(t=>e)}),this.monitorToDynamoItem=(e=>({hashKey:this.encodeHashKey(e),sortKey:this.encodeSortKey(e),name:e.name,body:r.get(e,"body"),scheduler:e.scheduler,apdex:r.get(e,"apdex"),endpoint:e.endpoint,headers:r.get(e,"headers"),isBookmarked:e.isBookmarked,location:e.location,regions:r.get(e,"regions"),type:e.type,created:M.getUnixTimestamp(e.created)})),this.monitorFromDynamoItem=(e=>{const{userId:t}=this.decodeHashKey(e.hashKey),{id:r}=this.decodeSortKey(e.sortKey);return{userId:t,id:r,created:M.dateFromUnixTimestamp(e.created),name:e.name,scheduler:e.scheduler,apdex:e.apdex,type:e.type,headers:e.headers,body:e.body,endpoint:e.endpoint,isBookmarked:e.isBookmarked,location:e.location,regions:e.regions}}),this.mapDynamoRows=(e=>{const t=e.Items;return t.map(this.monitorFromDynamoItem)}),this.documentClient=e,this.dynamoDB=t}encodeHashKey({userId:e}){return`user#${e}`}decodeHashKey(e){const t=e.split("#");return{userId:t[1]}}encodeSortKey({id:e}){return`id#${e}`}decodeSortKey(e){const t=e.split("#");return{id:t[1]}}},L=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,q=e=>L.test(e),O={userId:c.string().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>q(e)).required("userId is required"),isBookmarked:c.boolean().required("isBookmarked is required"),scheduler:c.string().required("Scheduler is required, so we know how often to monitor your endpoint"),name:c.string().required("Name is required, so you can find your monitors easily"),endpoint:c.string().required("Endpoint is required so we know where to send our requests"),type:c.string().oneOf(["latency-check","https-check","certificate-check"]).required("Monitor type is required")},P=c.object().shape(O).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),B=c.object().shape({...O,created:c.date().required("created is required"),id:c.string().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>q(e)).required("id is required")}).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),F={...O,body:c.string(),headers:c.object(),apdex:c.number().required("Apdex value is required so we can better measure performance of your endpoint"),regions:c.array().of(c.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}`",e=>(e||[]).length>0)},H=c.object().shape(F).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),G=c.object().shape({...F,id:c.string().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>q(e)).required("id is required")}).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),j=c.object().shape({completedOn:c.date().required("created is required"),userId:c.string().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>q(e)).required("userId is required"),taskName:c.string().required("taskName is required")});function V(e){return"latency-check"===e.type}function z(e,t,r={abortEarly:!1,strict:!0}){if(void 0===t||"function"==typeof t){const e={non_field_errors:`Object expected but got: \`${t}\``};return Promise.resolve({error:!0,errors:e})}return e.validate(t,r).then(e=>({error:!1})).catch(e=>{return{error:!0,errors:e.inner.reduce((e,t)=>{const r=t.path||"non_field_errors";return e[r]=t.message,e},{})}})}class W{constructor(e){this.canAccessEntity=(e=>e.userId===this.userId),this.cannotAccessEntity=(e=>!this.canAccessEntity(e)),this.userId=e}}W.of=(e=>new W(e));class Y{constructor(){this.errorServiceResponse=(e=>this.serviceResponseFromBoom(y(e))),this.serviceResponseFromBoom=(e=>({statusCode:e.statusCode,body:e})),this.serviceResponse=(e=>({statusCode:200,body:e})),this.tryExecute=(async e=>e().catch(this.errorServiceResponse))}}class X extends Y{constructor(e=D){super(),this.bookmarkMonitor=(async(e,t)=>this.tryExecute(async()=>{let r=await this.monitorStorage.getMonitor(e,t);if(W.of(e).cannotAccessEntity(r))throw new T("You are not authorized to perform this action");return r=await this.monitorStorage.bookmarkMonitor(r.userId,r.id,!r.isBookmarked),this.serviceResponse(r)})),this.getMonitor=(async(e,t)=>this.tryExecute(async()=>{const r=await this.monitorStorage.getMonitor(e,t);if(W.of(e).cannotAccessEntity(r))throw new T("You are not authorized to perform this action");return this.serviceResponse(r)})),this.deleteMonitor=(async(e,t)=>this.tryExecute(async()=>{const r=await this.monitorStorage.getMonitor(e,t);if(W.of(e).cannotAccessEntity(r))throw new T("You are not authorized to perform this action");return await this.monitorStorage.deleteMonitor(r.userId,r.id),this.serviceResponse(r)})),this.getMonitorsByScheduler=(async e=>this.tryExecute(async()=>{const t=await this.monitorStorage.getMonitorsByScheduler(e);return this.serviceResponse(t)})),this.getMonitors=(async e=>this.tryExecute(async()=>{const t=await this.monitorStorage.searchMonitors(e);return this.serviceResponse(t)})),this.updateMonitor=(async(e,t)=>{const r=await function(e){return V(e)?z(G,e):z(B,e)}(t);return r.error?this.serviceResponseFromBoom(p(r.errors)):this.tryExecute(async()=>{if(W.of(e).cannotAccessEntity(t))throw new T("You are not authorized to perform this action");const r=await this.monitorStorage.getMonitor(e,t.id);if(r.type!==t.type)throw new w("Monitor type cannot be changes. Please create a new monitor.");const o=await this.monitorStorage.updateMonitor({...t,created:r.created});return{statusCode:200,body:o}})}),this.createMonitor=(async(e,t)=>{const r=await function(e){return V(e)?z(H,e):z(P,e)}(t);return r.error?this.serviceResponseFromBoom(p(r.errors)):this.tryExecute(async()=>{if(W.of(e).cannotAccessEntity(t))throw new T("You are not authorized to perform this action");const r=await this.monitorStorage.createMonitor(t);return{statusCode:201,body:r}})}),this.monitorStorage=e}}const Q=new X,Z="user",J={TableName:Z,KeySchema:[{AttributeName:"hashKey",KeyType:"HASH"}],AttributeDefinitions:[{AttributeName:"hashKey",AttributeType:"S"}],ProvisionedThroughput:{ReadCapacityUnits:1,WriteCapacityUnits:1}},ee={gettingStarted:{completedTasks:{}}};const te=new class{constructor(e=x,t=k){this.createTable=(()=>A(this.dynamoDB,J)),this.getUserInfo=(e=>{const t=this.encodeHashKey({userId:e});return U(this.documentClient,{TableName:Z,KeyConditionExpression:"hashKey = :hashKey",ExpressionAttributeValues:{":hashKey":t}}).then(t=>{if(!t.Items)throw new b(`Could not find userInfo for user: ${e}`);return t.Items.length>0?this.userInfoFromDynamoRow(t.Items[0]):this.insertUserInfoRow({hashKey:this.encodeHashKey({userId:e}),...ee})})}),this.completeGettingStartedTask=(({userId:e,taskName:t,completedOn:o})=>{const s=this.encodeHashKey({userId:e});return R(this.documentClient,{TableName:Z,Key:{hashKey:s},UpdateExpression:`set gettingStarted.completedTasks.${t} = :created`,ConditionExpression:`attribute_not_exists(gettingStarted.completedTasks.${t})`,ExpressionAttributeValues:{":created":M.getUnixTimestamp(o)},ReturnValues:"UPDATED_NEW"}).then(t=>{if(!t.Attributes)throw new Error(`Could not update completeGettingStartedTask, userId: ${e}`);const o=t.Attributes.gettingStarted,s=r.mapValues(o.completedTasks,M.dateFromUnixTimestamp);return{userId:e,gettingStarted:{completedTasks:s}}}).catch(e=>{if("ConditionalCheckFailedException"===e.name)throw new w(`Task [${t}] is already completed`);if("ValidationException"===e.name&&"The document path provided in the update expression is invalid for update"===e.message)throw new b("Getting started task cannot be completed before userInfo is created");throw e})}),this.insertUserInfoRow=(e=>K(this.documentClient,{TableName:Z,Item:e}).then(t=>this.userInfoFromDynamoRow(e))),this.userInfoFromDynamoRow=(e=>{const{userId:t}=this.decodeHashKey(e.hashKey),o=r.mapValues(e.gettingStarted.completedTasks,M.dateFromUnixTimestamp);return{userId:t,gettingStarted:{...e.gettingStarted,completedTasks:o}}}),this.documentClient=e,this.dynamoDB=t}encodeHashKey({userId:e}){return`user#${e}`}decodeHashKey(e){const t=e.split("#");return{userId:t[1]}}};class re extends Y{constructor(e=te){super(),this.getUserInfo=(e=>this.tryExecute(async()=>{const t=await this.userStorage.getUserInfo(e);return this.serviceResponse(t)})),this.completeGettingStartedTask=(async(e,t)=>{const r={userId:e,taskName:t,completedOn:new Date},o=await function(e){return z(j,e)}(r);return o.error?this.serviceResponseFromBoom(p(o.errors)):this.tryExecute(async()=>{const e=await this.userStorage.completeGettingStartedTask(r);return this.serviceResponse(e)})}),this.userStorage=e}}const oe=new re;const se="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",ne={port:80,path:"/",method:"GET",headers:{"User-Agent":se}};class ie{static hasRedirect(e){return ie.isRedirectableStatus(e.statusCode)}static isRedirectableStatus(e){return void 0!==e&&(e>=300&&e<400)}}ie.timeoutableGet=((e,t)=>(function(e,t){return new Promise((r,o)=>{const s=setTimeout(()=>{o(`Action timed out using timeout of ${t}ms.`)},t);e().then(e=>{clearTimeout(s),r(e)}).catch(e=>{clearTimeout(s),o(e)})})})(()=>ie.get(e),t)),ie.hasHttpsRedirect=(e=>{if(ie.hasRedirect(e)){const t=e.headers.location;if(t&&"https:"===h.parse(t).protocol)return!0}return!1}),ie.get=(e=>(ie.isRequestOptionsAsObject(e)&&(e={...ne,...e}),new Promise((t,r)=>{n.get(e,e=>t(e)).on("error",e=>{r(e)})}))),ie.isRequestOptionsAsObject=(e=>!r.isString(e));const ae={port:443,path:"/",method:"GET",headers:{"User-Agent":se}};class ce{constructor(e=u){this.fetch=e}executeMonitorCheck(e){const t=h.parse(e.endpoint);switch(e.type){case"latency-check":return this.latencyCheck({url:t,headers:r.get(e,"headers",{})});case"certificate-check":return this.certificateCheck(t);default:return this.httpsCheck(t)}}latencyCheck({url:e,timeout:t=ce.DEFAULT_EXECUTOR_TIMEOUT,headers:r}){return new Promise(async o=>{const{href:s}=e;if(!s)return o({httpStatus:400,error:`Expected url, but got \`${s}\``});this.fetch(s,{timeout:t,headers:r}).then(e=>e.blob().then(t=>{const r=e.timings,s=t.size;o({blobSize:s,timings:r,httpStatus:e.status})})).catch(e=>{let r=e.message,s=400;e.message.includes("network timeout")&&(r=`Latency check timed out using timeout of ${t}ms.`,s=408),o({error:r,httpStatus:s})})})}async certificateCheck(e,t=ce.DEFAULT_EXECUTOR_TIMEOUT){const r=e.protocol?e.hostname:e.href;return r?new Promise(e=>{const o=setTimeout(()=>{e({hasCertificate:!1,error:`Certificate check timed out using timeout of ${t}ms.`})},t),s={...ae,hostname:r},n=a.get(s).on("error",t=>{clearTimeout(o);const r=t.message;e({hasCertificate:!1,error:r})});n.on("socket",t=>{t.on("secureConnect",()=>{const r=t.getPeerCertificate(),s=this.parseCertificate(r);e(s),clearTimeout(o)})})}):{hasCertificate:!1,error:`Expected hostname, but got \`${r}\``}}async httpsCheck(e,t=ce.DEFAULT_EXECUTOR_TIMEOUT){const r=e.protocol?e.hostname:e.href;return r?ie.timeoutableGet({hostname:r},t).then(e=>{const t=ie.hasHttpsRedirect(e);return t?{secure:t}:{secure:t,error:`Could not find https redirect from ${r}, statusMessage: ${e.statusMessage}`}}).catch(e=>{const t={secure:!1,error:e.message};return t}):{secure:!1,error:`Expected hostname, but got \`${r}\``}}parseCertificate(e){const t=new Date(e.valid_to),r=M.daysDateDiff(new Date,t),o=void 0!==r&&r>0;return o?{hasCertificate:o,valid_to:t,expires_in:r}:{hasCertificate:o,error:"Could not find PeerCertificate."}}}ce.DEFAULT_EXECUTOR_TIMEOUT=1e4;const ue=new ce,he=()=>m.performance.now(),de={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"},me=new d.Pool(de);async function pe(e,t=me){he();return t.query(e).then(e=>e).catch(e=>{const t=`Something went wrong while executing query: ${e.message}`;throw console.error(`[Postgres]: ${t}`),new f(t)})}me.on("error",e=>{console.error("[Postgres]: Unexpected error on idle client",e)});const ye="checks",le="latency",ge=`CREATE TABLE ${ye}.${le} (\n time TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n user_id UUID NOT NULL,\n monitor_id UUID NOT NULL,\n region TEXT NOT NULL,\n status SMALLINT NOT NULL,\n error TEXT NULL,\n total_time REAL NULL,\n tls_handshake_time REAL NULL,\n tcp_connection_time REAL NULL,\n dns_lookup_time REAL NULL,\n first_byte_time REAL NULL,\n blob_size REAL NULL\n);`,fe=`SELECT create_hypertable('${ye}.${le}', 'time');`;class be{constructor(e=me){this.createTable=(()=>this.queryPostgres(ge).then(e=>this.queryPostgres(fe))),this.getMonitorChecks=(e=>{const t=this.getMonitorChecksQueryConfig(e);return this.queryPostgres(t).then(e=>{const t=e.rows;return t})}),this.getMonitorChecksQueryConfig=(({monitorId:e,pagination:{limit:t,offset:r=0}})=>({name:"get-monitor-latency-checks",text:`SELECT * FROM ${ye}.${le} `+`WHERE ${ye}.${le}.monitor_id = $1 `+"ORDER BY time DESC LIMIT $2 OFFSET $3",values:[e,t,r]})),this.insertLatencyCheck=(e=>{const t=this.getInsertQueryConfig(e);return this.queryPostgres(t).then(e=>{const t=e.rows[0];return t})}),this.postgresPool=e}queryPostgres(e){return pe(e,this.postgresPool)}getInsertQueryConfig({userId:e,monitorId:t,region:r,data:o,created:s}){const n=o.timings||{},i=o.blobSize,a=o.error;return{name:"insert-latency-check",text:`INSERT INTO ${ye}.${le} `+"(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:[s,e,t,r,o.httpStatus,a,n.totalTime,n.tlsHandshakeTime,n.tcpConnectionTime,n.dnsLookupTime,n.firstByteTime,i]}}}const Te=new be;new class extends Y{constructor(e=Te,t=D){super(),this.getLatencyChecks=((e,t,r={limit:20})=>this.tryExecute(async()=>{const o=await this.monitorStorage.getMonitor(e,t);if(W.of(e).cannotAccessEntity(o))throw new T("You are not authorized to perform this action");return this.latencyCheckStorage.getMonitorChecks({monitorId:t,pagination:r}).then(e=>this.serviceResponse(e))})),this.latencyCheckStorage=e,this.monitorStorage=t}};class we{constructor(e=me){this.createChecksSchema=(()=>pe({name:"create-schema",text:`CREATE SCHEMA ${ye};`},this.postgresPool)),this.insertCheckData=((e,t)=>{switch(t){case"latency-check":const r=e;return this.latencyStorage.insertLatencyCheck(r);case"certificate-check":case"https-check":throw new Error("Not implemented");default:throw new E(t)}}),this.postgresPool=e,this.latencyStorage=new be(e)}}const Ee=new we;exports.MonitorExecutorService=ce,exports.MonitoringService=X,exports.TimescaleMonitorCheckStorage=we,exports.UserService=re,exports.boomify=y,exports.executorService=ue,exports.initializeTables=async function(e=k,t=me){await async function(e=k){await A(e,J),await A(e,$)}(e),await async function(e){await new we(e).createChecksSchema(),await new be(e).createTable()}(t)},exports.isBoom=function(e){const t=e;return void 0!==t.error},exports.monitorCheckStorage=Ee,exports.monitorService=Q,exports.userService=oe; //# sourceMappingURL=meshwatchbackend-core.cjs.production.js.map