Openstatus www.openstatus.dev

chore: clean up legacy trpc calls (#1787)

* chore: clean up legacy trpc calls

* fix: test and imports

authored by

Maximilian Kaske and committed by
GitHub
41600599 cabde919

+60 -2206
+1 -144
packages/api/src/router/incident.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { 4 - type SQL, 5 - and, 6 - asc, 7 - desc, 8 - eq, 9 - gte, 10 - isNull, 11 - schema, 12 - } from "@openstatus/db"; 3 + import { type SQL, and, asc, desc, eq, gte, schema } from "@openstatus/db"; 13 4 import { 14 5 incidentTable, 15 6 selectIncidentSchema, ··· 22 13 import { getPeriodDate, periods } from "./utils"; 23 14 24 15 export const incidentRouter = createTRPCRouter({ 25 - // TODO: rename getIncidentsByWorkspace to make it consistent with the other methods 26 - getIncidentsByWorkspace: protectedProcedure 27 - .output(z.array(selectIncidentSchema)) 28 - .query(async (opts) => { 29 - const result = await opts.ctx.db 30 - .select() 31 - .from(schema.incidentTable) 32 - .where(eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id)) 33 - .leftJoin( 34 - schema.monitor, 35 - eq(schema.incidentTable.monitorId, schema.monitor.id), 36 - ) 37 - .all(); 38 - return z 39 - .array(selectIncidentSchema) 40 - .parse( 41 - result.map((r) => ({ ...r.incident, monitorName: r.monitor?.name })), 42 - ); 43 - }), 44 - 45 - getIncidentById: protectedProcedure 46 - .input(z.object({ id: z.number() })) 47 - .output(selectIncidentSchema) 48 - .query(async (opts) => { 49 - const result = await opts.ctx.db 50 - .select() 51 - .from(schema.incidentTable) 52 - .where( 53 - and( 54 - eq(schema.incidentTable.id, opts.input.id), 55 - eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), 56 - ), 57 - ) 58 - .leftJoin( 59 - schema.monitor, 60 - eq(schema.incidentTable.monitorId, schema.monitor.id), 61 - ) 62 - .get(); 63 - 64 - return selectIncidentSchema.parse({ 65 - ...result?.incident, 66 - monitorName: result?.monitor?.name, 67 - }); 68 - }), 69 - 70 - getOpenIncidents: protectedProcedure.query(async (opts) => { 71 - return await opts.ctx.db 72 - .select() 73 - .from(schema.incidentTable) 74 - .where( 75 - and( 76 - eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), 77 - isNull(schema.incidentTable.resolvedAt), 78 - ), 79 - ) 80 - .all(); 81 - }), 82 - 83 - acknowledgeIncident: protectedProcedure 84 - .meta({ track: Events.AcknowledgeIncident }) 85 - .input(z.object({ id: z.number() })) 86 - .mutation(async (opts) => { 87 - const currentIncident = await opts.ctx.db 88 - .select() 89 - .from(schema.incidentTable) 90 - .where( 91 - and( 92 - eq(schema.incidentTable.id, opts.input.id), 93 - eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), 94 - ), 95 - ) 96 - .get(); 97 - if (!currentIncident) { 98 - throw new Error("Incident not found"); 99 - } 100 - if (currentIncident.acknowledgedAt) { 101 - throw new Error("Incident already acknowledged"); 102 - } 103 - await opts.ctx.db 104 - .update(schema.incidentTable) 105 - .set({ 106 - acknowledgedAt: new Date(), 107 - acknowledgedBy: opts.ctx.user.id, 108 - updatedAt: new Date(), 109 - }) 110 - .where( 111 - and( 112 - eq(schema.incidentTable.id, opts.input.id), 113 - eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), 114 - ), 115 - ); 116 - return true; 117 - }), 118 - resolvedIncident: protectedProcedure 119 - .meta({ track: Events.ResolveIncident }) 120 - .input(z.object({ id: z.number() })) 121 - .mutation(async (opts) => { 122 - const currentIncident = await opts.ctx.db 123 - .select() 124 - .from(schema.incidentTable) 125 - .where( 126 - and( 127 - eq(schema.incidentTable.id, opts.input.id), 128 - eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), 129 - ), 130 - ) 131 - .get(); 132 - if (!currentIncident) { 133 - throw new Error("Incident not found"); 134 - } 135 - if (!currentIncident.acknowledgedAt) { 136 - throw new Error("Incident not acknowledged"); 137 - } 138 - if (currentIncident.resolvedAt) { 139 - throw new Error("Incident already resolved"); 140 - } 141 - await opts.ctx.db 142 - .update(schema.incidentTable) 143 - .set({ 144 - resolvedAt: new Date(), 145 - resolvedBy: opts.ctx.user.id, 146 - updatedAt: new Date(), 147 - }) 148 - .where( 149 - and( 150 - eq(schema.incidentTable.id, opts.input.id), 151 - eq(schema.incidentTable.workspaceId, opts.ctx.workspace.id), 152 - ), 153 - ); 154 - return true; 155 - }), 156 - 157 16 delete: protectedProcedure 158 17 .meta({ track: Events.DeleteIncident }) 159 18 .input(z.object({ id: z.number() })) ··· 175 34 .where(eq(schema.incidentTable.id, incidentToDelete.id)) 176 35 .run(); 177 36 }), 178 - 179 - // DASHBOARD 180 37 181 38 list: protectedProcedure 182 39 .input(
+1 -90
packages/api/src/router/invitation.ts
··· 8 8 invitation, 9 9 selectInvitationSchema, 10 10 selectWorkspaceSchema, 11 - user, 12 11 usersToWorkspaces, 13 12 workspace, 14 13 } from "@openstatus/db/src/schema"; 15 14 16 - import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 15 + import { createTRPCRouter, protectedProcedure } from "../trpc"; 17 16 18 17 export const invitationRouter = createTRPCRouter({ 19 18 create: protectedProcedure ··· 83 82 ) 84 83 .run(); 85 84 }), 86 - 87 - getWorkspaceOpenInvitations: protectedProcedure.query(async (opts) => { 88 - const _invitations = await opts.ctx.db.query.invitation.findMany({ 89 - where: and( 90 - eq(invitation.workspaceId, opts.ctx.workspace.id), 91 - gte(invitation.expiresAt, new Date()), 92 - isNull(invitation.acceptedAt), 93 - ), 94 - }); 95 - return _invitations; 96 - }), 97 - 98 - getInvitationByToken: protectedProcedure 99 - .input(z.object({ token: z.string() })) 100 - .query(async (opts) => { 101 - const _invitation = await opts.ctx.db.query.invitation.findFirst({ 102 - where: and(eq(invitation.token, opts.input.token)), 103 - with: { 104 - workspace: true, 105 - }, 106 - }); 107 - return _invitation; 108 - }), 109 - 110 - /** 111 - * REMINDER: we are not using a protected procedure here of the `/invite` url 112 - * instead of `/app/workspace-slug/invite` as the user is not allowed to it yet. 113 - * We validate the auth token in the `acceptInvitation` procedure 114 - */ 115 - acceptInvitation: publicProcedure 116 - .input(z.object({ token: z.string() })) 117 - .meta({ track: Events.AcceptInvite }) 118 - .output( 119 - z.object({ 120 - message: z.string(), 121 - data: selectWorkspaceSchema.optional(), 122 - }), 123 - ) 124 - .mutation(async (opts) => { 125 - const _invitation = await opts.ctx.db.query.invitation.findFirst({ 126 - where: and( 127 - eq(invitation.token, opts.input.token), 128 - isNull(invitation.acceptedAt), 129 - ), 130 - with: { 131 - workspace: true, 132 - }, 133 - }); 134 - 135 - if (!opts.ctx.session?.user?.id) return { message: "Missing user." }; 136 - 137 - const _user = await opts.ctx.db.query.user.findFirst({ 138 - where: eq(user.id, Number(opts.ctx.session.user.id)), 139 - }); 140 - 141 - if (!_user) return { message: "Invalid user." }; 142 - 143 - if (!_invitation) return { message: "Invalid invitation token." }; 144 - 145 - if (_invitation.email !== _user.email) 146 - return { message: "You are not invited to this workspace." }; 147 - 148 - if (_invitation.expiresAt.getTime() < new Date().getTime()) { 149 - return { message: "Invitation expired." }; 150 - } 151 - 152 - await opts.ctx.db 153 - .update(invitation) 154 - .set({ acceptedAt: new Date() }) 155 - .where(eq(invitation.id, _invitation.id)) 156 - .run(); 157 - 158 - await opts.ctx.db 159 - .insert(usersToWorkspaces) 160 - .values({ 161 - userId: _user.id, 162 - workspaceId: _invitation.workspaceId, 163 - role: _invitation.role, 164 - }) 165 - .run(); 166 - 167 - return { 168 - message: "Invitation accepted.", 169 - data: _invitation.workspace, 170 - }; 171 - }), 172 - 173 - // DASHBOARD 174 85 175 86 list: protectedProcedure.query(async (opts) => { 176 87 const whereConditions: SQL[] = [
-125
packages/api/src/router/maintenance.test.ts
··· 1 - import { expect, test } from "bun:test"; 2 - import { appRouter } from "../root"; 3 - import { createInnerTRPCContext } from "../trpc"; 4 - 5 - function getTestContext() { 6 - return createInnerTRPCContext({ 7 - req: undefined, 8 - session: { 9 - user: { 10 - // @ts-expect-error 11 - id: 1, 12 - }, 13 - }, 14 - }); 15 - } 16 - 17 - const from = new Date(); 18 - const to = new Date(from.getTime() + 1000 * 60 * 60); 19 - 20 - test("Create Maintenance", async () => { 21 - const ctx = getTestContext(); 22 - const caller = appRouter.createCaller(ctx); 23 - 24 - const createdMaintenance = await caller.maintenance.new({ 25 - title: "Test Maintenance", 26 - message: "This is a test maintenance.", 27 - startDate: from, 28 - endDate: to, 29 - pageId: 1, 30 - }); 31 - 32 - expect(createdMaintenance).toMatchObject({ 33 - id: expect.any(Number), 34 - title: "Test Maintenance", 35 - message: "This is a test maintenance.", 36 - from, 37 - to, 38 - workspaceId: 1, 39 - }); 40 - }); 41 - 42 - test("Get Maintenance by ID", async () => { 43 - const ctx = getTestContext(); 44 - const caller = appRouter.createCaller(ctx); 45 - 46 - const createdMaintenance = await caller.maintenance.new({ 47 - title: "Test Maintenance", 48 - message: "This is a test maintenance.", 49 - startDate: from, 50 - endDate: to, 51 - pageId: 1, 52 - }); 53 - 54 - const createdMaintenanceId = createdMaintenance.id; 55 - 56 - const fetchedMaintenance = await caller.maintenance.getById({ 57 - id: createdMaintenanceId, 58 - }); 59 - 60 - expect(fetchedMaintenance).toMatchObject({ 61 - id: createdMaintenance.id, 62 - title: "Test Maintenance", 63 - message: "This is a test maintenance.", 64 - from, 65 - to, 66 - workspaceId: 1, 67 - }); 68 - }); 69 - 70 - test("Update Maintenance", async () => { 71 - const ctx = getTestContext(); 72 - const caller = appRouter.createCaller(ctx); 73 - 74 - const createdMaintenance = await caller.maintenance.new({ 75 - title: "Test Maintenance", 76 - message: "This is a test maintenance.", 77 - startDate: from, 78 - endDate: to, 79 - pageId: 1, 80 - }); 81 - 82 - const createdMaintenanceId = createdMaintenance.id; 83 - 84 - await caller.maintenance.update({ 85 - id: createdMaintenanceId, 86 - title: "Updated Test Maintenance", 87 - message: "This is an updated test maintenance.", 88 - startDate: from, 89 - endDate: to, 90 - }); 91 - 92 - const updatedMaintenance = await caller.maintenance.getById({ 93 - id: createdMaintenanceId, 94 - }); 95 - 96 - expect(updatedMaintenance).toMatchObject({ 97 - id: createdMaintenanceId, 98 - title: "Updated Test Maintenance", 99 - message: "This is an updated test maintenance.", 100 - from, 101 - to, 102 - workspaceId: 1, 103 - }); 104 - }); 105 - 106 - test("Delete Maintenance", async () => { 107 - const ctx = getTestContext(); 108 - const caller = appRouter.createCaller(ctx); 109 - 110 - const createdMaintenance = await caller.maintenance.new({ 111 - title: "Test Maintenance", 112 - message: "This is a test maintenance.", 113 - startDate: from, 114 - endDate: to, 115 - pageId: 1, 116 - }); 117 - 118 - const createdMaintenanceId = createdMaintenance.id; 119 - 120 - const deletedMaintenance = await caller.maintenance.delete({ 121 - id: createdMaintenanceId, 122 - }); 123 - 124 - expect(deletedMaintenance).toBeDefined(); 125 - });
-79
packages/api/src/router/maintenance.ts
··· 8 8 eq, 9 9 gte, 10 10 inArray, 11 - lte, 12 11 syncMaintenanceToPageComponentDeleteByMaintenance, 13 12 syncMaintenanceToPageComponentInsertMany, 14 13 } from "@openstatus/db"; 15 14 import { 16 15 maintenance, 17 - maintenancesToMonitors, 18 16 maintenancesToPageComponents, 19 17 pageComponent, 20 18 selectMaintenanceSchema, ··· 27 25 import { getPeriodDate, periods } from "./utils"; 28 26 29 27 export const maintenanceRouter = createTRPCRouter({ 30 - getById: protectedProcedure 31 - .input(z.object({ id: z.number() })) 32 - .query(async (opts) => { 33 - const _maintenance = await opts.ctx.db 34 - .select() 35 - .from(maintenance) 36 - .where( 37 - and( 38 - eq(maintenance.id, opts.input.id), 39 - eq(maintenance.workspaceId, opts.ctx.workspace.id), 40 - ), 41 - ) 42 - .get(); 43 - 44 - if (!_maintenance) return undefined; 45 - // TODO: make it work with `with: { maintenacesToMonitors: true }` 46 - const _monitors = await opts.ctx.db 47 - .select() 48 - .from(maintenancesToMonitors) 49 - .where(eq(maintenancesToMonitors.maintenanceId, _maintenance.id)) 50 - .all(); 51 - return { ..._maintenance, monitors: _monitors.map((m) => m.monitorId) }; 52 - }), 53 - getByWorkspace: protectedProcedure.query(async (opts) => { 54 - const _maintenances = await opts.ctx.db 55 - .select() 56 - .from(maintenance) 57 - .where(eq(maintenance.workspaceId, opts.ctx.workspace.id)) 58 - .all(); 59 - return _maintenances; 60 - }), 61 - getLast7DaysByWorkspace: protectedProcedure.query(async (opts) => { 62 - const _maintenances = await opts.ctx.db.query.maintenance.findMany({ 63 - where: and( 64 - eq(maintenance.workspaceId, opts.ctx.workspace.id), 65 - gte(maintenance.from, new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)), 66 - ), 67 - with: { maintenancesToMonitors: true }, 68 - }); 69 - return _maintenances.map((m) => ({ 70 - ...m, 71 - monitors: m.maintenancesToMonitors.map((m) => m.monitorId), 72 - })); 73 - }), 74 - getByPage: protectedProcedure 75 - .input(z.object({ id: z.number() })) 76 - .query(async (opts) => { 77 - const _maintenances = await opts.ctx.db 78 - .select() 79 - .from(maintenance) 80 - .where( 81 - and( 82 - eq(maintenance.pageId, opts.input.id), 83 - eq(maintenance.workspaceId, opts.ctx.workspace.id), 84 - ), 85 - ) 86 - .all(); 87 - // TODO: 88 - return _maintenances; 89 - }), 90 - getActiveByWorkspace: protectedProcedure.query(async (opts) => { 91 - const _maintenances = await opts.ctx.db 92 - .select() 93 - .from(maintenance) 94 - .where( 95 - and( 96 - eq(maintenance.workspaceId, opts.ctx.workspace.id), 97 - gte(maintenance.to, new Date()), 98 - lte(maintenance.from, new Date()), 99 - ), 100 - ) 101 - .all(); 102 - return _maintenances; 103 - }), 104 - 105 28 delete: protectedProcedure 106 29 .meta({ track: Events.DeleteMaintenance }) 107 30 .input(z.object({ id: z.number() })) ··· 116 39 ) 117 40 .returning(); 118 41 }), 119 - 120 - // DASHBOARD 121 42 122 43 list: protectedProcedure 123 44 .input(
-130
packages/api/src/router/monitor.test.ts
··· 1 - import { expect, test } from "bun:test"; 2 - 3 - import { flyRegions } from "@openstatus/db/src/schema/constants"; 4 - import { TRPCError } from "@trpc/server"; 5 - import { appRouter } from "../root"; 6 - import { createInnerTRPCContext } from "../trpc"; 7 - 8 - function getTestContext(limits?: unknown) { 9 - return createInnerTRPCContext({ 10 - req: undefined, 11 - session: { 12 - user: { 13 - id: "1", 14 - }, 15 - }, 16 - workspace: { 17 - id: 1, 18 - // @ts-expect-error 19 - limits: limits || { 20 - monitors: 10, 21 - periodicity: ["1m"], 22 - regions: ["fra"], 23 - }, 24 - }, 25 - }); 26 - } 27 - 28 - const monitorData = { 29 - name: "Test Monitor", 30 - url: "https://example.com", 31 - jobType: "http" as const, 32 - method: "GET" as const, 33 - periodicity: "1m" as const, 34 - regions: [flyRegions[0]], 35 - statusAssertions: [], 36 - headerAssertions: [], 37 - textBodyAssertions: [], 38 - notifications: [], 39 - pages: [], 40 - tags: [], 41 - }; 42 - 43 - test("Create Monitor", async () => { 44 - const ctx = getTestContext(); 45 - const caller = appRouter.createCaller(ctx); 46 - 47 - const createdMonitor = await caller.monitor.create(monitorData); 48 - 49 - expect(createdMonitor).toMatchObject({ 50 - id: expect.any(Number), 51 - name: "Test Monitor", 52 - url: "https://example.com", 53 - workspaceId: 1, 54 - }); 55 - }); 56 - 57 - test("Get Monitor by ID", async () => { 58 - const ctx = getTestContext(); 59 - const caller = appRouter.createCaller(ctx); 60 - 61 - const createdMonitor = await caller.monitor.create(monitorData); 62 - const monitorId = createdMonitor.id; 63 - 64 - const fetchedMonitor = await caller.monitor.get({ 65 - id: monitorId, 66 - }); 67 - 68 - expect(fetchedMonitor).not.toBeNull(); 69 - expect(fetchedMonitor).toMatchObject({ 70 - id: monitorId, 71 - name: "Test Monitor", 72 - url: "https://example.com", 73 - }); 74 - }); 75 - 76 - test("Update Monitor", async () => { 77 - const ctx = getTestContext(); 78 - const caller = appRouter.createCaller(ctx); 79 - 80 - const createdMonitor = await caller.monitor.create(monitorData); 81 - const monitorId = createdMonitor.id; 82 - 83 - await caller.monitor.update({ 84 - ...monitorData, 85 - id: monitorId, 86 - name: "Updated Test Monitor", 87 - }); 88 - 89 - const updatedMonitor = await caller.monitor.get({ 90 - id: monitorId, 91 - }); 92 - 93 - expect(updatedMonitor).not.toBeNull(); 94 - expect(updatedMonitor?.name).toBe("Updated Test Monitor"); 95 - }); 96 - 97 - test("Delete Monitor", async () => { 98 - const ctx = getTestContext(); 99 - const caller = appRouter.createCaller(ctx); 100 - 101 - const createdMonitor = await caller.monitor.create(monitorData); 102 - const monitorId = createdMonitor.id; 103 - 104 - await caller.monitor.delete({ 105 - id: monitorId, 106 - }); 107 - 108 - const fetchedMonitor = await caller.monitor.get({ 109 - id: monitorId, 110 - }); 111 - expect(fetchedMonitor).toBeNull(); 112 - }); 113 - 114 - test.todo("Monitor Limit Reached", async () => { 115 - const ctx = getTestContext(); 116 - // @ts-expect-error 117 - ctx.workspace.limits.monitors = 1; 118 - const caller = appRouter.createCaller(ctx); 119 - 120 - await caller.monitor.create(monitorData); 121 - 122 - await caller.monitor.create(monitorData); 123 - 124 - await expect(await caller.monitor.create(monitorData)).rejects.toThrow( 125 - new TRPCError({ 126 - code: "FORBIDDEN", 127 - message: "You reached your monitor limits.", 128 - }), 129 - ); 130 - });
+12 -688
packages/api/src/router/monitor.ts
··· 21 21 eq, 22 22 inArray, 23 23 isNull, 24 - sql, 25 24 syncMaintenanceToMonitorDeleteByMonitors, 26 25 syncMonitorsToPageDelete, 27 26 syncMonitorsToPageDeleteByMonitors, ··· 49 48 selectNotificationSchema, 50 49 selectPageSchema, 51 50 selectPrivateLocationSchema, 52 - selectPublicMonitorSchema, 53 51 } from "@openstatus/db/src/schema"; 54 52 55 53 import { Events } from "@openstatus/analytics"; ··· 59 57 monitorRegions, 60 58 } from "@openstatus/db/src/schema/constants"; 61 59 import { regionDict } from "@openstatus/regions"; 62 - import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 60 + import { createTRPCRouter, protectedProcedure } from "../trpc"; 63 61 import { testDns, testHttp, testTcp } from "./checker"; 64 62 65 63 export const monitorRouter = createTRPCRouter({ 66 - create: protectedProcedure 67 - .meta({ track: Events.CreateMonitor, trackProps: ["url", "jobType"] }) 68 - .input(insertMonitorSchema) 69 - .output(selectMonitorSchema) 70 - .mutation(async (opts) => { 71 - const monitorLimit = opts.ctx.workspace.limits.monitors; 72 - const periodicityLimit = opts.ctx.workspace.limits.periodicity; 73 - const regionsLimit = opts.ctx.workspace.limits.regions; 74 - 75 - const monitorNumbers = ( 76 - await opts.ctx.db.query.monitor.findMany({ 77 - where: and( 78 - eq(monitor.workspaceId, opts.ctx.workspace.id), 79 - isNull(monitor.deletedAt), 80 - ), 81 - }) 82 - ).length; 83 - 84 - // the user has reached the limits 85 - if (monitorNumbers >= monitorLimit) { 86 - throw new TRPCError({ 87 - code: "FORBIDDEN", 88 - message: "You reached your monitor limits.", 89 - }); 90 - } 91 - 92 - // the user is not allowed to use the cron job 93 - if ( 94 - opts.input.periodicity && 95 - !periodicityLimit.includes(opts.input.periodicity) 96 - ) { 97 - throw new TRPCError({ 98 - code: "FORBIDDEN", 99 - message: "You reached your cron job limits.", 100 - }); 101 - } 102 - 103 - if ( 104 - opts.input.regions !== undefined && 105 - opts.input.regions?.length !== 0 106 - ) { 107 - for (const region of opts.input.regions) { 108 - if (!regionsLimit.includes(region)) { 109 - throw new TRPCError({ 110 - code: "FORBIDDEN", 111 - message: "You don't have access to this region.", 112 - }); 113 - } 114 - } 115 - } 116 - 117 - // FIXME: this is a hotfix 118 - const { 119 - regions, 120 - headers, 121 - notifications, 122 - pages, 123 - tags, 124 - statusAssertions, 125 - headerAssertions, 126 - textBodyAssertions, 127 - otelHeaders, 128 - ...data 129 - } = opts.input; 130 - 131 - const assertions: Assertion[] = []; 132 - for (const a of statusAssertions ?? []) { 133 - assertions.push(new StatusAssertion(a)); 134 - } 135 - for (const a of headerAssertions ?? []) { 136 - assertions.push(new HeaderAssertion(a)); 137 - } 138 - for (const a of textBodyAssertions ?? []) { 139 - assertions.push(new TextBodyAssertion(a)); 140 - } 141 - 142 - const newMonitor = await opts.ctx.db 143 - .insert(monitor) 144 - .values({ 145 - // REMINDER: We should explicitly pass the corresponding attributes 146 - // otherwise, unexpected attributes will be passed 147 - ...data, 148 - workspaceId: opts.ctx.workspace.id, 149 - regions: regions?.join(","), 150 - headers: headers ? JSON.stringify(headers) : undefined, 151 - otelHeaders: otelHeaders ? JSON.stringify(otelHeaders) : undefined, 152 - assertions: assertions.length > 0 ? serialize(assertions) : undefined, 153 - }) 154 - .returning() 155 - .get(); 156 - 157 - if (notifications.length > 0) { 158 - const allNotifications = await opts.ctx.db.query.notification.findMany({ 159 - where: and( 160 - eq(notification.workspaceId, opts.ctx.workspace.id), 161 - inArray(notification.id, notifications), 162 - ), 163 - }); 164 - 165 - const values = allNotifications.map((notification) => ({ 166 - monitorId: newMonitor.id, 167 - notificationId: notification.id, 168 - })); 169 - 170 - await opts.ctx.db.insert(notificationsToMonitors).values(values).run(); 171 - } 172 - 173 - if (tags.length > 0) { 174 - const allTags = await opts.ctx.db.query.monitorTag.findMany({ 175 - where: and( 176 - eq(monitorTag.workspaceId, opts.ctx.workspace.id), 177 - inArray(monitorTag.id, tags), 178 - ), 179 - }); 180 - 181 - const values = allTags.map((monitorTag) => ({ 182 - monitorId: newMonitor.id, 183 - monitorTagId: monitorTag.id, 184 - })); 185 - 186 - await opts.ctx.db.insert(monitorTagsToMonitors).values(values).run(); 187 - } 188 - 189 - if (pages.length > 0) { 190 - const allPages = await opts.ctx.db.query.page.findMany({ 191 - where: and( 192 - eq(page.workspaceId, opts.ctx.workspace.id), 193 - inArray(page.id, pages), 194 - ), 195 - }); 196 - 197 - const values = allPages.map((page) => ({ 198 - monitorId: newMonitor.id, 199 - pageId: page.id, 200 - })); 201 - 202 - await opts.ctx.db.insert(monitorsToPages).values(values).run(); 203 - // Sync to page components 204 - await syncMonitorsToPageInsertMany(opts.ctx.db, values); 205 - } 206 - 207 - return selectMonitorSchema.parse(newMonitor); 208 - }), 209 - 210 - getMonitorById: protectedProcedure 211 - .input(z.object({ id: z.number() })) 212 - .query(async (opts) => { 213 - const _monitor = await opts.ctx.db.query.monitor.findFirst({ 214 - where: and( 215 - eq(monitor.id, opts.input.id), 216 - eq(monitor.workspaceId, opts.ctx.workspace.id), 217 - isNull(monitor.deletedAt), 218 - ), 219 - with: { 220 - monitorTagsToMonitors: { with: { monitorTag: true } }, 221 - maintenancesToMonitors: { 222 - with: { maintenance: true }, 223 - where: eq(maintenancesToMonitors.monitorId, opts.input.id), 224 - }, 225 - monitorsToNotifications: { with: { notification: true } }, 226 - }, 227 - }); 228 - 229 - const parsedMonitor = selectMonitorSchema 230 - .extend({ 231 - monitorTagsToMonitors: z 232 - .object({ 233 - monitorTag: selectMonitorTagSchema, 234 - }) 235 - .array(), 236 - maintenance: z.boolean().prefault(false).optional(), 237 - monitorsToNotifications: z 238 - .object({ 239 - notification: selectNotificationSchema, 240 - }) 241 - .array(), 242 - }) 243 - .safeParse({ 244 - ..._monitor, 245 - maintenance: _monitor?.maintenancesToMonitors.some( 246 - (item) => 247 - item.maintenance.from.getTime() <= Date.now() && 248 - item.maintenance.to.getTime() >= Date.now(), 249 - ), 250 - }); 251 - 252 - if (!parsedMonitor.success) { 253 - throw new TRPCError({ 254 - code: "UNAUTHORIZED", 255 - message: "You are not allowed to access the monitor.", 256 - }); 257 - } 258 - return parsedMonitor.data; 259 - }), 260 - 261 - getPublicMonitorById: publicProcedure 262 - // REMINDER: if on status page, we should check if the monitor is associated with the page 263 - // otherwise, using `/public` we don't need to check 264 - .input(z.object({ id: z.number(), slug: z.string().optional() })) 265 - .query(async (opts) => { 266 - const _monitor = await opts.ctx.db 267 - .select() 268 - .from(monitor) 269 - .where( 270 - and( 271 - eq(monitor.id, opts.input.id), 272 - isNull(monitor.deletedAt), 273 - eq(monitor.public, true), 274 - ), 275 - ) 276 - .get(); 277 - 278 - if (!_monitor) return undefined; 279 - 280 - if (opts.input.slug) { 281 - const _page = await opts.ctx.db.query.page.findFirst({ 282 - where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, 283 - with: { monitorsToPages: true }, 284 - }); 285 - 286 - const hasPageRelation = _page?.monitorsToPages.find( 287 - ({ monitorId }) => _monitor.id === monitorId, 288 - ); 289 - 290 - if (!hasPageRelation) return undefined; 291 - } 292 - 293 - return selectPublicMonitorSchema.parse(_monitor); 294 - }), 295 - 296 - update: protectedProcedure 297 - .meta({ track: Events.UpdateMonitor }) 298 - .input(insertMonitorSchema) 299 - .mutation(async (opts) => { 300 - if (!opts.input.id) return; 301 - 302 - const periodicityLimit = opts.ctx.workspace.limits.periodicity; 303 - 304 - const regionsLimit = opts.ctx.workspace.limits.regions; 305 - 306 - // the user is not allowed to use the cron job 307 - if ( 308 - opts.input?.periodicity && 309 - !periodicityLimit.includes(opts.input?.periodicity) 310 - ) { 311 - throw new TRPCError({ 312 - code: "FORBIDDEN", 313 - message: "You reached your cron job limits.", 314 - }); 315 - } 316 - 317 - if ( 318 - opts.input.regions !== undefined && 319 - opts.input.regions?.length !== 0 320 - ) { 321 - for (const region of opts.input.regions) { 322 - if (!regionsLimit.includes(region)) { 323 - throw new TRPCError({ 324 - code: "FORBIDDEN", 325 - message: "You don't have access to this region.", 326 - }); 327 - } 328 - } 329 - } 330 - 331 - const { 332 - regions, 333 - headers, 334 - notifications, 335 - pages, 336 - tags, 337 - statusAssertions, 338 - headerAssertions, 339 - textBodyAssertions, 340 - otelHeaders, 341 - 342 - ...data 343 - } = opts.input; 344 - 345 - const assertions: Assertion[] = []; 346 - for (const a of statusAssertions ?? []) { 347 - assertions.push(new StatusAssertion(a)); 348 - } 349 - for (const a of headerAssertions ?? []) { 350 - assertions.push(new HeaderAssertion(a)); 351 - } 352 - for (const a of textBodyAssertions ?? []) { 353 - assertions.push(new TextBodyAssertion(a)); 354 - } 355 - 356 - const currentMonitor = await opts.ctx.db 357 - .update(monitor) 358 - .set({ 359 - ...data, 360 - regions: regions?.join(","), 361 - updatedAt: new Date(), 362 - headers: headers ? JSON.stringify(headers) : undefined, 363 - otelHeaders: otelHeaders ? JSON.stringify(otelHeaders) : undefined, 364 - assertions: serialize(assertions), 365 - }) 366 - .where( 367 - and( 368 - eq(monitor.id, opts.input.id), 369 - eq(monitor.workspaceId, opts.ctx.workspace.id), 370 - isNull(monitor.deletedAt), 371 - ), 372 - ) 373 - .returning() 374 - .get(); 375 - 376 - console.log({ 377 - currentMonitor, 378 - id: opts.input.id, 379 - workspaceId: opts.ctx.workspace.id, 380 - }); 381 - 382 - const currentMonitorNotifications = await opts.ctx.db 383 - .select() 384 - .from(notificationsToMonitors) 385 - .where(eq(notificationsToMonitors.monitorId, currentMonitor.id)) 386 - .all(); 387 - 388 - const addedNotifications = notifications.filter( 389 - (x) => 390 - !currentMonitorNotifications 391 - .map(({ notificationId }) => notificationId) 392 - ?.includes(x), 393 - ); 394 - 395 - if (addedNotifications.length > 0) { 396 - const values = addedNotifications.map((notificationId) => ({ 397 - monitorId: currentMonitor.id, 398 - notificationId, 399 - })); 400 - 401 - await opts.ctx.db.insert(notificationsToMonitors).values(values).run(); 402 - } 403 - 404 - const removedNotifications = currentMonitorNotifications 405 - .map(({ notificationId }) => notificationId) 406 - .filter((x) => !notifications?.includes(x)); 407 - 408 - if (removedNotifications.length > 0) { 409 - await opts.ctx.db 410 - .delete(notificationsToMonitors) 411 - .where( 412 - and( 413 - eq(notificationsToMonitors.monitorId, currentMonitor.id), 414 - inArray( 415 - notificationsToMonitors.notificationId, 416 - removedNotifications, 417 - ), 418 - ), 419 - ) 420 - .run(); 421 - } 422 - 423 - const currentMonitorTags = await opts.ctx.db 424 - .select() 425 - .from(monitorTagsToMonitors) 426 - .where(eq(monitorTagsToMonitors.monitorId, currentMonitor.id)) 427 - .all(); 428 - 429 - const addedTags = tags.filter( 430 - (x) => 431 - !currentMonitorTags 432 - .map(({ monitorTagId }) => monitorTagId) 433 - ?.includes(x), 434 - ); 435 - 436 - if (addedTags.length > 0) { 437 - const values = addedTags.map((monitorTagId) => ({ 438 - monitorId: currentMonitor.id, 439 - monitorTagId, 440 - })); 441 - 442 - await opts.ctx.db.insert(monitorTagsToMonitors).values(values).run(); 443 - } 444 - 445 - const removedTags = currentMonitorTags 446 - .map(({ monitorTagId }) => monitorTagId) 447 - .filter((x) => !tags?.includes(x)); 448 - 449 - if (removedTags.length > 0) { 450 - await opts.ctx.db 451 - .delete(monitorTagsToMonitors) 452 - .where( 453 - and( 454 - eq(monitorTagsToMonitors.monitorId, currentMonitor.id), 455 - inArray(monitorTagsToMonitors.monitorTagId, removedTags), 456 - ), 457 - ) 458 - .run(); 459 - } 460 - 461 - const currentMonitorPages = await opts.ctx.db 462 - .select() 463 - .from(monitorsToPages) 464 - .where(eq(monitorsToPages.monitorId, currentMonitor.id)) 465 - .all(); 466 - 467 - const addedPages = pages.filter( 468 - (x) => !currentMonitorPages.map(({ pageId }) => pageId)?.includes(x), 469 - ); 470 - 471 - if (addedPages.length > 0) { 472 - const values = addedPages.map((pageId) => ({ 473 - monitorId: currentMonitor.id, 474 - pageId, 475 - })); 476 - 477 - await opts.ctx.db.insert(monitorsToPages).values(values).run(); 478 - // Sync to page components 479 - await syncMonitorsToPageInsertMany(opts.ctx.db, values); 480 - } 481 - 482 - const removedPages = currentMonitorPages 483 - .map(({ pageId }) => pageId) 484 - .filter((x) => !pages?.includes(x)); 485 - 486 - if (removedPages.length > 0) { 487 - await opts.ctx.db 488 - .delete(monitorsToPages) 489 - .where( 490 - and( 491 - eq(monitorsToPages.monitorId, currentMonitor.id), 492 - inArray(monitorsToPages.pageId, removedPages), 493 - ), 494 - ) 495 - .run(); 496 - // Sync delete to page components 497 - for (const pageId of removedPages) { 498 - await syncMonitorsToPageDelete(opts.ctx.db, { 499 - monitorId: currentMonitor.id, 500 - pageId, 501 - }); 502 - } 503 - } 504 - }), 505 - 506 - updateMonitors: protectedProcedure 507 - .input( 508 - insertMonitorSchema 509 - .pick({ public: true, active: true }) 510 - .partial() // batched updates 511 - .extend({ ids: z.number().array() }), // array of monitor ids to update 512 - ) 513 - .mutation(async (opts) => { 514 - const _monitors = await opts.ctx.db 515 - .update(monitor) 516 - .set(opts.input) 517 - .where( 518 - and( 519 - inArray(monitor.id, opts.input.ids), 520 - eq(monitor.workspaceId, opts.ctx.workspace.id), 521 - isNull(monitor.deletedAt), 522 - ), 523 - ); 524 - }), 525 - 526 - updateMonitorsTag: protectedProcedure 527 - .input( 528 - z.object({ 529 - ids: z.number().array(), 530 - tagId: z.number(), 531 - action: z.enum(["add", "remove"]), 532 - }), 533 - ) 534 - .mutation(async (opts) => { 535 - const _monitorTag = await opts.ctx.db.query.monitorTag.findFirst({ 536 - where: and( 537 - eq(monitorTag.workspaceId, opts.ctx.workspace.id), 538 - eq(monitorTag.id, opts.input.tagId), 539 - ), 540 - }); 541 - 542 - const _monitors = await opts.ctx.db.query.monitor.findMany({ 543 - where: and( 544 - eq(monitor.workspaceId, opts.ctx.workspace.id), 545 - inArray(monitor.id, opts.input.ids), 546 - ), 547 - }); 548 - 549 - if (!_monitorTag || _monitors.length !== opts.input.ids.length) { 550 - throw new TRPCError({ 551 - code: "BAD_REQUEST", 552 - message: "Invalid tag", 553 - }); 554 - } 555 - 556 - if (opts.input.action === "add") { 557 - await opts.ctx.db 558 - .insert(monitorTagsToMonitors) 559 - .values( 560 - opts.input.ids.map((id) => ({ 561 - monitorId: id, 562 - monitorTagId: opts.input.tagId, 563 - })), 564 - ) 565 - .onConflictDoNothing() 566 - .run(); 567 - } 568 - 569 - if (opts.input.action === "remove") { 570 - await opts.ctx.db 571 - .delete(monitorTagsToMonitors) 572 - .where( 573 - and( 574 - inArray(monitorTagsToMonitors.monitorId, opts.input.ids), 575 - eq(monitorTagsToMonitors.monitorTagId, opts.input.tagId), 576 - ), 577 - ) 578 - .run(); 579 - } 580 - }), 581 - 582 64 delete: protectedProcedure 583 65 .meta({ track: Events.DeleteMonitor }) 584 66 .input(z.object({ id: z.number() })) ··· 678 160 }); 679 161 }), 680 162 681 - getMonitorsByWorkspace: protectedProcedure.query(async (opts) => { 682 - const monitors = await opts.ctx.db.query.monitor.findMany({ 683 - where: and( 684 - eq(monitor.workspaceId, opts.ctx.workspace.id), 685 - isNull(monitor.deletedAt), 686 - ), 687 - with: { 688 - monitorTagsToMonitors: { with: { monitorTag: true } }, 689 - }, 690 - orderBy: (monitor, { desc }) => [desc(monitor.active)], 691 - }); 692 - 693 - return z 694 - .array( 695 - selectMonitorSchema.extend({ 696 - monitorTagsToMonitors: z 697 - .array(z.object({ monitorTag: selectMonitorTagSchema })) 698 - .prefault([]), 699 - }), 700 - ) 701 - .parse(monitors); 702 - }), 703 - 704 - getMonitorsByPageId: protectedProcedure 705 - .input(z.object({ id: z.number() })) 706 - .query(async (opts) => { 707 - const _page = await opts.ctx.db.query.page.findFirst({ 708 - where: and( 709 - eq(page.id, opts.input.id), 710 - eq(page.workspaceId, opts.ctx.workspace.id), 711 - ), 712 - }); 713 - 714 - if (!_page) return undefined; 715 - 716 - const monitors = await opts.ctx.db.query.monitor.findMany({ 717 - where: and( 718 - eq(monitor.workspaceId, opts.ctx.workspace.id), 719 - isNull(monitor.deletedAt), 720 - ), 721 - with: { 722 - monitorTagsToMonitors: { with: { monitorTag: true } }, 723 - monitorsToPages: { 724 - where: eq(monitorsToPages.pageId, _page.id), 725 - }, 726 - }, 727 - }); 728 - 729 - return z 730 - .array( 731 - selectMonitorSchema.extend({ 732 - monitorTagsToMonitors: z 733 - .array(z.object({ monitorTag: selectMonitorTagSchema })) 734 - .prefault([]), 735 - }), 736 - ) 737 - .parse( 738 - monitors.filter((monitor) => 739 - monitor.monitorsToPages 740 - .map(({ pageId }) => pageId) 741 - .includes(_page.id), 742 - ), 743 - ); 744 - }), 745 - 746 - toggleMonitorActive: protectedProcedure 747 - .input(z.object({ id: z.number() })) 163 + updateMonitors: protectedProcedure 164 + .input( 165 + insertMonitorSchema 166 + .pick({ public: true, active: true }) 167 + .partial() // batched updates 168 + .extend({ ids: z.number().array() }), // array of monitor ids to update 169 + ) 748 170 .mutation(async (opts) => { 749 - const monitorToUpdate = await opts.ctx.db 750 - .select() 751 - .from(monitor) 752 - .where( 753 - and( 754 - eq(monitor.id, opts.input.id), 755 - eq(monitor.workspaceId, opts.ctx.workspace.id), 756 - isNull(monitor.deletedAt), 757 - ), 758 - ) 759 - .get(); 760 - 761 - if (!monitorToUpdate) { 762 - throw new TRPCError({ 763 - code: "NOT_FOUND", 764 - message: "Monitor not found.", 765 - }); 766 - } 767 - 768 171 await opts.ctx.db 769 172 .update(monitor) 770 - .set({ 771 - active: !monitorToUpdate.active, 772 - }) 173 + .set(opts.input) 773 174 .where( 774 175 and( 775 - eq(monitor.id, opts.input.id), 176 + inArray(monitor.id, opts.input.ids), 776 177 eq(monitor.workspaceId, opts.ctx.workspace.id), 777 - ), 778 - ) 779 - .run(); 780 - }), 781 - 782 - // rename to getActiveMonitorsCount 783 - getTotalActiveMonitors: publicProcedure.query(async (opts) => { 784 - const monitors = await opts.ctx.db 785 - .select({ count: sql<number>`count(*)` }) 786 - .from(monitor) 787 - .where(eq(monitor.active, true)) 788 - .all(); 789 - if (monitors.length === 0) return 0; 790 - return monitors[0].count; 791 - }), 792 - 793 - // TODO: return the notifications inside of the `getMonitorById` like we do for the monitors on a status page 794 - getAllNotificationsForMonitor: protectedProcedure 795 - .input(z.object({ id: z.number() })) 796 - // .output(selectMonitorSchema) 797 - .query(async (opts) => { 798 - const data = await opts.ctx.db 799 - .select() 800 - .from(notificationsToMonitors) 801 - .innerJoin( 802 - notification, 803 - and( 804 - eq(notificationsToMonitors.notificationId, notification.id), 805 - eq(notification.workspaceId, opts.ctx.workspace.id), 178 + isNull(monitor.deletedAt), 806 179 ), 807 - ) 808 - .where(eq(notificationsToMonitors.monitorId, opts.input.id)) 809 - .all(); 810 - return data.map((d) => selectNotificationSchema.parse(d.notification)); 180 + ); 811 181 }), 812 - 813 - isMonitorLimitReached: protectedProcedure.query(async (opts) => { 814 - const monitorLimit = opts.ctx.workspace.limits.monitors; 815 - const monitorNumbers = ( 816 - await opts.ctx.db.query.monitor.findMany({ 817 - where: and( 818 - eq(monitor.workspaceId, opts.ctx.workspace.id), 819 - isNull(monitor.deletedAt), 820 - ), 821 - }) 822 - ).length; 823 - 824 - return monitorNumbers >= monitorLimit; 825 - }), 826 - getMonitorRelationsById: protectedProcedure 827 - .input(z.object({ id: z.number() })) 828 - .query(async (opts) => { 829 - const _monitor = await opts.ctx.db.query.monitor.findFirst({ 830 - where: and( 831 - eq(monitor.id, opts.input.id), 832 - eq(monitor.workspaceId, opts.ctx.workspace.id), 833 - isNull(monitor.deletedAt), 834 - ), 835 - with: { 836 - monitorTagsToMonitors: true, 837 - monitorsToNotifications: true, 838 - monitorsToPages: true, 839 - }, 840 - }); 841 - 842 - const parsedMonitorNotification = _monitor?.monitorsToNotifications.map( 843 - ({ notificationId }) => notificationId, 844 - ); 845 - const parsedPages = _monitor?.monitorsToPages.map((val) => val.pageId); 846 - const parsedTags = _monitor?.monitorTagsToMonitors.map( 847 - ({ monitorTagId }) => monitorTagId, 848 - ); 849 - 850 - return { 851 - notificationIds: parsedMonitorNotification, 852 - pageIds: parsedPages, 853 - monitorTagIds: parsedTags, 854 - }; 855 - }), 856 - 857 - // DASHBOARD 858 182 859 183 list: protectedProcedure 860 184 .input(
+1 -59
packages/api/src/router/monitorTag.ts
··· 1 1 import { z } from "zod"; 2 2 3 3 import { and, eq, inArray } from "@openstatus/db"; 4 - import { insertMonitorTagSchema, monitorTag } from "@openstatus/db/src/schema"; 4 + import { monitorTag } from "@openstatus/db/src/schema"; 5 5 6 6 import { createTRPCRouter, protectedProcedure } from "../trpc"; 7 7 ··· 12 12 }); 13 13 14 14 export const monitorTagRouter = createTRPCRouter({ 15 - getMonitorTagsByWorkspace: protectedProcedure.query(async (opts) => { 16 - return opts.ctx.db.query.monitorTag.findMany({ 17 - where: eq(monitorTag.workspaceId, opts.ctx.workspace.id), 18 - with: { monitor: true }, 19 - }); 20 - }), 21 - 22 - update: protectedProcedure 23 - .input(insertMonitorTagSchema) 24 - .mutation(async (opts) => { 25 - if (!opts.input.id) return; 26 - return await opts.ctx.db 27 - .update(monitorTag) 28 - .set({ 29 - name: opts.input.name, 30 - color: opts.input.color, 31 - updatedAt: new Date(), 32 - }) 33 - .where( 34 - and( 35 - eq(monitorTag.workspaceId, opts.ctx.workspace.id), 36 - eq(monitorTag.id, opts.input.id), 37 - ), 38 - ) 39 - .returning() 40 - .get(); 41 - }), 42 - 43 - delete: protectedProcedure 44 - .input(z.object({ id: z.number() })) 45 - .mutation(async (opts) => { 46 - await opts.ctx.db 47 - .delete(monitorTag) 48 - .where( 49 - and( 50 - eq(monitorTag.id, opts.input.id), 51 - eq(monitorTag.workspaceId, opts.ctx.workspace.id), 52 - ), 53 - ) 54 - .run(); 55 - }), 56 - 57 - create: protectedProcedure 58 - .input(z.object({ name: z.string(), color: z.string() })) 59 - .mutation(async (opts) => { 60 - return opts.ctx.db 61 - .insert(monitorTag) 62 - .values({ 63 - name: opts.input.name, 64 - color: opts.input.color, 65 - workspaceId: opts.ctx.workspace.id, 66 - }) 67 - .returning() 68 - .get(); 69 - }), 70 - 71 - // DASHBOARD 72 - 73 15 list: protectedProcedure.query(async (opts) => { 74 16 return opts.ctx.db.query.monitorTag.findMany({ 75 17 where: eq(monitorTag.workspaceId, opts.ctx.workspace.id),
-162
packages/api/src/router/page.ts
··· 6 6 and, 7 7 desc, 8 8 eq, 9 - gte, 10 9 inArray, 11 10 isNull, 12 - lte, 13 11 sql, 14 12 syncMonitorGroupDeleteMany, 15 13 syncMonitorGroupInsert, ··· 32 30 selectMonitorSchema, 33 31 selectPageComponentSchema, 34 32 selectPageSchema, 35 - selectPageSchemaWithMonitorsRelation, 36 33 statusReport, 37 34 subdomainSafeList, 38 35 workspace, ··· 154 151 155 152 return newPage; 156 153 }), 157 - getPageById: protectedProcedure 158 - .input(z.object({ id: z.number() })) 159 - .query(async (opts) => { 160 - const firstPage = await opts.ctx.db.query.page.findFirst({ 161 - where: and( 162 - eq(page.id, opts.input.id), 163 - eq(page.workspaceId, opts.ctx.workspace.id), 164 - ), 165 - with: { 166 - monitorsToPages: { 167 - with: { monitor: true }, 168 - orderBy: (monitorsToPages, { asc }) => [asc(monitorsToPages.order)], 169 - }, 170 - }, 171 - }); 172 - return selectPageSchemaWithMonitorsRelation.parse(firstPage); 173 - }), 174 154 175 - update: protectedProcedure 176 - .meta({ track: Events.UpdatePage }) 177 - .input(insertPageSchema) 178 - .mutation(async (opts) => { 179 - const { monitors, ...pageInput } = opts.input; 180 - if (!pageInput.id) return; 181 - 182 - const monitorIds = monitors?.map((item) => item.monitorId) || []; 183 - 184 - const limit = opts.ctx.workspace.limits; 185 - 186 - // the user is not eligible for password protection 187 - if ( 188 - limit["password-protection"] === false && 189 - opts.input.passwordProtected === true 190 - ) { 191 - throw new TRPCError({ 192 - code: "FORBIDDEN", 193 - message: 194 - "Password protection is not available for your current plan.", 195 - }); 196 - } 197 - 198 - const currentPage = await opts.ctx.db 199 - .update(page) 200 - .set({ 201 - ...pageInput, 202 - updatedAt: new Date(), 203 - authEmailDomains: pageInput.authEmailDomains?.join(","), 204 - }) 205 - .where( 206 - and( 207 - eq(page.id, pageInput.id), 208 - eq(page.workspaceId, opts.ctx.workspace.id), 209 - ), 210 - ) 211 - .returning() 212 - .get(); 213 - 214 - if (monitorIds.length) { 215 - // We should make sure the user has access to the monitors 216 - const allMonitors = await opts.ctx.db.query.monitor.findMany({ 217 - where: and( 218 - inArray(monitor.id, monitorIds), 219 - eq(monitor.workspaceId, opts.ctx.workspace.id), 220 - isNull(monitor.deletedAt), 221 - ), 222 - }); 223 - 224 - if (allMonitors.length !== monitorIds.length) { 225 - throw new TRPCError({ 226 - code: "FORBIDDEN", 227 - message: "You don't have access to all the monitors.", 228 - }); 229 - } 230 - } 231 - 232 - // TODO: check for monitor order! 233 - const currentMonitorsToPages = await opts.ctx.db 234 - .select() 235 - .from(monitorsToPages) 236 - .where(eq(monitorsToPages.pageId, currentPage.id)) 237 - .all(); 238 - 239 - const removedMonitors = currentMonitorsToPages 240 - .map(({ monitorId }) => monitorId) 241 - .filter((x) => !monitorIds?.includes(x)); 242 - 243 - if (removedMonitors.length) { 244 - await opts.ctx.db 245 - .delete(monitorsToPages) 246 - .where( 247 - and( 248 - inArray(monitorsToPages.monitorId, removedMonitors), 249 - eq(monitorsToPages.pageId, currentPage.id), 250 - ), 251 - ); 252 - // Sync delete to page components 253 - for (const monitorId of removedMonitors) { 254 - await syncMonitorsToPageDelete(opts.ctx.db, { 255 - monitorId, 256 - pageId: currentPage.id, 257 - }); 258 - } 259 - } 260 - 261 - const values = monitors.map(({ monitorId }, index) => ({ 262 - pageId: currentPage.id, 263 - order: index, 264 - monitorId, 265 - })); 266 - 267 - if (values.length) { 268 - await opts.ctx.db 269 - .insert(monitorsToPages) 270 - .values(values) 271 - .onConflictDoUpdate({ 272 - target: [monitorsToPages.monitorId, monitorsToPages.pageId], 273 - set: { order: sql.raw("excluded.`order`") }, 274 - }); 275 - // Sync new monitors to page components (existing ones will be ignored due to onConflictDoNothing) 276 - await syncMonitorsToPageInsertMany(opts.ctx.db, values); 277 - } 278 - }), 279 155 delete: protectedProcedure 280 156 .meta({ track: Events.DeletePage }) 281 157 .input(z.object({ id: z.number() })) ··· 290 166 .where(and(...whereConditions)) 291 167 .run(); 292 168 }), 293 - 294 - getPagesByWorkspace: protectedProcedure.query(async (opts) => { 295 - const allPages = await opts.ctx.db.query.page.findMany({ 296 - where: and(eq(page.workspaceId, opts.ctx.workspace.id)), 297 - with: { 298 - monitorsToPages: { with: { monitor: true } }, 299 - maintenances: { 300 - where: and( 301 - lte(maintenance.from, new Date()), 302 - gte(maintenance.to, new Date()), 303 - ), 304 - }, 305 - statusReports: { 306 - orderBy: (reports, { desc }) => desc(reports.updatedAt), 307 - with: { 308 - statusReportUpdates: { 309 - orderBy: (updates, { desc }) => desc(updates.date), 310 - }, 311 - }, 312 - }, 313 - }, 314 - }); 315 - return z.array(selectPageSchemaWithMonitorsRelation).parse(allPages); 316 - }), 317 - 318 169 // public if we use trpc hooks to get the page from the url 319 170 getPageBySlug: publicProcedure 320 171 .input(z.object({ slug: z.string().toLowerCase() })) ··· 459 310 .returning() 460 311 .get(); 461 312 }), 462 - 463 - isPageLimitReached: protectedProcedure.query(async (opts) => { 464 - const pageLimit = opts.ctx.workspace.limits["status-pages"]; 465 - const pageNumbers = ( 466 - await opts.ctx.db.query.page.findMany({ 467 - where: eq(monitor.workspaceId, opts.ctx.workspace.id), 468 - }) 469 - ).length; 470 - 471 - return pageNumbers >= pageLimit; 472 - }), 473 - 474 - // DASHBOARD 475 313 476 314 list: protectedProcedure 477 315 .input(
+2 -100
packages/api/src/router/pageSubscriber.ts
··· 1 + import { and, eq } from "@openstatus/db"; 2 + import { page, pageSubscriber } from "@openstatus/db/src/schema"; 1 3 import { TRPCError } from "@trpc/server"; 2 4 import { z } from "zod"; 3 5 4 - import { Events } from "@openstatus/analytics"; 5 - import { and, eq } from "@openstatus/db"; 6 - import { page, pageSubscriber } from "@openstatus/db/src/schema"; 7 - 8 6 import { createTRPCRouter, protectedProcedure } from "../trpc"; 9 7 10 8 export const pageSubscriberRouter = createTRPCRouter({ 11 - getPageSubscribersByPageId: protectedProcedure 12 - .input(z.object({ id: z.number() })) 13 - .query(async (opts) => { 14 - const _page = await opts.ctx.db.query.page.findFirst({ 15 - where: and( 16 - eq(page.workspaceId, opts.ctx.workspace.id), 17 - eq(page.id, opts.input.id), 18 - ), 19 - }); 20 - 21 - if (!_page) { 22 - throw new TRPCError({ 23 - code: "UNAUTHORIZED", 24 - message: "Unauthorized to get subscribers", 25 - }); 26 - } 27 - 28 - const data = await opts.ctx.db.query.pageSubscriber.findMany({ 29 - where: and(eq(pageSubscriber.pageId, _page.id)), 30 - }); 31 - return data; 32 - }), 33 - 34 - unsubscribeById: protectedProcedure 35 - .input(z.object({ id: z.number() })) 36 - .mutation(async (opts) => { 37 - const subscriber = await opts.ctx.db.query.pageSubscriber.findFirst({ 38 - where: and(eq(pageSubscriber.id, opts.input.id)), 39 - }); 40 - 41 - if (!subscriber) { 42 - throw new TRPCError({ 43 - code: "NOT_FOUND", 44 - message: "Subscriber not found", 45 - }); 46 - } 47 - 48 - const _page = await opts.ctx.db.query.page.findFirst({ 49 - where: and( 50 - eq(page.id, subscriber.pageId), 51 - eq(page.workspaceId, opts.ctx.workspace.id), 52 - ), 53 - }); 54 - 55 - if (!_page) { 56 - throw new TRPCError({ 57 - code: "UNAUTHORIZED", 58 - message: "Unauthorized to unsubscribe", 59 - }); 60 - } 61 - 62 - return await opts.ctx.db 63 - .delete(pageSubscriber) 64 - .where(eq(pageSubscriber.id, subscriber.id)); 65 - }), 66 - 67 - acceptSubscriberById: protectedProcedure 68 - .meta({ track: Events.SubscribePage }) 69 - .input(z.object({ id: z.number() })) 70 - .mutation(async (opts) => { 71 - const subscriber = await opts.ctx.db.query.pageSubscriber.findFirst({ 72 - where: and(eq(pageSubscriber.id, opts.input.id)), 73 - }); 74 - 75 - if (!subscriber) { 76 - throw new TRPCError({ 77 - code: "NOT_FOUND", 78 - message: "Subscriber not found", 79 - }); 80 - } 81 - 82 - const _page = await opts.ctx.db.query.page.findFirst({ 83 - where: and( 84 - eq(page.id, subscriber.pageId), 85 - eq(page.workspaceId, opts.ctx.workspace.id), 86 - ), 87 - }); 88 - 89 - if (!_page) { 90 - throw new TRPCError({ 91 - code: "UNAUTHORIZED", 92 - message: "Unauthorized to unsubscribe", 93 - }); 94 - } 95 - 96 - return await opts.ctx.db 97 - .update(pageSubscriber) 98 - .set({ 99 - acceptedAt: new Date(), 100 - updatedAt: new Date(), 101 - }) 102 - .where(eq(pageSubscriber.id, subscriber.id)); 103 - }), 104 - 105 - // DASHBOARD 106 - 107 9 list: protectedProcedure 108 10 .input( 109 11 z.object({
-3
packages/api/src/router/statusPage.e2e.test.ts
··· 332 332 }); 333 333 334 334 describe("Unsubscribed user does not receive new emails", () => { 335 - let _activeToken: string; 336 335 let unsubscribedToken: string; 337 336 let pendingToken: string; 338 337 ··· 365 364 if (!active.token) { 366 365 throw new Error("Active subscriber token is undefined"); 367 366 } 368 - 369 - _activeToken = active.token; 370 367 371 368 // Unsubscribed subscriber 372 369 const unsubscribed = await db
+8 -8
packages/api/src/router/statusPage.unsubscribe.test.ts
··· 5 5 6 6 // Test data setup 7 7 let testPageId: number; 8 - let _testSubscriberId: number; 9 8 let testToken: string; 10 9 const testWorkspaceId = 1; // Use existing test workspace from seed data 11 10 ··· 42 41 43 42 // Create a verified subscriber for testing 44 43 testToken = crypto.randomUUID(); 45 - const subscriber = await db 44 + await db 46 45 .insert(pageSubscriber) 47 46 .values({ 48 47 pageId: testPageId, ··· 53 52 }) 54 53 .returning() 55 54 .get(); 56 - 57 - _testSubscriberId = subscriber.id; 58 55 }); 59 56 60 57 afterAll(async () => { ··· 86 83 87 84 // Manually mask the email to test the masking logic 88 85 const email = subscriber?.email; 89 - const [localPart, domain] = email.split("@"); 90 - const maskedEmail = 91 - localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`; 86 + expect(email).toBeDefined(); 87 + if (email) { 88 + const [localPart, domain] = email.split("@"); 89 + const maskedEmail = 90 + localPart.length > 0 ? `${localPart[0]}***@${domain}` : `***@${domain}`; 92 91 93 - expect(maskedEmail).toBe("t***@example.com"); 92 + expect(maskedEmail).toBe("t***@example.com"); 93 + } 94 94 }); 95 95 96 96 test("should return null for non-existent token", async () => {
+1 -312
packages/api/src/router/statusReport.ts
··· 7 7 desc, 8 8 eq, 9 9 gte, 10 - inArray, 11 - sql, 12 - syncStatusReportToMonitorDelete, 13 - syncStatusReportToMonitorInsertMany, 14 10 syncStatusReportToPageComponentDeleteByStatusReport, 15 11 syncStatusReportToPageComponentInsertMany, 16 12 } from "@openstatus/db"; 17 13 import { 18 - insertStatusReportSchema, 19 14 insertStatusReportUpdateSchema, 20 - monitorsToStatusReport, 21 - page, 22 15 selectMonitorSchema, 23 16 selectPageComponentSchema, 24 17 selectPageSchema, 25 - selectPublicStatusReportSchemaWithRelation, 26 18 selectStatusReportSchema, 27 19 selectStatusReportUpdateSchema, 28 20 statusReport, 29 21 statusReportStatus, 30 - statusReportStatusSchema, 31 22 statusReportUpdate, 32 23 statusReportsToPageComponents, 33 24 } from "@openstatus/db/src/schema"; 34 25 35 26 import { Events } from "@openstatus/analytics"; 36 27 import { TRPCError } from "@trpc/server"; 37 - import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 28 + import { createTRPCRouter, protectedProcedure } from "../trpc"; 38 29 import { getPeriodDate, periods } from "./utils"; 39 30 40 31 export const statusReportRouter = createTRPCRouter({ 41 - createStatusReport: protectedProcedure 42 - .meta({ track: Events.CreateReport }) 43 - .input(insertStatusReportSchema) 44 - .mutation(async (opts) => { 45 - const { id, monitors, date, message, ...statusReportInput } = opts.input; 46 - 47 - const newStatusReport = await opts.ctx.db 48 - .insert(statusReport) 49 - .values({ 50 - workspaceId: opts.ctx.workspace.id, 51 - ...statusReportInput, 52 - }) 53 - .returning() 54 - .get(); 55 - 56 - if (monitors.length > 0) { 57 - await opts.ctx.db 58 - .insert(monitorsToStatusReport) 59 - .values( 60 - monitors.map((monitor) => ({ 61 - monitorId: monitor, 62 - statusReportId: newStatusReport.id, 63 - })), 64 - ) 65 - .returning() 66 - .get(); 67 - // Sync to page components 68 - await syncStatusReportToMonitorInsertMany( 69 - opts.ctx.db, 70 - newStatusReport.id, 71 - monitors, 72 - ); 73 - } 74 - 75 - return newStatusReport; 76 - }), 77 - 78 32 createStatusReportUpdate: protectedProcedure 79 33 .meta({ track: Events.CreateReportUpdate }) 80 34 .input( ··· 112 66 }; 113 67 }), 114 68 115 - updateStatusReport: protectedProcedure 116 - .meta({ track: Events.UpdateReport }) 117 - .input(insertStatusReportSchema) 118 - .mutation(async (opts) => { 119 - const { monitors, ...statusReportInput } = opts.input; 120 - 121 - if (!statusReportInput.id) return; 122 - 123 - const { title, status } = statusReportInput; 124 - 125 - const currentStatusReport = await opts.ctx.db 126 - .update(statusReport) 127 - .set({ title, status, updatedAt: new Date() }) 128 - .where( 129 - and( 130 - eq(statusReport.id, statusReportInput.id), 131 - eq(statusReport.workspaceId, opts.ctx.workspace.id), 132 - ), 133 - ) 134 - .returning() 135 - .get(); 136 - 137 - const currentMonitorsToStatusReport = await opts.ctx.db 138 - .select() 139 - .from(monitorsToStatusReport) 140 - .where( 141 - eq(monitorsToStatusReport.statusReportId, currentStatusReport.id), 142 - ) 143 - .all(); 144 - 145 - const addedMonitors = monitors.filter( 146 - (x) => 147 - !currentMonitorsToStatusReport 148 - .map(({ monitorId }) => monitorId) 149 - .includes(x), 150 - ); 151 - 152 - if (addedMonitors.length) { 153 - const values = addedMonitors.map((monitorId) => ({ 154 - monitorId: monitorId, 155 - statusReportId: currentStatusReport.id, 156 - })); 157 - 158 - await opts.ctx.db.insert(monitorsToStatusReport).values(values).run(); 159 - // Sync to page components 160 - await syncStatusReportToMonitorInsertMany( 161 - opts.ctx.db, 162 - currentStatusReport.id, 163 - addedMonitors, 164 - ); 165 - } 166 - 167 - const removedMonitors = currentMonitorsToStatusReport 168 - .map(({ monitorId }) => monitorId) 169 - .filter((x) => !monitors?.includes(x)); 170 - 171 - if (removedMonitors.length) { 172 - await opts.ctx.db 173 - .delete(monitorsToStatusReport) 174 - .where( 175 - and( 176 - eq(monitorsToStatusReport.statusReportId, currentStatusReport.id), 177 - inArray(monitorsToStatusReport.monitorId, removedMonitors), 178 - ), 179 - ) 180 - .run(); 181 - // Sync delete to page components for each removed monitor 182 - for (const monitorId of removedMonitors) { 183 - await syncStatusReportToMonitorDelete(opts.ctx.db, { 184 - statusReportId: currentStatusReport.id, 185 - monitorId, 186 - }); 187 - } 188 - } 189 - 190 - return currentStatusReport; 191 - }), 192 - 193 69 updateStatusReportUpdate: protectedProcedure 194 70 .meta({ track: Events.UpdateReportUpdate }) 195 71 .input(insertStatusReportUpdateSchema) ··· 207 83 208 84 return selectStatusReportUpdateSchema.parse(currentStatusReportUpdate); 209 85 }), 210 - 211 - deleteStatusReport: protectedProcedure 212 - .meta({ track: Events.DeleteReport }) 213 - .input(z.object({ id: z.number() })) 214 - .mutation(async (opts) => { 215 - const statusReportToDelete = await opts.ctx.db 216 - .select() 217 - .from(statusReport) 218 - .where( 219 - and( 220 - eq(statusReport.id, opts.input.id), 221 - eq(statusReport.workspaceId, opts.ctx.workspace.id), 222 - ), 223 - ) 224 - .get(); 225 - if (!statusReportToDelete) return; 226 - 227 - await opts.ctx.db 228 - .delete(statusReport) 229 - .where(eq(statusReport.id, statusReportToDelete.id)) 230 - .run(); 231 - }), 232 - 233 - deleteStatusReportUpdate: protectedProcedure 234 - .meta({ track: Events.DeleteReportUpdate }) 235 - .input(z.object({ id: z.number() })) 236 - .mutation(async (opts) => { 237 - const statusReportUpdateToDelete = await opts.ctx.db 238 - .select() 239 - .from(statusReportUpdate) 240 - .where(and(eq(statusReportUpdate.id, opts.input.id))) 241 - .get(); 242 - 243 - if (!statusReportUpdateToDelete) return; 244 - 245 - await opts.ctx.db 246 - .delete(statusReportUpdate) 247 - .where(eq(statusReportUpdate.id, opts.input.id)) 248 - .run(); 249 - }), 250 - 251 - getStatusReportById: protectedProcedure 252 - .input(z.object({ id: z.number(), pageId: z.number().optional() })) 253 - .query(async (opts) => { 254 - const selectPublicStatusReportSchemaWithRelation = 255 - selectStatusReportSchema.extend({ 256 - status: statusReportStatusSchema.prefault("investigating"), // TODO: remove! 257 - monitorsToStatusReports: z 258 - .array( 259 - z.object({ 260 - statusReportId: z.number(), 261 - monitorId: z.number(), 262 - monitor: selectMonitorSchema, 263 - }), 264 - ) 265 - .prefault([]), 266 - statusReportUpdates: z.array(selectStatusReportUpdateSchema), 267 - date: z.date().prefault(new Date()), 268 - }); 269 - 270 - const data = await opts.ctx.db.query.statusReport.findFirst({ 271 - where: and( 272 - eq(statusReport.id, opts.input.id), 273 - eq(statusReport.workspaceId, opts.ctx.workspace.id), 274 - // only allow to fetch status report if it belongs to the page 275 - opts.input.pageId 276 - ? eq(statusReport.pageId, opts.input.pageId) 277 - : undefined, 278 - ), 279 - with: { 280 - monitorsToStatusReports: { with: { monitor: true } }, 281 - statusReportUpdates: { 282 - orderBy: (statusReportUpdate, { desc }) => [ 283 - desc(statusReportUpdate.createdAt), 284 - ], 285 - }, 286 - }, 287 - }); 288 - 289 - return selectPublicStatusReportSchemaWithRelation.parse(data); 290 - }), 291 - 292 - getStatusReportUpdateById: protectedProcedure 293 - .input(z.object({ id: z.number() })) 294 - .query(async (opts) => { 295 - const data = await opts.ctx.db.query.statusReportUpdate.findFirst({ 296 - where: and(eq(statusReportUpdate.id, opts.input.id)), 297 - }); 298 - return selectStatusReportUpdateSchema.parse(data); 299 - }), 300 - 301 - getStatusReportByWorkspace: protectedProcedure.query(async (opts) => { 302 - // FIXME: can we get rid of that? 303 - const selectStatusSchemaWithRelation = selectStatusReportSchema.extend({ 304 - status: statusReportStatusSchema.prefault("investigating"), // TODO: remove! 305 - monitorsToStatusReports: z 306 - .array( 307 - z.object({ 308 - statusReportId: z.number(), 309 - monitorId: z.number(), 310 - monitor: selectMonitorSchema, 311 - }), 312 - ) 313 - .prefault([]), 314 - statusReportUpdates: z.array(selectStatusReportUpdateSchema), 315 - }); 316 - 317 - const result = await opts.ctx.db.query.statusReport.findMany({ 318 - where: eq(statusReport.workspaceId, opts.ctx.workspace.id), 319 - with: { 320 - monitorsToStatusReports: { with: { monitor: true } }, 321 - statusReportUpdates: { 322 - orderBy: (statusReportUpdate, { desc }) => [ 323 - desc(statusReportUpdate.createdAt), 324 - ], 325 - }, 326 - }, 327 - orderBy: (statusReport, { desc }) => [desc(statusReport.updatedAt)], 328 - }); 329 - return z.array(selectStatusSchemaWithRelation).parse(result); 330 - }), 331 - 332 - getStatusReportByPageId: protectedProcedure 333 - .input(z.object({ id: z.number() })) 334 - .query(async (opts) => { 335 - // FIXME: can we get rid of that? 336 - const selectStatusSchemaWithRelation = selectStatusReportSchema.extend({ 337 - status: statusReportStatusSchema.prefault("investigating"), // TODO: remove! 338 - monitorsToStatusReports: z 339 - .array( 340 - z.object({ 341 - statusReportId: z.number(), 342 - monitorId: z.number(), 343 - monitor: selectMonitorSchema, 344 - }), 345 - ) 346 - .prefault([]), 347 - statusReportUpdates: z.array(selectStatusReportUpdateSchema), 348 - }); 349 - 350 - const result = await opts.ctx.db.query.statusReport.findMany({ 351 - where: and( 352 - eq(statusReport.workspaceId, opts.ctx.workspace.id), 353 - eq(statusReport.pageId, opts.input.id), 354 - ), 355 - with: { 356 - monitorsToStatusReports: { with: { monitor: true } }, 357 - statusReportUpdates: { 358 - orderBy: (statusReportUpdate, { desc }) => [ 359 - desc(statusReportUpdate.createdAt), 360 - ], 361 - }, 362 - }, 363 - orderBy: (statusReport, { desc }) => [desc(statusReport.updatedAt)], 364 - }); 365 - return z.array(selectStatusSchemaWithRelation).parse(result); 366 - }), 367 - 368 - getPublicStatusReportById: publicProcedure 369 - .input(z.object({ slug: z.string().toLowerCase(), id: z.number() })) 370 - .query(async (opts) => { 371 - const result = await opts.ctx.db.query.page.findFirst({ 372 - where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, 373 - }); 374 - 375 - if (!result) return; 376 - 377 - const _statusReport = await opts.ctx.db.query.statusReport.findFirst({ 378 - where: and( 379 - eq(statusReport.id, opts.input.id), 380 - eq(statusReport.pageId, result.id), 381 - eq(statusReport.workspaceId, result.workspaceId), 382 - ), 383 - with: { 384 - monitorsToStatusReports: { with: { monitor: true } }, 385 - statusReportUpdates: { 386 - orderBy: (reports, { desc }) => desc(reports.date), 387 - }, 388 - }, 389 - }); 390 - 391 - if (!_statusReport) return; 392 - 393 - return selectPublicStatusReportSchemaWithRelation.parse(_statusReport); 394 - }), 395 - 396 - // DASHBOARD 397 86 398 87 list: protectedProcedure 399 88 .input(
+33 -19
packages/api/src/router/sync.test.ts
··· 108 108 // Create test monitor using tRPC 109 109 const ctx = getTestContext(); 110 110 const caller = appRouter.createCaller(ctx); 111 - const createdMonitor = await caller.monitor.create(monitorData); 111 + const createdMonitor = await caller.monitor.new({ 112 + name: monitorData.name, 113 + url: monitorData.url, 114 + jobType: monitorData.jobType, 115 + method: monitorData.method, 116 + headers: [], 117 + assertions: [], 118 + active: false, 119 + skipCheck: true, 120 + }); 112 121 testMonitorId = createdMonitor.id; 113 122 114 123 const createdPageComponent = await db ··· 189 198 const caller = appRouter.createCaller(ctx); 190 199 191 200 // Update monitor to add it to the test page 192 - await caller.monitor.update({ 193 - ...monitorData, 201 + await caller.monitor.updateStatusPages({ 194 202 id: testMonitorId, 195 - pages: [testPageId], 203 + statusPages: [testPageId], 196 204 }); 197 205 198 206 // Verify monitors_to_pages was created ··· 221 229 const caller = appRouter.createCaller(ctx); 222 230 223 231 // First add the monitor to page if not already 224 - await caller.monitor.update({ 225 - ...monitorData, 232 + await caller.monitor.updateStatusPages({ 226 233 id: testMonitorId, 227 - pages: [testPageId], 234 + statusPages: [testPageId], 228 235 }); 229 236 230 237 // Verify page_component exists ··· 237 244 expect(component).toBeDefined(); 238 245 239 246 // Remove monitor from page 240 - await caller.monitor.update({ 241 - ...monitorData, 247 + await caller.monitor.updateStatusPages({ 242 248 id: testMonitorId, 243 - pages: [], 249 + statusPages: [], 244 250 }); 245 251 246 252 // Verify page_component was deleted ··· 357 363 const caller = appRouter.createCaller(ctx); 358 364 359 365 // Ensure monitor is on the page first 360 - await caller.monitor.update({ 361 - ...monitorData, 366 + await caller.monitor.updateStatusPages({ 362 367 id: testMonitorId, 363 - pages: [testPageId], 368 + statusPages: [testPageId], 364 369 }); 365 370 }); 366 371 ··· 475 480 const caller = appRouter.createCaller(ctx); 476 481 477 482 // Ensure monitor is on the page first 478 - await caller.monitor.update({ 479 - ...monitorData, 483 + await caller.monitor.updateStatusPages({ 480 484 id: testMonitorId, 481 - pages: [testPageId], 485 + statusPages: [testPageId], 482 486 }); 483 487 }); 484 488 ··· 591 595 const caller = appRouter.createCaller(ctx); 592 596 593 597 // Create a monitor specifically for deletion tests 594 - const deletableMonitor = await caller.monitor.create({ 595 - ...monitorData, 598 + const deletableMonitor = await caller.monitor.new({ 596 599 name: `${TEST_PREFIX}-deletable-monitor`, 597 600 url: "https://delete-test.example.com", 598 - pages: [testPageId], 601 + jobType: "http" as const, 602 + method: "GET" as const, 603 + headers: [], 604 + assertions: [], 605 + active: false, 606 + skipCheck: true, 599 607 }); 600 608 deletableMonitorId = deletableMonitor.id; 609 + 610 + // Add monitor to page 611 + await caller.monitor.updateStatusPages({ 612 + id: deletableMonitorId, 613 + statusPages: [testPageId], 614 + }); 601 615 }); 602 616 603 617 test("Deleting monitor removes related page_component entries", async () => {
-44
packages/api/src/router/workspace.test.ts
··· 62 62 endsAt: null, 63 63 }); 64 64 }); 65 - 66 - test("All workspaces", async () => { 67 - const ctx = createInnerTRPCContext({ 68 - req: undefined, 69 - session: { 70 - user: { 71 - id: "1", 72 - }, 73 - }, 74 - }); 75 - 76 - const caller = edgeRouter.createCaller(ctx); 77 - const result = await caller.workspace.getUserWithWorkspace(); 78 - expect(result).toMatchObject([ 79 - { 80 - createdAt: expect.any(Date), 81 - email: "ping@openstatus.dev", 82 - firstName: "Speed", 83 - id: 1, 84 - lastName: "Matters", 85 - photoUrl: "", 86 - tenantId: "1", 87 - updatedAt: expect.any(Date), 88 - usersToWorkspaces: [ 89 - { 90 - userId: 1, 91 - workspace: { 92 - createdAt: expect.any(Date), 93 - endsAt: null, 94 - id: 1, 95 - name: "test", 96 - paidUntil: null, 97 - plan: "team", 98 - slug: "love-openstatus", 99 - stripeId: "stripeId1", 100 - subscriptionId: "subscriptionId", 101 - updatedAt: expect.any(Date), 102 - }, 103 - workspaceId: 1, 104 - }, 105 - ], 106 - }, 107 - ]); 108 - });
+1 -243
packages/api/src/router/workspace.ts
··· 1 - import { TRPCError } from "@trpc/server"; 2 - import * as randomWordSlugs from "random-word-slugs"; 3 1 import { z } from "zod"; 4 2 5 3 import { Events } from "@openstatus/analytics"; 6 - import { type SQL, and, eq, gte, isNull, sql } from "@openstatus/db"; 4 + import { type SQL, and, eq, isNull } from "@openstatus/db"; 7 5 import { 8 - application, 9 6 monitor, 10 - monitorRun, 11 - notification, 12 - page, 13 - selectApplicationSchema, 14 - selectUserSchema, 15 7 selectWorkspaceSchema, 16 - user, 17 8 usersToWorkspaces, 18 9 workspace, 19 - workspacePlanSchema, 20 - workspaceRole, 21 10 } from "@openstatus/db/src/schema"; 22 - import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 23 11 24 12 import { createTRPCRouter, protectedProcedure } from "../trpc"; 25 13 26 14 export const workspaceRouter = createTRPCRouter({ 27 - createWorkspace: protectedProcedure 28 - .input(z.object({ userId: z.string() })) 29 - .mutation(async (opts) => { 30 - // guarantee the slug is unique accross our workspace entries 31 - let slug: string | undefined = undefined; 32 - 33 - while (!slug) { 34 - slug = randomWordSlugs.generateSlug(2); 35 - const slugAlreadyExists = await opts.ctx.db 36 - .select() 37 - .from(workspace) 38 - .where(eq(workspace.slug, slug)) 39 - .get(); 40 - if (slugAlreadyExists) { 41 - console.log(`slug already exists: '${slug}'`); 42 - slug = undefined; 43 - } 44 - } 45 - 46 - const _workspace = await opts.ctx.db 47 - .insert(workspace) 48 - .values({ slug, name: "" }) 49 - .returning({ id: workspace.id }) 50 - .get(); 51 - 52 - await opts.ctx.db 53 - .insert(usersToWorkspaces) 54 - .values({ 55 - userId: opts.ctx.user.id, 56 - workspaceId: _workspace.id, 57 - role: "owner", 58 - }) 59 - .returning() 60 - .get(); 61 - }), 62 - getUserWithWorkspace: protectedProcedure.query(async (opts) => { 63 - return await opts.ctx.db.query.user.findMany({ 64 - with: { 65 - usersToWorkspaces: { 66 - with: { 67 - workspace: true, 68 - }, 69 - }, 70 - }, 71 - where: eq(user.id, opts.ctx.user.id), 72 - }); 73 - }), 74 - 75 15 getWorkspace: protectedProcedure.query(async (opts) => { 76 16 const result = await opts.ctx.db.query.workspace.findFirst({ 77 17 where: eq(workspace.id, opts.ctx.workspace.id), ··· 80 20 return selectWorkspaceSchema.parse(result); 81 21 }), 82 22 83 - getApplicationWorkspaces: protectedProcedure.query(async (opts) => { 84 - const result = await opts.ctx.db.query.application.findMany({ 85 - where: eq(application.workspaceId, opts.ctx.workspace.id), 86 - }); 87 - 88 - return selectApplicationSchema.array().parse(result); 89 - }), 90 - 91 - getUserWorkspaces: protectedProcedure.query(async (opts) => { 92 - const result = await opts.ctx.db.query.usersToWorkspaces.findMany({ 93 - where: eq(usersToWorkspaces.userId, opts.ctx.user.id), 94 - with: { 95 - workspace: true, 96 - }, 97 - }); 98 - 99 - return selectWorkspaceSchema 100 - .array() 101 - .parse(result.map(({ workspace }) => workspace)); 102 - }), 103 - 104 - getWorkspaceUsers: protectedProcedure.query(async (opts) => { 105 - const result = await opts.ctx.db.query.usersToWorkspaces.findMany({ 106 - with: { 107 - user: true, 108 - }, 109 - where: eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 110 - }); 111 - return result; 112 - }), 113 - 114 - updateWorkspace: protectedProcedure 115 - .meta({ track: Events.UpdateWorkspace }) 116 - .input(z.object({ name: z.string() })) 117 - .mutation(async (opts) => { 118 - return await opts.ctx.db 119 - .update(workspace) 120 - .set({ name: opts.input.name, updatedAt: new Date() }) 121 - .where(eq(workspace.id, opts.ctx.workspace.id)); 122 - }), 123 - 124 - removeWorkspaceUser: protectedProcedure 125 - .input(z.object({ id: z.number() })) 126 - .mutation(async (opts) => { 127 - const _userToWorkspace = 128 - await opts.ctx.db.query.usersToWorkspaces.findFirst({ 129 - where: and( 130 - eq(usersToWorkspaces.userId, opts.ctx.user.id), 131 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 132 - ), 133 - }); 134 - 135 - if (!_userToWorkspace) throw new Error("No user to workspace found"); 136 - 137 - if (!["owner"].includes(_userToWorkspace.role)) 138 - throw new Error("Not authorized to remove user from workspace"); 139 - 140 - if (opts.input.id === opts.ctx.user.id) 141 - throw new Error("Cannot remove yourself from workspace"); 142 - 143 - await opts.ctx.db 144 - .delete(usersToWorkspaces) 145 - .where( 146 - and( 147 - eq(usersToWorkspaces.userId, opts.input.id), 148 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 149 - ), 150 - ) 151 - .run(); 152 - }), 153 - 154 - changePlan: protectedProcedure 155 - .input(z.object({ plan: workspacePlanSchema })) 156 - .mutation(async (opts) => { 157 - const _userToWorkspace = 158 - await opts.ctx.db.query.usersToWorkspaces.findFirst({ 159 - where: and( 160 - eq(usersToWorkspaces.userId, opts.ctx.user.id), 161 - eq(usersToWorkspaces.workspaceId, opts.ctx.workspace.id), 162 - ), 163 - }); 164 - 165 - if (!_userToWorkspace) throw new Error("No user to workspace found"); 166 - 167 - if (!["owner"].includes(_userToWorkspace.role)) 168 - throw new TRPCError({ 169 - code: "PRECONDITION_FAILED", 170 - message: "Not authorized to change plan", 171 - }); 172 - 173 - if (!opts.ctx.workspace.stripeId) { 174 - throw new TRPCError({ 175 - code: "PRECONDITION_FAILED", 176 - message: "No Stripe ID found for workspace", 177 - }); 178 - } 179 - 180 - // TODO: Create subscription 181 - switch (opts.input.plan) { 182 - case "free": { 183 - break; 184 - } 185 - case "starter": { 186 - break; 187 - } 188 - case "team": { 189 - break; 190 - } 191 - default: { 192 - } 193 - } 194 - 195 - await opts.ctx.db 196 - .update(workspace) 197 - .set({ plan: opts.input.plan, updatedAt: new Date() }) 198 - .where(eq(workspace.id, opts.ctx.workspace.id)); 199 - }), 200 - 201 - getCurrentWorkspaceNumbers: protectedProcedure.query(async (opts) => { 202 - const lastMonth = new Date().setMonth(new Date().getMonth() - 1); 203 - 204 - const currentNumbers = await opts.ctx.db.transaction(async (tx) => { 205 - const notifications = await tx 206 - .select({ count: sql<number>`count(*)` }) 207 - .from(notification) 208 - .where(eq(notification.workspaceId, opts.ctx.workspace.id)); 209 - const monitors = await tx 210 - .select({ count: sql<number>`count(*)` }) 211 - .from(monitor) 212 - .where( 213 - and( 214 - eq(monitor.workspaceId, opts.ctx.workspace.id), 215 - isNull(monitor.deletedAt), 216 - ), 217 - ); 218 - const pages = await tx 219 - .select({ count: sql<number>`count(*)` }) 220 - .from(page) 221 - .where(eq(page.workspaceId, opts.ctx.workspace.id)); 222 - 223 - const runs = await tx 224 - .select({ count: sql<number>`count(*)` }) 225 - .from(monitorRun) 226 - .where( 227 - and( 228 - eq(monitorRun.workspaceId, opts.ctx.workspace.id), 229 - gte(monitorRun.createdAt, new Date(lastMonth)), 230 - ), 231 - ) 232 - .all(); 233 - 234 - return { 235 - "notification-channels": notifications?.[0].count || 0, 236 - monitors: monitors?.[0].count || 0, 237 - "status-pages": pages?.[0].count || 0, 238 - "synthetic-checks": runs?.[0].count || 0, 239 - } satisfies Partial<Limits>; 240 - }); 241 - 242 - return currentNumbers; 243 - }), 244 - 245 - // DASHBOARD 246 - 247 23 get: protectedProcedure.query(async (opts) => { 248 24 const whereConditions: SQL[] = [eq(workspace.id, opts.ctx.workspace.id)]; 249 25 ··· 268 44 checks: 0, 269 45 }, 270 46 }); 271 - }), 272 - 273 - getMembers: protectedProcedure.query(async (opts) => { 274 - const result = await opts.ctx.db.query.usersToWorkspaces.findMany({ 275 - where: eq(usersToWorkspaces.userId, opts.ctx.workspace.id), 276 - with: { 277 - user: true, 278 - }, 279 - }); 280 - 281 - return z 282 - .object({ 283 - role: z.enum(workspaceRole), 284 - createdAt: z.coerce.date(), 285 - user: selectUserSchema, 286 - }) 287 - .array() 288 - .parse(result); 289 47 }), 290 48 291 49 list: protectedProcedure.query(async (opts) => {