feat: refactor stage 2
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { requireAuth } from "./auth";
|
||||
import sql from "../db";
|
||||
import { dispatch, knownProviderKinds, type ChannelRow } from "../notifications";
|
||||
|
||||
const ChannelBody = t.Object({
|
||||
name: t.String({ minLength: 1, maxLength: 200 }),
|
||||
kind: t.String({ description: "Provider kind, e.g. 'webhook'" }),
|
||||
config: t.Any({ description: "Provider-specific config object" }),
|
||||
enabled: t.Optional(t.Boolean()),
|
||||
});
|
||||
|
||||
function validateKind(kind: string): string | null {
|
||||
if (!knownProviderKinds().includes(kind)) {
|
||||
return `Unknown provider kind '${kind}'. Known: ${knownProviderKinds().join(", ")}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateWebhookConfig(config: any): string | null {
|
||||
if (!config || typeof config !== "object") return "config must be an object";
|
||||
if (typeof config.url !== "string" || !/^https?:\/\//.test(config.url)) {
|
||||
return "webhook config.url must be a http(s) URL";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateConfig(kind: string, config: any): string | null {
|
||||
if (kind === "webhook") return validateWebhookConfig(config);
|
||||
return null;
|
||||
}
|
||||
|
||||
export const channels = new Elysia({ prefix: "/notifications/channels" })
|
||||
.use(requireAuth)
|
||||
|
||||
.get("/", async ({ accountId }) => {
|
||||
return sql`
|
||||
SELECT id, name, kind, config, enabled, created_at
|
||||
FROM notification_channels
|
||||
WHERE account_id = ${accountId}
|
||||
ORDER BY created_at DESC
|
||||
`;
|
||||
}, { detail: { summary: "List notification channels", tags: ["notifications"] } })
|
||||
|
||||
.post("/", async ({ accountId, body, set }) => {
|
||||
const kindErr = validateKind(body.kind);
|
||||
if (kindErr) { set.status = 400; return { error: kindErr }; }
|
||||
const cfgErr = validateConfig(body.kind, body.config);
|
||||
if (cfgErr) { set.status = 400; return { error: cfgErr }; }
|
||||
|
||||
const [row] = await sql`
|
||||
INSERT INTO notification_channels (account_id, name, kind, config, enabled)
|
||||
VALUES (${accountId}, ${body.name}, ${body.kind}, ${sql.json(body.config)}, ${body.enabled ?? true})
|
||||
RETURNING id, name, kind, config, enabled, created_at
|
||||
`;
|
||||
return row;
|
||||
}, { body: ChannelBody, detail: { summary: "Create notification channel", tags: ["notifications"] } })
|
||||
|
||||
.patch("/:id", async ({ accountId, params, body, set }) => {
|
||||
if (body.kind != null) {
|
||||
const kindErr = validateKind(body.kind);
|
||||
if (kindErr) { set.status = 400; return { error: kindErr }; }
|
||||
}
|
||||
if (body.config != null) {
|
||||
const kind = body.kind ?? (await sql`SELECT kind FROM notification_channels WHERE id = ${params.id} AND account_id = ${accountId}`)[0]?.kind;
|
||||
if (kind) {
|
||||
const cfgErr = validateConfig(kind, body.config);
|
||||
if (cfgErr) { set.status = 400; return { error: cfgErr }; }
|
||||
}
|
||||
}
|
||||
|
||||
const [row] = await sql`
|
||||
UPDATE notification_channels SET
|
||||
name = COALESCE(${body.name ?? null}, name),
|
||||
kind = COALESCE(${body.kind ?? null}, kind),
|
||||
config = COALESCE(${body.config != null ? sql.json(body.config) : null}, config),
|
||||
enabled = COALESCE(${body.enabled ?? null}, enabled)
|
||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
RETURNING id, name, kind, config, enabled, created_at
|
||||
`;
|
||||
if (!row) { set.status = 404; return { error: "Not found" }; }
|
||||
return row;
|
||||
}, { body: t.Partial(ChannelBody), detail: { summary: "Update notification channel", tags: ["notifications"] } })
|
||||
|
||||
.delete("/:id", async ({ accountId, params, set }) => {
|
||||
const [row] = await sql`
|
||||
DELETE FROM notification_channels
|
||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
RETURNING id
|
||||
`;
|
||||
if (!row) { set.status = 404; return { error: "Not found" }; }
|
||||
return { deleted: true };
|
||||
}, { detail: { summary: "Delete notification channel", tags: ["notifications"] } })
|
||||
|
||||
.post("/:id/test", async ({ accountId, params, set }) => {
|
||||
const [row] = await sql<ChannelRow[]>`
|
||||
SELECT id, account_id, name, kind, config, enabled
|
||||
FROM notification_channels
|
||||
WHERE id = ${params.id} AND account_id = ${accountId}
|
||||
`;
|
||||
if (!row) { set.status = 404; return { error: "Not found" }; }
|
||||
try {
|
||||
await dispatch(row, {
|
||||
kind: "test",
|
||||
monitor: { id: "test", name: "Test event", url: "https://example.com", region: "" },
|
||||
});
|
||||
return { ok: true };
|
||||
} catch (e: any) {
|
||||
set.status = 502;
|
||||
return { error: e?.message || String(e) };
|
||||
}
|
||||
}, { detail: { summary: "Send a test event to a channel", tags: ["notifications"] } });
|
||||
Reference in New Issue
Block a user