Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 1566 lines 47 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 sql, 25 syncMaintenanceToMonitorDeleteByMonitors, 26 syncMonitorsToPageDelete, 27 syncMonitorsToPageDeleteByMonitors, 28 syncMonitorsToPageInsertMany, 29 syncStatusReportToMonitorDeleteByMonitors, 30} from "@openstatus/db"; 31import { 32 insertMonitorSchema, 33 maintenancesToMonitors, 34 monitor, 35 monitorJobTypes, 36 monitorMethods, 37 monitorTag, 38 monitorTagsToMonitors, 39 monitorsToPages, 40 monitorsToStatusReport, 41 notification, 42 notificationsToMonitors, 43 page, 44 privateLocationToMonitors, 45 selectIncidentSchema, 46 selectMaintenanceSchema, 47 selectMonitorSchema, 48 selectMonitorTagSchema, 49 selectNotificationSchema, 50 selectPageSchema, 51 selectPrivateLocationSchema, 52 selectPublicMonitorSchema, 53} from "@openstatus/db/src/schema"; 54 55import { Events } from "@openstatus/analytics"; 56import { 57 freeFlyRegions, 58 monitorPeriodicity, 59 monitorRegions, 60} from "@openstatus/db/src/schema/constants"; 61import { regionDict } from "@openstatus/regions"; 62import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 63import { testDns, testHttp, testTcp } from "./checker"; 64 65export const monitorRouter = createTRPCRouter({ 66 create: protectedProcedure 67 .meta({ track: Events.CreateMonitor, trackProps: ["url", "jobType"] }) 68 .input(insertMonitorSchema) 69 .output(selectMonitorSchema) 70 .mutation(async (opts) => { 71 const monitorLimit = opts.ctx.workspace.limits.monitors; 72 const periodicityLimit = opts.ctx.workspace.limits.periodicity; 73 const regionsLimit = opts.ctx.workspace.limits.regions; 74 75 const monitorNumbers = ( 76 await opts.ctx.db.query.monitor.findMany({ 77 where: and( 78 eq(monitor.workspaceId, opts.ctx.workspace.id), 79 isNull(monitor.deletedAt), 80 ), 81 }) 82 ).length; 83 84 // the user has reached the limits 85 if (monitorNumbers >= monitorLimit) { 86 throw new TRPCError({ 87 code: "FORBIDDEN", 88 message: "You reached your monitor limits.", 89 }); 90 } 91 92 // the user is not allowed to use the cron job 93 if ( 94 opts.input.periodicity && 95 !periodicityLimit.includes(opts.input.periodicity) 96 ) { 97 throw new TRPCError({ 98 code: "FORBIDDEN", 99 message: "You reached your cron job limits.", 100 }); 101 } 102 103 if ( 104 opts.input.regions !== undefined && 105 opts.input.regions?.length !== 0 106 ) { 107 for (const region of opts.input.regions) { 108 if (!regionsLimit.includes(region)) { 109 throw new TRPCError({ 110 code: "FORBIDDEN", 111 message: "You don't have access to this region.", 112 }); 113 } 114 } 115 } 116 117 // FIXME: this is a hotfix 118 const { 119 regions, 120 headers, 121 notifications, 122 pages, 123 tags, 124 statusAssertions, 125 headerAssertions, 126 textBodyAssertions, 127 otelHeaders, 128 ...data 129 } = opts.input; 130 131 const assertions: Assertion[] = []; 132 for (const a of statusAssertions ?? []) { 133 assertions.push(new StatusAssertion(a)); 134 } 135 for (const a of headerAssertions ?? []) { 136 assertions.push(new HeaderAssertion(a)); 137 } 138 for (const a of textBodyAssertions ?? []) { 139 assertions.push(new TextBodyAssertion(a)); 140 } 141 142 const newMonitor = await opts.ctx.db 143 .insert(monitor) 144 .values({ 145 // REMINDER: We should explicitly pass the corresponding attributes 146 // otherwise, unexpected attributes will be passed 147 ...data, 148 workspaceId: opts.ctx.workspace.id, 149 regions: regions?.join(","), 150 headers: headers ? JSON.stringify(headers) : undefined, 151 otelHeaders: otelHeaders ? JSON.stringify(otelHeaders) : undefined, 152 assertions: assertions.length > 0 ? serialize(assertions) : undefined, 153 }) 154 .returning() 155 .get(); 156 157 if (notifications.length > 0) { 158 const allNotifications = await opts.ctx.db.query.notification.findMany({ 159 where: and( 160 eq(notification.workspaceId, opts.ctx.workspace.id), 161 inArray(notification.id, notifications), 162 ), 163 }); 164 165 const values = allNotifications.map((notification) => ({ 166 monitorId: newMonitor.id, 167 notificationId: notification.id, 168 })); 169 170 await opts.ctx.db.insert(notificationsToMonitors).values(values).run(); 171 } 172 173 if (tags.length > 0) { 174 const allTags = await opts.ctx.db.query.monitorTag.findMany({ 175 where: and( 176 eq(monitorTag.workspaceId, opts.ctx.workspace.id), 177 inArray(monitorTag.id, tags), 178 ), 179 }); 180 181 const values = allTags.map((monitorTag) => ({ 182 monitorId: newMonitor.id, 183 monitorTagId: monitorTag.id, 184 })); 185 186 await opts.ctx.db.insert(monitorTagsToMonitors).values(values).run(); 187 } 188 189 if (pages.length > 0) { 190 const allPages = await opts.ctx.db.query.page.findMany({ 191 where: and( 192 eq(page.workspaceId, opts.ctx.workspace.id), 193 inArray(page.id, pages), 194 ), 195 }); 196 197 const values = allPages.map((page) => ({ 198 monitorId: newMonitor.id, 199 pageId: page.id, 200 })); 201 202 await opts.ctx.db.insert(monitorsToPages).values(values).run(); 203 // Sync to page components 204 await syncMonitorsToPageInsertMany(opts.ctx.db, values); 205 } 206 207 return selectMonitorSchema.parse(newMonitor); 208 }), 209 210 getMonitorById: protectedProcedure 211 .input(z.object({ id: z.number() })) 212 .query(async (opts) => { 213 const _monitor = await opts.ctx.db.query.monitor.findFirst({ 214 where: and( 215 eq(monitor.id, opts.input.id), 216 eq(monitor.workspaceId, opts.ctx.workspace.id), 217 isNull(monitor.deletedAt), 218 ), 219 with: { 220 monitorTagsToMonitors: { with: { monitorTag: true } }, 221 maintenancesToMonitors: { 222 with: { maintenance: true }, 223 where: eq(maintenancesToMonitors.monitorId, opts.input.id), 224 }, 225 monitorsToNotifications: { with: { notification: true } }, 226 }, 227 }); 228 229 const parsedMonitor = selectMonitorSchema 230 .extend({ 231 monitorTagsToMonitors: z 232 .object({ 233 monitorTag: selectMonitorTagSchema, 234 }) 235 .array(), 236 maintenance: z.boolean().prefault(false).optional(), 237 monitorsToNotifications: z 238 .object({ 239 notification: selectNotificationSchema, 240 }) 241 .array(), 242 }) 243 .safeParse({ 244 ..._monitor, 245 maintenance: _monitor?.maintenancesToMonitors.some( 246 (item) => 247 item.maintenance.from.getTime() <= Date.now() && 248 item.maintenance.to.getTime() >= Date.now(), 249 ), 250 }); 251 252 if (!parsedMonitor.success) { 253 throw new TRPCError({ 254 code: "UNAUTHORIZED", 255 message: "You are not allowed to access the monitor.", 256 }); 257 } 258 return parsedMonitor.data; 259 }), 260 261 getPublicMonitorById: publicProcedure 262 // REMINDER: if on status page, we should check if the monitor is associated with the page 263 // otherwise, using `/public` we don't need to check 264 .input(z.object({ id: z.number(), slug: z.string().optional() })) 265 .query(async (opts) => { 266 const _monitor = await opts.ctx.db 267 .select() 268 .from(monitor) 269 .where( 270 and( 271 eq(monitor.id, opts.input.id), 272 isNull(monitor.deletedAt), 273 eq(monitor.public, true), 274 ), 275 ) 276 .get(); 277 278 if (!_monitor) return undefined; 279 280 if (opts.input.slug) { 281 const _page = await opts.ctx.db.query.page.findFirst({ 282 where: sql`lower(${page.slug}) = ${opts.input.slug} OR lower(${page.customDomain}) = ${opts.input.slug}`, 283 with: { monitorsToPages: true }, 284 }); 285 286 const hasPageRelation = _page?.monitorsToPages.find( 287 ({ monitorId }) => _monitor.id === monitorId, 288 ); 289 290 if (!hasPageRelation) return undefined; 291 } 292 293 return selectPublicMonitorSchema.parse(_monitor); 294 }), 295 296 update: protectedProcedure 297 .meta({ track: Events.UpdateMonitor }) 298 .input(insertMonitorSchema) 299 .mutation(async (opts) => { 300 if (!opts.input.id) return; 301 302 const periodicityLimit = opts.ctx.workspace.limits.periodicity; 303 304 const regionsLimit = opts.ctx.workspace.limits.regions; 305 306 // the user is not allowed to use the cron job 307 if ( 308 opts.input?.periodicity && 309 !periodicityLimit.includes(opts.input?.periodicity) 310 ) { 311 throw new TRPCError({ 312 code: "FORBIDDEN", 313 message: "You reached your cron job limits.", 314 }); 315 } 316 317 if ( 318 opts.input.regions !== undefined && 319 opts.input.regions?.length !== 0 320 ) { 321 for (const region of opts.input.regions) { 322 if (!regionsLimit.includes(region)) { 323 throw new TRPCError({ 324 code: "FORBIDDEN", 325 message: "You don't have access to this region.", 326 }); 327 } 328 } 329 } 330 331 const { 332 regions, 333 headers, 334 notifications, 335 pages, 336 tags, 337 statusAssertions, 338 headerAssertions, 339 textBodyAssertions, 340 otelHeaders, 341 342 ...data 343 } = opts.input; 344 345 const assertions: Assertion[] = []; 346 for (const a of statusAssertions ?? []) { 347 assertions.push(new StatusAssertion(a)); 348 } 349 for (const a of headerAssertions ?? []) { 350 assertions.push(new HeaderAssertion(a)); 351 } 352 for (const a of textBodyAssertions ?? []) { 353 assertions.push(new TextBodyAssertion(a)); 354 } 355 356 const currentMonitor = await opts.ctx.db 357 .update(monitor) 358 .set({ 359 ...data, 360 regions: regions?.join(","), 361 updatedAt: new Date(), 362 headers: headers ? JSON.stringify(headers) : undefined, 363 otelHeaders: otelHeaders ? JSON.stringify(otelHeaders) : undefined, 364 assertions: serialize(assertions), 365 }) 366 .where( 367 and( 368 eq(monitor.id, opts.input.id), 369 eq(monitor.workspaceId, opts.ctx.workspace.id), 370 isNull(monitor.deletedAt), 371 ), 372 ) 373 .returning() 374 .get(); 375 376 console.log({ 377 currentMonitor, 378 id: opts.input.id, 379 workspaceId: opts.ctx.workspace.id, 380 }); 381 382 const currentMonitorNotifications = await opts.ctx.db 383 .select() 384 .from(notificationsToMonitors) 385 .where(eq(notificationsToMonitors.monitorId, currentMonitor.id)) 386 .all(); 387 388 const addedNotifications = notifications.filter( 389 (x) => 390 !currentMonitorNotifications 391 .map(({ notificationId }) => notificationId) 392 ?.includes(x), 393 ); 394 395 if (addedNotifications.length > 0) { 396 const values = addedNotifications.map((notificationId) => ({ 397 monitorId: currentMonitor.id, 398 notificationId, 399 })); 400 401 await opts.ctx.db.insert(notificationsToMonitors).values(values).run(); 402 } 403 404 const removedNotifications = currentMonitorNotifications 405 .map(({ notificationId }) => notificationId) 406 .filter((x) => !notifications?.includes(x)); 407 408 if (removedNotifications.length > 0) { 409 await opts.ctx.db 410 .delete(notificationsToMonitors) 411 .where( 412 and( 413 eq(notificationsToMonitors.monitorId, currentMonitor.id), 414 inArray( 415 notificationsToMonitors.notificationId, 416 removedNotifications, 417 ), 418 ), 419 ) 420 .run(); 421 } 422 423 const currentMonitorTags = await opts.ctx.db 424 .select() 425 .from(monitorTagsToMonitors) 426 .where(eq(monitorTagsToMonitors.monitorId, currentMonitor.id)) 427 .all(); 428 429 const addedTags = tags.filter( 430 (x) => 431 !currentMonitorTags 432 .map(({ monitorTagId }) => monitorTagId) 433 ?.includes(x), 434 ); 435 436 if (addedTags.length > 0) { 437 const values = addedTags.map((monitorTagId) => ({ 438 monitorId: currentMonitor.id, 439 monitorTagId, 440 })); 441 442 await opts.ctx.db.insert(monitorTagsToMonitors).values(values).run(); 443 } 444 445 const removedTags = currentMonitorTags 446 .map(({ monitorTagId }) => monitorTagId) 447 .filter((x) => !tags?.includes(x)); 448 449 if (removedTags.length > 0) { 450 await opts.ctx.db 451 .delete(monitorTagsToMonitors) 452 .where( 453 and( 454 eq(monitorTagsToMonitors.monitorId, currentMonitor.id), 455 inArray(monitorTagsToMonitors.monitorTagId, removedTags), 456 ), 457 ) 458 .run(); 459 } 460 461 const currentMonitorPages = await opts.ctx.db 462 .select() 463 .from(monitorsToPages) 464 .where(eq(monitorsToPages.monitorId, currentMonitor.id)) 465 .all(); 466 467 const addedPages = pages.filter( 468 (x) => !currentMonitorPages.map(({ pageId }) => pageId)?.includes(x), 469 ); 470 471 if (addedPages.length > 0) { 472 const values = addedPages.map((pageId) => ({ 473 monitorId: currentMonitor.id, 474 pageId, 475 })); 476 477 await opts.ctx.db.insert(monitorsToPages).values(values).run(); 478 // Sync to page components 479 await syncMonitorsToPageInsertMany(opts.ctx.db, values); 480 } 481 482 const removedPages = currentMonitorPages 483 .map(({ pageId }) => pageId) 484 .filter((x) => !pages?.includes(x)); 485 486 if (removedPages.length > 0) { 487 await opts.ctx.db 488 .delete(monitorsToPages) 489 .where( 490 and( 491 eq(monitorsToPages.monitorId, currentMonitor.id), 492 inArray(monitorsToPages.pageId, removedPages), 493 ), 494 ) 495 .run(); 496 // Sync delete to page components 497 for (const pageId of removedPages) { 498 await syncMonitorsToPageDelete(opts.ctx.db, { 499 monitorId: currentMonitor.id, 500 pageId, 501 }); 502 } 503 } 504 }), 505 506 updateMonitors: protectedProcedure 507 .input( 508 insertMonitorSchema 509 .pick({ public: true, active: true }) 510 .partial() // batched updates 511 .extend({ ids: z.number().array() }), // array of monitor ids to update 512 ) 513 .mutation(async (opts) => { 514 const _monitors = await opts.ctx.db 515 .update(monitor) 516 .set(opts.input) 517 .where( 518 and( 519 inArray(monitor.id, opts.input.ids), 520 eq(monitor.workspaceId, opts.ctx.workspace.id), 521 isNull(monitor.deletedAt), 522 ), 523 ); 524 }), 525 526 updateMonitorsTag: protectedProcedure 527 .input( 528 z.object({ 529 ids: z.number().array(), 530 tagId: z.number(), 531 action: z.enum(["add", "remove"]), 532 }), 533 ) 534 .mutation(async (opts) => { 535 const _monitorTag = await opts.ctx.db.query.monitorTag.findFirst({ 536 where: and( 537 eq(monitorTag.workspaceId, opts.ctx.workspace.id), 538 eq(monitorTag.id, opts.input.tagId), 539 ), 540 }); 541 542 const _monitors = await opts.ctx.db.query.monitor.findMany({ 543 where: and( 544 eq(monitor.workspaceId, opts.ctx.workspace.id), 545 inArray(monitor.id, opts.input.ids), 546 ), 547 }); 548 549 if (!_monitorTag || _monitors.length !== opts.input.ids.length) { 550 throw new TRPCError({ 551 code: "BAD_REQUEST", 552 message: "Invalid tag", 553 }); 554 } 555 556 if (opts.input.action === "add") { 557 await opts.ctx.db 558 .insert(monitorTagsToMonitors) 559 .values( 560 opts.input.ids.map((id) => ({ 561 monitorId: id, 562 monitorTagId: opts.input.tagId, 563 })), 564 ) 565 .onConflictDoNothing() 566 .run(); 567 } 568 569 if (opts.input.action === "remove") { 570 await opts.ctx.db 571 .delete(monitorTagsToMonitors) 572 .where( 573 and( 574 inArray(monitorTagsToMonitors.monitorId, opts.input.ids), 575 eq(monitorTagsToMonitors.monitorTagId, opts.input.tagId), 576 ), 577 ) 578 .run(); 579 } 580 }), 581 582 delete: protectedProcedure 583 .meta({ track: Events.DeleteMonitor }) 584 .input(z.object({ id: z.number() })) 585 .mutation(async (opts) => { 586 const monitorToDelete = await opts.ctx.db 587 .select() 588 .from(monitor) 589 .where( 590 and( 591 eq(monitor.id, opts.input.id), 592 eq(monitor.workspaceId, opts.ctx.workspace.id), 593 ), 594 ) 595 .get(); 596 if (!monitorToDelete) return; 597 598 await opts.ctx.db 599 .update(monitor) 600 .set({ deletedAt: new Date(), active: false }) 601 .where(eq(monitor.id, monitorToDelete.id)) 602 .run(); 603 604 await opts.ctx.db.transaction(async (tx) => { 605 await tx 606 .delete(monitorsToPages) 607 .where(eq(monitorsToPages.monitorId, monitorToDelete.id)); 608 await tx 609 .delete(monitorTagsToMonitors) 610 .where(eq(monitorTagsToMonitors.monitorId, monitorToDelete.id)); 611 await tx 612 .delete(monitorsToStatusReport) 613 .where(eq(monitorsToStatusReport.monitorId, monitorToDelete.id)); 614 await tx 615 .delete(notificationsToMonitors) 616 .where(eq(notificationsToMonitors.monitorId, monitorToDelete.id)); 617 await tx 618 .delete(maintenancesToMonitors) 619 .where(eq(maintenancesToMonitors.monitorId, monitorToDelete.id)); 620 // Sync deletes to page components 621 await syncMonitorsToPageDeleteByMonitors(tx, [monitorToDelete.id]); 622 await syncStatusReportToMonitorDeleteByMonitors(tx, [ 623 monitorToDelete.id, 624 ]); 625 await syncMaintenanceToMonitorDeleteByMonitors(tx, [ 626 monitorToDelete.id, 627 ]); 628 }); 629 }), 630 631 deleteMonitors: protectedProcedure 632 .input(z.object({ ids: z.number().array() })) 633 .mutation(async (opts) => { 634 const _monitors = await opts.ctx.db 635 .select() 636 .from(monitor) 637 .where( 638 and( 639 inArray(monitor.id, opts.input.ids), 640 eq(monitor.workspaceId, opts.ctx.workspace.id), 641 ), 642 ) 643 .all(); 644 645 if (_monitors.length !== opts.input.ids.length) { 646 throw new TRPCError({ 647 code: "NOT_FOUND", 648 message: "Monitor not found.", 649 }); 650 } 651 652 await opts.ctx.db 653 .update(monitor) 654 .set({ deletedAt: new Date(), active: false }) 655 .where(inArray(monitor.id, opts.input.ids)) 656 .run(); 657 658 await opts.ctx.db.transaction(async (tx) => { 659 await tx 660 .delete(monitorsToPages) 661 .where(inArray(monitorsToPages.monitorId, opts.input.ids)); 662 await tx 663 .delete(monitorTagsToMonitors) 664 .where(inArray(monitorTagsToMonitors.monitorId, opts.input.ids)); 665 await tx 666 .delete(monitorsToStatusReport) 667 .where(inArray(monitorsToStatusReport.monitorId, opts.input.ids)); 668 await tx 669 .delete(notificationsToMonitors) 670 .where(inArray(notificationsToMonitors.monitorId, opts.input.ids)); 671 await tx 672 .delete(maintenancesToMonitors) 673 .where(inArray(maintenancesToMonitors.monitorId, opts.input.ids)); 674 // Sync deletes to page components 675 await syncMonitorsToPageDeleteByMonitors(tx, opts.input.ids); 676 await syncStatusReportToMonitorDeleteByMonitors(tx, opts.input.ids); 677 await syncMaintenanceToMonitorDeleteByMonitors(tx, opts.input.ids); 678 }); 679 }), 680 681 getMonitorsByWorkspace: protectedProcedure.query(async (opts) => { 682 const monitors = await opts.ctx.db.query.monitor.findMany({ 683 where: and( 684 eq(monitor.workspaceId, opts.ctx.workspace.id), 685 isNull(monitor.deletedAt), 686 ), 687 with: { 688 monitorTagsToMonitors: { with: { monitorTag: true } }, 689 }, 690 orderBy: (monitor, { desc }) => [desc(monitor.active)], 691 }); 692 693 return z 694 .array( 695 selectMonitorSchema.extend({ 696 monitorTagsToMonitors: z 697 .array(z.object({ monitorTag: selectMonitorTagSchema })) 698 .prefault([]), 699 }), 700 ) 701 .parse(monitors); 702 }), 703 704 getMonitorsByPageId: protectedProcedure 705 .input(z.object({ id: z.number() })) 706 .query(async (opts) => { 707 const _page = await opts.ctx.db.query.page.findFirst({ 708 where: and( 709 eq(page.id, opts.input.id), 710 eq(page.workspaceId, opts.ctx.workspace.id), 711 ), 712 }); 713 714 if (!_page) return undefined; 715 716 const monitors = await opts.ctx.db.query.monitor.findMany({ 717 where: and( 718 eq(monitor.workspaceId, opts.ctx.workspace.id), 719 isNull(monitor.deletedAt), 720 ), 721 with: { 722 monitorTagsToMonitors: { with: { monitorTag: true } }, 723 monitorsToPages: { 724 where: eq(monitorsToPages.pageId, _page.id), 725 }, 726 }, 727 }); 728 729 return z 730 .array( 731 selectMonitorSchema.extend({ 732 monitorTagsToMonitors: z 733 .array(z.object({ monitorTag: selectMonitorTagSchema })) 734 .prefault([]), 735 }), 736 ) 737 .parse( 738 monitors.filter((monitor) => 739 monitor.monitorsToPages 740 .map(({ pageId }) => pageId) 741 .includes(_page.id), 742 ), 743 ); 744 }), 745 746 toggleMonitorActive: protectedProcedure 747 .input(z.object({ id: z.number() })) 748 .mutation(async (opts) => { 749 const monitorToUpdate = await opts.ctx.db 750 .select() 751 .from(monitor) 752 .where( 753 and( 754 eq(monitor.id, opts.input.id), 755 eq(monitor.workspaceId, opts.ctx.workspace.id), 756 isNull(monitor.deletedAt), 757 ), 758 ) 759 .get(); 760 761 if (!monitorToUpdate) { 762 throw new TRPCError({ 763 code: "NOT_FOUND", 764 message: "Monitor not found.", 765 }); 766 } 767 768 await opts.ctx.db 769 .update(monitor) 770 .set({ 771 active: !monitorToUpdate.active, 772 }) 773 .where( 774 and( 775 eq(monitor.id, opts.input.id), 776 eq(monitor.workspaceId, opts.ctx.workspace.id), 777 ), 778 ) 779 .run(); 780 }), 781 782 // rename to getActiveMonitorsCount 783 getTotalActiveMonitors: publicProcedure.query(async (opts) => { 784 const monitors = await opts.ctx.db 785 .select({ count: sql<number>`count(*)` }) 786 .from(monitor) 787 .where(eq(monitor.active, true)) 788 .all(); 789 if (monitors.length === 0) return 0; 790 return monitors[0].count; 791 }), 792 793 // TODO: return the notifications inside of the `getMonitorById` like we do for the monitors on a status page 794 getAllNotificationsForMonitor: protectedProcedure 795 .input(z.object({ id: z.number() })) 796 // .output(selectMonitorSchema) 797 .query(async (opts) => { 798 const data = await opts.ctx.db 799 .select() 800 .from(notificationsToMonitors) 801 .innerJoin( 802 notification, 803 and( 804 eq(notificationsToMonitors.notificationId, notification.id), 805 eq(notification.workspaceId, opts.ctx.workspace.id), 806 ), 807 ) 808 .where(eq(notificationsToMonitors.monitorId, opts.input.id)) 809 .all(); 810 return data.map((d) => selectNotificationSchema.parse(d.notification)); 811 }), 812 813 isMonitorLimitReached: protectedProcedure.query(async (opts) => { 814 const monitorLimit = opts.ctx.workspace.limits.monitors; 815 const monitorNumbers = ( 816 await opts.ctx.db.query.monitor.findMany({ 817 where: and( 818 eq(monitor.workspaceId, opts.ctx.workspace.id), 819 isNull(monitor.deletedAt), 820 ), 821 }) 822 ).length; 823 824 return monitorNumbers >= monitorLimit; 825 }), 826 getMonitorRelationsById: protectedProcedure 827 .input(z.object({ id: z.number() })) 828 .query(async (opts) => { 829 const _monitor = await opts.ctx.db.query.monitor.findFirst({ 830 where: and( 831 eq(monitor.id, opts.input.id), 832 eq(monitor.workspaceId, opts.ctx.workspace.id), 833 isNull(monitor.deletedAt), 834 ), 835 with: { 836 monitorTagsToMonitors: true, 837 monitorsToNotifications: true, 838 monitorsToPages: true, 839 }, 840 }); 841 842 const parsedMonitorNotification = _monitor?.monitorsToNotifications.map( 843 ({ notificationId }) => notificationId, 844 ); 845 const parsedPages = _monitor?.monitorsToPages.map((val) => val.pageId); 846 const parsedTags = _monitor?.monitorTagsToMonitors.map( 847 ({ monitorTagId }) => monitorTagId, 848 ); 849 850 return { 851 notificationIds: parsedMonitorNotification, 852 pageIds: parsedPages, 853 monitorTagIds: parsedTags, 854 }; 855 }), 856 857 // DASHBOARD 858 859 list: protectedProcedure 860 .input( 861 z 862 .object({ 863 order: z.enum(["asc", "desc"]).optional(), 864 }) 865 .optional(), 866 ) 867 .query(async (opts) => { 868 const whereConditions: SQL[] = [ 869 eq(monitor.workspaceId, opts.ctx.workspace.id), 870 isNull(monitor.deletedAt), 871 ]; 872 873 const result = await opts.ctx.db.query.monitor.findMany({ 874 where: and(...whereConditions), 875 with: { 876 monitorTagsToMonitors: { 877 with: { monitorTag: true }, 878 }, 879 incidents: { 880 orderBy: (incident, { desc }) => [desc(incident.createdAt)], 881 }, 882 }, 883 orderBy: (monitor, { asc, desc }) => 884 opts.input?.order === "asc" 885 ? [asc(monitor.active), asc(monitor.createdAt)] 886 : [desc(monitor.active), desc(monitor.createdAt)], 887 }); 888 889 return z 890 .array( 891 selectMonitorSchema.extend({ 892 tags: z.array(selectMonitorTagSchema).prefault([]), 893 incidents: z.array(selectIncidentSchema).prefault([]), 894 }), 895 ) 896 .parse( 897 result.map((data) => ({ 898 ...data, 899 tags: data.monitorTagsToMonitors.map((t) => t.monitorTag), 900 })), 901 ); 902 }), 903 904 get: protectedProcedure 905 .input(z.object({ id: z.coerce.number() })) 906 .query(async ({ ctx, input }) => { 907 const whereConditions: SQL[] = [ 908 eq(monitor.id, input.id), 909 eq(monitor.workspaceId, ctx.workspace.id), 910 isNull(monitor.deletedAt), 911 ]; 912 913 const data = await ctx.db.query.monitor.findFirst({ 914 where: and(...whereConditions), 915 with: { 916 monitorsToNotifications: { 917 with: { notification: true }, 918 }, 919 monitorsToPages: { 920 with: { page: true }, 921 }, 922 monitorTagsToMonitors: { 923 with: { monitorTag: true }, 924 }, 925 maintenancesToMonitors: { 926 with: { maintenance: true }, 927 }, 928 incidents: true, 929 privateLocationToMonitors: { 930 with: { privateLocation: true }, 931 }, 932 }, 933 }); 934 935 if (!data) return null; 936 937 return selectMonitorSchema 938 .extend({ 939 notifications: z.array(selectNotificationSchema).prefault([]), 940 pages: z.array(selectPageSchema).prefault([]), 941 tags: z.array(selectMonitorTagSchema).prefault([]), 942 maintenances: z.array(selectMaintenanceSchema).prefault([]), 943 incidents: z.array(selectIncidentSchema).prefault([]), 944 privateLocations: z.array(selectPrivateLocationSchema).prefault([]), 945 }) 946 .parse({ 947 ...data, 948 notifications: data.monitorsToNotifications.map( 949 (m) => m.notification, 950 ), 951 pages: data.monitorsToPages.map((p) => p.page), 952 tags: data.monitorTagsToMonitors.map((t) => t.monitorTag), 953 maintenances: data.maintenancesToMonitors.map((m) => m.maintenance), 954 incidents: data.incidents, 955 privateLocations: data.privateLocationToMonitors.map( 956 (p) => p.privateLocation, 957 ), 958 }); 959 }), 960 961 clone: protectedProcedure 962 .meta({ track: Events.CloneMonitor }) 963 .input(z.object({ id: z.number() })) 964 .mutation(async ({ ctx, input }) => { 965 const whereConditions: SQL[] = [ 966 eq(monitor.id, input.id), 967 eq(monitor.workspaceId, ctx.workspace.id), 968 isNull(monitor.deletedAt), 969 ]; 970 971 const _monitors = await ctx.db.query.monitor.findMany({ 972 where: and( 973 eq(monitor.workspaceId, ctx.workspace.id), 974 isNull(monitor.deletedAt), 975 ), 976 }); 977 978 if (_monitors.length >= ctx.workspace.limits.monitors) { 979 throw new TRPCError({ 980 code: "FORBIDDEN", 981 message: "You have reached the maximum number of monitors.", 982 }); 983 } 984 985 const data = await ctx.db.query.monitor.findFirst({ 986 where: and(...whereConditions), 987 }); 988 989 if (!data) { 990 throw new TRPCError({ 991 code: "NOT_FOUND", 992 message: "Monitor not found.", 993 }); 994 } 995 996 const [newMonitor] = await ctx.db 997 .insert(monitor) 998 .values({ 999 ...data, 1000 id: undefined, // let the db generate the id 1001 name: `${data.name} (Copy)`, 1002 createdAt: new Date(), 1003 updatedAt: new Date(), 1004 }) 1005 .returning(); 1006 1007 if (!newMonitor) { 1008 throw new TRPCError({ 1009 code: "INTERNAL_SERVER_ERROR", 1010 message: "Failed to clone monitor.", 1011 }); 1012 } 1013 1014 return newMonitor; 1015 }), 1016 1017 updateRetry: protectedProcedure 1018 .meta({ track: Events.UpdateMonitor }) 1019 .input(z.object({ id: z.number(), retry: z.number() })) 1020 .mutation(async ({ ctx, input }) => { 1021 const whereConditions: SQL[] = [ 1022 eq(monitor.id, input.id), 1023 eq(monitor.workspaceId, ctx.workspace.id), 1024 isNull(monitor.deletedAt), 1025 ]; 1026 1027 await ctx.db 1028 .update(monitor) 1029 .set({ retry: input.retry, updatedAt: new Date() }) 1030 .where(and(...whereConditions)) 1031 .run(); 1032 }), 1033 1034 updateFollowRedirects: protectedProcedure 1035 .meta({ track: Events.UpdateMonitor }) 1036 .input(z.object({ id: z.number(), followRedirects: z.boolean() })) 1037 .mutation(async ({ ctx, input }) => { 1038 const whereConditions: SQL[] = [ 1039 eq(monitor.id, input.id), 1040 eq(monitor.workspaceId, ctx.workspace.id), 1041 isNull(monitor.deletedAt), 1042 ]; 1043 1044 await ctx.db 1045 .update(monitor) 1046 .set({ 1047 followRedirects: input.followRedirects, 1048 updatedAt: new Date(), 1049 }) 1050 .where(and(...whereConditions)) 1051 .run(); 1052 }), 1053 1054 updateOtel: protectedProcedure 1055 .meta({ track: Events.UpdateMonitor }) 1056 .input( 1057 z.object({ 1058 id: z.number(), 1059 otelEndpoint: z.string(), 1060 otelHeaders: z 1061 .array(z.object({ key: z.string(), value: z.string() })) 1062 .optional(), 1063 }), 1064 ) 1065 .mutation(async ({ ctx, input }) => { 1066 const whereConditions: SQL[] = [ 1067 eq(monitor.id, input.id), 1068 eq(monitor.workspaceId, ctx.workspace.id), 1069 isNull(monitor.deletedAt), 1070 ]; 1071 1072 await ctx.db 1073 .update(monitor) 1074 .set({ 1075 otelEndpoint: input.otelEndpoint, 1076 otelHeaders: input.otelHeaders 1077 ? JSON.stringify(input.otelHeaders) 1078 : undefined, 1079 updatedAt: new Date(), 1080 }) 1081 .where(and(...whereConditions)) 1082 .run(); 1083 }), 1084 1085 updatePublic: protectedProcedure 1086 .meta({ track: Events.UpdateMonitor }) 1087 .input(z.object({ id: z.number(), public: z.boolean() })) 1088 .mutation(async ({ ctx, input }) => { 1089 const whereConditions: SQL[] = [ 1090 eq(monitor.id, input.id), 1091 eq(monitor.workspaceId, ctx.workspace.id), 1092 isNull(monitor.deletedAt), 1093 ]; 1094 1095 await ctx.db 1096 .update(monitor) 1097 .set({ public: input.public, updatedAt: new Date() }) 1098 .where(and(...whereConditions)) 1099 .run(); 1100 }), 1101 1102 updateSchedulingRegions: protectedProcedure 1103 .meta({ track: Events.UpdateMonitor }) 1104 .input( 1105 z.object({ 1106 id: z.number(), 1107 regions: z.array(z.string()), 1108 periodicity: z.enum(monitorPeriodicity), 1109 privateLocations: z.array(z.number()), 1110 }), 1111 ) 1112 .mutation(async ({ ctx, input }) => { 1113 const whereConditions: SQL[] = [ 1114 eq(monitor.id, input.id), 1115 eq(monitor.workspaceId, ctx.workspace.id), 1116 isNull(monitor.deletedAt), 1117 ]; 1118 1119 const limits = ctx.workspace.limits; 1120 1121 if (!limits.periodicity.includes(input.periodicity)) { 1122 throw new TRPCError({ 1123 code: "FORBIDDEN", 1124 message: "Upgrade to check more often.", 1125 }); 1126 } 1127 1128 if (limits["max-regions"] < input.regions.length) { 1129 throw new TRPCError({ 1130 code: "FORBIDDEN", 1131 message: "You have reached the maximum number of regions.", 1132 }); 1133 } 1134 1135 if ( 1136 input.regions.length > 0 && 1137 !input.regions.every((r) => 1138 limits.regions.includes(r as (typeof limits)["regions"][number]), 1139 ) 1140 ) { 1141 throw new TRPCError({ 1142 code: "FORBIDDEN", 1143 message: "You don't have access to this region.", 1144 }); 1145 } 1146 1147 await ctx.db.transaction(async (tx) => { 1148 await tx 1149 .update(monitor) 1150 .set({ 1151 regions: input.regions.join(","), 1152 periodicity: input.periodicity, 1153 updatedAt: new Date(), 1154 }) 1155 .where(and(...whereConditions)) 1156 .run(); 1157 1158 await tx 1159 .delete(privateLocationToMonitors) 1160 .where(eq(privateLocationToMonitors.monitorId, input.id)); 1161 1162 if (input.privateLocations && input.privateLocations.length > 0) { 1163 await tx.insert(privateLocationToMonitors).values( 1164 input.privateLocations.map((privateLocationId) => ({ 1165 monitorId: input.id, 1166 privateLocationId, 1167 })), 1168 ); 1169 } 1170 }); 1171 }), 1172 1173 updateResponseTime: protectedProcedure 1174 .meta({ track: Events.UpdateMonitor }) 1175 .input( 1176 z.object({ 1177 id: z.number(), 1178 timeout: z.number(), 1179 degradedAfter: z.number().nullish(), 1180 }), 1181 ) 1182 .mutation(async ({ ctx, input }) => { 1183 const whereConditions: SQL[] = [ 1184 eq(monitor.id, input.id), 1185 eq(monitor.workspaceId, ctx.workspace.id), 1186 isNull(monitor.deletedAt), 1187 ]; 1188 1189 await ctx.db 1190 .update(monitor) 1191 .set({ 1192 timeout: input.timeout, 1193 degradedAfter: input.degradedAfter, 1194 updatedAt: new Date(), 1195 }) 1196 .where(and(...whereConditions)) 1197 .run(); 1198 }), 1199 1200 updateTags: protectedProcedure 1201 .meta({ track: Events.UpdateMonitor }) 1202 .input(z.object({ id: z.number(), tags: z.array(z.number()) })) 1203 .mutation(async ({ ctx, input }) => { 1204 const allTags = await ctx.db.query.monitorTag.findMany({ 1205 where: and( 1206 eq(monitorTag.workspaceId, ctx.workspace.id), 1207 inArray(monitorTag.id, input.tags), 1208 ), 1209 }); 1210 1211 if (allTags.length !== input.tags.length) { 1212 throw new TRPCError({ 1213 code: "FORBIDDEN", 1214 message: "You don't have access to this tag.", 1215 }); 1216 } 1217 1218 await ctx.db.transaction(async (tx) => { 1219 await tx 1220 .delete(monitorTagsToMonitors) 1221 .where(and(eq(monitorTagsToMonitors.monitorId, input.id))); 1222 1223 if (input.tags.length > 0) { 1224 await tx.insert(monitorTagsToMonitors).values( 1225 input.tags.map((tagId) => ({ 1226 monitorId: input.id, 1227 monitorTagId: tagId, 1228 })), 1229 ); 1230 } 1231 }); 1232 }), 1233 1234 updateStatusPages: protectedProcedure 1235 .meta({ track: Events.UpdateMonitor }) 1236 .input( 1237 z.object({ 1238 id: z.number(), 1239 statusPages: z.array(z.number()), 1240 description: z.string().optional(), 1241 externalName: z.string().optional(), 1242 }), 1243 ) 1244 .mutation(async ({ ctx, input }) => { 1245 const allPages = await ctx.db.query.page.findMany({ 1246 where: and( 1247 eq(page.workspaceId, ctx.workspace.id), 1248 inArray(page.id, input.statusPages), 1249 ), 1250 }); 1251 1252 if (allPages.length !== input.statusPages.length) { 1253 throw new TRPCError({ 1254 code: "FORBIDDEN", 1255 message: "You don't have access to this status page.", 1256 }); 1257 } 1258 1259 await ctx.db.transaction(async (tx) => { 1260 // REMINDER: why do we need to do this complex logic instead of just deleting and inserting? 1261 // Because we need to preserve the group information when updating the status pages. 1262 1263 const existingEntries = await tx.query.monitorsToPages.findMany({ 1264 where: eq(monitorsToPages.monitorId, input.id), 1265 }); 1266 1267 const existingPageIds = new Set( 1268 existingEntries.map((entry) => entry.pageId), 1269 ); 1270 const inputPageIds = new Set(input.statusPages); 1271 1272 const pageIdsToDelete = existingEntries 1273 .filter((entry) => !inputPageIds.has(entry.pageId)) 1274 .map((entry) => entry.pageId); 1275 1276 const pageIdsToInsert = input.statusPages.filter( 1277 (pageId) => !existingPageIds.has(pageId), 1278 ); 1279 1280 if (pageIdsToDelete.length > 0) { 1281 await tx 1282 .delete(monitorsToPages) 1283 .where( 1284 and( 1285 eq(monitorsToPages.monitorId, input.id), 1286 inArray(monitorsToPages.pageId, pageIdsToDelete), 1287 ), 1288 ); 1289 // Sync delete to page components 1290 for (const pageId of pageIdsToDelete) { 1291 await syncMonitorsToPageDelete(tx, { monitorId: input.id, pageId }); 1292 } 1293 } 1294 1295 if (pageIdsToInsert.length > 0) { 1296 const values = pageIdsToInsert.map((pageId) => ({ 1297 monitorId: input.id, 1298 pageId, 1299 })); 1300 await tx.insert(monitorsToPages).values(values); 1301 // Sync to page components 1302 await syncMonitorsToPageInsertMany(tx, values); 1303 } 1304 1305 await tx 1306 .update(monitor) 1307 .set({ 1308 description: input.description, 1309 externalName: input.externalName, 1310 updatedAt: new Date(), 1311 }) 1312 .where(and(eq(monitor.id, input.id))); 1313 }); 1314 }), 1315 1316 updateGeneral: protectedProcedure 1317 .meta({ track: Events.UpdateMonitor }) 1318 .input( 1319 z.object({ 1320 id: z.number(), 1321 jobType: z.enum(monitorJobTypes), 1322 url: z.string(), 1323 method: z.enum(monitorMethods), 1324 headers: z.array(z.object({ key: z.string(), value: z.string() })), 1325 body: z.string().optional(), 1326 name: z.string(), 1327 assertions: z.array( 1328 z.discriminatedUnion("type", [ 1329 statusAssertion, 1330 headerAssertion, 1331 textBodyAssertion, 1332 jsonBodyAssertion, 1333 recordAssertion, 1334 ]), 1335 ), 1336 active: z.boolean().prefault(true), 1337 // skip the test check if assertions are OK 1338 skipCheck: z.boolean().prefault(true), 1339 // save check in db (iff success? -> e.g. onboarding to get a first ping) 1340 saveCheck: z.boolean().prefault(false), 1341 }), 1342 ) 1343 .mutation(async ({ ctx, input }) => { 1344 const whereConditions: SQL[] = [ 1345 eq(monitor.id, input.id), 1346 eq(monitor.workspaceId, ctx.workspace.id), 1347 isNull(monitor.deletedAt), 1348 ]; 1349 1350 const assertions: Assertion[] = []; 1351 for (const a of input.assertions ?? []) { 1352 if (a.type === "status") { 1353 assertions.push(new StatusAssertion(a)); 1354 } 1355 if (a.type === "header") { 1356 assertions.push(new HeaderAssertion(a)); 1357 } 1358 if (a.type === "textBody") { 1359 assertions.push(new TextBodyAssertion(a)); 1360 } 1361 if (a.type === "dnsRecord") { 1362 assertions.push(new DnsRecordAssertion(a)); 1363 } 1364 } 1365 1366 // NOTE: we are checking the endpoint before saving 1367 if (!input.skipCheck && input.active) { 1368 if (input.jobType === "http") { 1369 await testHttp({ 1370 url: input.url, 1371 method: input.method, 1372 headers: input.headers, 1373 body: input.body, 1374 // Filter out DNS record assertions as they can't be validated via HTTP 1375 assertions: input.assertions.filter((a) => a.type !== "dnsRecord"), 1376 region: "ams", 1377 }); 1378 } else if (input.jobType === "tcp") { 1379 await testTcp({ 1380 url: input.url, 1381 region: "ams", 1382 }); 1383 } else if (input.jobType === "dns") { 1384 await testDns({ 1385 url: input.url, 1386 region: "ams", 1387 assertions: input.assertions.filter((a) => a.type === "dnsRecord"), 1388 }); 1389 } 1390 } 1391 1392 await ctx.db 1393 .update(monitor) 1394 .set({ 1395 name: input.name, 1396 jobType: input.jobType, 1397 url: input.url, 1398 method: input.method, 1399 headers: input.headers ? JSON.stringify(input.headers) : undefined, 1400 body: input.body, 1401 active: input.active, 1402 assertions: serialize(assertions), 1403 updatedAt: new Date(), 1404 }) 1405 .where(and(...whereConditions)) 1406 .run(); 1407 }), 1408 1409 updateNotifiers: protectedProcedure 1410 .meta({ track: Events.UpdateMonitor }) 1411 .input(z.object({ id: z.number(), notifiers: z.array(z.number()) })) 1412 .mutation(async ({ ctx, input }) => { 1413 const allNotifiers = await ctx.db.query.notification.findMany({ 1414 where: and( 1415 eq(notification.workspaceId, ctx.workspace.id), 1416 inArray(notification.id, input.notifiers), 1417 ), 1418 }); 1419 1420 if (allNotifiers.length !== input.notifiers.length) { 1421 throw new TRPCError({ 1422 code: "FORBIDDEN", 1423 message: "You don't have access to this notifier.", 1424 }); 1425 } 1426 1427 await ctx.db.transaction(async (tx) => { 1428 await tx 1429 .delete(notificationsToMonitors) 1430 .where(and(eq(notificationsToMonitors.monitorId, input.id))); 1431 1432 if (input.notifiers.length > 0) { 1433 await tx.insert(notificationsToMonitors).values( 1434 input.notifiers.map((notifierId) => ({ 1435 monitorId: input.id, 1436 notificationId: notifierId, 1437 })), 1438 ); 1439 } 1440 }); 1441 }), 1442 1443 new: protectedProcedure 1444 .meta({ track: Events.CreateMonitor, trackProps: ["url", "jobType"] }) 1445 .input( 1446 z.object({ 1447 name: z.string(), 1448 jobType: z.enum(monitorJobTypes), 1449 url: z.string(), 1450 method: z.enum(monitorMethods), 1451 headers: z.array(z.object({ key: z.string(), value: z.string() })), 1452 body: z.string().optional(), 1453 assertions: z.array( 1454 z.discriminatedUnion("type", [ 1455 statusAssertion, 1456 headerAssertion, 1457 textBodyAssertion, 1458 jsonBodyAssertion, 1459 recordAssertion, 1460 ]), 1461 ), 1462 active: z.boolean().prefault(false), 1463 saveCheck: z.boolean().prefault(false), 1464 skipCheck: z.boolean().prefault(false), 1465 }), 1466 ) 1467 .mutation(async ({ ctx, input }) => { 1468 const limits = ctx.workspace.limits; 1469 1470 const res = await ctx.db 1471 .select({ count: count() }) 1472 .from(monitor) 1473 .where( 1474 and( 1475 eq(monitor.workspaceId, ctx.workspace.id), 1476 isNull(monitor.deletedAt), 1477 ), 1478 ) 1479 .get(); 1480 1481 // the user has reached the limits 1482 if (res && res.count >= limits.monitors) { 1483 throw new TRPCError({ 1484 code: "FORBIDDEN", 1485 message: "You reached your monitor limits.", 1486 }); 1487 } 1488 1489 const assertions: Assertion[] = []; 1490 for (const a of input.assertions ?? []) { 1491 if (a.type === "status") { 1492 assertions.push(new StatusAssertion(a)); 1493 } 1494 if (a.type === "header") { 1495 assertions.push(new HeaderAssertion(a)); 1496 } 1497 if (a.type === "textBody") { 1498 assertions.push(new TextBodyAssertion(a)); 1499 } 1500 if (a.type === "dnsRecord") { 1501 assertions.push(new DnsRecordAssertion(a)); 1502 } 1503 } 1504 1505 // NOTE: we are checking the endpoint before saving 1506 if (!input.skipCheck) { 1507 if (input.jobType === "http") { 1508 await testHttp({ 1509 url: input.url, 1510 method: input.method, 1511 headers: input.headers, 1512 body: input.body, 1513 // Filter out DNS record assertions as they can't be validated via HTTP 1514 assertions: input.assertions.filter((a) => a.type !== "dnsRecord"), 1515 region: "ams", 1516 }); 1517 } else if (input.jobType === "tcp") { 1518 await testTcp({ 1519 url: input.url, 1520 region: "ams", 1521 }); 1522 } else if (input.jobType === "dns") { 1523 await testDns({ 1524 url: input.url, 1525 region: "ams", 1526 assertions: input.assertions.filter((a) => a.type === "dnsRecord"), 1527 }); 1528 } 1529 } 1530 1531 const selectableRegions = 1532 ctx.workspace.plan === "free" ? freeFlyRegions : monitorRegions; 1533 const randomRegions = ctx.workspace.plan === "free" ? 4 : 6; 1534 1535 const regions = [...selectableRegions] 1536 // NOTE: make sure we don't use deprecated regions 1537 .filter((r) => { 1538 const deprecated = regionDict[r].deprecated; 1539 if (!deprecated) return true; 1540 return false; 1541 }) 1542 .sort(() => 0.5 - Math.random()) 1543 .slice(0, randomRegions); 1544 1545 const newMonitor = await ctx.db 1546 .insert(monitor) 1547 .values({ 1548 name: input.name, 1549 jobType: input.jobType, 1550 url: input.url, 1551 method: input.method, 1552 headers: input.headers ? JSON.stringify(input.headers) : undefined, 1553 body: input.body, 1554 active: input.active, 1555 workspaceId: ctx.workspace.id, 1556 periodicity: ctx.workspace.plan === "free" ? "30m" : "1m", 1557 regions: regions.join(","), 1558 assertions: serialize(assertions), 1559 updatedAt: new Date(), 1560 }) 1561 .returning() 1562 .get(); 1563 1564 return newMonitor; 1565 }), 1566});