Openstatus www.openstatus.dev

chore: add combobox to regions (#119)

* chore: add combobox to regions

* fix regions

* fix build

* 🚀 update

* 🔥 fix

---------

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

authored by

Maximilian Kaske
Thibault Le Ouay
and committed by
GitHub
49106c9a 8bec1a0f

+741 -38
+26 -5
apps/web/src/app/api/checker/cron/_cron.ts
··· 1 1 import { Client } from "@upstash/qstash/cloudflare"; 2 - import type { z } from "zod"; 2 + import { z } from "zod"; 3 3 4 4 import { and, db, eq } from "@openstatus/db"; 5 5 import { 6 6 monitor, 7 7 monitorsToPages, 8 + RegionEnum, 8 9 selectMonitorSchema, 9 10 } from "@openstatus/db/src/schema"; 10 11 import { availableRegions } from "@openstatus/tinybird"; ··· 13 14 import type { payloadSchema } from "../schema"; 14 15 15 16 const periodicityAvailable = selectMonitorSchema.pick({ periodicity: true }); 17 + 18 + // FIXME: do coerce in zod instead 19 + const currentRegions = z.string().transform((val) => val.split(",")); 16 20 17 21 const DEFAULT_URL = process.env.VERCEL_URL 18 22 ? `https://${process.env.VERCEL_URL}` ··· 48 52 .where(eq(monitorsToPages.monitorId, row.id)) 49 53 .all(); 50 54 51 - for (const region of availableRegions) { 55 + const allMonitorsRegions = currentRegions.parse(row.regions); 56 + if (allMonitorsRegions.length === 0) { 52 57 const payload: z.infer<typeof payloadSchema> = { 53 58 workspaceId: String(row.workspaceId), 54 59 monitorId: String(row.id), ··· 59 64 60 65 // TODO: fetch + try - catch + retry once 61 66 const result = c.publishJSON({ 62 - url: `${DEFAULT_URL}/api/checker/regions/${region}`, 67 + url: `${DEFAULT_URL}/api/checker/regions/random`, 63 68 body: payload, 64 - delay: Math.random() * 180, 69 + delay: Math.random() * 90, 65 70 }); 66 71 allResult.push(result); 72 + } else { 73 + for (const region of allMonitorsRegions) { 74 + const payload: z.infer<typeof payloadSchema> = { 75 + workspaceId: String(row.workspaceId), 76 + monitorId: String(row.id), 77 + url: row.url, 78 + cronTimestamp: timestamp, 79 + pageIds: allPages.map((p) => String(p.pageId)), 80 + }; 81 + 82 + const result = c.publishJSON({ 83 + url: `${DEFAULT_URL}/api/checker/regions/${region}`, 84 + body: payload, 85 + }); 86 + allResult.push(result); 87 + } 67 88 } 68 89 } 69 90 // our first legacy monitor ··· 82 103 const result = c.publishJSON({ 83 104 url: `${DEFAULT_URL}/api/checker/regions/${region}`, 84 105 body: payload, 85 - delay: Math.random() * 180, 106 + delay: Math.random() * 90, 86 107 }); 87 108 allResult.push(result); 88 109 }
+1 -1
apps/web/src/app/api/checker/regions/_checker.ts
··· 86 86 await monitor(res, result.data, region, latency); 87 87 } catch (e) { 88 88 // if on the third retry we still get an error, we should report it 89 - if (request.headers.get("Upstash-Retried") === "3") { 89 + if (request.headers.get("Upstash-Retried") === "2") { 90 90 await monitor( 91 91 { status: 500, text: () => Promise.resolve(`${e}`) }, 92 92 result.data,
+17
apps/web/src/app/api/checker/regions/random/route.ts
··· 1 + import { NextResponse } from "next/server"; 2 + 3 + import { checker } from "../_checker"; 4 + 5 + export const runtime = "edge"; 6 + export const preferredRegion = "auto"; 7 + export const dynamic = "force-dynamic"; 8 + // Fix is a random region let's figure where does vercel push it 9 + 10 + export async function POST(request: Request) { 11 + const region = process.env.VERCEL_REGION; 12 + if (!region) { 13 + throw new Error("No region"); 14 + } 15 + await checker(request, region); 16 + return NextResponse.json({ success: true }); 17 + }
+1 -1
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/_components/action-button.tsx
··· 2 2 3 3 import * as React from "react"; 4 4 import Link from "next/link"; 5 - import { usePathname, useRouter } from "next/navigation"; 5 + import { useRouter } from "next/navigation"; 6 6 import { MoreVertical } from "lucide-react"; 7 7 import type * as z from "zod"; 8 8
+1 -1
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/page.tsx
··· 57 57 </dd> 58 58 </div> 59 59 <div className="flex min-w-0 items-center justify-between gap-3"> 60 - <dt>Periodicity</dt> 60 + <dt>Frequency</dt> 61 61 <dd className="font-mono">{monitor.periodicity}</dd> 62 62 </div> 63 63 <div className="flex min-w-0 items-center justify-between gap-3">
-3
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/page.tsx
··· 14 14 15 15 const limit = allPlans.free.limits["status-pages"]; 16 16 17 - // export const revalidate = 300; // revalidate this page every 5 minutes 18 - export const dynamic = "force-dynamic"; 19 - 20 17 export default async function Page({ 21 18 params, 22 19 }: {
+87 -6
apps/web/src/components/forms/montitor-form.tsx
··· 2 2 3 3 import * as React from "react"; 4 4 import { zodResolver } from "@hookform/resolvers/zod"; 5 + import { Check, ChevronsUpDown } from "lucide-react"; 5 6 import { useForm } from "react-hook-form"; 6 7 import type * as z from "zod"; 7 8 ··· 11 12 } from "@openstatus/db/src/schema"; 12 13 import { allPlans } from "@openstatus/plans"; 13 14 15 + import { Button } from "@/components/ui/button"; 16 + import { 17 + Command, 18 + CommandEmpty, 19 + CommandGroup, 20 + CommandInput, 21 + CommandItem, 22 + } from "@/components/ui/command"; 14 23 import { 15 24 Form, 16 25 FormControl, ··· 22 31 } from "@/components/ui/form"; 23 32 import { Input } from "@/components/ui/input"; 24 33 import { 34 + Popover, 35 + PopoverContent, 36 + PopoverTrigger, 37 + } from "@/components/ui/popover"; 38 + import { 25 39 Select, 26 40 SelectContent, 27 41 SelectItem, ··· 29 43 SelectValue, 30 44 } from "@/components/ui/select"; 31 45 import { Switch } from "@/components/ui/switch"; 46 + import { regionsDict } from "@/data/regions-dictionary"; 47 + import { cn } from "@/lib/utils"; 32 48 33 49 const limit = allPlans.free.limits.periodicity; 34 50 const cronJobs = [ ··· 38 54 { value: "30m", label: "30 minutes" }, 39 55 { value: "1h", label: "1 hour" }, 40 56 ] as const; 41 - 42 - type Schema = z.infer<typeof insertMonitorSchema>; 43 57 44 58 interface Props { 45 59 id: string; 46 - defaultValues?: Schema; 47 - onSubmit: (values: Schema) => Promise<void>; 60 + defaultValues?: z.infer<typeof insertMonitorSchema>; 61 + onSubmit: (values: z.infer<typeof insertMonitorSchema>) => Promise<void>; 48 62 } 49 63 50 64 export function MonitorForm({ id, defaultValues, onSubmit }: Props) { 51 - const form = useForm<Schema>({ 65 + const form = useForm<z.infer<typeof insertMonitorSchema>>({ 52 66 resolver: zodResolver(insertMonitorSchema), // too much - we should only validate the values we ask inside of the form! 53 67 defaultValues: { 54 68 url: defaultValues?.url || "", ··· 57 71 periodicity: defaultValues?.periodicity || undefined, 58 72 active: defaultValues?.active || true, 59 73 id: defaultValues?.id || undefined, 74 + regions: defaultValues?.regions || [], 60 75 }, 61 76 }); 62 77 ··· 136 151 /> 137 152 <FormField 138 153 control={form.control} 154 + name="regions" 155 + render={({ field }) => ( 156 + <FormItem className="flex flex-col"> 157 + <FormLabel>Regions</FormLabel> 158 + <Popover> 159 + <PopoverTrigger asChild> 160 + <FormControl> 161 + <Button 162 + variant="outline" 163 + role="combobox" 164 + className={cn( 165 + "w-full justify-between", 166 + !field.value && "text-muted-foreground", 167 + )} 168 + > 169 + {/* This is a hotfix */} 170 + {field.value?.length === 1 && field.value[0].length > 0 171 + ? regionsDict[ 172 + field.value[0] as keyof typeof regionsDict 173 + ].location 174 + : "Select region"} 175 + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> 176 + </Button> 177 + </FormControl> 178 + </PopoverTrigger> 179 + <PopoverContent className="w-full p-0"> 180 + <Command> 181 + <CommandInput placeholder="Select a region..." /> 182 + <CommandEmpty>No regions found.</CommandEmpty> 183 + <CommandGroup> 184 + {Object.keys(regionsDict).map((region) => { 185 + const { code, location } = 186 + regionsDict[region as keyof typeof regionsDict]; 187 + const isSelected = field.value?.includes(code); 188 + return ( 189 + <CommandItem 190 + value={code} 191 + key={code} 192 + onSelect={() => { 193 + form.setValue("regions", [code]); // TODO: allow more than one to be selected in the future 194 + }} 195 + > 196 + <Check 197 + className={cn( 198 + "mr-2 h-4 w-4", 199 + isSelected ? "opacity-100" : "opacity-0", 200 + )} 201 + /> 202 + {location} 203 + </CommandItem> 204 + ); 205 + })} 206 + </CommandGroup> 207 + </Command> 208 + </PopoverContent> 209 + </Popover> 210 + <FormDescription> 211 + Select the regions you want to monitor, or leave it blank for 212 + randomly picked regions. 213 + </FormDescription> 214 + <FormMessage /> 215 + </FormItem> 216 + )} 217 + /> 218 + <FormField 219 + control={form.control} 139 220 name="periodicity" 140 221 render={({ field }) => ( 141 222 <FormItem> 142 - <FormLabel>Periodicity</FormLabel> 223 + <FormLabel>Frequency</FormLabel> 143 224 <Select 144 225 onValueChange={(value) => 145 226 field.onChange(periodicityEnum.parse(value))
+6 -3
packages/api/src/router/monitor.ts
··· 66 66 message: "You reached your cron job limits.", 67 67 }); 68 68 } 69 + const { regions, ...data } = opts.input.data; 69 70 70 71 const newMonitor = await opts.ctx.db 71 72 .insert(monitor) 72 73 .values({ 73 - ...opts.input.data, 74 + ...data, 74 75 workspaceId: currentWorkspace.id, 76 + regions: regions?.join(","), 75 77 }) 76 78 .returning() 77 79 .get(); ··· 184 186 message: "You reached your cron job limits.", 185 187 }); 186 188 } 187 - 189 + console.log(opts.input.regions?.join(",")); 190 + const { regions, ...data } = opts.input; 188 191 await opts.ctx.db 189 192 .update(monitor) 190 - .set(opts.input) 193 + .set({ ...data, regions: regions?.join(",") }) 191 194 .where(eq(monitor.id, opts.input.id)) 192 195 .returning() 193 196 .get();
-17
packages/db/drizzle/0000_lively_master_chief.sql
··· 1 - DROP TABLE IF EXISTS `incident`; 2 - --> statement-breakpoint 3 - DROP TABLE IF EXISTS `incident_update`; 4 - --> statement-breakpoint 5 - DROP TABLE IF EXISTS `monitors_to_pages`; 6 - --> statement-breakpoint 7 - DROP TABLE IF EXISTS `monitor`; 8 - --> statement-breakpoint 9 - DROP TABLE IF EXISTS `page`; 10 - --> statement-breakpoint 11 - DROP TABLE IF EXISTS `users_to_workspaces`; 12 - --> statement-breakpoint 13 - DROP TABLE IF EXISTS `user`; 14 - --> statement-breakpoint 15 - DROP TABLE IF EXISTS `workspace`; 16 - --> statement-breakpoint 17 - 18 1 CREATE TABLE `incident` ( 19 2 `id` integer PRIMARY KEY NOT NULL, 20 3 `status` text(2) NOT NULL,
+2
packages/db/drizzle/0001_brainy_beast.sql
··· 1 + ALTER TABLE monitor ADD `regions` text DEFAULT '' NOT NULL; 2 +
+564
packages/db/drizzle/meta/0001_snapshot.json
··· 1 + { 2 + "version": "5", 3 + "dialect": "sqlite", 4 + "id": "762acc59-e72a-4ca4-b3fa-2cfd65cdf49a", 5 + "prevId": "41c8003d-9f2a-4d3e-9e11-b3d4076fe395", 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(2)", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "page_id": { 25 + "name": "page_id", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "updated_at": { 32 + "name": "updated_at", 33 + "type": "integer", 34 + "primaryKey": false, 35 + "notNull": false, 36 + "autoincrement": false, 37 + "default": "(strftime('%s', 'now'))" 38 + } 39 + }, 40 + "indexes": {}, 41 + "foreignKeys": { 42 + "incident_page_id_page_id_fk": { 43 + "name": "incident_page_id_page_id_fk", 44 + "tableFrom": "incident", 45 + "tableTo": "page", 46 + "columnsFrom": [ 47 + "page_id" 48 + ], 49 + "columnsTo": [ 50 + "id" 51 + ], 52 + "onDelete": "cascade", 53 + "onUpdate": "no action" 54 + } 55 + }, 56 + "compositePrimaryKeys": {}, 57 + "uniqueConstraints": {} 58 + }, 59 + "incident_update": { 60 + "name": "incident_update", 61 + "columns": { 62 + "id": { 63 + "name": "id", 64 + "type": "integer", 65 + "primaryKey": true, 66 + "notNull": true, 67 + "autoincrement": false 68 + }, 69 + "uuid": { 70 + "name": "uuid", 71 + "type": "text", 72 + "primaryKey": false, 73 + "notNull": true, 74 + "autoincrement": false 75 + }, 76 + "incident_date": { 77 + "name": "incident_date", 78 + "type": "integer", 79 + "primaryKey": false, 80 + "notNull": false, 81 + "autoincrement": false 82 + }, 83 + "title": { 84 + "name": "title", 85 + "type": "text(256)", 86 + "primaryKey": false, 87 + "notNull": false, 88 + "autoincrement": false 89 + }, 90 + "message": { 91 + "name": "message", 92 + "type": "text", 93 + "primaryKey": false, 94 + "notNull": false, 95 + "autoincrement": false 96 + }, 97 + "incident_id": { 98 + "name": "incident_id", 99 + "type": "integer", 100 + "primaryKey": false, 101 + "notNull": true, 102 + "autoincrement": false 103 + }, 104 + "updated_at": { 105 + "name": "updated_at", 106 + "type": "integer", 107 + "primaryKey": false, 108 + "notNull": false, 109 + "autoincrement": false, 110 + "default": "(strftime('%s', 'now'))" 111 + } 112 + }, 113 + "indexes": { 114 + "incident_update_uuid_unique": { 115 + "name": "incident_update_uuid_unique", 116 + "columns": [ 117 + "uuid" 118 + ], 119 + "isUnique": true 120 + } 121 + }, 122 + "foreignKeys": { 123 + "incident_update_incident_id_incident_id_fk": { 124 + "name": "incident_update_incident_id_incident_id_fk", 125 + "tableFrom": "incident_update", 126 + "tableTo": "incident", 127 + "columnsFrom": [ 128 + "incident_id" 129 + ], 130 + "columnsTo": [ 131 + "id" 132 + ], 133 + "onDelete": "cascade", 134 + "onUpdate": "no action" 135 + } 136 + }, 137 + "compositePrimaryKeys": {}, 138 + "uniqueConstraints": {} 139 + }, 140 + "page": { 141 + "name": "page", 142 + "columns": { 143 + "id": { 144 + "name": "id", 145 + "type": "integer", 146 + "primaryKey": true, 147 + "notNull": true, 148 + "autoincrement": false 149 + }, 150 + "workspace_id": { 151 + "name": "workspace_id", 152 + "type": "integer", 153 + "primaryKey": false, 154 + "notNull": true, 155 + "autoincrement": false 156 + }, 157 + "title": { 158 + "name": "title", 159 + "type": "text", 160 + "primaryKey": false, 161 + "notNull": true, 162 + "autoincrement": false 163 + }, 164 + "description": { 165 + "name": "description", 166 + "type": "text", 167 + "primaryKey": false, 168 + "notNull": true, 169 + "autoincrement": false 170 + }, 171 + "icon": { 172 + "name": "icon", 173 + "type": "text(256)", 174 + "primaryKey": false, 175 + "notNull": false, 176 + "autoincrement": false 177 + }, 178 + "slug": { 179 + "name": "slug", 180 + "type": "text(256)", 181 + "primaryKey": false, 182 + "notNull": true, 183 + "autoincrement": false 184 + }, 185 + "custom_domain": { 186 + "name": "custom_domain", 187 + "type": "text(256)", 188 + "primaryKey": false, 189 + "notNull": true, 190 + "autoincrement": false 191 + }, 192 + "published": { 193 + "name": "published", 194 + "type": "integer", 195 + "primaryKey": false, 196 + "notNull": false, 197 + "autoincrement": false, 198 + "default": false 199 + }, 200 + "updated_at": { 201 + "name": "updated_at", 202 + "type": "integer", 203 + "primaryKey": false, 204 + "notNull": false, 205 + "autoincrement": false, 206 + "default": "(strftime('%s', 'now'))" 207 + } 208 + }, 209 + "indexes": { 210 + "page_slug_unique": { 211 + "name": "page_slug_unique", 212 + "columns": [ 213 + "slug" 214 + ], 215 + "isUnique": true 216 + } 217 + }, 218 + "foreignKeys": { 219 + "page_workspace_id_workspace_id_fk": { 220 + "name": "page_workspace_id_workspace_id_fk", 221 + "tableFrom": "page", 222 + "tableTo": "workspace", 223 + "columnsFrom": [ 224 + "workspace_id" 225 + ], 226 + "columnsTo": [ 227 + "id" 228 + ], 229 + "onDelete": "cascade", 230 + "onUpdate": "no action" 231 + } 232 + }, 233 + "compositePrimaryKeys": {}, 234 + "uniqueConstraints": {} 235 + }, 236 + "monitor": { 237 + "name": "monitor", 238 + "columns": { 239 + "id": { 240 + "name": "id", 241 + "type": "integer", 242 + "primaryKey": true, 243 + "notNull": true, 244 + "autoincrement": false 245 + }, 246 + "job_type": { 247 + "name": "job_type", 248 + "type": "text(3)", 249 + "primaryKey": false, 250 + "notNull": true, 251 + "autoincrement": false, 252 + "default": "'other'" 253 + }, 254 + "periodicity": { 255 + "name": "periodicity", 256 + "type": "text(6)", 257 + "primaryKey": false, 258 + "notNull": true, 259 + "autoincrement": false, 260 + "default": "'other'" 261 + }, 262 + "status": { 263 + "name": "status", 264 + "type": "text(2)", 265 + "primaryKey": false, 266 + "notNull": true, 267 + "autoincrement": false, 268 + "default": "'inactive'" 269 + }, 270 + "active": { 271 + "name": "active", 272 + "type": "integer", 273 + "primaryKey": false, 274 + "notNull": false, 275 + "autoincrement": false, 276 + "default": false 277 + }, 278 + "regions": { 279 + "name": "regions", 280 + "type": "text", 281 + "primaryKey": false, 282 + "notNull": true, 283 + "autoincrement": false, 284 + "default": "''" 285 + }, 286 + "url": { 287 + "name": "url", 288 + "type": "text(512)", 289 + "primaryKey": false, 290 + "notNull": true, 291 + "autoincrement": false 292 + }, 293 + "name": { 294 + "name": "name", 295 + "type": "text(256)", 296 + "primaryKey": false, 297 + "notNull": true, 298 + "autoincrement": false, 299 + "default": "''" 300 + }, 301 + "description": { 302 + "name": "description", 303 + "type": "text", 304 + "primaryKey": false, 305 + "notNull": true, 306 + "autoincrement": false, 307 + "default": "''" 308 + }, 309 + "workspace_id": { 310 + "name": "workspace_id", 311 + "type": "integer", 312 + "primaryKey": false, 313 + "notNull": false, 314 + "autoincrement": false 315 + }, 316 + "updated_at": { 317 + "name": "updated_at", 318 + "type": "integer", 319 + "primaryKey": false, 320 + "notNull": false, 321 + "autoincrement": false, 322 + "default": "(strftime('%s', 'now'))" 323 + } 324 + }, 325 + "indexes": {}, 326 + "foreignKeys": { 327 + "monitor_workspace_id_workspace_id_fk": { 328 + "name": "monitor_workspace_id_workspace_id_fk", 329 + "tableFrom": "monitor", 330 + "tableTo": "workspace", 331 + "columnsFrom": [ 332 + "workspace_id" 333 + ], 334 + "columnsTo": [ 335 + "id" 336 + ], 337 + "onDelete": "no action", 338 + "onUpdate": "no action" 339 + } 340 + }, 341 + "compositePrimaryKeys": {}, 342 + "uniqueConstraints": {} 343 + }, 344 + "monitors_to_pages": { 345 + "name": "monitors_to_pages", 346 + "columns": { 347 + "monitor_id": { 348 + "name": "monitor_id", 349 + "type": "integer", 350 + "primaryKey": false, 351 + "notNull": true, 352 + "autoincrement": false 353 + }, 354 + "page_id": { 355 + "name": "page_id", 356 + "type": "integer", 357 + "primaryKey": false, 358 + "notNull": true, 359 + "autoincrement": false 360 + } 361 + }, 362 + "indexes": {}, 363 + "foreignKeys": { 364 + "monitors_to_pages_monitor_id_monitor_id_fk": { 365 + "name": "monitors_to_pages_monitor_id_monitor_id_fk", 366 + "tableFrom": "monitors_to_pages", 367 + "tableTo": "monitor", 368 + "columnsFrom": [ 369 + "monitor_id" 370 + ], 371 + "columnsTo": [ 372 + "id" 373 + ], 374 + "onDelete": "cascade", 375 + "onUpdate": "no action" 376 + }, 377 + "monitors_to_pages_page_id_page_id_fk": { 378 + "name": "monitors_to_pages_page_id_page_id_fk", 379 + "tableFrom": "monitors_to_pages", 380 + "tableTo": "page", 381 + "columnsFrom": [ 382 + "page_id" 383 + ], 384 + "columnsTo": [ 385 + "id" 386 + ], 387 + "onDelete": "cascade", 388 + "onUpdate": "no action" 389 + } 390 + }, 391 + "compositePrimaryKeys": { 392 + "monitors_to_pages_monitor_id_page_id_pk": { 393 + "columns": [ 394 + "monitor_id", 395 + "page_id" 396 + ] 397 + } 398 + }, 399 + "uniqueConstraints": {} 400 + }, 401 + "user": { 402 + "name": "user", 403 + "columns": { 404 + "id": { 405 + "name": "id", 406 + "type": "integer", 407 + "primaryKey": true, 408 + "notNull": true, 409 + "autoincrement": false 410 + }, 411 + "tenant_id": { 412 + "name": "tenant_id", 413 + "type": "text(256)", 414 + "primaryKey": false, 415 + "notNull": false, 416 + "autoincrement": false 417 + }, 418 + "updated_at": { 419 + "name": "updated_at", 420 + "type": "integer", 421 + "primaryKey": false, 422 + "notNull": false, 423 + "autoincrement": false, 424 + "default": "(strftime('%s', 'now'))" 425 + } 426 + }, 427 + "indexes": { 428 + "user_tenant_id_unique": { 429 + "name": "user_tenant_id_unique", 430 + "columns": [ 431 + "tenant_id" 432 + ], 433 + "isUnique": true 434 + } 435 + }, 436 + "foreignKeys": {}, 437 + "compositePrimaryKeys": {}, 438 + "uniqueConstraints": {} 439 + }, 440 + "users_to_workspaces": { 441 + "name": "users_to_workspaces", 442 + "columns": { 443 + "user_id": { 444 + "name": "user_id", 445 + "type": "integer", 446 + "primaryKey": false, 447 + "notNull": true, 448 + "autoincrement": false 449 + }, 450 + "workspace_id": { 451 + "name": "workspace_id", 452 + "type": "integer", 453 + "primaryKey": false, 454 + "notNull": true, 455 + "autoincrement": false 456 + } 457 + }, 458 + "indexes": {}, 459 + "foreignKeys": { 460 + "users_to_workspaces_user_id_user_id_fk": { 461 + "name": "users_to_workspaces_user_id_user_id_fk", 462 + "tableFrom": "users_to_workspaces", 463 + "tableTo": "user", 464 + "columnsFrom": [ 465 + "user_id" 466 + ], 467 + "columnsTo": [ 468 + "id" 469 + ], 470 + "onDelete": "no action", 471 + "onUpdate": "no action" 472 + }, 473 + "users_to_workspaces_workspace_id_workspace_id_fk": { 474 + "name": "users_to_workspaces_workspace_id_workspace_id_fk", 475 + "tableFrom": "users_to_workspaces", 476 + "tableTo": "workspace", 477 + "columnsFrom": [ 478 + "workspace_id" 479 + ], 480 + "columnsTo": [ 481 + "id" 482 + ], 483 + "onDelete": "no action", 484 + "onUpdate": "no action" 485 + } 486 + }, 487 + "compositePrimaryKeys": { 488 + "users_to_workspaces_user_id_workspace_id_pk": { 489 + "columns": [ 490 + "user_id", 491 + "workspace_id" 492 + ] 493 + } 494 + }, 495 + "uniqueConstraints": {} 496 + }, 497 + "workspace": { 498 + "name": "workspace", 499 + "columns": { 500 + "id": { 501 + "name": "id", 502 + "type": "integer", 503 + "primaryKey": true, 504 + "notNull": true, 505 + "autoincrement": false 506 + }, 507 + "slug": { 508 + "name": "slug", 509 + "type": "text", 510 + "primaryKey": false, 511 + "notNull": true, 512 + "autoincrement": false 513 + }, 514 + "stripe_id": { 515 + "name": "stripe_id", 516 + "type": "text(256)", 517 + "primaryKey": false, 518 + "notNull": false, 519 + "autoincrement": false 520 + }, 521 + "name": { 522 + "name": "name", 523 + "type": "text", 524 + "primaryKey": false, 525 + "notNull": false, 526 + "autoincrement": false 527 + }, 528 + "updated_at": { 529 + "name": "updated_at", 530 + "type": "integer", 531 + "primaryKey": false, 532 + "notNull": false, 533 + "autoincrement": false, 534 + "default": "(strftime('%s', 'now'))" 535 + } 536 + }, 537 + "indexes": { 538 + "workspace_slug_unique": { 539 + "name": "workspace_slug_unique", 540 + "columns": [ 541 + "slug" 542 + ], 543 + "isUnique": true 544 + }, 545 + "workspace_stripe_id_unique": { 546 + "name": "workspace_stripe_id_unique", 547 + "columns": [ 548 + "stripe_id" 549 + ], 550 + "isUnique": true 551 + } 552 + }, 553 + "foreignKeys": {}, 554 + "compositePrimaryKeys": {}, 555 + "uniqueConstraints": {} 556 + } 557 + }, 558 + "enums": {}, 559 + "_meta": { 560 + "schemas": {}, 561 + "tables": {}, 562 + "columns": {} 563 + } 564 + }
+7
packages/db/drizzle/meta/_journal.json
··· 8 8 "when": 1690309905039, 9 9 "tag": "0000_lively_master_chief", 10 10 "breakpoints": true 11 + }, 12 + { 13 + "idx": 1, 14 + "version": "5", 15 + "when": 1690892003254, 16 + "tag": "0001_brainy_beast", 17 + "breakpoints": true 11 18 } 12 19 ] 13 20 }
-1
packages/db/src/migrate.mts
··· 10 10 const db = drizzle( 11 11 createClient({ url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN }), 12 12 ); 13 - 14 13 console.log("Running migrations"); 15 14 16 15 await migrate(db, { migrationsFolder: "drizzle" });
+29
packages/db/src/schema/monitor.ts
··· 11 11 import { page } from "./page"; 12 12 import { workspace } from "./workspace"; 13 13 14 + export const availableRegions = [ 15 + "arn1", 16 + "bom1", 17 + "cdg1", 18 + "cle1", 19 + "cpt1", 20 + "dub1", 21 + "fra1", 22 + "gru1", 23 + "hkg1", 24 + "hnd1", 25 + "iad1", 26 + "icn1", 27 + "kix1", 28 + "lhr1", 29 + "pdx1", 30 + "sfo1", 31 + "sin1", 32 + "syd1", 33 + ] as const; 34 + 35 + export const RegionEnum = z.enum(availableRegions); 36 + 14 37 export const monitor = sqliteTable("monitor", { 15 38 id: integer("id").primaryKey(), 16 39 jobType: text("job_type", ["website", "cron", "other"]) ··· 21 44 .notNull(), 22 45 status: text("status", ["active", "inactive"]).default("inactive").notNull(), 23 46 active: integer("active", { mode: "boolean" }).default(false), 47 + 48 + regions: text("regions").default("").notNull(), 24 49 25 50 url: text("url", { length: 512 }).notNull(), 26 51 ··· 88 113 url: z.string().url(), 89 114 status: z.enum(["active", "inactive"]).default("inactive"), 90 115 active: z.boolean().default(false), 116 + regions: z.array(RegionEnum).default([]).optional(), 91 117 }); 92 118 93 119 // Schema for selecting a Monitor - can be used to validate API responses ··· 96 122 status: z.enum(["active", "inactive"]).default("inactive"), 97 123 jobType: z.enum(["website", "cron", "other"]).default("other"), 98 124 active: z.boolean().default(false), 125 + regions: z 126 + .preprocess((val) => String(val).split(","), z.array(RegionEnum)) 127 + .default([]), 99 128 }); 100 129 101 130 export const allMonitorsSchema = z.array(selectMonitorSchema);