Sites and health checks are many to one

This commit is contained in:
Owen
2026-04-15 15:57:25 -07:00
parent 55595ec042
commit b070570cb6

View File

@@ -15,6 +15,8 @@ import { and, eq, isNull, or } from "drizzle-orm";
import { db } from "@server/db"; import { db } from "@server/db";
import { import {
alertRules, alertRules,
alertSites,
alertHealthChecks,
alertEmailActions, alertEmailActions,
alertEmailRecipients, alertEmailRecipients,
alertWebhookActions, alertWebhookActions,
@@ -33,7 +35,9 @@ import { sendAlertEmail } from "./sendAlertEmail";
* *
* Given an `AlertContext`, this function: * Given an `AlertContext`, this function:
* 1. Finds all enabled `alertRules` whose `eventType` matches and whose * 1. Finds all enabled `alertRules` whose `eventType` matches and whose
* `siteId` / `healthCheckId` matches (or is null, meaning "all"). * `siteId` / `healthCheckId` is listed in the `alertSites` /
* `alertHealthChecks` junction tables (or has no junction entries,
* meaning "match all").
* 2. Applies per-rule cooldown gating. * 2. Applies per-rule cooldown gating.
* 3. Dispatches emails and webhook POSTs for every attached action. * 3. Dispatches emails and webhook POSTs for every attached action.
* 4. Updates `lastTriggeredAt` and `lastSentAt` timestamps. * 4. Updates `lastTriggeredAt` and `lastSentAt` timestamps.
@@ -44,34 +48,58 @@ export async function processAlerts(context: AlertContext): Promise<void> {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// 1. Find matching alert rules // 1. Find matching alert rules
// ------------------------------------------------------------------ // ------------------------------------------------------------------
const siteCondition = // Rules with no junction-table entries match ALL sites / health checks.
context.siteId != null // Rules with junction entries match only those specific IDs.
? or( // We implement this with a LEFT JOIN: a NULL join result means the rule
eq(alertRules.siteId, context.siteId), // has no scope restrictions (match all); a non-NULL result that satisfies
isNull(alertRules.siteId) // the id equality filter means an explicit match.
) const baseConditions = and(
: isNull(alertRules.siteId); eq(alertRules.orgId, context.orgId),
eq(alertRules.eventType, context.eventType),
eq(alertRules.enabled, true)
);
const healthCheckCondition = let rules: (typeof alertRules.$inferSelect)[];
context.healthCheckId != null
? or(
eq(alertRules.healthCheckId, context.healthCheckId),
isNull(alertRules.healthCheckId)
)
: isNull(alertRules.healthCheckId);
const rules = await db if (context.siteId != null) {
.select() const rows = await db
.from(alertRules) .select()
.where( .from(alertRules)
and( .leftJoin(
eq(alertRules.orgId, context.orgId), alertSites,
eq(alertRules.eventType, context.eventType), eq(alertSites.alertRuleId, alertRules.alertRuleId)
eq(alertRules.enabled, true),
// Apply the right scope filter based on event type
context.siteId != null ? siteCondition : healthCheckCondition
) )
); .where(
and(
baseConditions,
or(
eq(alertSites.siteId, context.siteId),
isNull(alertSites.alertRuleId)
)
)
);
rules = rows.map((r) => r.alertRules);
} else if (context.healthCheckId != null) {
const rows = await db
.select()
.from(alertRules)
.leftJoin(
alertHealthChecks,
eq(alertHealthChecks.alertRuleId, alertRules.alertRuleId)
)
.where(
and(
baseConditions,
or(
eq(alertHealthChecks.healthCheckId, context.healthCheckId),
isNull(alertHealthChecks.alertRuleId)
)
)
);
rules = rows.map((r) => r.alertRules);
} else {
rules = [];
}
if (rules.length === 0) { if (rules.length === 0) {
logger.debug( logger.debug(