-
Notifications
You must be signed in to change notification settings - Fork 158
feat: Implement Alarms System backend (#267) #357
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5d12bf5
416f602
3e909f3
9bba76c
0d5662a
a6ec718
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { alarms } from "@databuddy/db/schema"; | ||
| import { eq, and } from "drizzle-orm"; | ||
|
|
||
| export const sendNotification = async (db: any, websiteId: string, status: string) => { | ||
| const siteAlarms = await db | ||
| .select() | ||
| .from(alarms) | ||
| .where( | ||
| and( | ||
| eq(alarms.websiteId, websiteId), | ||
| // Условие: проверяем статус внутри JSON triggerConditions | ||
| eq(alarms.triggerConditions, { status: status }) | ||
| ) | ||
| ); | ||
|
|
||
| for (const alarm of siteAlarms) { | ||
| // В консоль для логов сервера | ||
| console.log(`[Alarm] Site ${websiteId} is ${status}. Notifying: ${alarm.channels.join(", ")}`); | ||
|
|
||
| // Интеграция с провайдерами (Slack/Discord) будет добавлена в следующих итерациях | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1116,3 +1116,12 @@ export type Feedback = typeof feedback.$inferSelect; | |
| export type FeedbackInsert = typeof feedback.$inferInsert; | ||
| export type FeedbackRedemption = typeof feedbackRedemptions.$inferSelect; | ||
| export type FeedbackRedemptionInsert = typeof feedbackRedemptions.$inferInsert; | ||
| // --- ALARMS SYSTEM --- | ||
| export const alarms = pgTable("alarms", { | ||
| id: text("id").primaryKey(), // ID аларма (обычно генерируется через createId) | ||
| websiteId: text("website_id").notNullable(), // Привязка к сайту | ||
| channels: jsonb("channels").$type<string[]>().notNullable().default([]), // ['slack', 'discord', 'email'] | ||
| triggerConditions: jsonb("trigger_conditions").notNullable(), // Условия срабатывания | ||
| createdAt: timestamp("created_at").defaultNow().notNullable(), | ||
| updatedAt: timestamp("updated_at").defaultNow().notNullable(), | ||
| }); | ||
|
Comment on lines
+1119
to
+1127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Every other table with a
Without the foreign key + cascade, orphaned alarm rows will accumulate. Without the index, listing alarms per website will degrade to a full table scan. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { z } from "zod"; | ||
| import { eq } from "drizzle-orm"; | ||
| import { protectedProcedure, router } from "../trpc"; | ||
| import { alarms } from "@databuddy/db/schema"; | ||
|
Comment on lines
+3
to
+4
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The entire router imports The pattern used by every other router (e.g. import { protectedProcedure, publicProcedure } from "../orpc";And procedures are defined via The entire router needs to be rewritten to use the oRPC API. |
||
| import { createId } from "@paralleldrive/cuid2"; | ||
|
Comment on lines
+1
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
// root.ts
import { alarmsRouter } from "./routers/alarms";
// ...
export const appRouter = {
alarms: alarmsRouter,
// existing entries...
}; |
||
|
|
||
| export const createAlarmSchema = z.object({ | ||
| websiteId: z.string().min(1), | ||
| channels: z.array(z.enum(["slack", "discord", "email"])).min(1), | ||
| triggerConditions: z.object({ | ||
| status: z.enum(["down", "up", "degraded"]), | ||
| durationMinutes: z.number().int().min(1).default(5), | ||
| }), | ||
| }); | ||
|
|
||
| export const updateAlarmSchema = z.object({ | ||
| id: z.string().min(1), | ||
| data: createAlarmSchema.partial(), | ||
| }); | ||
|
|
||
| export const alarmsRouter = router({ | ||
| list: protectedProcedure | ||
| .input(z.object({ websiteId: z.string() })) | ||
| .query(async ({ ctx, input }) => { | ||
| return await ctx.db | ||
| .select() | ||
| .from(alarms) | ||
| .where(eq(alarms.websiteId, input.websiteId)); | ||
| }), | ||
|
|
||
| get: protectedProcedure | ||
| .input(z.object({ id: z.string() })) | ||
| .query(async ({ ctx, input }) => { | ||
| const result = await ctx.db | ||
| .select() | ||
| .from(alarms) | ||
| .where(eq(alarms.id, input.id)) | ||
| .limit(1); | ||
|
|
||
| if (!result.length) throw new Error("Alarm not found"); | ||
| return result[0]; | ||
| }), | ||
|
|
||
| create: protectedProcedure | ||
| .input(createAlarmSchema) | ||
| .mutation(async ({ ctx, input }) => { | ||
| const newAlarm = await ctx.db | ||
| .insert(alarms) | ||
| .values({ | ||
| id: createId(), | ||
| websiteId: input.websiteId, | ||
| channels: input.channels, | ||
| triggerConditions: input.triggerConditions, | ||
| }) | ||
| .returning(); | ||
|
|
||
| return newAlarm[0]; | ||
| }), | ||
|
|
||
| update: protectedProcedure | ||
| .input(updateAlarmSchema) | ||
| .mutation(async ({ ctx, input }) => { | ||
| const updatedAlarm = await ctx.db | ||
| .update(alarms) | ||
| .set({ | ||
| ...input.data, | ||
| updatedAt: new Date(), | ||
| }) | ||
| .where(eq(alarms.id, input.id)) | ||
| .returning(); | ||
|
|
||
| if (!updatedAlarm.length) throw new Error("Alarm not found"); | ||
| return updatedAlarm[0]; | ||
| }), | ||
|
|
||
| delete: protectedProcedure | ||
| .input(z.object({ id: z.string() })) | ||
| .mutation(async ({ ctx, input }) => { | ||
| const deletedAlarm = await ctx.db | ||
| .delete(alarms) | ||
| .where(eq(alarms.id, input.id)) | ||
| .returning(); | ||
|
|
||
| if (!deletedAlarm.length) throw new Error("Alarm not found"); | ||
| return deletedAlarm[0]; | ||
|
Comment on lines
+22
to
+85
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
None of the procedures call Every other router that operates on website-scoped data (annotations, goals, funnels, etc.) gates access through await withWorkspace(context, {
websiteId: input.websiteId,
permissions: ["read"], // or ["update"] for mutations
});Additionally, This needs to be addressed before the router goes live. |
||
| }), | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.notNullable()is not a valid Drizzle ORM method — should be.notNull()Every existing column in this schema that is non-nullable uses
.notNull(). Drizzle ORM's column builder API exposes.notNull(), not.notNullable(). Calling.notNullable()will throw aTypeError: ... is not a functionat startup.