Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
6.4 kB ยท 209 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210import { eq } from "@openstatus/db";import { monitor, monitorTagsToMonitors, notificationsToMonitors, privateLocationToMonitors,} from "@openstatus/db/src/schema";
import { emitAudit } from "../audit";import { requireScope } from "../auth";import { type ServiceContext, withTransaction } from "../context";import { LimitExceededError } from "../errors";import { getMonitorInWorkspace, validateNotificationIds, validatePrivateLocationIds, validateTagIds,} from "./internal";import { UpdateMonitorNotifiersInput, UpdateMonitorSchedulingRegionsInput, UpdateMonitorTagsInput,} from "./schemas";
/** * Update the monitor's `regions` / `periodicity` + private-location set in * a single transaction. Enforces plan limits on periodicity, region count, * and individual region access. */export async function updateMonitorSchedulingRegions(args: { ctx: ServiceContext; input: UpdateMonitorSchedulingRegionsInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorSchedulingRegionsInput.parse(args.input); const limits = ctx.workspace.limits;
if (!limits.periodicity.includes(input.periodicity)) { throw new LimitExceededError("periodicity", limits.periodicity.length); } if (limits["max-regions"] < input.regions.length) { throw new LimitExceededError("max-regions", limits["max-regions"]); } if ( input.regions.length > 0 && !input.regions.every((r) => limits.regions.includes(r as (typeof limits)["regions"][number]), ) ) { throw new LimitExceededError("regions", limits.regions.length); }
await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, });
const validatedLocations = await validatePrivateLocationIds({ tx, workspaceId: ctx.workspace.id, privateLocationIds: input.privateLocations, });
const updated = await tx .update(monitor) .set({ regions: input.regions.join(","), periodicity: input.periodicity, updatedAt: new Date(), }) .where(eq(monitor.id, existing.id)) .returning() .get();
await tx .delete(privateLocationToMonitors) .where(eq(privateLocationToMonitors.monitorId, existing.id)); if (validatedLocations.length > 0) { await tx.insert(privateLocationToMonitors).values( validatedLocations.map((privateLocationId) => ({ monitorId: existing.id, privateLocationId, })), ); }
// `before`/`after` carry the `regions` + `periodicity` diff. // Private-location membership lives in a join table and isn't // reflected in the snapshot โ documented gap, not a bug. await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: existing, after: updated, }); });}
/** Replace the full monitor-tag association set. */export async function updateMonitorTags(args: { ctx: ServiceContext; input: UpdateMonitorTagsInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorTagsInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const validatedTags = await validateTagIds({ tx, workspaceId: ctx.workspace.id, tagIds: input.tags, });
// Capture the pre-mutation tag set *before* the delete โ tag // membership lives on the join table so the "before" snapshot must // be read while the old rows still exist. const existingTagIds = ( await tx .select({ tagId: monitorTagsToMonitors.monitorTagId }) .from(monitorTagsToMonitors) .where(eq(monitorTagsToMonitors.monitorId, existing.id)) .all() ).map((r) => r.tagId);
await tx .delete(monitorTagsToMonitors) .where(eq(monitorTagsToMonitors.monitorId, existing.id)); if (validatedTags.length > 0) { await tx.insert(monitorTagsToMonitors).values( validatedTags.map((tagId) => ({ monitorId: existing.id, monitorTagId: tagId, })), ); }
await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: { tagIds: [...existingTagIds].sort() }, after: { tagIds: [...validatedTags].sort() }, }); });}
/** Replace the full monitor-to-notification association set. */export async function updateMonitorNotifiers(args: { ctx: ServiceContext; input: UpdateMonitorNotifiersInput;}): Promise<void> { const { ctx } = args; requireScope(ctx, "write"); const input = UpdateMonitorNotifiersInput.parse(args.input);
await withTransaction(ctx, async (tx) => { const existing = await getMonitorInWorkspace({ tx, id: input.id, workspaceId: ctx.workspace.id, }); const validatedNotifiers = await validateNotificationIds({ tx, workspaceId: ctx.workspace.id, notificationIds: input.notifiers, });
// Capture the pre-mutation notifier set *before* the delete โ the // relation lives on the join table so reading it after we'd wipe // the old rows would record the new state as the "before". const existingNotifierIds = ( await tx .select({ notificationId: notificationsToMonitors.notificationId }) .from(notificationsToMonitors) .where(eq(notificationsToMonitors.monitorId, existing.id)) .all() ).map((r) => r.notificationId);
await tx .delete(notificationsToMonitors) .where(eq(notificationsToMonitors.monitorId, existing.id)); if (validatedNotifiers.length > 0) { await tx.insert(notificationsToMonitors).values( validatedNotifiers.map((notificationId) => ({ monitorId: existing.id, notificationId, })), ); }
await emitAudit(tx, ctx, { action: "monitor.update", entityType: "monitor", entityId: existing.id, before: { notificationIds: [...existingNotifierIds].sort() }, after: { notificationIds: [...validatedNotifiers].sort() }, }); });}