Working on implementation of alert policies.

This commit is contained in:
charlesgauthereau
2025-11-23 17:13:07 +01:00
parent eb471c7cdb
commit d968aa9c16
20 changed files with 2598 additions and 56 deletions
+5 -1
View File
@@ -10,6 +10,8 @@ import * as agent from "./schema/08_agent";
import * as database from "./schema/07_database";
import * as notificationChannel from "./schema/09_notification-channel";
import * as organizationNotificationChannel from "./schema/09_notification-channel";
import * as alertPolicy from "./schema/10_alert-policy";
import * as notificationLog from "./schema/11_notification-log";
import {Pool} from "pg";
@@ -36,7 +38,9 @@ export const schemas = {
...agent,
...database,
...notificationChannel,
...organizationNotificationChannel
...organizationNotificationChannel,
...alertPolicy,
...notificationLog
};
export const db = drizzle({
@@ -0,0 +1,36 @@
CREATE TYPE "public"."event_kind" AS ENUM('error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report');--> statement-breakpoint
CREATE TYPE "public"."level" AS ENUM('critical', 'warning', 'info');--> statement-breakpoint
CREATE TABLE "alert_policy" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"notification_channel_id" uuid NOT NULL,
"event_kind" "event_kind"[] NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"database_id" uuid NOT NULL,
"updated_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp
);
--> statement-breakpoint
CREATE TABLE "notification_log" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"channel_id" uuid NOT NULL,
"policy_id" uuid,
"organization_id" uuid,
"title" varchar(255) NOT NULL,
"message" text NOT NULL,
"level" "level" NOT NULL,
"payload" jsonb,
"success" boolean NOT NULL,
"error" text,
"provider_response" jsonb,
"sent_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"deleted_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "alert_policy" ADD CONSTRAINT "alert_policy_notification_channel_id_notification_channel_id_fk" FOREIGN KEY ("notification_channel_id") REFERENCES "public"."notification_channel"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "alert_policy" ADD CONSTRAINT "alert_policy_database_id_databases_id_fk" FOREIGN KEY ("database_id") REFERENCES "public"."databases"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification_log" ADD CONSTRAINT "notification_log_channel_id_notification_channel_id_fk" FOREIGN KEY ("channel_id") REFERENCES "public"."notification_channel"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification_log" ADD CONSTRAINT "notification_log_policy_id_alert_policy_id_fk" FOREIGN KEY ("policy_id") REFERENCES "public"."alert_policy"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notification_log" ADD CONSTRAINT "notification_log_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -57,6 +57,13 @@
"when": 1762685571404,
"tag": "0007_last_umar",
"breakpoints": true
},
{
"idx": 8,
"version": "7",
"when": 1763914207236,
"tag": "0008_aberrant_scorpion",
"breakpoints": true
}
]
}
+19 -9
View File
@@ -1,13 +1,16 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { project } from "./06_project";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
import {pgTable, text, uuid} from "drizzle-orm/pg-core";
import {relations} from "drizzle-orm";
import {Project, project} from "./06_project";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {invitation, OrganizationInvitation} from "@/db/schema/05_invitation";
import {member, OrganizationMember} from "@/db/schema/04_member";
import {User} from "@/db/schema/02_user";
import {timestamps} from "@/db/schema/00_common";
import {organizationNotificationChannel} from "@/db/schema/09_notification-channel";
import {NotificationChannel, organizationNotificationChannel} from "@/db/schema/09_notification-channel";
import {Agent} from "@/db/schema/08_agent";
import {AlertPolicy} from "@/db/schema/10_alert-policy";
import {Backup, Database, Restoration, RetentionPolicy} from "@/db/schema/07_database";
export const organization = pgTable("organization", {
id: uuid("id").defaultRandom().primaryKey(),
@@ -19,7 +22,7 @@ export const organization = pgTable("organization", {
});
export const organizationRelations = relations(organization, ({ many }) => ({
export const organizationRelations = relations(organization, ({many}) => ({
members: many(member),
invitations: many(invitation),
projects: many(project),
@@ -27,7 +30,6 @@ export const organizationRelations = relations(organization, ({ many }) => ({
}));
export const organizationSchema = createSelectSchema(organization);
export type Organization = z.infer<typeof organizationSchema>;
@@ -42,4 +44,12 @@ export type MemberWithUser = OrganizationMember & {
export type OrganizationWithMembersAndUsers = Organization & {
members: MemberWithUser[];
invitations: OrganizationInvitation[];
};
};
export type OrganizationWith = Organization & {
user?: User | null;
members?: MemberWithUser[] | null;
invitations?: OrganizationInvitation[] | null;
notificationChannels?: NotificationChannel[] | null;
projects?: Project[] | null;
};
+8 -1
View File
@@ -6,6 +6,11 @@ import {dbmsEnum, statusEnum} from "./types";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
import {timestamps} from "@/db/schema/00_common";
import {member} from "@/db/schema/04_member";
import {invitation} from "@/db/schema/05_invitation";
import {organizationNotificationChannel} from "@/db/schema/09_notification-channel";
import {organization} from "@/db/schema/03_organization";
import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy";
export const database = pgTable("databases", {
id: uuid("id").primaryKey().defaultRandom(),
@@ -20,13 +25,13 @@ export const database = pgTable("databases", {
.notNull()
.references(() => agent.id, {onDelete: "cascade"}),
lastContact: timestamp("last_contact"),
projectId: uuid("project_id")
.references(() => project.id),
...timestamps
});
export const backup = pgTable(
"backups",
{
@@ -77,6 +82,7 @@ export const databaseRelations = relations(database, ({one, many}) => ({
project: one(project, {fields: [database.projectId], references: [project.id]}),
backups: many(backup),
restorations: many(restoration),
alertPolicies: many(alertPolicy),
}));
export const backupRelations = relations(backup, ({one, many}) => ({
@@ -116,5 +122,6 @@ export type DatabaseWith = Database & {
backups?: Backup[] | null;
restorations?: Restoration[] | null;
retentionPolicy?: RetentionPolicy | null;
alertPolicies?: AlertPolicy[] | null;
};
+41
View File
@@ -0,0 +1,41 @@
import {boolean, pgEnum, pgTable, uuid} from "drizzle-orm/pg-core";
import {notificationChannel} from "@/db/schema/09_notification-channel";
import {timestamps} from "@/db/schema/00_common";
import {relations} from "drizzle-orm";
import {database, retentionPolicy} from "@/db/schema/07_database";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
export const eventKindEnum = pgEnum('event_kind', ['error_backup', 'error_restore', 'success_restore', 'success_backup', 'weekly_report']);
export const alertPolicy = pgTable('alert_policy', {
id: uuid('id').defaultRandom().primaryKey(),
notificationChannelId: uuid('notification_channel_id')
.notNull()
.references(() => notificationChannel.id, {onDelete: 'restrict'}),
eventKinds: eventKindEnum("event_kind").array().notNull(),
enabled: boolean('enabled').default(true).notNull(),
databaseId: uuid('database_id')
.notNull()
.references(() => database.id, { onDelete: 'cascade' }),
...timestamps
});
export const alertPolicyRelations = relations(alertPolicy, ({one}) => ({
notificationChannel: one(notificationChannel, {
fields: [alertPolicy.notificationChannelId],
references: [notificationChannel.id],
}),
database: one(database, {
fields: [alertPolicy.databaseId],
references: [database.id],
}),
}));
export const notificationChannelsToAlertPoliciesRelations = relations(notificationChannel, ({many}) => ({
alertPolicies: many(alertPolicy),
}));
export const alertPolicySchema = createSelectSchema(alertPolicy);
export type AlertPolicy = z.infer<typeof alertPolicySchema>;
+32
View File
@@ -0,0 +1,32 @@
import {pgTable, uuid, timestamp, jsonb, varchar, boolean, text, pgEnum} from 'drizzle-orm/pg-core';
import {notificationChannel, providerKindEnum} from "@/db/schema/09_notification-channel";
import {alertPolicy} from "@/db/schema/10_alert-policy";
import {organization} from "@/db/schema/03_organization";
import {timestamps} from "@/db/schema/00_common";
export const levelEnum = pgEnum('level', ['critical', 'warning', 'info']);
export const notificationLog = pgTable('notification_log', {
id: uuid('id').defaultRandom().primaryKey(),
channelId: uuid('channel_id')
.notNull()
.references(() => notificationChannel.id, { onDelete: 'restrict' }),
policyId: uuid('policy_id')
.references(() => alertPolicy.id, { onDelete: 'restrict' }),
organizationId: uuid('organization_id')
.references(() => organization.id, { onDelete: 'set null' }),
title: varchar('title', { length: 255 }).notNull(),
message: text('message').notNull(),
level: levelEnum('level').notNull(),
payload: jsonb('payload'),
success: boolean('success').notNull(),
error: text('error'),
providerResponse: jsonb('provider_response'),
sentAt: timestamp('sent_at').defaultNow().notNull(),
...timestamps
});
+55
View File
@@ -0,0 +1,55 @@
import {desc, eq, and, gte, lte} from 'drizzle-orm';
import {
notificationLog
} from "@/db/schema/11_notification-log";
import {
notificationChannel,
} from "@/db/schema/09_notification-channel";
import {db} from "@/db";
import {alertPolicy} from "@/db/schema/10_alert-policy";
export async function getNotificationHistory(filters?: {
channelId?: string;
policyId?: string;
organizationId?: string;
level?: string;
success?: boolean;
from?: Date;
to?: Date;
limit?: number;
}) {
const where = [];
if (filters?.channelId) where.push(eq(notificationLog.channelId, filters.channelId));
if (filters?.policyId) where.push(eq(notificationLog.policyId, filters.policyId));
if (filters?.organizationId) where.push(eq(notificationLog.organizationId, filters.organizationId));
if (filters?.level) where.push(eq(notificationLog.level, filters.level));
if (typeof filters?.success === 'boolean') where.push(eq(notificationLog.success, filters.success));
if (filters?.from) where.push(gte(notificationLog.sentAt, filters.from));
if (filters?.to) where.push(lte(notificationLog.sentAt, filters.to));
return await db
.select({
id: notificationLog.id,
title: notificationLog.title,
level: notificationLog.level,
success: notificationLog.success,
error: notificationLog.error,
sentAt: notificationLog.sentAt,
channel: {
id: notificationLog.id,
name: notificationChannel.name,
provider: notificationChannel.provider,
},
policy: {
id: alertPolicy.id,
eventKind: alertPolicy.eventKind,
},
})
.from(notificationLog)
.leftJoin(notificationChannel, eq(notificationLog.channelId, notificationChannel.id))
.leftJoin(alertPolicy, eq(notificationLog.policyId, alertPolicy.id))
.where(and(...where))
.orderBy(desc(notificationLog.sentAt))
.limit(filters?.limit || 100);
}