Openstatus www.openstatus.dev
at main 793 lines 24 kB view raw
1import { TRPCError } from "@trpc/server"; 2import { z } from "zod"; 3 4import { 5 type Assertion, 6 DnsRecordAssertion, 7 HeaderAssertion, 8 StatusAssertion, 9 TextBodyAssertion, 10 headerAssertion, 11 jsonBodyAssertion, 12 recordAssertion, 13 serialize, 14 statusAssertion, 15 textBodyAssertion, 16} from "@openstatus/assertions"; 17import { 18 type SQL, 19 and, 20 count, 21 eq, 22 inArray, 23 isNull, 24 syncMaintenanceToMonitorDeleteByMonitors, 25 syncMonitorsToPageDeleteByMonitors, 26 syncStatusReportToMonitorDeleteByMonitors, 27} from "@openstatus/db"; 28import { 29 insertMonitorSchema, 30 maintenancesToMonitors, 31 monitor, 32 monitorJobTypes, 33 monitorMethods, 34 monitorTag, 35 monitorTagsToMonitors, 36 monitorsToPages, 37 monitorsToStatusReport, 38 notification, 39 notificationsToMonitors, 40 privateLocationToMonitors, 41 selectIncidentSchema, 42 selectMonitorSchema, 43 selectMonitorTagSchema, 44 selectNotificationSchema, 45 selectPrivateLocationSchema, 46} from "@openstatus/db/src/schema"; 47 48import { Events } from "@openstatus/analytics"; 49import { 50 freeFlyRegions, 51 monitorPeriodicity, 52 monitorRegions, 53} from "@openstatus/db/src/schema/constants"; 54import { regionDict } from "@openstatus/regions"; 55import { createTRPCRouter, protectedProcedure } from "../trpc"; 56import { testDns, testHttp, testTcp } from "./checker"; 57 58export const monitorRouter = createTRPCRouter({ 59 delete: protectedProcedure 60 .meta({ track: Events.DeleteMonitor }) 61 .input(z.object({ id: z.number() })) 62 .mutation(async (opts) => { 63 const monitorToDelete = await opts.ctx.db 64 .select() 65 .from(monitor) 66 .where( 67 and( 68 eq(monitor.id, opts.input.id), 69 eq(monitor.workspaceId, opts.ctx.workspace.id), 70 ), 71 ) 72 .get(); 73 if (!monitorToDelete) return; 74 75 await opts.ctx.db 76 .update(monitor) 77 .set({ deletedAt: new Date(), active: false }) 78 .where(eq(monitor.id, monitorToDelete.id)) 79 .run(); 80 81 await opts.ctx.db.transaction(async (tx) => { 82 await tx 83 .delete(monitorsToPages) 84 .where(eq(monitorsToPages.monitorId, monitorToDelete.id)); 85 await tx 86 .delete(monitorTagsToMonitors) 87 .where(eq(monitorTagsToMonitors.monitorId, monitorToDelete.id)); 88 await tx 89 .delete(monitorsToStatusReport) 90 .where(eq(monitorsToStatusReport.monitorId, monitorToDelete.id)); 91 await tx 92 .delete(notificationsToMonitors) 93 .where(eq(notificationsToMonitors.monitorId, monitorToDelete.id)); 94 await tx 95 .delete(maintenancesToMonitors) 96 .where(eq(maintenancesToMonitors.monitorId, monitorToDelete.id)); 97 // Sync deletes to page components 98 await syncMonitorsToPageDeleteByMonitors(tx, [monitorToDelete.id]); 99 await syncStatusReportToMonitorDeleteByMonitors(tx, [ 100 monitorToDelete.id, 101 ]); 102 await syncMaintenanceToMonitorDeleteByMonitors(tx, [ 103 monitorToDelete.id, 104 ]); 105 }); 106 }), 107 108 deleteMonitors: protectedProcedure 109 .input(z.object({ ids: z.number().array() })) 110 .mutation(async (opts) => { 111 const _monitors = await opts.ctx.db 112 .select() 113 .from(monitor) 114 .where( 115 and( 116 inArray(monitor.id, opts.input.ids), 117 eq(monitor.workspaceId, opts.ctx.workspace.id), 118 ), 119 ) 120 .all(); 121 122 if (_monitors.length !== opts.input.ids.length) { 123 throw new TRPCError({ 124 code: "NOT_FOUND", 125 message: "Monitor not found.", 126 }); 127 } 128 129 await opts.ctx.db 130 .update(monitor) 131 .set({ deletedAt: new Date(), active: false }) 132 .where(inArray(monitor.id, opts.input.ids)) 133 .run(); 134 135 await opts.ctx.db.transaction(async (tx) => { 136 await tx 137 .delete(monitorsToPages) 138 .where(inArray(monitorsToPages.monitorId, opts.input.ids)); 139 await tx 140 .delete(monitorTagsToMonitors) 141 .where(inArray(monitorTagsToMonitors.monitorId, opts.input.ids)); 142 await tx 143 .delete(monitorsToStatusReport) 144 .where(inArray(monitorsToStatusReport.monitorId, opts.input.ids)); 145 await tx 146 .delete(notificationsToMonitors) 147 .where(inArray(notificationsToMonitors.monitorId, opts.input.ids)); 148 await tx 149 .delete(maintenancesToMonitors) 150 .where(inArray(maintenancesToMonitors.monitorId, opts.input.ids)); 151 // Sync deletes to page components 152 await syncMonitorsToPageDeleteByMonitors(tx, opts.input.ids); 153 await syncStatusReportToMonitorDeleteByMonitors(tx, opts.input.ids); 154 await syncMaintenanceToMonitorDeleteByMonitors(tx, opts.input.ids); 155 }); 156 }), 157 158 updateMonitors: protectedProcedure 159 .input( 160 insertMonitorSchema 161 .pick({ public: true, active: true }) 162 .partial() // batched updates 163 .extend({ ids: z.number().array() }), // array of monitor ids to update 164 ) 165 .mutation(async (opts) => { 166 await opts.ctx.db 167 .update(monitor) 168 .set(opts.input) 169 .where( 170 and( 171 inArray(monitor.id, opts.input.ids), 172 eq(monitor.workspaceId, opts.ctx.workspace.id), 173 isNull(monitor.deletedAt), 174 ), 175 ); 176 }), 177 178 list: protectedProcedure 179 .input( 180 z 181 .object({ 182 order: z.enum(["asc", "desc"]).optional(), 183 }) 184 .optional(), 185 ) 186 .query(async (opts) => { 187 const whereConditions: SQL[] = [ 188 eq(monitor.workspaceId, opts.ctx.workspace.id), 189 isNull(monitor.deletedAt), 190 ]; 191 192 const result = await opts.ctx.db.query.monitor.findMany({ 193 where: and(...whereConditions), 194 with: { 195 monitorTagsToMonitors: { 196 with: { monitorTag: true }, 197 }, 198 incidents: { 199 orderBy: (incident, { desc }) => [desc(incident.createdAt)], 200 }, 201 }, 202 orderBy: (monitor, { asc, desc }) => 203 opts.input?.order === "asc" 204 ? [asc(monitor.active), asc(monitor.createdAt)] 205 : [desc(monitor.active), desc(monitor.createdAt)], 206 }); 207 208 return z 209 .array( 210 selectMonitorSchema.extend({ 211 tags: z.array(selectMonitorTagSchema).prefault([]), 212 incidents: z.array(selectIncidentSchema).prefault([]), 213 }), 214 ) 215 .parse( 216 result.map((data) => ({ 217 ...data, 218 tags: data.monitorTagsToMonitors.map((t) => t.monitorTag), 219 })), 220 ); 221 }), 222 223 get: protectedProcedure 224 .input(z.object({ id: z.coerce.number() })) 225 .query(async ({ ctx, input }) => { 226 const whereConditions: SQL[] = [ 227 eq(monitor.id, input.id), 228 eq(monitor.workspaceId, ctx.workspace.id), 229 isNull(monitor.deletedAt), 230 ]; 231 232 const data = await ctx.db.query.monitor.findFirst({ 233 where: and(...whereConditions), 234 with: { 235 monitorsToNotifications: { 236 with: { notification: true }, 237 }, 238 monitorTagsToMonitors: { 239 with: { monitorTag: true }, 240 }, 241 incidents: true, 242 privateLocationToMonitors: { 243 with: { privateLocation: true }, 244 }, 245 }, 246 }); 247 248 if (!data) return null; 249 250 return selectMonitorSchema 251 .extend({ 252 notifications: z.array(selectNotificationSchema).prefault([]), 253 tags: z.array(selectMonitorTagSchema).prefault([]), 254 incidents: z.array(selectIncidentSchema).prefault([]), 255 privateLocations: z.array(selectPrivateLocationSchema).prefault([]), 256 }) 257 .parse({ 258 ...data, 259 notifications: data.monitorsToNotifications.map( 260 (m) => m.notification, 261 ), 262 tags: data.monitorTagsToMonitors.map((t) => t.monitorTag), 263 incidents: data.incidents, 264 privateLocations: data.privateLocationToMonitors.map( 265 (p) => p.privateLocation, 266 ), 267 }); 268 }), 269 270 clone: protectedProcedure 271 .meta({ track: Events.CloneMonitor }) 272 .input(z.object({ id: z.number() })) 273 .mutation(async ({ ctx, input }) => { 274 const whereConditions: SQL[] = [ 275 eq(monitor.id, input.id), 276 eq(monitor.workspaceId, ctx.workspace.id), 277 isNull(monitor.deletedAt), 278 ]; 279 280 const _monitors = await ctx.db.query.monitor.findMany({ 281 where: and( 282 eq(monitor.workspaceId, ctx.workspace.id), 283 isNull(monitor.deletedAt), 284 ), 285 }); 286 287 if (_monitors.length >= ctx.workspace.limits.monitors) { 288 throw new TRPCError({ 289 code: "FORBIDDEN", 290 message: "You have reached the maximum number of monitors.", 291 }); 292 } 293 294 const data = await ctx.db.query.monitor.findFirst({ 295 where: and(...whereConditions), 296 }); 297 298 if (!data) { 299 throw new TRPCError({ 300 code: "NOT_FOUND", 301 message: "Monitor not found.", 302 }); 303 } 304 305 const [newMonitor] = await ctx.db 306 .insert(monitor) 307 .values({ 308 ...data, 309 id: undefined, // let the db generate the id 310 name: `${data.name} (Copy)`, 311 createdAt: new Date(), 312 updatedAt: new Date(), 313 }) 314 .returning(); 315 316 if (!newMonitor) { 317 throw new TRPCError({ 318 code: "INTERNAL_SERVER_ERROR", 319 message: "Failed to clone monitor.", 320 }); 321 } 322 323 return newMonitor; 324 }), 325 326 updateRetry: protectedProcedure 327 .meta({ track: Events.UpdateMonitor }) 328 .input(z.object({ id: z.number(), retry: z.number() })) 329 .mutation(async ({ ctx, input }) => { 330 const whereConditions: SQL[] = [ 331 eq(monitor.id, input.id), 332 eq(monitor.workspaceId, ctx.workspace.id), 333 isNull(monitor.deletedAt), 334 ]; 335 336 await ctx.db 337 .update(monitor) 338 .set({ retry: input.retry, updatedAt: new Date() }) 339 .where(and(...whereConditions)) 340 .run(); 341 }), 342 343 updateFollowRedirects: protectedProcedure 344 .meta({ track: Events.UpdateMonitor }) 345 .input(z.object({ id: z.number(), followRedirects: z.boolean() })) 346 .mutation(async ({ ctx, input }) => { 347 const whereConditions: SQL[] = [ 348 eq(monitor.id, input.id), 349 eq(monitor.workspaceId, ctx.workspace.id), 350 isNull(monitor.deletedAt), 351 ]; 352 353 await ctx.db 354 .update(monitor) 355 .set({ 356 followRedirects: input.followRedirects, 357 updatedAt: new Date(), 358 }) 359 .where(and(...whereConditions)) 360 .run(); 361 }), 362 363 updateOtel: protectedProcedure 364 .meta({ track: Events.UpdateMonitor }) 365 .input( 366 z.object({ 367 id: z.number(), 368 otelEndpoint: z.string(), 369 otelHeaders: z 370 .array(z.object({ key: z.string(), value: z.string() })) 371 .optional(), 372 }), 373 ) 374 .mutation(async ({ ctx, input }) => { 375 const whereConditions: SQL[] = [ 376 eq(monitor.id, input.id), 377 eq(monitor.workspaceId, ctx.workspace.id), 378 isNull(monitor.deletedAt), 379 ]; 380 381 await ctx.db 382 .update(monitor) 383 .set({ 384 otelEndpoint: input.otelEndpoint, 385 otelHeaders: input.otelHeaders 386 ? JSON.stringify(input.otelHeaders) 387 : undefined, 388 updatedAt: new Date(), 389 }) 390 .where(and(...whereConditions)) 391 .run(); 392 }), 393 394 updatePublic: protectedProcedure 395 .meta({ track: Events.UpdateMonitor }) 396 .input(z.object({ id: z.number(), public: z.boolean() })) 397 .mutation(async ({ ctx, input }) => { 398 const whereConditions: SQL[] = [ 399 eq(monitor.id, input.id), 400 eq(monitor.workspaceId, ctx.workspace.id), 401 isNull(monitor.deletedAt), 402 ]; 403 404 await ctx.db 405 .update(monitor) 406 .set({ public: input.public, updatedAt: new Date() }) 407 .where(and(...whereConditions)) 408 .run(); 409 }), 410 411 updateSchedulingRegions: protectedProcedure 412 .meta({ track: Events.UpdateMonitor }) 413 .input( 414 z.object({ 415 id: z.number(), 416 regions: z.array(z.string()), 417 periodicity: z.enum(monitorPeriodicity), 418 privateLocations: z.array(z.number()), 419 }), 420 ) 421 .mutation(async ({ ctx, input }) => { 422 const whereConditions: SQL[] = [ 423 eq(monitor.id, input.id), 424 eq(monitor.workspaceId, ctx.workspace.id), 425 isNull(monitor.deletedAt), 426 ]; 427 428 const limits = ctx.workspace.limits; 429 430 if (!limits.periodicity.includes(input.periodicity)) { 431 throw new TRPCError({ 432 code: "FORBIDDEN", 433 message: "Upgrade to check more often.", 434 }); 435 } 436 437 if (limits["max-regions"] < input.regions.length) { 438 throw new TRPCError({ 439 code: "FORBIDDEN", 440 message: "You have reached the maximum number of regions.", 441 }); 442 } 443 444 if ( 445 input.regions.length > 0 && 446 !input.regions.every((r) => 447 limits.regions.includes(r as (typeof limits)["regions"][number]), 448 ) 449 ) { 450 throw new TRPCError({ 451 code: "FORBIDDEN", 452 message: "You don't have access to this region.", 453 }); 454 } 455 456 await ctx.db.transaction(async (tx) => { 457 await tx 458 .update(monitor) 459 .set({ 460 regions: input.regions.join(","), 461 periodicity: input.periodicity, 462 updatedAt: new Date(), 463 }) 464 .where(and(...whereConditions)) 465 .run(); 466 467 await tx 468 .delete(privateLocationToMonitors) 469 .where(eq(privateLocationToMonitors.monitorId, input.id)); 470 471 if (input.privateLocations && input.privateLocations.length > 0) { 472 await tx.insert(privateLocationToMonitors).values( 473 input.privateLocations.map((privateLocationId) => ({ 474 monitorId: input.id, 475 privateLocationId, 476 })), 477 ); 478 } 479 }); 480 }), 481 482 updateResponseTime: protectedProcedure 483 .meta({ track: Events.UpdateMonitor }) 484 .input( 485 z.object({ 486 id: z.number(), 487 timeout: z.number(), 488 degradedAfter: z.number().nullish(), 489 }), 490 ) 491 .mutation(async ({ ctx, input }) => { 492 const whereConditions: SQL[] = [ 493 eq(monitor.id, input.id), 494 eq(monitor.workspaceId, ctx.workspace.id), 495 isNull(monitor.deletedAt), 496 ]; 497 498 await ctx.db 499 .update(monitor) 500 .set({ 501 timeout: input.timeout, 502 degradedAfter: input.degradedAfter, 503 updatedAt: new Date(), 504 }) 505 .where(and(...whereConditions)) 506 .run(); 507 }), 508 509 updateTags: protectedProcedure 510 .meta({ track: Events.UpdateMonitor }) 511 .input(z.object({ id: z.number(), tags: z.array(z.number()) })) 512 .mutation(async ({ ctx, input }) => { 513 const allTags = await ctx.db.query.monitorTag.findMany({ 514 where: and( 515 eq(monitorTag.workspaceId, ctx.workspace.id), 516 inArray(monitorTag.id, input.tags), 517 ), 518 }); 519 520 if (allTags.length !== input.tags.length) { 521 throw new TRPCError({ 522 code: "FORBIDDEN", 523 message: "You don't have access to this tag.", 524 }); 525 } 526 527 await ctx.db.transaction(async (tx) => { 528 await tx 529 .delete(monitorTagsToMonitors) 530 .where(and(eq(monitorTagsToMonitors.monitorId, input.id))); 531 532 if (input.tags.length > 0) { 533 await tx.insert(monitorTagsToMonitors).values( 534 input.tags.map((tagId) => ({ 535 monitorId: input.id, 536 monitorTagId: tagId, 537 })), 538 ); 539 } 540 }); 541 }), 542 543 updateGeneral: protectedProcedure 544 .meta({ track: Events.UpdateMonitor }) 545 .input( 546 z.object({ 547 id: z.number(), 548 jobType: z.enum(monitorJobTypes), 549 url: z.string(), 550 method: z.enum(monitorMethods), 551 headers: z.array(z.object({ key: z.string(), value: z.string() })), 552 body: z.string().optional(), 553 name: z.string(), 554 assertions: z.array( 555 z.discriminatedUnion("type", [ 556 statusAssertion, 557 headerAssertion, 558 textBodyAssertion, 559 jsonBodyAssertion, 560 recordAssertion, 561 ]), 562 ), 563 active: z.boolean().prefault(true), 564 // skip the test check if assertions are OK 565 skipCheck: z.boolean().prefault(true), 566 // save check in db (iff success? -> e.g. onboarding to get a first ping) 567 saveCheck: z.boolean().prefault(false), 568 }), 569 ) 570 .mutation(async ({ ctx, input }) => { 571 const whereConditions: SQL[] = [ 572 eq(monitor.id, input.id), 573 eq(monitor.workspaceId, ctx.workspace.id), 574 isNull(monitor.deletedAt), 575 ]; 576 577 const assertions: Assertion[] = []; 578 for (const a of input.assertions ?? []) { 579 if (a.type === "status") { 580 assertions.push(new StatusAssertion(a)); 581 } 582 if (a.type === "header") { 583 assertions.push(new HeaderAssertion(a)); 584 } 585 if (a.type === "textBody") { 586 assertions.push(new TextBodyAssertion(a)); 587 } 588 if (a.type === "dnsRecord") { 589 assertions.push(new DnsRecordAssertion(a)); 590 } 591 } 592 593 // NOTE: we are checking the endpoint before saving 594 if (!input.skipCheck && input.active) { 595 if (input.jobType === "http") { 596 await testHttp({ 597 url: input.url, 598 method: input.method, 599 headers: input.headers, 600 body: input.body, 601 // Filter out DNS record assertions as they can't be validated via HTTP 602 assertions: input.assertions.filter((a) => a.type !== "dnsRecord"), 603 region: "ams", 604 }); 605 } else if (input.jobType === "tcp") { 606 await testTcp({ 607 url: input.url, 608 region: "ams", 609 }); 610 } else if (input.jobType === "dns") { 611 await testDns({ 612 url: input.url, 613 region: "ams", 614 assertions: input.assertions.filter((a) => a.type === "dnsRecord"), 615 }); 616 } 617 } 618 619 await ctx.db 620 .update(monitor) 621 .set({ 622 name: input.name, 623 jobType: input.jobType, 624 url: input.url, 625 method: input.method, 626 headers: input.headers ? JSON.stringify(input.headers) : undefined, 627 body: input.body, 628 active: input.active, 629 assertions: serialize(assertions), 630 updatedAt: new Date(), 631 }) 632 .where(and(...whereConditions)) 633 .run(); 634 }), 635 636 updateNotifiers: protectedProcedure 637 .meta({ track: Events.UpdateMonitor }) 638 .input(z.object({ id: z.number(), notifiers: z.array(z.number()) })) 639 .mutation(async ({ ctx, input }) => { 640 const allNotifiers = await ctx.db.query.notification.findMany({ 641 where: and( 642 eq(notification.workspaceId, ctx.workspace.id), 643 inArray(notification.id, input.notifiers), 644 ), 645 }); 646 647 if (allNotifiers.length !== input.notifiers.length) { 648 throw new TRPCError({ 649 code: "FORBIDDEN", 650 message: "You don't have access to this notifier.", 651 }); 652 } 653 654 await ctx.db.transaction(async (tx) => { 655 await tx 656 .delete(notificationsToMonitors) 657 .where(and(eq(notificationsToMonitors.monitorId, input.id))); 658 659 if (input.notifiers.length > 0) { 660 await tx.insert(notificationsToMonitors).values( 661 input.notifiers.map((notifierId) => ({ 662 monitorId: input.id, 663 notificationId: notifierId, 664 })), 665 ); 666 } 667 }); 668 }), 669 670 new: protectedProcedure 671 .meta({ track: Events.CreateMonitor, trackProps: ["url", "jobType"] }) 672 .input( 673 z.object({ 674 name: z.string(), 675 jobType: z.enum(monitorJobTypes), 676 url: z.string(), 677 method: z.enum(monitorMethods), 678 headers: z.array(z.object({ key: z.string(), value: z.string() })), 679 body: z.string().optional(), 680 assertions: z.array( 681 z.discriminatedUnion("type", [ 682 statusAssertion, 683 headerAssertion, 684 textBodyAssertion, 685 jsonBodyAssertion, 686 recordAssertion, 687 ]), 688 ), 689 active: z.boolean().prefault(false), 690 saveCheck: z.boolean().prefault(false), 691 skipCheck: z.boolean().prefault(false), 692 }), 693 ) 694 .mutation(async ({ ctx, input }) => { 695 const limits = ctx.workspace.limits; 696 697 const res = await ctx.db 698 .select({ count: count() }) 699 .from(monitor) 700 .where( 701 and( 702 eq(monitor.workspaceId, ctx.workspace.id), 703 isNull(monitor.deletedAt), 704 ), 705 ) 706 .get(); 707 708 // the user has reached the limits 709 if (res && res.count >= limits.monitors) { 710 throw new TRPCError({ 711 code: "FORBIDDEN", 712 message: "You reached your monitor limits.", 713 }); 714 } 715 716 const assertions: Assertion[] = []; 717 for (const a of input.assertions ?? []) { 718 if (a.type === "status") { 719 assertions.push(new StatusAssertion(a)); 720 } 721 if (a.type === "header") { 722 assertions.push(new HeaderAssertion(a)); 723 } 724 if (a.type === "textBody") { 725 assertions.push(new TextBodyAssertion(a)); 726 } 727 if (a.type === "dnsRecord") { 728 assertions.push(new DnsRecordAssertion(a)); 729 } 730 } 731 732 // NOTE: we are checking the endpoint before saving 733 if (!input.skipCheck) { 734 if (input.jobType === "http") { 735 await testHttp({ 736 url: input.url, 737 method: input.method, 738 headers: input.headers, 739 body: input.body, 740 // Filter out DNS record assertions as they can't be validated via HTTP 741 assertions: input.assertions.filter((a) => a.type !== "dnsRecord"), 742 region: "ams", 743 }); 744 } else if (input.jobType === "tcp") { 745 await testTcp({ 746 url: input.url, 747 region: "ams", 748 }); 749 } else if (input.jobType === "dns") { 750 await testDns({ 751 url: input.url, 752 region: "ams", 753 assertions: input.assertions.filter((a) => a.type === "dnsRecord"), 754 }); 755 } 756 } 757 758 const selectableRegions = 759 ctx.workspace.plan === "free" ? freeFlyRegions : monitorRegions; 760 const randomRegions = ctx.workspace.plan === "free" ? 4 : 6; 761 762 const regions = [...selectableRegions] 763 // NOTE: make sure we don't use deprecated regions 764 .filter((r) => { 765 const deprecated = regionDict[r].deprecated; 766 if (!deprecated) return true; 767 return false; 768 }) 769 .sort(() => 0.5 - Math.random()) 770 .slice(0, randomRegions); 771 772 const newMonitor = await ctx.db 773 .insert(monitor) 774 .values({ 775 name: input.name, 776 jobType: input.jobType, 777 url: input.url, 778 method: input.method, 779 headers: input.headers ? JSON.stringify(input.headers) : undefined, 780 body: input.body, 781 active: input.active, 782 workspaceId: ctx.workspace.id, 783 periodicity: ctx.workspace.plan === "free" ? "30m" : "1m", 784 regions: regions.join(","), 785 assertions: serialize(assertions), 786 updatedAt: new Date(), 787 }) 788 .returning() 789 .get(); 790 791 return newMonitor; 792 }), 793});