Openstatus www.openstatus.dev

feat: advanced monitor form (#210)

* wip: form component

* 🚧 wip

* 🔥

* 🔥

* fire

* 🟢

* 🟢

* chor: test and clean up

---------

Co-authored-by: Thibault Le Ouay <thibaultleouay@gmail.Com>

authored by

Maximilian Kaske
Thibault Le Ouay
and committed by
GitHub
7d0a5b0c 2edf121f

+1347 -165
+1 -1
apps/web/.env.example
··· 29 29 30 30 # JITSU - no need to touch on local development 31 31 JITSU_HOST="https://your-jitsu-domain.com" 32 - JITSU_WRITE_KEY="your-jitsu-write-key" 32 + JITSU_WRITE_KEY="jitsu-key:jitsu-secret" 33 33 34 34 # Solves 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', see https://github.com/nextauthjs/next-auth/issues/3580 35 35 # NODE_TLS_REJECT_UNAUTHORIZED="0"
+1 -1
apps/web/src/app/api/checker/cron/10m/route.ts
··· 8 8 9 9 export async function GET(req: Request) { 10 10 if (isAuthorizedDomain(req.url)) { 11 - await cron({ periodicity: "10m" }); 11 + await cron({ periodicity: "10m", req }); 12 12 } 13 13 return NextResponse.json({ success: true }); 14 14 }
+1 -1
apps/web/src/app/api/checker/cron/1h/route.ts
··· 8 8 9 9 export async function GET(req: Request) { 10 10 if (isAuthorizedDomain(req.url)) { 11 - await cron({ periodicity: "1h" }); 11 + await cron({ periodicity: "1h", req }); 12 12 } 13 13 return NextResponse.json({ success: true }); 14 14 }
+1 -1
apps/web/src/app/api/checker/cron/1m/route.ts
··· 8 8 9 9 export async function GET(req: Request) { 10 10 if (isAuthorizedDomain(req.url)) { 11 - await cron({ periodicity: "1m" }); 11 + await cron({ periodicity: "1m", req }); 12 12 } 13 13 return NextResponse.json({ success: true }); 14 14 }
+1 -1
apps/web/src/app/api/checker/cron/30m/route.ts
··· 8 8 9 9 export async function GET(req: Request) { 10 10 if (isAuthorizedDomain(req.url)) { 11 - await cron({ periodicity: "30m" }); 11 + await cron({ periodicity: "30m", req }); 12 12 } 13 13 return NextResponse.json({ success: true }); 14 14 }
+1 -1
apps/web/src/app/api/checker/cron/5m/route.ts
··· 8 8 9 9 export async function GET(req: Request) { 10 10 if (isAuthorizedDomain(req.url)) { 11 - await cron({ periodicity: "5m" }); 11 + await cron({ periodicity: "5m", req }); 12 12 } 13 13 return NextResponse.json({ success: true }); 14 14 }
+29 -25
apps/web/src/app/api/checker/cron/_cron.ts
··· 1 + import type { SignedInAuthObject } from "@clerk/nextjs/api"; 1 2 import { Client } from "@upstash/qstash/cloudflare"; 2 - import { z } from "zod"; 3 + import type { z } from "zod"; 3 4 4 - import { and, db, eq } from "@openstatus/db"; 5 - import { 6 - monitor, 7 - monitorsToPages, 8 - selectMonitorSchema, 9 - } from "@openstatus/db/src/schema"; 5 + import { createTRPCContext } from "@openstatus/api"; 6 + import { edgeRouter } from "@openstatus/api/src/edge"; 7 + import { selectMonitorSchema } from "@openstatus/db/src/schema"; 10 8 import { availableRegions } from "@openstatus/tinybird"; 11 9 12 10 import { env } from "@/env"; ··· 15 13 const periodicityAvailable = selectMonitorSchema.pick({ periodicity: true }); 16 14 17 15 // FIXME: do coerce in zod instead 18 - const currentRegions = z.string().transform((val) => val.split(",")); 19 16 20 17 const DEFAULT_URL = process.env.VERCEL_URL 21 18 ? `https://${process.env.VERCEL_URL}` ··· 28 25 29 26 export const cron = async ({ 30 27 periodicity, 31 - }: z.infer<typeof periodicityAvailable>) => { 28 + req, 29 + }: z.infer<typeof periodicityAvailable> & { req: Request }) => { 32 30 const c = new Client({ 33 31 token: env.QSTASH_TOKEN, 34 32 }); 35 - console.info(`Start cron for ${periodicity}`); 33 + console.log(`Start cron for ${periodicity}`); 36 34 const timestamp = Date.now(); 37 - // FIXME: Wait until db is ready 38 - const result = await db 39 - .select() 40 - .from(monitor) 41 - .where(and(eq(monitor.periodicity, periodicity), eq(monitor.active, true))) 42 - .all(); 35 + 36 + const ctx = createTRPCContext({ req, serverSideCall: true }); 37 + ctx.auth = { userId: "cron" } as SignedInAuthObject; 38 + const caller = edgeRouter.createCaller(ctx); 39 + 40 + const monitors = await caller.monitor.getMonitorsForPeriodicity({ 41 + periodicity: periodicity, 42 + }); 43 43 44 44 const allResult = []; 45 45 46 - for (const row of result) { 47 - // could be improved with a single query 48 - const allPages = await db 49 - .select() 50 - .from(monitorsToPages) 51 - .where(eq(monitorsToPages.monitorId, row.id)) 52 - .all(); 46 + for (const row of monitors) { 47 + const allPages = await caller.monitor.getAllPagesForMonitor({ 48 + monitorId: row.id, 49 + }); 53 50 54 51 if (row.regions.length === 0) { 55 52 const payload: z.infer<typeof payloadSchema> = { 56 53 workspaceId: String(row.workspaceId), 54 + method: row.method || "GET", 57 55 monitorId: String(row.id), 58 56 url: row.url, 57 + headers: row.headers, 58 + body: row.body, 59 59 cronTimestamp: timestamp, 60 60 pageIds: allPages.map((p) => String(p.pageId)), 61 61 }; ··· 68 68 }); 69 69 allResult.push(result); 70 70 } else { 71 - const allMonitorsRegions = currentRegions.parse(row.regions); 71 + const allMonitorsRegions = row.regions; 72 72 for (const region of allMonitorsRegions) { 73 73 const payload: z.infer<typeof payloadSchema> = { 74 74 workspaceId: String(row.workspaceId), 75 75 monitorId: String(row.id), 76 76 url: row.url, 77 + method: row.method || "GET", 77 78 cronTimestamp: timestamp, 79 + body: row.body, 80 + headers: row.headers, 78 81 pageIds: allPages.map((p) => String(p.pageId)), 79 82 }; 80 83 ··· 95 98 monitorId: "openstatusPing", 96 99 url: `${DEFAULT_URL}/api/ping`, 97 100 cronTimestamp: timestamp, 101 + method: "GET", 98 102 pageIds: ["openstatus"], 99 103 }; 100 104 ··· 108 112 } 109 113 } 110 114 await Promise.all(allResult); 111 - console.info(`End cron for ${periodicity} with ${allResult.length} jobs`); 115 + console.log(`End cron for ${periodicity} with ${allResult.length} jobs`); 112 116 };
+11 -1
apps/web/src/app/api/checker/regions/_checker.ts
··· 74 74 const result = payloadSchema.safeParse(jsonData); 75 75 76 76 if (!result.success) { 77 + console.error(result.error); 77 78 throw new Error("Invalid response body"); 78 79 } 79 80 81 + const headers = 82 + result.data?.headers?.reduce((o, v) => ({ ...o, [v.key]: v.value }), {}) || 83 + {}; 84 + 80 85 try { 81 86 const startTime = Date.now(); 82 - const res = await fetch(result.data.url, { cache: "no-store" }); 87 + const res = await fetch(result.data?.url, { 88 + method: result.data?.method, 89 + cache: "no-store", 90 + headers: { ...headers }, 91 + body: result.data?.body, 92 + }); 83 93 84 94 const endTime = Date.now(); 85 95 const latency = endTime - startTime;
+5
apps/web/src/app/api/checker/schema.ts
··· 1 1 import { z } from "zod"; 2 2 3 + import { METHODS } from "@openstatus/db/src/schema"; 4 + 3 5 export const payloadSchema = z.object({ 4 6 workspaceId: z.string(), 5 7 monitorId: z.string(), 8 + method: z.enum(METHODS), 9 + body: z.string().optional(), 10 + headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(), 6 11 url: z.string(), 7 12 cronTimestamp: z.number(), 8 13 pageIds: z.array(z.string()),
+5
apps/web/src/app/api/ping/route.ts
··· 5 5 export async function GET() { 6 6 return NextResponse.json({ ping: "pong" }, { status: 200 }); 7 7 } 8 + 9 + export async function POST(req: Request) { 10 + const body = await req.json(); 11 + return NextResponse.json({ ping: body }, { status: 200 }); 12 + }
+32 -6
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/edit/page.tsx
··· 2 2 import * as z from "zod"; 3 3 4 4 import { Header } from "@/components/dashboard/header"; 5 + import { AdvancedMonitorForm } from "@/components/forms/advanced-monitor-form"; 5 6 import { MonitorForm } from "@/components/forms/montitor-form"; 7 + import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 6 8 import { api } from "@/trpc/server"; 7 9 8 10 /** ··· 28 30 const { id } = search.data; 29 31 30 32 const monitor = id && (await api.monitor.getMonitorByID.query({ id })); 31 - 32 33 const workspace = await api.workspace.getWorkspace.query({ 33 34 slug: params.workspaceSlug, 34 35 }); ··· 37 38 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 38 39 <Header title="Monitor" description="Upsert your monitor." /> 39 40 <div className="col-span-full"> 40 - <MonitorForm 41 - workspaceSlug={params.workspaceSlug} 42 - defaultValues={monitor || undefined} 43 - plan={workspace?.plan} 44 - /> 41 + <Tabs defaultValue="settings" className="relative mr-auto w-full"> 42 + <TabsList className="h-9 w-full justify-start rounded-none border-b bg-transparent p-0"> 43 + <TabsTrigger 44 + className="text-muted-foreground data-[state=active]:border-b-primary data-[state=active]:text-foreground relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold shadow-none transition-none data-[state=active]:shadow-none" 45 + value="settings" 46 + > 47 + Settings 48 + </TabsTrigger> 49 + <TabsTrigger 50 + className="text-muted-foreground data-[state=active]:border-b-primary data-[state=active]:text-foreground relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold shadow-none transition-none data-[state=active]:shadow-none" 51 + value="advanced" 52 + disabled={!monitor} 53 + > 54 + Advanced 55 + </TabsTrigger> 56 + </TabsList> 57 + <TabsContent value="settings" className="pt-3"> 58 + <MonitorForm 59 + workspaceSlug={params.workspaceSlug} 60 + defaultValues={monitor || undefined} 61 + plan={workspace?.plan} 62 + /> 63 + </TabsContent> 64 + <TabsContent value="advanced" className="pt-3"> 65 + <AdvancedMonitorForm 66 + workspaceSlug={params.workspaceSlug} 67 + defaultValues={monitor || undefined} 68 + /> 69 + </TabsContent> 70 + </Tabs> 45 71 </div> 46 72 </div> 47 73 );
+251
apps/web/src/components/forms/advanced-monitor-form.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { useRouter, useSearchParams } from "next/navigation"; 5 + import { zodResolver } from "@hookform/resolvers/zod"; 6 + import { Wand2, X } from "lucide-react"; 7 + import { useFieldArray, useForm } from "react-hook-form"; 8 + import * as z from "zod"; 9 + 10 + import { Button } from "@/components/ui/button"; 11 + import { 12 + Form, 13 + FormControl, 14 + FormDescription, 15 + FormField, 16 + FormItem, 17 + FormLabel, 18 + FormMessage, 19 + } from "@/components/ui/form"; 20 + import { Input } from "@/components/ui/input"; 21 + import { 22 + Select, 23 + SelectContent, 24 + SelectItem, 25 + SelectTrigger, 26 + SelectValue, 27 + } from "@/components/ui/select"; 28 + import { Textarea } from "@/components/ui/textarea"; 29 + import { 30 + Tooltip, 31 + TooltipContent, 32 + TooltipProvider, 33 + TooltipTrigger, 34 + } from "@/components/ui/tooltip"; 35 + import { api } from "@/trpc/client"; 36 + import { LoadingAnimation } from "../loading-animation"; 37 + 38 + /** 39 + * GET requests don't need a body, we could think of using a z.discriminatedUnion? 40 + * https://github.com/colinhacks/zod#discriminated-unions 41 + */ 42 + 43 + const methods = ["POST", "GET"] as const; 44 + const methodsEnum = z.enum(methods); 45 + 46 + const advancedSchema = z.object({ 47 + headers: z.array(z.object({ key: z.string(), value: z.string() })).optional(), 48 + body: z.string().optional(), 49 + method: methodsEnum.default("GET"), 50 + }); // insertMonitorSchema.pick() 51 + 52 + type AdvancedMonitorProps = z.infer<typeof advancedSchema>; 53 + 54 + interface Props { 55 + defaultValues?: AdvancedMonitorProps; 56 + workspaceSlug: string; 57 + } 58 + 59 + export function AdvancedMonitorForm({ defaultValues, workspaceSlug }: Props) { 60 + const form = useForm<AdvancedMonitorProps>({ 61 + resolver: zodResolver(advancedSchema), 62 + defaultValues: { 63 + headers: defaultValues?.headers ?? [{ key: "", value: "" }], 64 + body: defaultValues?.body ?? "", 65 + method: defaultValues?.method ?? "GET", 66 + }, 67 + }); 68 + const router = useRouter(); 69 + const searchParams = useSearchParams(); 70 + const monitorId = searchParams.get("id"); 71 + console.log(defaultValues); 72 + const [isPending, startTransition] = React.useTransition(); 73 + 74 + const { fields, append, remove } = useFieldArray({ 75 + name: "headers", 76 + control: form.control, 77 + }); 78 + 79 + const onSubmit = ({ ...props }: AdvancedMonitorProps) => { 80 + startTransition(async () => { 81 + console.log(props); 82 + if (!monitorId) return; 83 + if (validateJSON(props.body) === false) return; 84 + await api.monitor.updateMonitorAdvanced.mutate({ 85 + id: Number(monitorId), 86 + ...props, 87 + }); 88 + router.refresh(); 89 + // router.push("./"); // TODO: we need a better UX flow here. 90 + }); 91 + }; 92 + 93 + const validateJSON = (value?: string) => { 94 + if (!value) return; 95 + try { 96 + const obj = JSON.parse(value) as Record<string, unknown>; 97 + form.clearErrors("body"); 98 + return obj; 99 + } catch (e) { 100 + form.setError("body", { 101 + message: "Not a valid JSON object", 102 + }); 103 + return false; 104 + } 105 + }; 106 + 107 + const onPrettifyJSON = () => { 108 + const body = form.getValues("body"); 109 + const obj = validateJSON(body); 110 + if (obj) { 111 + const pretty = JSON.stringify(obj, undefined, 4); 112 + form.setValue("body", pretty); 113 + } 114 + }; 115 + 116 + console.log(form.formState.errors); 117 + 118 + return ( 119 + <Form {...form}> 120 + <form 121 + onSubmit={form.handleSubmit(onSubmit)} 122 + className="grid w-full grid-cols-1 items-center gap-6 sm:grid-cols-6" 123 + > 124 + <div className="space-y-2 sm:col-span-full"> 125 + <FormLabel>Request Header</FormLabel> 126 + {/* TODO: add FormDescription for latest key/value */} 127 + {fields.map((field, index) => ( 128 + <div key={field.id} className="grid grid-cols-6 gap-6"> 129 + <FormField 130 + control={form.control} 131 + name={`headers.${index}.key`} 132 + render={({ field }) => ( 133 + <FormItem className="col-span-2"> 134 + <FormControl> 135 + <Input placeholder="key" {...field} /> 136 + </FormControl> 137 + </FormItem> 138 + )} 139 + /> 140 + <div className="col-span-4 flex items-center space-x-2"> 141 + <FormField 142 + control={form.control} 143 + name={`headers.${index}.value`} 144 + render={({ field }) => ( 145 + <FormItem className="w-full"> 146 + <FormControl> 147 + <Input placeholder="value" {...field} /> 148 + </FormControl> 149 + </FormItem> 150 + )} 151 + /> 152 + <Button 153 + size="icon" 154 + variant="ghost" 155 + type="button" 156 + onClick={() => remove(Number(field.id))} 157 + > 158 + <X className="h-4 w-4" /> 159 + </Button> 160 + </div> 161 + </div> 162 + ))} 163 + <div> 164 + <Button 165 + type="button" 166 + variant="outline" 167 + size="sm" 168 + onClick={() => append({ key: "", value: "" })} 169 + > 170 + Add Custom Header 171 + </Button> 172 + </div> 173 + </div> 174 + <FormField 175 + control={form.control} 176 + name="method" 177 + render={({ field }) => ( 178 + <FormItem className="sm:col-span-2 sm:col-start-1 sm:self-baseline"> 179 + <FormLabel>Method</FormLabel> 180 + <Select 181 + onValueChange={(value) => 182 + field.onChange(methodsEnum.parse(value)) 183 + } 184 + defaultValue={field.value} 185 + > 186 + <FormControl> 187 + <SelectTrigger> 188 + <SelectValue placeholder="Select" /> 189 + </SelectTrigger> 190 + </FormControl> 191 + <SelectContent> 192 + {methods.map((method) => ( 193 + <SelectItem key={method} value={method}> 194 + {method} 195 + </SelectItem> 196 + ))} 197 + </SelectContent> 198 + </Select> 199 + <FormDescription>What method to use?</FormDescription> 200 + <FormMessage /> 201 + </FormItem> 202 + )} 203 + /> 204 + <div className="sm:col-span-4 sm:col-start-1"> 205 + <FormField 206 + control={form.control} 207 + name="body" 208 + render={({ field }) => ( 209 + <FormItem> 210 + <div className="flex items-end justify-between"> 211 + <FormLabel>Body</FormLabel> 212 + <TooltipProvider> 213 + <Tooltip> 214 + <TooltipTrigger asChild> 215 + <Button 216 + type="button" 217 + variant="ghost" 218 + size="icon" 219 + onClick={onPrettifyJSON} 220 + > 221 + <Wand2 className="h-4 w-4" /> 222 + </Button> 223 + </TooltipTrigger> 224 + <TooltipContent> 225 + <p>Prettify JSON</p> 226 + </TooltipContent> 227 + </Tooltip> 228 + </TooltipProvider> 229 + </div> 230 + <FormControl> 231 + <Textarea 232 + rows={8} 233 + placeholder='{ "hello": "world" }' 234 + {...field} 235 + /> 236 + </FormControl> 237 + <FormDescription>Write your json payload.</FormDescription> 238 + <FormMessage /> 239 + </FormItem> 240 + )} 241 + /> 242 + </div> 243 + <div className="sm:col-span-full"> 244 + <Button className="w-full sm:w-auto"> 245 + {!isPending ? "Confirm" : <LoadingAnimation />} 246 + </Button> 247 + </div> 248 + </form> 249 + </Form> 250 + ); 251 + }
+2 -2
apps/web/src/components/forms/incident-form.tsx
··· 6 6 import { useForm } from "react-hook-form"; 7 7 import * as z from "zod"; 8 8 9 - import type { allMonitorsSchema } from "@openstatus/db/src/schema"; 9 + import type { allMonitorsExtendedSchema } from "@openstatus/db/src/schema"; 10 10 import { 11 11 availableStatus, 12 12 insertIncidentSchema, ··· 43 43 }); 44 44 45 45 type IncidentProps = z.infer<typeof insertSchema>; 46 - type MonitorsProps = z.infer<typeof allMonitorsSchema>; 46 + type MonitorsProps = z.infer<typeof allMonitorsExtendedSchema>; 47 47 48 48 interface Props { 49 49 defaultValues?: IncidentProps;
+2 -2
apps/web/src/components/forms/status-page-form.tsx
··· 9 9 import { useForm } from "react-hook-form"; 10 10 import type * as z from "zod"; 11 11 12 - import type { allMonitorsSchema } from "@openstatus/db/src/schema"; 12 + import type { allMonitorsExtendedSchema } from "@openstatus/db/src/schema"; 13 13 import { insertPageSchemaWithMonitors } from "@openstatus/db/src/schema"; 14 14 15 15 import { Button } from "@/components/ui/button"; ··· 37 37 interface Props { 38 38 defaultValues?: Schema; 39 39 workspaceSlug: string; 40 - allMonitors?: z.infer<typeof allMonitorsSchema>; 40 + allMonitors?: z.infer<typeof allMonitorsExtendedSchema>; 41 41 } 42 42 43 43 export function StatusPageForm({
+1 -1
packages/analytics/.env.example
··· 1 1 JITSU_HOST="https://your-jitsu-domain.com" 2 - JITSU_WRITE_KEY="your-jitsu-write-key" 2 + JITSU_WRITE_KEY="jitsu-key:jitsu-secret"
+103 -118
packages/api/src/router/monitor.ts
··· 2 2 import { z } from "zod"; 3 3 4 4 import { analytics, trackAnalytics } from "@openstatus/analytics"; 5 - import { eq } from "@openstatus/db"; 5 + import { and, eq } from "@openstatus/db"; 6 6 import { 7 + allMonitorsExtendedSchema, 7 8 allMonitorsSchema, 8 9 insertMonitorSchema, 10 + METHODS, 9 11 monitor, 12 + monitorsToPages, 13 + periodicityEnum, 14 + selectMonitorExtendedSchema, 10 15 selectMonitorSchema, 11 - user, 12 - usersToWorkspaces, 13 - workspace, 14 16 } from "@openstatus/db/src/schema"; 15 17 import { allPlans } from "@openstatus/plans"; 16 18 17 19 import { createTRPCRouter, protectedProcedure } from "../trpc"; 18 - import { hasUserAccessToWorkspace } from "./utils"; 20 + import { hasUserAccessToMonitor, hasUserAccessToWorkspace } from "./utils"; 19 21 20 22 export const monitorRouter = createTRPCRouter({ 21 23 createMonitor: protectedProcedure ··· 55 57 message: "You reached your cron job limits.", 56 58 }); 57 59 } 58 - const { regions, ...data } = opts.input.data; 60 + // FIXME: this is a hotfix 61 + const { regions, headers, ...data } = opts.input.data; 59 62 60 63 const newMonitor = await opts.ctx.db 61 64 .insert(monitor) ··· 79 82 80 83 getMonitorByID: protectedProcedure 81 84 .input(z.object({ id: z.number() })) 85 + // .output(selectMonitorSchema) 82 86 .query(async (opts) => { 83 - const currentMonitor = await opts.ctx.db.query.monitor.findFirst({ 84 - where: eq(monitor.id, opts.input.id), 87 + if (!opts.input.id) return; 88 + const result = await hasUserAccessToMonitor({ 89 + monitorId: opts.input.id, 90 + ctx: opts.ctx, 85 91 }); 86 - 87 - if (!currentMonitor || !currentMonitor.workspaceId) return; 88 - // We make sure user as access to this workspace 89 - const currentUser = opts.ctx.db 90 - .select() 91 - .from(user) 92 - .where(eq(user.tenantId, opts.ctx.auth.userId)) 93 - .as("currentUser"); 94 - const result = await opts.ctx.db 95 - .select() 96 - .from(usersToWorkspaces) 97 - .where(eq(usersToWorkspaces.workspaceId, currentMonitor.workspaceId)) 98 - .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id)) 99 - .get(); 92 + if (!result) return; 100 93 101 - if (!result || !result.users_to_workspaces) return; 102 - const _monitor = selectMonitorSchema.parse(currentMonitor); 94 + const _monitor = selectMonitorExtendedSchema.parse(result.monitor); 95 + _monitor.headers; 103 96 return _monitor; 104 97 }), 105 98 ··· 113 106 ) 114 107 .mutation(async (opts) => { 115 108 // We make sure user as access to this workspace and the monitor 116 - const currentMonitor = await opts.ctx.db 117 - .select() 118 - .from(monitor) 119 - .where(eq(monitor.id, opts.input.id)) 120 - .get(); 121 - 122 - if (!currentMonitor || !currentMonitor.workspaceId) return; 123 - 124 - const currentUser = opts.ctx.db 125 - .select() 126 - .from(user) 127 - .where(eq(user.tenantId, opts.ctx.auth.userId)) 128 - .as("currentUser"); 129 - const result = await opts.ctx.db 130 - .select() 131 - .from(usersToWorkspaces) 132 - .where(eq(usersToWorkspaces.workspaceId, currentMonitor.workspaceId)) 133 - .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id)) 134 - .get(); 135 - 136 - if (!result || !result.users_to_workspaces) return; 109 + if (!opts.input.id) return; 110 + const result = await hasUserAccessToMonitor({ 111 + monitorId: opts.input.id, 112 + ctx: opts.ctx, 113 + }); 114 + if (!result) return; 137 115 138 116 await opts.ctx.db 139 117 .update(monitor) ··· 145 123 .input(insertMonitorSchema) 146 124 .mutation(async (opts) => { 147 125 if (!opts.input.id) return; 148 - const currentMonitor = await opts.ctx.db 149 - .select() 150 - .from(monitor) 151 - .where(eq(monitor.id, opts.input.id)) 152 - .get(); 153 - if (!currentMonitor || !currentMonitor.workspaceId) return; 154 - 155 - // TODO: we should use hasUserAccess and pass `workspaceId` instead of `workspaceSlug` 156 - const currentUser = opts.ctx.db 157 - .select() 158 - .from(user) 159 - .where(eq(user.tenantId, opts.ctx.auth.userId)) 160 - .as("currentUser"); 161 - 162 - const result = await opts.ctx.db 163 - .select() 164 - .from(usersToWorkspaces) 165 - .where(eq(usersToWorkspaces.workspaceId, currentMonitor.workspaceId)) 166 - .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id)) 167 - .get(); 168 - 169 - if (!result || !result.users_to_workspaces) return; 170 - 171 - const currentWorkspace = await opts.ctx.db.query.workspace.findFirst({ 172 - where: eq(workspace.id, result.users_to_workspaces.workspaceId), 126 + const result = await hasUserAccessToMonitor({ 127 + monitorId: opts.input.id, 128 + ctx: opts.ctx, 173 129 }); 130 + if (!result) return; 174 131 175 - if (!currentWorkspace) return; 176 - 177 - const plan = (currentWorkspace?.plan || "free") as "free" | "pro"; 132 + const plan = (result.workspace?.plan || "free") as "free" | "pro"; 178 133 179 134 const periodicityLimit = allPlans[plan].limits.periodicity; 180 135 ··· 188 143 message: "You reached your cron job limits.", 189 144 }); 190 145 } 191 - console.log(opts.input.regions?.join(",")); 192 - const { regions, ...data } = opts.input; 146 + const { regions, headers, ...data } = opts.input; 193 147 await opts.ctx.db 194 148 .update(monitor) 195 149 .set({ ...data, regions: regions?.join(","), updatedAt: new Date() }) ··· 206 160 }), 207 161 ) 208 162 .mutation(async (opts) => { 209 - const currentMonitor = await opts.ctx.db 210 - .select() 211 - .from(monitor) 212 - .where(eq(monitor.id, opts.input.id)) 213 - .get(); 214 - if (!currentMonitor || !currentMonitor.workspaceId) return; 215 - 216 - const currentUser = opts.ctx.db 217 - .select() 218 - .from(user) 219 - .where(eq(user.tenantId, opts.ctx.auth.userId)) 220 - .as("currentUser"); 221 - const result = await opts.ctx.db 222 - .select() 223 - .from(usersToWorkspaces) 224 - .where(eq(usersToWorkspaces.workspaceId, currentMonitor.workspaceId)) 225 - .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id)) 226 - .get(); 227 - 228 - if (!result?.users_to_workspaces) return; 163 + const result = await hasUserAccessToMonitor({ 164 + monitorId: opts.input.id, 165 + ctx: opts.ctx, 166 + }); 167 + if (!result) return; 229 168 230 169 await opts.ctx.db 231 170 .update(monitor) ··· 240 179 deleteMonitor: protectedProcedure 241 180 .input(z.object({ id: z.number() })) 242 181 .mutation(async (opts) => { 243 - const currentMonitor = await opts.ctx.db 244 - .select() 245 - .from(monitor) 246 - .where(eq(monitor.id, opts.input.id)) 247 - .get(); 248 - if (!currentMonitor || !currentMonitor.workspaceId) return; 249 - 250 - const currentUser = opts.ctx.db 251 - .select() 252 - .from(user) 253 - .where(eq(user.tenantId, opts.ctx.auth.userId)) 254 - .as("currentUser"); 255 - const result = await opts.ctx.db 256 - .select() 257 - .from(usersToWorkspaces) 258 - .where(eq(usersToWorkspaces.workspaceId, currentMonitor.workspaceId)) 259 - .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id)) 260 - .get(); 261 - 262 - if (!result || !result.users_to_workspaces) return; 263 - 182 + const result = await hasUserAccessToMonitor({ 183 + monitorId: opts.input.id, 184 + ctx: opts.ctx, 185 + }); 186 + if (!result) return; 264 187 await opts.ctx.db 265 188 .delete(monitor) 266 - .where(eq(monitor.id, currentMonitor.id)) 189 + .where(eq(monitor.id, result.monitor.id)) 267 190 .run(); 268 191 }), 269 192 getMonitorsByWorkspace: protectedProcedure ··· 283 206 .all(); 284 207 // const selectMonitorsArray = selectMonitorSchema.array(); 285 208 286 - return allMonitorsSchema.parse(monitors); 209 + try { 210 + return allMonitorsExtendedSchema.parse(monitors); 211 + } catch (e) { 212 + console.log(e); 213 + } 214 + return; 215 + }), 216 + 217 + updateMonitorAdvanced: protectedProcedure 218 + .input( 219 + z.object({ 220 + id: z.number(), 221 + method: z.enum(METHODS).default("GET"), 222 + body: z.string().default("").optional(), 223 + headers: z 224 + .array(z.object({ key: z.string(), value: z.string() })) 225 + .transform((val) => JSON.stringify(val)) 226 + .default([]) 227 + .optional(), 228 + }), 229 + ) 230 + .mutation(async (opts) => { 231 + const result = await hasUserAccessToMonitor({ 232 + monitorId: opts.input.id, 233 + ctx: opts.ctx, 234 + }); 235 + if (!result) return; 236 + await opts.ctx.db 237 + .update(monitor) 238 + .set({ 239 + method: opts.input.method, 240 + body: opts.input.body, 241 + headers: opts.input.headers, 242 + }) 243 + .where(eq(monitor.id, opts.input.id)) 244 + .run(); 245 + }), 246 + 247 + getMonitorsForPeriodicity: protectedProcedure 248 + .input(z.object({ periodicity: periodicityEnum })) 249 + .query(async (opts) => { 250 + const result = await opts.ctx.db 251 + .select() 252 + .from(monitor) 253 + .where( 254 + and( 255 + eq(monitor.periodicity, opts.input.periodicity), 256 + eq(monitor.active, true), 257 + ), 258 + ) 259 + .all(); 260 + return allMonitorsExtendedSchema.parse(result); 261 + }), 262 + 263 + getAllPagesForMonitor: protectedProcedure 264 + .input(z.object({ monitorId: z.number() })) 265 + .query(async (opts) => { 266 + const allPages = await opts.ctx.db 267 + .select() 268 + .from(monitorsToPages) 269 + .where(eq(monitorsToPages.monitorId, opts.input.monitorId)) 270 + .all(); 271 + return allPages; 287 272 }), 288 273 });
+50 -1
packages/api/src/router/utils.ts
··· 1 1 import { eq } from "@openstatus/db"; 2 - import { user, usersToWorkspaces, workspace } from "@openstatus/db/src/schema"; 2 + import { 3 + monitor, 4 + user, 5 + usersToWorkspaces, 6 + workspace, 7 + } from "@openstatus/db/src/schema"; 3 8 import { allPlans } from "@openstatus/plans"; 4 9 5 10 import { Context } from "../trpc"; ··· 48 53 plan: allPlans[plan], 49 54 }; 50 55 }; 56 + 57 + export const hasUserAccessToMonitor = async ({ 58 + monitorId, 59 + ctx, 60 + }: { 61 + monitorId: number; 62 + ctx: Context; 63 + }) => { 64 + if (!ctx.auth?.userId) return; 65 + 66 + const currentMonitor = await ctx.db 67 + .select() 68 + .from(monitor) 69 + .where(eq(monitor.id, monitorId)) 70 + .get(); 71 + if (!currentMonitor || !currentMonitor.workspaceId) return; 72 + 73 + // TODO: we should use hasUserAccess and pass `workspaceId` instead of `workspaceSlug` 74 + const currentUser = ctx.db 75 + .select() 76 + .from(user) 77 + .where(eq(user.tenantId, ctx.auth.userId)) 78 + .as("currentUser"); 79 + 80 + const result = await ctx.db 81 + .select() 82 + .from(usersToWorkspaces) 83 + .where(eq(usersToWorkspaces.workspaceId, currentMonitor.workspaceId)) 84 + .innerJoin(currentUser, eq(usersToWorkspaces.userId, currentUser.id)) 85 + .get(); 86 + 87 + if (!result || !result.users_to_workspaces) return; 88 + 89 + const currentWorkspace = await ctx.db.query.workspace.findFirst({ 90 + where: eq(workspace.id, result.users_to_workspaces.workspaceId), 91 + }); 92 + 93 + if (!currentWorkspace) return; 94 + return { 95 + workspace: currentWorkspace, 96 + user: result.currentUser, 97 + monitor: currentMonitor, 98 + }; 99 + };
+5 -2
packages/api/src/trpc.ts
··· 46 46 * process every request that goes through your tRPC endpoint 47 47 * @link https://trpc.io/docs/context 48 48 */ 49 - export const createTRPCContext = (opts: { req: NextRequest }) => { 50 - const auth = getAuth(opts.req); 49 + export const createTRPCContext = (opts: { 50 + req: NextRequest; 51 + serverSideCall?: boolean; 52 + }) => { 53 + const auth = !opts.serverSideCall ? getAuth(opts.req) : null; 51 54 52 55 return createInnerTRPCContext({ 53 56 auth,
+60
packages/db/drizzle/0006_tired_anita_blake.sql
··· 1 + /* 2 + SQLite does not support "Changing existing column type" out of the box, we do not generate automatic migration for that, so it has to be done manually 3 + Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 4 + https://www.sqlite.org/lang_altertable.html 5 + https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3 6 + 7 + Due to that we don't generate migration automatically and it has to be done manually 8 + 9 + */ 10 + PRAGMA foreign_keys=OFF; 11 + --> statement-breakpoint 12 + 13 + CREATE TABLE `monitor_new` ( 14 + `id` integer PRIMARY KEY NOT NULL, 15 + `job_type` text(3) DEFAULT 'other' NOT NULL, 16 + `periodicity` text(6) DEFAULT 'other' NOT NULL, 17 + `status` text(2) DEFAULT 'inactive' NOT NULL, 18 + `active` integer DEFAULT false, 19 + `url` text(512) NOT NULL, 20 + `name` text(256) DEFAULT '' NOT NULL, 21 + `description` text DEFAULT '' NOT NULL, 22 + `workspace_id` integer, 23 + `headers` text DEFAULT '', 24 + `body` text DEFAULT '', 25 + `method` text(5) DEFAULT 'GET', 26 + `created_at` integer DEFAULT (strftime('%s', 'now')), `regions` text DEFAULT '' NOT NULL, `updated_at` integer, 27 + FOREIGN KEY (`workspace_id`) REFERENCES `workspace`(`id`) ON UPDATE no action ON DELETE no action 28 + ); 29 + --> statement-breakpoint 30 + INSERT INTO monitor_new(`id`,`job_type`,`periodicity`,`status`,`active`,`url`,`name`,`description`,`workspace_id`,`created_at`,`regions`,`updated_at`) SELECT * FROM monitor; 31 + --> statement-breakpoint 32 + 33 + CREATE TABLE `monitors_to_pages_new` ( 34 + `monitor_id` integer NOT NULL, 35 + `page_id` integer NOT NULL, 36 + PRIMARY KEY(`monitor_id`, `page_id`) 37 + ); 38 + --> statement-breakpoint 39 + INSERT INTO monitors_to_pages_new(`monitor_id`,`page_id`) SELECT * FROM monitors_to_pages; 40 + --> statement-breakpoint 41 + 42 + CREATE TABLE `incidents_to_monitors_new` ( 43 + `monitor_id` integer NOT NULL, 44 + `incident_id` integer NOT NULL, 45 + PRIMARY KEY(`incident_id`, `monitor_id`) 46 + ); 47 + --> statement-breakpoint 48 + INSERT INTO incidents_to_monitors_new(`monitor_id`,`incident_id`) SELECT * FROM incidents_to_monitors; 49 + --> statement-breakpoint 50 + DROP TABLE monitor; 51 + --> statement-breakpoint 52 + ALTER TABLE monitor_new RENAME TO monitor; 53 + --> statement-breakpoint 54 + INSERT INTO monitors_to_pages(`monitor_id`,`page_id`) SELECT * FROM monitors_to_pages_new; 55 + --> statement-breakpoint 56 + INSERT INTO incidents_to_monitors(`monitor_id`,`incident_id`) SELECT * FROM incidents_to_monitors_new; 57 + --> statement-breakpoint 58 + DROP TABLE monitors_to_pages_new; 59 + --> statement-breakpoint 60 + DROP TABLE incidents_to_monitors_new;
+746
packages/db/drizzle/meta/0006_snapshot.json
··· 1 + { 2 + "version": "5", 3 + "dialect": "sqlite", 4 + "id": "fee77e21-b52b-49c0-b109-5ccc37821935", 5 + "prevId": "84447f0c-af79-4add-93d7-eb6868983258", 6 + "tables": { 7 + "incident": { 8 + "name": "incident", 9 + "columns": { 10 + "id": { 11 + "name": "id", 12 + "type": "integer", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "status": { 18 + "name": "status", 19 + "type": "text(4)", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "title": { 25 + "name": "title", 26 + "type": "text(256)", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "workspace_id": { 32 + "name": "workspace_id", 33 + "type": "integer", 34 + "primaryKey": false, 35 + "notNull": false, 36 + "autoincrement": false 37 + }, 38 + "created_at": { 39 + "name": "created_at", 40 + "type": "integer", 41 + "primaryKey": false, 42 + "notNull": false, 43 + "autoincrement": false, 44 + "default": "(strftime('%s', 'now'))" 45 + }, 46 + "updated_at": { 47 + "name": "updated_at", 48 + "type": "integer", 49 + "primaryKey": false, 50 + "notNull": false, 51 + "autoincrement": false, 52 + "default": "(strftime('%s', 'now'))" 53 + } 54 + }, 55 + "indexes": {}, 56 + "foreignKeys": { 57 + "incident_workspace_id_workspace_id_fk": { 58 + "name": "incident_workspace_id_workspace_id_fk", 59 + "tableFrom": "incident", 60 + "tableTo": "workspace", 61 + "columnsFrom": [ 62 + "workspace_id" 63 + ], 64 + "columnsTo": [ 65 + "id" 66 + ], 67 + "onDelete": "no action", 68 + "onUpdate": "no action" 69 + } 70 + }, 71 + "compositePrimaryKeys": {}, 72 + "uniqueConstraints": {} 73 + }, 74 + "incident_update": { 75 + "name": "incident_update", 76 + "columns": { 77 + "id": { 78 + "name": "id", 79 + "type": "integer", 80 + "primaryKey": true, 81 + "notNull": true, 82 + "autoincrement": false 83 + }, 84 + "status": { 85 + "name": "status", 86 + "type": "text(4)", 87 + "primaryKey": false, 88 + "notNull": true, 89 + "autoincrement": false 90 + }, 91 + "date": { 92 + "name": "date", 93 + "type": "integer", 94 + "primaryKey": false, 95 + "notNull": true, 96 + "autoincrement": false 97 + }, 98 + "message": { 99 + "name": "message", 100 + "type": "text", 101 + "primaryKey": false, 102 + "notNull": true, 103 + "autoincrement": false 104 + }, 105 + "incident_id": { 106 + "name": "incident_id", 107 + "type": "integer", 108 + "primaryKey": false, 109 + "notNull": true, 110 + "autoincrement": false 111 + }, 112 + "created_at": { 113 + "name": "created_at", 114 + "type": "integer", 115 + "primaryKey": false, 116 + "notNull": false, 117 + "autoincrement": false, 118 + "default": "(strftime('%s', 'now'))" 119 + }, 120 + "updated_at": { 121 + "name": "updated_at", 122 + "type": "integer", 123 + "primaryKey": false, 124 + "notNull": false, 125 + "autoincrement": false, 126 + "default": "(strftime('%s', 'now'))" 127 + } 128 + }, 129 + "indexes": {}, 130 + "foreignKeys": { 131 + "incident_update_incident_id_incident_id_fk": { 132 + "name": "incident_update_incident_id_incident_id_fk", 133 + "tableFrom": "incident_update", 134 + "tableTo": "incident", 135 + "columnsFrom": [ 136 + "incident_id" 137 + ], 138 + "columnsTo": [ 139 + "id" 140 + ], 141 + "onDelete": "cascade", 142 + "onUpdate": "no action" 143 + } 144 + }, 145 + "compositePrimaryKeys": {}, 146 + "uniqueConstraints": {} 147 + }, 148 + "incidents_to_monitors": { 149 + "name": "incidents_to_monitors", 150 + "columns": { 151 + "monitor_id": { 152 + "name": "monitor_id", 153 + "type": "integer", 154 + "primaryKey": false, 155 + "notNull": true, 156 + "autoincrement": false 157 + }, 158 + "incident_id": { 159 + "name": "incident_id", 160 + "type": "integer", 161 + "primaryKey": false, 162 + "notNull": true, 163 + "autoincrement": false 164 + } 165 + }, 166 + "indexes": {}, 167 + "foreignKeys": { 168 + "incidents_to_monitors_monitor_id_monitor_id_fk": { 169 + "name": "incidents_to_monitors_monitor_id_monitor_id_fk", 170 + "tableFrom": "incidents_to_monitors", 171 + "tableTo": "monitor", 172 + "columnsFrom": [ 173 + "monitor_id" 174 + ], 175 + "columnsTo": [ 176 + "id" 177 + ], 178 + "onDelete": "cascade", 179 + "onUpdate": "no action" 180 + }, 181 + "incidents_to_monitors_incident_id_incident_id_fk": { 182 + "name": "incidents_to_monitors_incident_id_incident_id_fk", 183 + "tableFrom": "incidents_to_monitors", 184 + "tableTo": "incident", 185 + "columnsFrom": [ 186 + "incident_id" 187 + ], 188 + "columnsTo": [ 189 + "id" 190 + ], 191 + "onDelete": "cascade", 192 + "onUpdate": "no action" 193 + } 194 + }, 195 + "compositePrimaryKeys": { 196 + "incidents_to_monitors_monitor_id_incident_id_pk": { 197 + "columns": [ 198 + "incident_id", 199 + "monitor_id" 200 + ] 201 + } 202 + }, 203 + "uniqueConstraints": {} 204 + }, 205 + "page": { 206 + "name": "page", 207 + "columns": { 208 + "id": { 209 + "name": "id", 210 + "type": "integer", 211 + "primaryKey": true, 212 + "notNull": true, 213 + "autoincrement": false 214 + }, 215 + "workspace_id": { 216 + "name": "workspace_id", 217 + "type": "integer", 218 + "primaryKey": false, 219 + "notNull": true, 220 + "autoincrement": false 221 + }, 222 + "title": { 223 + "name": "title", 224 + "type": "text", 225 + "primaryKey": false, 226 + "notNull": true, 227 + "autoincrement": false 228 + }, 229 + "description": { 230 + "name": "description", 231 + "type": "text", 232 + "primaryKey": false, 233 + "notNull": true, 234 + "autoincrement": false 235 + }, 236 + "icon": { 237 + "name": "icon", 238 + "type": "text(256)", 239 + "primaryKey": false, 240 + "notNull": false, 241 + "autoincrement": false, 242 + "default": "''" 243 + }, 244 + "slug": { 245 + "name": "slug", 246 + "type": "text(256)", 247 + "primaryKey": false, 248 + "notNull": true, 249 + "autoincrement": false 250 + }, 251 + "custom_domain": { 252 + "name": "custom_domain", 253 + "type": "text(256)", 254 + "primaryKey": false, 255 + "notNull": true, 256 + "autoincrement": false 257 + }, 258 + "published": { 259 + "name": "published", 260 + "type": "integer", 261 + "primaryKey": false, 262 + "notNull": false, 263 + "autoincrement": false, 264 + "default": false 265 + }, 266 + "created_at": { 267 + "name": "created_at", 268 + "type": "integer", 269 + "primaryKey": false, 270 + "notNull": false, 271 + "autoincrement": false, 272 + "default": "(strftime('%s', 'now'))" 273 + }, 274 + "updated_at": { 275 + "name": "updated_at", 276 + "type": "integer", 277 + "primaryKey": false, 278 + "notNull": false, 279 + "autoincrement": false, 280 + "default": "(strftime('%s', 'now'))" 281 + } 282 + }, 283 + "indexes": { 284 + "page_slug_unique": { 285 + "name": "page_slug_unique", 286 + "columns": [ 287 + "slug" 288 + ], 289 + "isUnique": true 290 + } 291 + }, 292 + "foreignKeys": { 293 + "page_workspace_id_workspace_id_fk": { 294 + "name": "page_workspace_id_workspace_id_fk", 295 + "tableFrom": "page", 296 + "tableTo": "workspace", 297 + "columnsFrom": [ 298 + "workspace_id" 299 + ], 300 + "columnsTo": [ 301 + "id" 302 + ], 303 + "onDelete": "cascade", 304 + "onUpdate": "no action" 305 + } 306 + }, 307 + "compositePrimaryKeys": {}, 308 + "uniqueConstraints": {} 309 + }, 310 + "monitor": { 311 + "name": "monitor", 312 + "columns": { 313 + "id": { 314 + "name": "id", 315 + "type": "integer", 316 + "primaryKey": true, 317 + "notNull": true, 318 + "autoincrement": false 319 + }, 320 + "job_type": { 321 + "name": "job_type", 322 + "type": "text(3)", 323 + "primaryKey": false, 324 + "notNull": true, 325 + "autoincrement": false, 326 + "default": "'other'" 327 + }, 328 + "periodicity": { 329 + "name": "periodicity", 330 + "type": "text(6)", 331 + "primaryKey": false, 332 + "notNull": true, 333 + "autoincrement": false, 334 + "default": "'other'" 335 + }, 336 + "status": { 337 + "name": "status", 338 + "type": "text(2)", 339 + "primaryKey": false, 340 + "notNull": true, 341 + "autoincrement": false, 342 + "default": "'inactive'" 343 + }, 344 + "active": { 345 + "name": "active", 346 + "type": "integer", 347 + "primaryKey": false, 348 + "notNull": false, 349 + "autoincrement": false, 350 + "default": false 351 + }, 352 + "regions": { 353 + "name": "regions", 354 + "type": "text", 355 + "primaryKey": false, 356 + "notNull": true, 357 + "autoincrement": false, 358 + "default": "''" 359 + }, 360 + "url": { 361 + "name": "url", 362 + "type": "text(512)", 363 + "primaryKey": false, 364 + "notNull": true, 365 + "autoincrement": false 366 + }, 367 + "name": { 368 + "name": "name", 369 + "type": "text(256)", 370 + "primaryKey": false, 371 + "notNull": true, 372 + "autoincrement": false, 373 + "default": "''" 374 + }, 375 + "description": { 376 + "name": "description", 377 + "type": "text", 378 + "primaryKey": false, 379 + "notNull": true, 380 + "autoincrement": false, 381 + "default": "''" 382 + }, 383 + "headers": { 384 + "name": "headers", 385 + "type": "text", 386 + "primaryKey": false, 387 + "notNull": false, 388 + "autoincrement": false, 389 + "default": "''" 390 + }, 391 + "body": { 392 + "name": "body", 393 + "type": "text", 394 + "primaryKey": false, 395 + "notNull": false, 396 + "autoincrement": false, 397 + "default": "''" 398 + }, 399 + "method": { 400 + "name": "method", 401 + "type": "text(5)", 402 + "primaryKey": false, 403 + "notNull": false, 404 + "autoincrement": false, 405 + "default": "'GET'" 406 + }, 407 + "workspace_id": { 408 + "name": "workspace_id", 409 + "type": "integer", 410 + "primaryKey": false, 411 + "notNull": false, 412 + "autoincrement": false 413 + }, 414 + "created_at": { 415 + "name": "created_at", 416 + "type": "integer", 417 + "primaryKey": false, 418 + "notNull": false, 419 + "autoincrement": false, 420 + "default": "(strftime('%s', 'now'))" 421 + }, 422 + "updated_at": { 423 + "name": "updated_at", 424 + "type": "integer", 425 + "primaryKey": false, 426 + "notNull": false, 427 + "autoincrement": false, 428 + "default": "(strftime('%s', 'now'))" 429 + } 430 + }, 431 + "indexes": {}, 432 + "foreignKeys": { 433 + "monitor_workspace_id_workspace_id_fk": { 434 + "name": "monitor_workspace_id_workspace_id_fk", 435 + "tableFrom": "monitor", 436 + "tableTo": "workspace", 437 + "columnsFrom": [ 438 + "workspace_id" 439 + ], 440 + "columnsTo": [ 441 + "id" 442 + ], 443 + "onDelete": "no action", 444 + "onUpdate": "no action" 445 + } 446 + }, 447 + "compositePrimaryKeys": {}, 448 + "uniqueConstraints": {} 449 + }, 450 + "monitors_to_pages": { 451 + "name": "monitors_to_pages", 452 + "columns": { 453 + "monitor_id": { 454 + "name": "monitor_id", 455 + "type": "integer", 456 + "primaryKey": false, 457 + "notNull": true, 458 + "autoincrement": false 459 + }, 460 + "page_id": { 461 + "name": "page_id", 462 + "type": "integer", 463 + "primaryKey": false, 464 + "notNull": true, 465 + "autoincrement": false 466 + } 467 + }, 468 + "indexes": {}, 469 + "foreignKeys": { 470 + "monitors_to_pages_monitor_id_monitor_id_fk": { 471 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 472 + "tableFrom": "monitors_to_pages", 473 + "tableTo": "monitor", 474 + "columnsFrom": [ 475 + "monitor_id" 476 + ], 477 + "columnsTo": [ 478 + "id" 479 + ], 480 + "onDelete": "cascade", 481 + "onUpdate": "no action" 482 + }, 483 + "monitors_to_pages_page_id_page_id_fk": { 484 + "name": "monitors_to_pages_page_id_page_id_fk", 485 + "tableFrom": "monitors_to_pages", 486 + "tableTo": "page", 487 + "columnsFrom": [ 488 + "page_id" 489 + ], 490 + "columnsTo": [ 491 + "id" 492 + ], 493 + "onDelete": "cascade", 494 + "onUpdate": "no action" 495 + } 496 + }, 497 + "compositePrimaryKeys": { 498 + "monitors_to_pages_monitor_id_page_id_pk": { 499 + "columns": [ 500 + "monitor_id", 501 + "page_id" 502 + ] 503 + } 504 + }, 505 + "uniqueConstraints": {} 506 + }, 507 + "user": { 508 + "name": "user", 509 + "columns": { 510 + "id": { 511 + "name": "id", 512 + "type": "integer", 513 + "primaryKey": true, 514 + "notNull": true, 515 + "autoincrement": false 516 + }, 517 + "tenant_id": { 518 + "name": "tenant_id", 519 + "type": "text(256)", 520 + "primaryKey": false, 521 + "notNull": false, 522 + "autoincrement": false 523 + }, 524 + "first_name": { 525 + "name": "first_name", 526 + "type": "text", 527 + "primaryKey": false, 528 + "notNull": false, 529 + "autoincrement": false, 530 + "default": "''" 531 + }, 532 + "last_name": { 533 + "name": "last_name", 534 + "type": "text", 535 + "primaryKey": false, 536 + "notNull": false, 537 + "autoincrement": false, 538 + "default": "''" 539 + }, 540 + "email": { 541 + "name": "email", 542 + "type": "text", 543 + "primaryKey": false, 544 + "notNull": false, 545 + "autoincrement": false, 546 + "default": "''" 547 + }, 548 + "photo_url": { 549 + "name": "photo_url", 550 + "type": "text", 551 + "primaryKey": false, 552 + "notNull": false, 553 + "autoincrement": false, 554 + "default": "''" 555 + }, 556 + "created_at": { 557 + "name": "created_at", 558 + "type": "integer", 559 + "primaryKey": false, 560 + "notNull": false, 561 + "autoincrement": false, 562 + "default": "(strftime('%s', 'now'))" 563 + }, 564 + "updated_at": { 565 + "name": "updated_at", 566 + "type": "integer", 567 + "primaryKey": false, 568 + "notNull": false, 569 + "autoincrement": false, 570 + "default": "(strftime('%s', 'now'))" 571 + } 572 + }, 573 + "indexes": { 574 + "user_tenant_id_unique": { 575 + "name": "user_tenant_id_unique", 576 + "columns": [ 577 + "tenant_id" 578 + ], 579 + "isUnique": true 580 + } 581 + }, 582 + "foreignKeys": {}, 583 + "compositePrimaryKeys": {}, 584 + "uniqueConstraints": {} 585 + }, 586 + "users_to_workspaces": { 587 + "name": "users_to_workspaces", 588 + "columns": { 589 + "user_id": { 590 + "name": "user_id", 591 + "type": "integer", 592 + "primaryKey": false, 593 + "notNull": true, 594 + "autoincrement": false 595 + }, 596 + "workspace_id": { 597 + "name": "workspace_id", 598 + "type": "integer", 599 + "primaryKey": false, 600 + "notNull": true, 601 + "autoincrement": false 602 + } 603 + }, 604 + "indexes": {}, 605 + "foreignKeys": { 606 + "users_to_workspaces_user_id_user_id_fk": { 607 + "name": "users_to_workspaces_user_id_user_id_fk", 608 + "tableFrom": "users_to_workspaces", 609 + "tableTo": "user", 610 + "columnsFrom": [ 611 + "user_id" 612 + ], 613 + "columnsTo": [ 614 + "id" 615 + ], 616 + "onDelete": "no action", 617 + "onUpdate": "no action" 618 + }, 619 + "users_to_workspaces_workspace_id_workspace_id_fk": { 620 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 621 + "tableFrom": "users_to_workspaces", 622 + "tableTo": "workspace", 623 + "columnsFrom": [ 624 + "workspace_id" 625 + ], 626 + "columnsTo": [ 627 + "id" 628 + ], 629 + "onDelete": "no action", 630 + "onUpdate": "no action" 631 + } 632 + }, 633 + "compositePrimaryKeys": { 634 + "users_to_workspaces_user_id_workspace_id_pk": { 635 + "columns": [ 636 + "user_id", 637 + "workspace_id" 638 + ] 639 + } 640 + }, 641 + "uniqueConstraints": {} 642 + }, 643 + "workspace": { 644 + "name": "workspace", 645 + "columns": { 646 + "id": { 647 + "name": "id", 648 + "type": "integer", 649 + "primaryKey": true, 650 + "notNull": true, 651 + "autoincrement": false 652 + }, 653 + "slug": { 654 + "name": "slug", 655 + "type": "text", 656 + "primaryKey": false, 657 + "notNull": true, 658 + "autoincrement": false 659 + }, 660 + "name": { 661 + "name": "name", 662 + "type": "text", 663 + "primaryKey": false, 664 + "notNull": false, 665 + "autoincrement": false 666 + }, 667 + "stripe_id": { 668 + "name": "stripe_id", 669 + "type": "text(256)", 670 + "primaryKey": false, 671 + "notNull": false, 672 + "autoincrement": false 673 + }, 674 + "subscription_id": { 675 + "name": "subscription_id", 676 + "type": "text", 677 + "primaryKey": false, 678 + "notNull": false, 679 + "autoincrement": false 680 + }, 681 + "plan": { 682 + "name": "plan", 683 + "type": "text(2)", 684 + "primaryKey": false, 685 + "notNull": false, 686 + "autoincrement": false 687 + }, 688 + "ends_at": { 689 + "name": "ends_at", 690 + "type": "integer", 691 + "primaryKey": false, 692 + "notNull": false, 693 + "autoincrement": false 694 + }, 695 + "paid_until": { 696 + "name": "paid_until", 697 + "type": "integer", 698 + "primaryKey": false, 699 + "notNull": false, 700 + "autoincrement": false 701 + }, 702 + "created_at": { 703 + "name": "created_at", 704 + "type": "integer", 705 + "primaryKey": false, 706 + "notNull": false, 707 + "autoincrement": false, 708 + "default": "(strftime('%s', 'now'))" 709 + }, 710 + "updated_at": { 711 + "name": "updated_at", 712 + "type": "integer", 713 + "primaryKey": false, 714 + "notNull": false, 715 + "autoincrement": false, 716 + "default": "(strftime('%s', 'now'))" 717 + } 718 + }, 719 + "indexes": { 720 + "workspace_slug_unique": { 721 + "name": "workspace_slug_unique", 722 + "columns": [ 723 + "slug" 724 + ], 725 + "isUnique": true 726 + }, 727 + "workspace_stripe_id_unique": { 728 + "name": "workspace_stripe_id_unique", 729 + "columns": [ 730 + "stripe_id" 731 + ], 732 + "isUnique": true 733 + } 734 + }, 735 + "foreignKeys": {}, 736 + "compositePrimaryKeys": {}, 737 + "uniqueConstraints": {} 738 + } 739 + }, 740 + "enums": {}, 741 + "_meta": { 742 + "schemas": {}, 743 + "tables": {}, 744 + "columns": {} 745 + } 746 + }
+7
packages/db/drizzle/meta/_journal.json
··· 43 43 "when": 1691930414569, 44 44 "tag": "0005_even_baron_strucker", 45 45 "breakpoints": true 46 + }, 47 + { 48 + "idx": 6, 49 + "version": "5", 50 + "when": 1692646649111, 51 + "tag": "0006_tired_anita_blake", 52 + "breakpoints": true 46 53 } 47 54 ] 48 55 }
+32 -1
packages/db/src/schema/monitor.ts
··· 34 34 "syd1", 35 35 ] as const; 36 36 37 + export const METHODS = ["GET", "POST"] as const; 38 + 37 39 export const RegionEnum = z.enum(availableRegions); 38 40 39 41 export const monitor = sqliteTable("monitor", { ··· 54 56 name: text("name", { length: 256 }).default("").notNull(), 55 57 description: text("description").default("").notNull(), 56 58 59 + headers: text("headers").default(""), 60 + body: text("body").default(""), 61 + method: text("method", METHODS).default("GET"), 57 62 workspaceId: integer("workspace_id").references(() => workspace.id), 58 63 59 64 createdAt: integer("created_at", { mode: "timestamp" }).default( ··· 117 122 status: z.enum(["active", "inactive"]).default("inactive"), 118 123 active: z.boolean().default(false), 119 124 regions: z.array(RegionEnum).default([]).optional(), 125 + method: z.enum(METHODS).default("GET"), 126 + body: z.string().default(""), 127 + headers: z 128 + .array(z.object({ key: z.string(), value: z.string() })) 129 + .default([]), 120 130 }); 121 131 122 132 // Schema for selecting a Monitor - can be used to validate API responses ··· 136 146 .default([]), 137 147 }); 138 148 139 - export const allMonitorsSchema = z.array(selectMonitorSchema); 149 + export const selectMonitorExtendedSchema = selectMonitorSchema.extend({ 150 + method: z.enum(METHODS).default("GET"), 151 + body: z 152 + .preprocess((val) => { 153 + return String(val); 154 + }, z.string()) 155 + .default(""), 156 + headers: z.preprocess( 157 + (val) => { 158 + if (String(val).length > 0) { 159 + return JSON.parse(String(val)); 160 + } else { 161 + return []; 162 + } 163 + }, 164 + z.array(z.object({ key: z.string(), value: z.string() })).default([]), 165 + ), 166 + }); 167 + 168 + export const allMonitorsSchema = z.array(selectMonitorExtendedSchema); 169 + 170 + export const allMonitorsExtendedSchema = z.array(selectMonitorExtendedSchema);