-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8666c10
commit d24e923
Showing
13 changed files
with
585 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Response } from "express" | ||
import ApiResponseHandler from "api-response-handler" | ||
import { createNewWebhook, deleteWebhook, getWebhooks } from "services/webhook" | ||
import { MetloRequest } from "types" | ||
import { CreateWebhookParams } from "@common/types" | ||
|
||
export const getWebhooksHandler = async ( | ||
req: MetloRequest, | ||
res: Response, | ||
): Promise<void> => { | ||
try { | ||
const webhooks = await getWebhooks(req.ctx) | ||
await ApiResponseHandler.success(res, webhooks) | ||
} catch (err) { | ||
await ApiResponseHandler.error(res, err) | ||
} | ||
} | ||
|
||
export const createWebhookHandler = async ( | ||
req: MetloRequest, | ||
res: Response, | ||
): Promise<void> => { | ||
try { | ||
const createWebhookParams: CreateWebhookParams = req.body | ||
const webhooks = await createNewWebhook(req.ctx, createWebhookParams) | ||
await ApiResponseHandler.success(res, webhooks) | ||
} catch (err) { | ||
await ApiResponseHandler.error(res, err) | ||
} | ||
} | ||
|
||
export const deleteWebhookHandler = async ( | ||
req: MetloRequest, | ||
res: Response, | ||
): Promise<void> => { | ||
try { | ||
const { webhookId } = req.params | ||
const webhooks = await deleteWebhook(req.ctx, webhookId) | ||
await ApiResponseHandler.success(res, webhooks) | ||
} catch (err) { | ||
await ApiResponseHandler.error(res, err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { MigrationInterface, QueryRunner } from "typeorm" | ||
|
||
export class addWebhookTable1670447292139 implements MigrationInterface { | ||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query( | ||
` | ||
CREATE TABLE IF NOT EXISTS "webhook" ( | ||
"uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), | ||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), | ||
"url" character varying NOT NULL, | ||
"maxRetries" integer NOT NULL DEFAULT '3', | ||
"alertTypes" character varying array NOT NULL DEFAULT '{}', | ||
"runs" jsonb NOT NULL DEFAULT '[]', | ||
CONSTRAINT "PK_bb57c3c8886ef87304032c70af35b765" PRIMARY KEY ("uuid") | ||
) | ||
`, | ||
) | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(`DROP TABLE IF EXISTS "webhook"`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { | ||
Column, | ||
CreateDateColumn, | ||
Entity, | ||
PrimaryGeneratedColumn, | ||
} from "typeorm" | ||
import { AlertType } from "@common/enums" | ||
import { WebhookRun } from "@common/types" | ||
import MetloBaseEntity from "./metlo-base-entity" | ||
|
||
@Entity() | ||
export class Webhook extends MetloBaseEntity { | ||
@PrimaryGeneratedColumn("uuid") | ||
uuid: string | ||
|
||
@CreateDateColumn({ type: "timestamptz" }) | ||
createdAt: Date | ||
|
||
@Column({ nullable: false }) | ||
url: string | ||
|
||
@Column({ type: "integer", nullable: false, default: 3 }) | ||
maxRetries: number | ||
|
||
@Column({ type: "varchar", array: true, default: [] }) | ||
alertTypes: AlertType[] | ||
|
||
@Column({ type: "jsonb", nullable: false, default: [] }) | ||
runs: WebhookRun[] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import axios from "axios" | ||
import { Brackets } from "typeorm" | ||
import { Alert, Webhook } from "models" | ||
import { createQB, getQB, insertValueBuilder } from "services/database/utils" | ||
import { MetloContext } from "types" | ||
import { AppDataSource } from "data-source" | ||
import { CreateWebhookParams } from "@common/types" | ||
import Error400BadRequest from "errors/error-400-bad-request" | ||
import Error500InternalServer from "errors/error-500-internal-server" | ||
|
||
const urlRegexp = new RegExp( | ||
/[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/, | ||
) | ||
|
||
const validUrl = (url: string) => urlRegexp.test(url) | ||
|
||
const delay = (fn: any, ms: number) => | ||
new Promise(resolve => setTimeout(() => resolve(fn()), ms)) | ||
|
||
const retryRequest = async (fn: any, maxRetries: number) => { | ||
const executeRequest = async (attempt: number) => { | ||
try { | ||
return await fn() | ||
} catch (err) { | ||
if (err?.response?.status > 400 && attempt <= maxRetries) { | ||
return delay(() => executeRequest(attempt + 1), 500) | ||
} else { | ||
throw err?.response?.data ?? err?.message | ||
} | ||
} | ||
} | ||
return executeRequest(1) | ||
} | ||
|
||
export const sendWebhookRequests = async ( | ||
ctx: MetloContext, | ||
alerts: Alert[], | ||
) => { | ||
const queryRunner = AppDataSource.createQueryRunner() | ||
try { | ||
await queryRunner.connect() | ||
for (const alert of alerts) { | ||
const webhooks: Webhook[] = await getQB(ctx, queryRunner) | ||
.from(Webhook, "webhook") | ||
.andWhere( | ||
new Brackets(qb => { | ||
qb.where(`:type = ANY("alertTypes")`, { type: alert.type }).orWhere( | ||
`cardinality("alertTypes") = 0`, | ||
) | ||
}), | ||
) | ||
.getRawMany() | ||
for (const webhook of webhooks) { | ||
let runs = webhook.runs | ||
if (runs.length >= 10) { | ||
runs = runs.slice(1) | ||
} | ||
try { | ||
await retryRequest( | ||
() => axios.post(webhook.url, alert, { timeout: 250 }), | ||
webhook.maxRetries, | ||
) | ||
await getQB(ctx, queryRunner) | ||
.update(Webhook) | ||
.set({ runs: [...runs, { ok: true, msg: "", payload: alert }] }) | ||
.andWhere("uuid = :id", { id: webhook.uuid }) | ||
.execute() | ||
} catch (err) { | ||
await getQB(ctx, queryRunner) | ||
.update(Webhook) | ||
.set({ runs: [...runs, { ok: false, msg: err, payload: alert }] }) | ||
.andWhere("uuid = :id", { id: webhook.uuid }) | ||
.execute() | ||
} | ||
} | ||
} | ||
} catch { | ||
} finally { | ||
await queryRunner.release() | ||
} | ||
} | ||
|
||
export const getWebhooks = async (ctx: MetloContext) => { | ||
return await createQB(ctx) | ||
.from(Webhook, "webhook") | ||
.orderBy(`"createdAt"`, "DESC") | ||
.getRawMany() | ||
} | ||
|
||
export const createNewWebhook = async ( | ||
ctx: MetloContext, | ||
createWebhookParams: CreateWebhookParams, | ||
) => { | ||
if (!createWebhookParams.url) { | ||
throw new Error400BadRequest("Must provide url for webhook.") | ||
} | ||
if (!validUrl(createWebhookParams.url)) { | ||
throw new Error400BadRequest("Please enter a valid url.") | ||
} | ||
const queryRunner = AppDataSource.createQueryRunner() | ||
try { | ||
await queryRunner.connect() | ||
const webhook = new Webhook() | ||
webhook.url = createWebhookParams.url.trim() | ||
if (createWebhookParams.alertTypes?.length > 0) { | ||
webhook.alertTypes = createWebhookParams.alertTypes | ||
} | ||
await insertValueBuilder(ctx, queryRunner, Webhook, webhook).execute() | ||
return await getQB(ctx, queryRunner) | ||
.from(Webhook, "webhook") | ||
.orderBy(`"createdAt"`, "DESC") | ||
.getRawMany() | ||
} catch { | ||
throw new Error500InternalServer( | ||
"Encountered error while creating new webhook.", | ||
) | ||
} finally { | ||
await queryRunner.release() | ||
} | ||
} | ||
|
||
export const deleteWebhook = async (ctx: MetloContext, webhookId: string) => { | ||
if (!webhookId) { | ||
throw new Error400BadRequest("Must provide id of webhook to delete.") | ||
} | ||
const queryRunner = AppDataSource.createQueryRunner() | ||
try { | ||
await queryRunner.connect() | ||
await getQB(ctx, queryRunner) | ||
.delete() | ||
.from(Webhook, "webhook") | ||
.andWhere("uuid = :id", { id: webhookId }) | ||
.execute() | ||
return await getQB(ctx, queryRunner) | ||
.from(Webhook, "webhook") | ||
.orderBy(`"createdAt"`, "DESC") | ||
.getRawMany() | ||
} catch { | ||
throw new Error500InternalServer( | ||
"Encountered error while deleting webhook.", | ||
) | ||
} finally { | ||
await queryRunner.release() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.