feat: Implement Alarms System backend (#267)#357
feat: Implement Alarms System backend (#267)#357K09-0 wants to merge 6 commits intodatabuddy-analytics:mainfrom
Conversation
Add alarms table schema
Implement alarms router with CRUD operations (list, get, create, update, delete)
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@K09-0 is attempting to deploy a commit to the Databuddy OSS Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR introduces the backend for an Alarms System — a new Key issues found:
Confidence Score: 0/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant appRouter
participant alarmsRouter
participant withWorkspace
participant DB
Note over appRouter,alarmsRouter: ❌ alarmsRouter not registered in root.ts
Client->>appRouter: alarms.list({ websiteId })
appRouter-->>Client: ❌ Endpoint not found
Note over alarmsRouter: If router were registered & fixed:
Client->>alarmsRouter: alarms.list({ websiteId })
alarmsRouter->>withWorkspace: verify user has read access to websiteId
Note over withWorkspace: ❌ Currently missing — no auth check
withWorkspace-->>alarmsRouter: workspace context
alarmsRouter->>DB: SELECT * FROM alarms WHERE website_id = ?
DB-->>alarmsRouter: alarm rows
alarmsRouter-->>Client: Alarm[]
Client->>alarmsRouter: alarms.create({ websiteId, channels, triggerConditions })
alarmsRouter->>withWorkspace: verify user has update access
Note over withWorkspace: ❌ Currently missing
withWorkspace-->>alarmsRouter: workspace context
alarmsRouter->>DB: INSERT INTO alarms VALUES (...)
DB-->>alarmsRouter: new alarm
alarmsRouter-->>Client: Alarm
Reviews (1): Last reviewed commit: "Refactor alarms router and validation sc..." | Re-trigger Greptile |
| import { protectedProcedure, router } from "../trpc"; | ||
| import { alarms } from "@databuddy/db/schema"; |
There was a problem hiding this comment.
Import from non-existent tRPC module
The entire router imports protectedProcedure and router from "../trpc", but no trpc.ts file exists in this package. The project has migrated to oRPC (@orpc/server). The correct module is "../orpc", and it does not export a router helper — routers are plain objects in this codebase. This will cause a build-time module-not-found error.
The pattern used by every other router (e.g. annotations.ts) is:
import { protectedProcedure, publicProcedure } from "../orpc";And procedures are defined via .route({...}).input(...).output(...).handler(async ({ context, input }) => { ... }) instead of .query()/.mutation() with { ctx, input }.
The entire router needs to be rewritten to use the oRPC API.
| 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]; |
There was a problem hiding this comment.
Missing workspace authorization — any authenticated user can access any website's alarms
None of the procedures call withWorkspace() to verify that the authenticated user actually has access to the target website. This means any user with a valid session can list, create, update, or delete alarms for any website in the system, including websites they don't own.
Every other router that operates on website-scoped data (annotations, goals, funnels, etc.) gates access through withWorkspace, for example:
await withWorkspace(context, {
websiteId: input.websiteId,
permissions: ["read"], // or ["update"] for mutations
});Additionally, get, update, and delete accept an alarm id without first loading the associated websiteId and checking access against it — an attacker can enumerate and manipulate alarms across all websites.
This needs to be addressed before the router goes live.
| // --- 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(), | ||
| }); |
There was a problem hiding this comment.
.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 a TypeError: ... is not a function at startup.
| // --- 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(), | |
| }); | |
| export const alarms = pgTable("alarms", { | |
| id: text("id").primaryKey().notNull(), | |
| websiteId: text("website_id").notNull(), | |
| channels: jsonb("channels").$type<string[]>().notNull().default([]), | |
| triggerConditions: jsonb("trigger_conditions").$type<{ status: string; durationMinutes: number }>().notNull(), | |
| createdAt: timestamp("created_at").defaultNow().notNull(), | |
| updatedAt: timestamp("updated_at").defaultNow().notNull(), | |
| }); |
| // --- 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(), | ||
| }); |
There was a problem hiding this comment.
Missing
website_id index, foreign key constraint, and type exports
Every other table with a website_id foreign key in this schema has:
- A
btreeindex onwebsite_idfor query performance (e.g.index("alarms_website_id_idx").using("btree", table.websiteId.asc().nullsLast().op("text_ops"))). - A
foreignKeyconstraint referencingwebsites.idwith.onDelete("cascade")so alarms are cleaned up when a website is deleted. - Corresponding type exports at the bottom of the file (
export type Alarm = typeof alarms.$inferSelect; export type AlarmInsert = typeof alarms.$inferInsert;).
Without the foreign key + cascade, orphaned alarm rows will accumulate. Without the index, listing alarms per website will degrade to a full table scan.
| import { z } from "zod"; | ||
| import { eq } from "drizzle-orm"; | ||
| import { protectedProcedure, router } from "../trpc"; | ||
| import { alarms } from "@databuddy/db/schema"; | ||
| import { createId } from "@paralleldrive/cuid2"; |
There was a problem hiding this comment.
alarmsRouter is not registered in root.ts
packages/rpc/src/root.ts defines appRouter by composing all sub-routers, but alarmsRouter is never imported or added to that object. Even after the other issues are fixed, these endpoints will be completely unreachable through the API.
alarmsRouter needs to be imported and added:
// root.ts
import { alarmsRouter } from "./routers/alarms";
// ...
export const appRouter = {
alarms: alarmsRouter,
// existing entries...
};|
"I've implemented the basic schema and router. Since I'm working in a web editor, I couldn't run the migrations locally. Please let me know if you need me to adjust the types or add anything else to the implementation." |
Implement notification trigger logic
Implement notification trigger logic
|
Fixed the import path for notifications. The logic is now correctly linked to the uptime service. Ready for review. |
/claim #267
/claim #268
Description
Implemented the core backend for the Alarms System as specified in the bounty.
Integrated uptime service with the new alarms logic to trigger notifications.
Changes
alarmstable to the Drizzle schema with fields forwebsite_id,channels, andtrigger_conditions.alarmsrouter with full CRUD functionality (list, get, create, update, delete).Checklist