@meshwatch/backend-core
Version:
Meshwatch backend core services.
3 lines (2 loc) • 20 kB
JavaScript
import e from"@hapi/boom";import{pickBy as t,get as r,mapValues as o,isString as s}from"lodash";import n from"uuid";import i from"aws-sdk/clients/dynamodb";import{get as a}from"http";import c,{Agent as u}from"https";import{string as h,boolean as d,object as m,date as p,number as y,array as l}from"yup";import g from"@meshwatch/node-fetch";import{parse as f}from"url";import{Pool as T}from"pg";import{performance as w}from"perf_hooks";function E(t){return function(t,r,o){const s=new e(r,{statusCode:t});return C(s.output.payload,o)}(400,"Something went wrong while validating your payload",t)}function b(t,r={}){let o=r.statusCode;t instanceof k&&(o=t.httpStatusCode);const s=e.boomify(t,{...r,statusCode:o}),n={...s.output.payload,message:t.message||s.output.payload.message};return C(n)}function S(e){const t=e;return void 0!==t.error}function C(e,t){return t?{...e,errors:t}:e}class k extends Error{constructor(e,t,r){super(e),this.name=r,this.httpStatusCode=t,this.constructor=k,Error.captureStackTrace(this,k),Object.setPrototypeOf(this,k.prototype)}}class I extends k{constructor(e){super(e,500,"DatabaseException")}}class x extends k{constructor(e){super(e,404,"NotFoundException")}}class A extends k{constructor(e){super(e,403,"ForbiddenException")}}class K extends k{constructor(e){super(e,400,"BadRequestException")}}class R extends k{constructor(e){super(`Unreachable case: ${e}`,500,"UnreachableCaseError")}}const U=new u({keepAlive:!0,maxSockets:50,rejectUnauthorized:!0});const N=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:U},...e}}(),{dynamodb:_,dynamodbDocumentClient:v}=(M=N,{dynamodb:new i(M),dynamodbDocumentClient:new i.DocumentClient(M)});var M;function $(e,t){return e.createTable(t).promise().then(e=>e).catch(e=>e)}function D(e,r){const{TableName:o}=r,s=t(r.Item,e=>""!==e);return e.put({TableName:o,Item:s}).promise().catch(e=>{throw e})}function L(e,t){return e.query(t).promise().catch(e=>{throw e})}function q(e,t){return e.update(t).promise().catch(e=>{throw e})}const O=864e5;class P{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)/O)}}const F="monitor",B="schedulerGSI",H={TableName:F,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:B,KeySchema:[{AttributeName:"scheduler",KeyType:"HASH"},{AttributeName:"sortKey",KeyType:"RANGE"}],Projection:{ProjectionType:"ALL"},ProvisionedThroughput:{ReadCapacityUnits:1,WriteCapacityUnits:1}}]};const G=new class{constructor(e=v,t=_){this.createTable=(()=>$(this.dynamoDB,H)),this.bookmarkMonitor=((e,t,r)=>{const o=this.encodeHashKey({userId:e}),s=this.encodeSortKey({id:t});return q(this.documentClient,{TableName:F,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 L(this.documentClient,{TableName:F,KeyConditionExpression:"hashKey = :hashKey and sortKey = :sortKey",ExpressionAttributeValues:{":hashKey":r,":sortKey":o}}).then(e=>{if(!e.Items||0===e.Items.length)throw new x(`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:F,Key:{hashKey:r,sortKey:o}})}),this.searchMonitors=(e=>L(this.documentClient,{TableName:F,KeyConditionExpression:"hashKey = :hashKey",ExpressionAttributeValues:{":hashKey":this.encodeHashKey({userId:e})}}).then(this.mapDynamoRows)),this.getMonitorsByScheduler=(e=>L(this.documentClient,{TableName:F,KeyConditionExpression:"scheduler = :scheduler",IndexName:B,ExpressionAttributeValues:{":scheduler":e}}).then(this.mapDynamoRows)),this.createMonitor=(e=>{const t={...e,id:n.v4(),created:new Date};return this._putMonitor(t)}),this.updateMonitor=(e=>this._putMonitor(e)),this._putMonitor=(e=>{const t=this.monitorToDynamoItem(e);return D(this.documentClient,{TableName:F,Item:t}).then(t=>e)}),this.monitorToDynamoItem=(e=>({hashKey:this.encodeHashKey(e),sortKey:this.encodeSortKey(e),name:e.name,body:r(e,"body"),scheduler:e.scheduler,apdex:r(e,"apdex"),endpoint:e.endpoint,headers:r(e,"headers"),isBookmarked:e.isBookmarked,location:e.location,regions:r(e,"regions"),type:e.type,created:P.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:P.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]}}},V=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,z=e=>V.test(e),W={userId:h().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>z(e)).required("userId is required"),isBookmarked:d().required("isBookmarked is required"),scheduler:h().required("Scheduler is required, so we know how often to monitor your endpoint"),name:h().required("Name is required, so you can find your monitors easily"),endpoint:h().required("Endpoint is required so we know where to send our requests"),type:h().oneOf(["latency-check","https-check","certificate-check"]).required("Monitor type is required")},j=m().shape(W).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),Y=m().shape({...W,created:p().required("created is required"),id:h().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>z(e)).required("id is required")}).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),X={...W,body:h(),headers:m(),apdex:y().required("Apdex value is required so we can better measure performance of your endpoint"),regions:l().of(h()).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)},Q=m().shape(X).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),Z=m().shape({...X,id:h().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>z(e)).required("id is required")}).typeError(e=>`Monitor object expected but got: \`${e.originalValue}\``),J=m().shape({completedOn:p().required("created is required"),userId:h().test("isUUIDv4","uuid/v4 expected but got: `${value}`",e=>z(e)).required("userId is required"),taskName:h().required("taskName is required")});function ee(e){return"latency-check"===e.type}function te(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 re{constructor(e){this.canAccessEntity=(e=>e.userId===this.userId),this.cannotAccessEntity=(e=>!this.canAccessEntity(e)),this.userId=e}}re.of=(e=>new re(e));class oe{constructor(){this.errorServiceResponse=(e=>this.serviceResponseFromBoom(b(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 se extends oe{constructor(e=G){super(),this.bookmarkMonitor=(async(e,t)=>this.tryExecute(async()=>{let r=await this.monitorStorage.getMonitor(e,t);if(re.of(e).cannotAccessEntity(r))throw new A("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(re.of(e).cannotAccessEntity(r))throw new A("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(re.of(e).cannotAccessEntity(r))throw new A("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 ee(e)?te(Z,e):te(Y,e)}(t);return r.error?this.serviceResponseFromBoom(E(r.errors)):this.tryExecute(async()=>{if(re.of(e).cannotAccessEntity(t))throw new A("You are not authorized to perform this action");const r=await this.monitorStorage.getMonitor(e,t.id);if(r.type!==t.type)throw new K("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 ee(e)?te(Q,e):te(j,e)}(t);return r.error?this.serviceResponseFromBoom(E(r.errors)):this.tryExecute(async()=>{if(re.of(e).cannotAccessEntity(t))throw new A("You are not authorized to perform this action");const r=await this.monitorStorage.createMonitor(t);return{statusCode:201,body:r}})}),this.monitorStorage=e}}const ne=new se,ie="user",ae={TableName:ie,KeySchema:[{AttributeName:"hashKey",KeyType:"HASH"}],AttributeDefinitions:[{AttributeName:"hashKey",AttributeType:"S"}],ProvisionedThroughput:{ReadCapacityUnits:1,WriteCapacityUnits:1}},ce={gettingStarted:{completedTasks:{}}};const ue=new class{constructor(e=v,t=_){this.createTable=(()=>$(this.dynamoDB,ae)),this.getUserInfo=(e=>{const t=this.encodeHashKey({userId:e});return L(this.documentClient,{TableName:ie,KeyConditionExpression:"hashKey = :hashKey",ExpressionAttributeValues:{":hashKey":t}}).then(t=>{if(!t.Items)throw new x(`Could not find userInfo for user: ${e}`);return t.Items.length>0?this.userInfoFromDynamoRow(t.Items[0]):this.insertUserInfoRow({hashKey:this.encodeHashKey({userId:e}),...ce})})}),this.completeGettingStartedTask=(({userId:e,taskName:t,completedOn:r})=>{const s=this.encodeHashKey({userId:e});return q(this.documentClient,{TableName:ie,Key:{hashKey:s},UpdateExpression:`set gettingStarted.completedTasks.${t} = :created`,ConditionExpression:`attribute_not_exists(gettingStarted.completedTasks.${t})`,ExpressionAttributeValues:{":created":P.getUnixTimestamp(r)},ReturnValues:"UPDATED_NEW"}).then(t=>{if(!t.Attributes)throw new Error(`Could not update completeGettingStartedTask, userId: ${e}`);const r=t.Attributes.gettingStarted,s=o(r.completedTasks,P.dateFromUnixTimestamp);return{userId:e,gettingStarted:{completedTasks:s}}}).catch(e=>{if("ConditionalCheckFailedException"===e.name)throw new K(`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 x("Getting started task cannot be completed before userInfo is created");throw e})}),this.insertUserInfoRow=(e=>D(this.documentClient,{TableName:ie,Item:e}).then(t=>this.userInfoFromDynamoRow(e))),this.userInfoFromDynamoRow=(e=>{const{userId:t}=this.decodeHashKey(e.hashKey),r=o(e.gettingStarted.completedTasks,P.dateFromUnixTimestamp);return{userId:t,gettingStarted:{...e.gettingStarted,completedTasks:r}}}),this.documentClient=e,this.dynamoDB=t}encodeHashKey({userId:e}){return`user#${e}`}decodeHashKey(e){const t=e.split("#");return{userId:t[1]}}};class he extends oe{constructor(e=ue){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 te(J,e)}(r);return o.error?this.serviceResponseFromBoom(E(o.errors)):this.tryExecute(async()=>{const e=await this.userStorage.completeGettingStartedTask(r);return this.serviceResponse(e)})}),this.userStorage=e}}const de=new he;const me="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36",pe={port:80,path:"/",method:"GET",headers:{"User-Agent":me}};class ye{static hasRedirect(e){return ye.isRedirectableStatus(e.statusCode)}static isRedirectableStatus(e){return void 0!==e&&(e>=300&&e<400)}}ye.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)})})})(()=>ye.get(e),t)),ye.hasHttpsRedirect=(e=>{if(ye.hasRedirect(e)){const t=e.headers.location;if(t&&"https:"===f(t).protocol)return!0}return!1}),ye.get=(e=>(ye.isRequestOptionsAsObject(e)&&(e={...pe,...e}),new Promise((t,r)=>{a(e,e=>t(e)).on("error",e=>{r(e)})}))),ye.isRequestOptionsAsObject=(e=>!s(e));const le={port:443,path:"/",method:"GET",headers:{"User-Agent":me}};class ge{constructor(e=g){this.fetch=e}executeMonitorCheck(e){const t=f(e.endpoint);switch(e.type){case"latency-check":return this.latencyCheck({url:t,headers:r(e,"headers",{})});case"certificate-check":return this.certificateCheck(t);default:return this.httpsCheck(t)}}latencyCheck({url:e,timeout:t=ge.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=ge.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={...le,hostname:r},n=c.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=ge.DEFAULT_EXECUTOR_TIMEOUT){const r=e.protocol?e.hostname:e.href;return r?ye.timeoutableGet({hostname:r},t).then(e=>{const t=ye.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=P.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."}}}ge.DEFAULT_EXECUTOR_TIMEOUT=1e4;const fe=new ge,Te=()=>w.now(),we={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"},Ee=new T(we);async function be(e,t=Ee){Te();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 I(t)})}Ee.on("error",e=>{console.error("[Postgres]: Unexpected error on idle client",e)});const Se="checks",Ce="latency",ke=`CREATE TABLE ${Se}.${Ce} (\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);`,Ie=`SELECT create_hypertable('${Se}.${Ce}', 'time');`;class xe{constructor(e=Ee){this.createTable=(()=>this.queryPostgres(ke).then(e=>this.queryPostgres(Ie))),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 ${Se}.${Ce} `+`WHERE ${Se}.${Ce}.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 be(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 ${Se}.${Ce} `+"(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 Ae=new xe;new class extends oe{constructor(e=Ae,t=G){super(),this.getLatencyChecks=((e,t,r={limit:20})=>this.tryExecute(async()=>{const o=await this.monitorStorage.getMonitor(e,t);if(re.of(e).cannotAccessEntity(o))throw new A("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 Ke{constructor(e=Ee){this.createChecksSchema=(()=>be({name:"create-schema",text:`CREATE SCHEMA ${Se};`},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 R(t)}}),this.postgresPool=e,this.latencyStorage=new xe(e)}}const Re=new Ke;async function Ue(e=_,t=Ee){await async function(e=_){await $(e,ae),await $(e,H)}(e),await async function(e){await new Ke(e).createChecksSchema(),await new xe(e).createTable()}(t)}export{ge as MonitorExecutorService,se as MonitoringService,Ke as TimescaleMonitorCheckStorage,he as UserService,b as boomify,fe as executorService,Ue as initializeTables,S as isBoom,Re as monitorCheckStorage,ne as monitorService,de as userService};
//# sourceMappingURL=meshwatchbackend-core.es.production.js.map