Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update procedure tcp/dns/http monitors(#1793)

* feat(proto): add Update*Monitor RPC definitions for partial updates

Add proto definitions for UpdateHTTPMonitor, UpdateTCPMonitor, and
UpdateDNSMonitor RPCs that support partial updates with optional
nested monitor fields.

* feat(rpc): implement update methods for HTTP, TCP, and DNS monitors

Add UpdateHTTPMonitor, UpdateTCPMonitor, and UpdateDNSMonitor RPC
procedures that support partial updates - only provided fields are
changed while preserving existing values.

- Add getCommonDbValuesForUpdate helper for partial update field extraction
- Add error types: MONITOR_UPDATE_FAILED, MONITOR_TYPE_MISMATCH, MONITOR_ID_REQUIRED
- Skip protovalidate for update methods to allow optional nested fields
- Verify monitor type matches the update method being called
- Add comprehensive tests for all update scenarios

* improve typing

authored by

Thibault Le Ouay and committed by
GitHub
b6a8e105 dda4b2f8

+1051 -18
+23 -1
apps/server/src/routes/rpc/interceptors/validation.ts
··· 1 + import type { Interceptor } from "@connectrpc/connect"; 1 2 import { createValidateInterceptor } from "@connectrpc/validate"; 2 3 4 + // Methods that skip standard protovalidate (they do manual validation in handlers) 5 + // These methods use partial updates where nested message fields are optional 6 + const SKIP_VALIDATION_METHODS = new Set([ 7 + "UpdateHTTPMonitor", 8 + "UpdateTCPMonitor", 9 + "UpdateDNSMonitor", 10 + ]); 11 + 3 12 /** 4 13 * Validation interceptor for ConnectRPC using protovalidate. 5 14 * Validates incoming request messages against their proto constraints. ··· 8 17 * - Validates request messages using protovalidate rules 9 18 * - Returns InvalidArgument error for validation failures 10 19 * - Works with all message types defined with buf.validate constraints 20 + * 21 + * Note: Update methods skip validation because they support partial updates 22 + * where nested message fields are optional. Validation for these methods 23 + * is done in the service handlers. 11 24 */ 12 - export const validationInterceptor = createValidateInterceptor; 25 + export function validationInterceptor(): Interceptor { 26 + const baseInterceptor = createValidateInterceptor(); 27 + return (next) => async (req) => { 28 + // Skip validation for update methods that support partial updates 29 + if (SKIP_VALIDATION_METHODS.has(req.method.name)) { 30 + return next(req); 31 + } 32 + return baseInterceptor(next)(req); 33 + }; 34 + }
+419
apps/server/src/routes/rpc/services/monitor/__tests__/monitor.test.ts
··· 600 600 }); 601 601 }); 602 602 603 + describe("MonitorService.UpdateHTTPMonitor", () => { 604 + test("successfully updates HTTP monitor with partial data", async () => { 605 + const res = await connectRequest( 606 + "UpdateHTTPMonitor", 607 + { 608 + id: String(testHttpMonitorId), 609 + monitor: { 610 + name: "updated-http-name", 611 + }, 612 + }, 613 + { "x-openstatus-key": "1" }, 614 + ); 615 + 616 + expect(res.status).toBe(200); 617 + 618 + const data = await res.json(); 619 + expect(data.monitor).toBeDefined(); 620 + expect(data.monitor.name).toBe("updated-http-name"); 621 + // Original URL should be preserved 622 + expect(data.monitor.url).toBe("https://example.com"); 623 + 624 + // Restore original name 625 + await connectRequest( 626 + "UpdateHTTPMonitor", 627 + { 628 + id: String(testHttpMonitorId), 629 + monitor: { 630 + name: `${TEST_PREFIX}-http`, 631 + }, 632 + }, 633 + { "x-openstatus-key": "1" }, 634 + ); 635 + }); 636 + 637 + test("successfully updates HTTP monitor URL", async () => { 638 + const res = await connectRequest( 639 + "UpdateHTTPMonitor", 640 + { 641 + id: String(testHttpMonitorId), 642 + monitor: { 643 + url: "https://updated-example.com", 644 + }, 645 + }, 646 + { "x-openstatus-key": "1" }, 647 + ); 648 + 649 + expect(res.status).toBe(200); 650 + 651 + const data = await res.json(); 652 + expect(data.monitor.url).toBe("https://updated-example.com"); 653 + 654 + // Restore original URL 655 + await connectRequest( 656 + "UpdateHTTPMonitor", 657 + { 658 + id: String(testHttpMonitorId), 659 + monitor: { 660 + url: "https://example.com", 661 + }, 662 + }, 663 + { "x-openstatus-key": "1" }, 664 + ); 665 + }); 666 + 667 + test("successfully updates HTTP method", async () => { 668 + const res = await connectRequest( 669 + "UpdateHTTPMonitor", 670 + { 671 + id: String(testHttpMonitorId), 672 + monitor: { 673 + method: "HTTP_METHOD_POST", 674 + }, 675 + }, 676 + { "x-openstatus-key": "1" }, 677 + ); 678 + 679 + expect(res.status).toBe(200); 680 + 681 + const data = await res.json(); 682 + expect(data.monitor.method).toBe("HTTP_METHOD_POST"); 683 + 684 + // Restore original method 685 + await connectRequest( 686 + "UpdateHTTPMonitor", 687 + { 688 + id: String(testHttpMonitorId), 689 + monitor: { 690 + method: "HTTP_METHOD_GET", 691 + }, 692 + }, 693 + { "x-openstatus-key": "1" }, 694 + ); 695 + }); 696 + 697 + test("returns current monitor when no monitor data provided", async () => { 698 + const res = await connectRequest( 699 + "UpdateHTTPMonitor", 700 + { 701 + id: String(testHttpMonitorId), 702 + }, 703 + { "x-openstatus-key": "1" }, 704 + ); 705 + 706 + expect(res.status).toBe(200); 707 + 708 + const data = await res.json(); 709 + expect(data.monitor).toBeDefined(); 710 + expect(data.monitor.id).toBe(String(testHttpMonitorId)); 711 + }); 712 + 713 + test("returns 404 for non-existent monitor", async () => { 714 + const res = await connectRequest( 715 + "UpdateHTTPMonitor", 716 + { 717 + id: "99999", 718 + monitor: { name: "test" }, 719 + }, 720 + { "x-openstatus-key": "1" }, 721 + ); 722 + 723 + expect(res.status).toBe(404); 724 + }); 725 + 726 + test("returns error when trying to update TCP monitor as HTTP", async () => { 727 + const res = await connectRequest( 728 + "UpdateHTTPMonitor", 729 + { 730 + id: String(testTcpMonitorId), 731 + monitor: { name: "test" }, 732 + }, 733 + { "x-openstatus-key": "1" }, 734 + ); 735 + 736 + expect(res.status).toBe(400); 737 + const data = await res.json(); 738 + expect(data.message).toContain("type mismatch"); 739 + }); 740 + 741 + test("returns 401 when no auth key provided", async () => { 742 + const res = await connectRequest("UpdateHTTPMonitor", { 743 + id: String(testHttpMonitorId), 744 + monitor: { name: "test" }, 745 + }); 746 + 747 + expect(res.status).toBe(401); 748 + }); 749 + }); 750 + 751 + describe("MonitorService.UpdateTCPMonitor", () => { 752 + test("successfully updates TCP monitor with partial data", async () => { 753 + const res = await connectRequest( 754 + "UpdateTCPMonitor", 755 + { 756 + id: String(testTcpMonitorId), 757 + monitor: { 758 + name: "updated-tcp-name", 759 + }, 760 + }, 761 + { "x-openstatus-key": "1" }, 762 + ); 763 + 764 + expect(res.status).toBe(200); 765 + 766 + const data = await res.json(); 767 + expect(data.monitor).toBeDefined(); 768 + expect(data.monitor.name).toBe("updated-tcp-name"); 769 + // Original URI should be preserved 770 + expect(data.monitor.uri).toBe("tcp://example.com:443"); 771 + 772 + // Restore original name 773 + await connectRequest( 774 + "UpdateTCPMonitor", 775 + { 776 + id: String(testTcpMonitorId), 777 + monitor: { 778 + name: `${TEST_PREFIX}-tcp`, 779 + }, 780 + }, 781 + { "x-openstatus-key": "1" }, 782 + ); 783 + }); 784 + 785 + test("successfully updates TCP monitor URI", async () => { 786 + const res = await connectRequest( 787 + "UpdateTCPMonitor", 788 + { 789 + id: String(testTcpMonitorId), 790 + monitor: { 791 + uri: "tcp://updated-example.com:8080", 792 + }, 793 + }, 794 + { "x-openstatus-key": "1" }, 795 + ); 796 + 797 + expect(res.status).toBe(200); 798 + 799 + const data = await res.json(); 800 + expect(data.monitor.uri).toBe("tcp://updated-example.com:8080"); 801 + 802 + // Restore original URI 803 + await connectRequest( 804 + "UpdateTCPMonitor", 805 + { 806 + id: String(testTcpMonitorId), 807 + monitor: { 808 + uri: "tcp://example.com:443", 809 + }, 810 + }, 811 + { "x-openstatus-key": "1" }, 812 + ); 813 + }); 814 + 815 + test("returns current monitor when no monitor data provided", async () => { 816 + const res = await connectRequest( 817 + "UpdateTCPMonitor", 818 + { 819 + id: String(testTcpMonitorId), 820 + }, 821 + { "x-openstatus-key": "1" }, 822 + ); 823 + 824 + expect(res.status).toBe(200); 825 + 826 + const data = await res.json(); 827 + expect(data.monitor).toBeDefined(); 828 + expect(data.monitor.id).toBe(String(testTcpMonitorId)); 829 + }); 830 + 831 + test("returns 404 for non-existent monitor", async () => { 832 + const res = await connectRequest( 833 + "UpdateTCPMonitor", 834 + { 835 + id: "99999", 836 + monitor: { name: "test" }, 837 + }, 838 + { "x-openstatus-key": "1" }, 839 + ); 840 + 841 + expect(res.status).toBe(404); 842 + }); 843 + 844 + test("returns error when trying to update HTTP monitor as TCP", async () => { 845 + const res = await connectRequest( 846 + "UpdateTCPMonitor", 847 + { 848 + id: String(testHttpMonitorId), 849 + monitor: { name: "test" }, 850 + }, 851 + { "x-openstatus-key": "1" }, 852 + ); 853 + 854 + expect(res.status).toBe(400); 855 + const data = await res.json(); 856 + expect(data.message).toContain("type mismatch"); 857 + }); 858 + 859 + test("returns 401 when no auth key provided", async () => { 860 + const res = await connectRequest("UpdateTCPMonitor", { 861 + id: String(testTcpMonitorId), 862 + monitor: { name: "test" }, 863 + }); 864 + 865 + expect(res.status).toBe(401); 866 + }); 867 + }); 868 + 869 + describe("MonitorService.UpdateDNSMonitor", () => { 870 + test("successfully updates DNS monitor with partial data", async () => { 871 + const res = await connectRequest( 872 + "UpdateDNSMonitor", 873 + { 874 + id: String(testDnsMonitorId), 875 + monitor: { 876 + name: "updated-dns-name", 877 + }, 878 + }, 879 + { "x-openstatus-key": "1" }, 880 + ); 881 + 882 + expect(res.status).toBe(200); 883 + 884 + const data = await res.json(); 885 + expect(data.monitor).toBeDefined(); 886 + expect(data.monitor.name).toBe("updated-dns-name"); 887 + // Original URI should be preserved 888 + expect(data.monitor.uri).toBe("example.com"); 889 + 890 + // Restore original name 891 + await connectRequest( 892 + "UpdateDNSMonitor", 893 + { 894 + id: String(testDnsMonitorId), 895 + monitor: { 896 + name: `${TEST_PREFIX}-dns`, 897 + }, 898 + }, 899 + { "x-openstatus-key": "1" }, 900 + ); 901 + }); 902 + 903 + test("successfully updates DNS monitor URI", async () => { 904 + const res = await connectRequest( 905 + "UpdateDNSMonitor", 906 + { 907 + id: String(testDnsMonitorId), 908 + monitor: { 909 + uri: "updated-example.com", 910 + }, 911 + }, 912 + { "x-openstatus-key": "1" }, 913 + ); 914 + 915 + expect(res.status).toBe(200); 916 + 917 + const data = await res.json(); 918 + expect(data.monitor.uri).toBe("updated-example.com"); 919 + 920 + // Restore original URI 921 + await connectRequest( 922 + "UpdateDNSMonitor", 923 + { 924 + id: String(testDnsMonitorId), 925 + monitor: { 926 + uri: "example.com", 927 + }, 928 + }, 929 + { "x-openstatus-key": "1" }, 930 + ); 931 + }); 932 + 933 + test("successfully updates DNS record assertions", async () => { 934 + const res = await connectRequest( 935 + "UpdateDNSMonitor", 936 + { 937 + id: String(testDnsMonitorId), 938 + monitor: { 939 + recordAssertions: [ 940 + { record: "A", target: "1.2.3.4", comparator: 1 }, 941 + { record: "AAAA", target: "2001:db8::1", comparator: 1 }, 942 + ], 943 + }, 944 + }, 945 + { "x-openstatus-key": "1" }, 946 + ); 947 + 948 + expect(res.status).toBe(200); 949 + 950 + const data = await res.json(); 951 + expect(data.monitor.recordAssertions).toHaveLength(2); 952 + 953 + // Restore original assertions 954 + await connectRequest( 955 + "UpdateDNSMonitor", 956 + { 957 + id: String(testDnsMonitorId), 958 + monitor: { 959 + recordAssertions: [ 960 + { record: "A", target: "93.184.216.34", comparator: 1 }, 961 + ], 962 + }, 963 + }, 964 + { "x-openstatus-key": "1" }, 965 + ); 966 + }); 967 + 968 + test("returns current monitor when no monitor data provided", async () => { 969 + const res = await connectRequest( 970 + "UpdateDNSMonitor", 971 + { 972 + id: String(testDnsMonitorId), 973 + }, 974 + { "x-openstatus-key": "1" }, 975 + ); 976 + 977 + expect(res.status).toBe(200); 978 + 979 + const data = await res.json(); 980 + expect(data.monitor).toBeDefined(); 981 + expect(data.monitor.id).toBe(String(testDnsMonitorId)); 982 + }); 983 + 984 + test("returns 404 for non-existent monitor", async () => { 985 + const res = await connectRequest( 986 + "UpdateDNSMonitor", 987 + { 988 + id: "99999", 989 + monitor: { name: "test" }, 990 + }, 991 + { "x-openstatus-key": "1" }, 992 + ); 993 + 994 + expect(res.status).toBe(404); 995 + }); 996 + 997 + test("returns error when trying to update HTTP monitor as DNS", async () => { 998 + const res = await connectRequest( 999 + "UpdateDNSMonitor", 1000 + { 1001 + id: String(testHttpMonitorId), 1002 + monitor: { name: "test" }, 1003 + }, 1004 + { "x-openstatus-key": "1" }, 1005 + ); 1006 + 1007 + expect(res.status).toBe(400); 1008 + const data = await res.json(); 1009 + expect(data.message).toContain("type mismatch"); 1010 + }); 1011 + 1012 + test("returns 401 when no auth key provided", async () => { 1013 + const res = await connectRequest("UpdateDNSMonitor", { 1014 + id: String(testDnsMonitorId), 1015 + monitor: { name: "test" }, 1016 + }); 1017 + 1018 + expect(res.status).toBe(401); 1019 + }); 1020 + }); 1021 + 603 1022 describe("MonitorService.TriggerMonitor", () => { 604 1023 test("returns 404 for non-existent monitor", async () => { 605 1024 const res = await connectRequest(
+8 -2
apps/server/src/routes/rpc/services/monitor/converters/enums.ts
··· 5 5 TimeRange, 6 6 } from "@openstatus/proto/monitor/v1"; 7 7 8 + import type { monitorPeriodicitySchema } from "@openstatus/db/src/schema/constants"; 9 + import type { z } from "zod"; 10 + 8 11 // ============================================================ 9 12 // Periodicity Conversions 10 13 // ============================================================ ··· 18 21 "1h": Periodicity.PERIODICITY_1H, 19 22 }; 20 23 21 - const PERIODICITY_TO_DB: Record<Periodicity, string> = { 24 + const PERIODICITY_TO_DB: Record< 25 + Periodicity, 26 + z.infer<typeof monitorPeriodicitySchema> 27 + > = { 22 28 [Periodicity.PERIODICITY_30S]: "30s", 23 29 [Periodicity.PERIODICITY_1M]: "1m", 24 30 [Periodicity.PERIODICITY_5M]: "5m", ··· 32 38 return DB_TO_PERIODICITY[value] ?? Periodicity.PERIODICITY_UNSPECIFIED; 33 39 } 34 40 35 - export function periodicityToString(value: Periodicity): string { 41 + export function periodicityToString(value: Periodicity) { 36 42 return PERIODICITY_TO_DB[value] ?? "1m"; 37 43 } 38 44
+46
apps/server/src/routes/rpc/services/monitor/errors.ts
··· 6 6 export const ErrorReason = { 7 7 MONITOR_NOT_FOUND: "MONITOR_NOT_FOUND", 8 8 MONITOR_REQUIRED: "MONITOR_REQUIRED", 9 + MONITOR_ID_REQUIRED: "MONITOR_ID_REQUIRED", 9 10 MONITOR_CREATE_FAILED: "MONITOR_CREATE_FAILED", 11 + MONITOR_UPDATE_FAILED: "MONITOR_UPDATE_FAILED", 10 12 MONITOR_PARSE_FAILED: "MONITOR_PARSE_FAILED", 11 13 MONITOR_RUN_CREATE_FAILED: "MONITOR_RUN_CREATE_FAILED", 12 14 MONITOR_INVALID_DATA: "MONITOR_INVALID_DATA", 15 + MONITOR_TYPE_MISMATCH: "MONITOR_TYPE_MISMATCH", 13 16 RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED", 14 17 } as const; 15 18 ··· 67 70 } 68 71 69 72 /** 73 + * Creates a "monitor ID required" error. 74 + */ 75 + export function monitorIdRequiredError(): ConnectError { 76 + return createError( 77 + "Monitor ID is required", 78 + Code.InvalidArgument, 79 + ErrorReason.MONITOR_ID_REQUIRED, 80 + ); 81 + } 82 + 83 + /** 70 84 * Creates a "failed to create monitor" error. 71 85 */ 72 86 export function monitorCreateFailedError(): ConnectError { ··· 74 88 "Failed to create monitor", 75 89 Code.Internal, 76 90 ErrorReason.MONITOR_CREATE_FAILED, 91 + ); 92 + } 93 + 94 + /** 95 + * Creates a "failed to update monitor" error. 96 + */ 97 + export function monitorUpdateFailedError(monitorId: string): ConnectError { 98 + return createError( 99 + "Failed to update monitor", 100 + Code.Internal, 101 + ErrorReason.MONITOR_UPDATE_FAILED, 102 + { "monitor-id": monitorId }, 103 + ); 104 + } 105 + 106 + /** 107 + * Creates a "monitor type mismatch" error when trying to update with wrong type. 108 + */ 109 + export function monitorTypeMismatchError( 110 + monitorId: string, 111 + expectedType: string, 112 + actualType: string, 113 + ): ConnectError { 114 + return createError( 115 + `Monitor type mismatch: expected ${expectedType}, got ${actualType}`, 116 + Code.InvalidArgument, 117 + ErrorReason.MONITOR_TYPE_MISMATCH, 118 + { 119 + "monitor-id": monitorId, 120 + "expected-type": expectedType, 121 + "actual-type": actualType, 122 + }, 77 123 ); 78 124 } 79 125
+239
apps/server/src/routes/rpc/services/monitor/index.ts
··· 35 35 } from "./converters"; 36 36 import { 37 37 monitorCreateFailedError, 38 + monitorIdRequiredError, 38 39 monitorInvalidDataError, 39 40 monitorNotFoundError, 40 41 monitorParseFailedError, 41 42 monitorRequiredError, 42 43 monitorRunCreateFailedError, 44 + monitorTypeMismatchError, 45 + monitorUpdateFailedError, 43 46 rateLimitExceededError, 44 47 } from "./errors"; 45 48 import { checkMonitorLimits } from "./limits"; 46 49 import { 47 50 getCommonDbValues, 51 + getCommonDbValuesForUpdate, 48 52 toValidMethod, 49 53 validateCommonMonitorFields, 50 54 } from "./validators"; ··· 64 68 ), 65 69 ) 66 70 .get(); 71 + } 72 + 73 + type DBMonitor = NonNullable<Awaited<ReturnType<typeof getMonitorById>>>; 74 + 75 + /** 76 + * Helper to validate and get a monitor for update operations. 77 + * Validates ID, fetches the monitor, and verifies the job type. 78 + */ 79 + async function validateAndGetMonitor( 80 + id: string | undefined, 81 + workspaceId: number, 82 + expectedJobType: "http" | "tcp" | "dns", 83 + ): Promise<DBMonitor> { 84 + if (!id || id.trim() === "") { 85 + throw monitorIdRequiredError(); 86 + } 87 + 88 + const dbMon = await getMonitorById(Number(id), workspaceId); 89 + if (!dbMon) { 90 + throw monitorNotFoundError(id); 91 + } 92 + 93 + if (dbMon.jobType !== expectedJobType) { 94 + throw monitorTypeMismatchError(id, expectedJobType, dbMon.jobType); 95 + } 96 + 97 + return dbMon; 98 + } 99 + 100 + type ParsedMonitor = ReturnType<typeof selectMonitorSchema.parse>; 101 + 102 + /** 103 + * Helper to perform update and return the updated monitor. 104 + */ 105 + async function performUpdateAndReturn<T>( 106 + monitorId: number, 107 + requestId: string, 108 + updateValues: Record<string, unknown>, 109 + converter: (data: ParsedMonitor) => T, 110 + ): Promise<{ monitor: T }> { 111 + const updatedMonitor = await db 112 + .update(monitor) 113 + .set(updateValues) 114 + .where(eq(monitor.id, monitorId)) 115 + .returning() 116 + .get(); 117 + 118 + if (!updatedMonitor) { 119 + throw monitorUpdateFailedError(requestId); 120 + } 121 + 122 + const parsed = selectMonitorSchema.safeParse(updatedMonitor); 123 + if (!parsed.success) { 124 + throw monitorParseFailedError(requestId); 125 + } 126 + 127 + return { monitor: converter(parsed.data) }; 67 128 } 68 129 69 130 /** ··· 227 288 return { 228 289 monitor: dbMonitorToDnsProto(parsed.data), 229 290 }; 291 + }, 292 + 293 + async updateHTTPMonitor(req, ctx) { 294 + const rpcCtx = getRpcContext(ctx); 295 + const workspaceId = rpcCtx.workspace.id; 296 + const limits = rpcCtx.workspace.limits; 297 + 298 + const dbMon = await validateAndGetMonitor(req.id, workspaceId, "http"); 299 + 300 + // If no monitor data provided, return current monitor 301 + if (!req.monitor) { 302 + const parsed = selectMonitorSchema.safeParse(dbMon); 303 + if (!parsed.success) { 304 + throw monitorParseFailedError(req.id); 305 + } 306 + return { monitor: dbMonitorToHttpProto(parsed.data) }; 307 + } 308 + 309 + const mon = req.monitor; 310 + 311 + // Validate regions if provided 312 + validateCommonMonitorFields(mon); 313 + 314 + // Check workspace limits if periodicity or regions are changing 315 + if (mon.periodicity || (mon.regions && mon.regions.length > 0)) { 316 + await checkMonitorLimits( 317 + workspaceId, 318 + limits, 319 + mon.periodicity || undefined, 320 + mon.regions && mon.regions.length > 0 ? mon.regions : undefined, 321 + ); 322 + } 323 + 324 + // Build update values - only include fields that are provided 325 + const updateValues: Record<string, unknown> = 326 + getCommonDbValuesForUpdate(mon); 327 + 328 + // Handle HTTP-specific fields 329 + if (mon.url !== undefined && mon.url !== "") { 330 + updateValues.url = mon.url; 331 + } 332 + 333 + if (mon.method !== undefined && mon.method !== 0) { 334 + updateValues.method = toValidMethod(httpMethodToString(mon.method)); 335 + } 336 + 337 + if (mon.body !== undefined) { 338 + updateValues.body = mon.body || undefined; 339 + } 340 + 341 + if (mon.followRedirects !== undefined) { 342 + updateValues.followRedirects = mon.followRedirects; 343 + } 344 + 345 + if (mon.headers !== undefined) { 346 + updateValues.headers = headersToDbJson(mon.headers); 347 + } 348 + 349 + // Handle assertions - update if any assertion type is provided 350 + if ( 351 + mon.statusCodeAssertions !== undefined || 352 + mon.bodyAssertions !== undefined || 353 + mon.headerAssertions !== undefined 354 + ) { 355 + updateValues.assertions = httpAssertionsToDbJson( 356 + mon.statusCodeAssertions ?? [], 357 + mon.bodyAssertions ?? [], 358 + mon.headerAssertions ?? [], 359 + ); 360 + } 361 + 362 + return performUpdateAndReturn( 363 + dbMon.id, 364 + req.id, 365 + updateValues, 366 + dbMonitorToHttpProto, 367 + ); 368 + }, 369 + 370 + async updateTCPMonitor(req, ctx) { 371 + const rpcCtx = getRpcContext(ctx); 372 + const workspaceId = rpcCtx.workspace.id; 373 + const limits = rpcCtx.workspace.limits; 374 + 375 + const dbMon = await validateAndGetMonitor(req.id, workspaceId, "tcp"); 376 + 377 + // If no monitor data provided, return current monitor 378 + if (!req.monitor) { 379 + const parsed = selectMonitorSchema.safeParse(dbMon); 380 + if (!parsed.success) { 381 + throw monitorParseFailedError(req.id); 382 + } 383 + return { monitor: dbMonitorToTcpProto(parsed.data) }; 384 + } 385 + 386 + const mon = req.monitor; 387 + 388 + // Validate regions if provided 389 + validateCommonMonitorFields(mon); 390 + 391 + // Check workspace limits if periodicity or regions are changing 392 + if (mon.periodicity || (mon.regions && mon.regions.length > 0)) { 393 + await checkMonitorLimits( 394 + workspaceId, 395 + limits, 396 + mon.periodicity || undefined, 397 + mon.regions && mon.regions.length > 0 ? mon.regions : undefined, 398 + ); 399 + } 400 + 401 + // Build update values - only include fields that are provided 402 + const updateValues: Record<string, unknown> = 403 + getCommonDbValuesForUpdate(mon); 404 + 405 + // Handle TCP-specific fields 406 + if (mon.uri !== undefined && mon.uri !== "") { 407 + updateValues.url = mon.uri; 408 + } 409 + 410 + return performUpdateAndReturn( 411 + dbMon.id, 412 + req.id, 413 + updateValues, 414 + dbMonitorToTcpProto, 415 + ); 416 + }, 417 + 418 + async updateDNSMonitor(req, ctx) { 419 + const rpcCtx = getRpcContext(ctx); 420 + const workspaceId = rpcCtx.workspace.id; 421 + const limits = rpcCtx.workspace.limits; 422 + 423 + const dbMon = await validateAndGetMonitor(req.id, workspaceId, "dns"); 424 + 425 + // If no monitor data provided, return current monitor 426 + if (!req.monitor) { 427 + const parsed = selectMonitorSchema.safeParse(dbMon); 428 + if (!parsed.success) { 429 + throw monitorParseFailedError(req.id); 430 + } 431 + return { monitor: dbMonitorToDnsProto(parsed.data) }; 432 + } 433 + 434 + const mon = req.monitor; 435 + 436 + // Validate regions if provided 437 + validateCommonMonitorFields(mon); 438 + 439 + // Check workspace limits if periodicity or regions are changing 440 + if (mon.periodicity || (mon.regions && mon.regions.length > 0)) { 441 + await checkMonitorLimits( 442 + workspaceId, 443 + limits, 444 + mon.periodicity || undefined, 445 + mon.regions && mon.regions.length > 0 ? mon.regions : undefined, 446 + ); 447 + } 448 + 449 + // Build update values - only include fields that are provided 450 + const updateValues: Record<string, unknown> = 451 + getCommonDbValuesForUpdate(mon); 452 + 453 + // Handle DNS-specific fields 454 + if (mon.uri !== undefined && mon.uri !== "") { 455 + updateValues.url = mon.uri; 456 + } 457 + 458 + // Handle DNS assertions 459 + if (mon.recordAssertions !== undefined) { 460 + updateValues.assertions = dnsAssertionsToDbJson(mon.recordAssertions); 461 + } 462 + 463 + return performUpdateAndReturn( 464 + dbMon.id, 465 + req.id, 466 + updateValues, 467 + dbMonitorToDnsProto, 468 + ); 230 469 }, 231 470 232 471 async triggerMonitor(req, ctx) {
+5 -1
apps/server/src/routes/rpc/services/monitor/limits.ts
··· 1 1 import { Code, ConnectError } from "@connectrpc/connect"; 2 2 import { and, db, eq, isNull, sql } from "@openstatus/db"; 3 3 import { monitor } from "@openstatus/db/src/schema"; 4 + import { monitorRegionSchema } from "@openstatus/db/src/schema/constants"; 4 5 import type { Limits } from "@openstatus/db/src/schema/plan/schema"; 5 6 import type { Periodicity, Region } from "@openstatus/proto/monitor/v1"; 7 + import { z } from "zod"; 6 8 7 9 import { periodicityToString, regionsToStrings } from "./converters"; 8 10 ··· 41 43 42 44 // Check regions limits 43 45 if (regions && regions.length > 0) { 44 - const regionStrings = regionsToStrings(regions); 46 + const regionStrings = z 47 + .array(monitorRegionSchema) 48 + .parse(regionsToStrings(regions)); 45 49 46 50 // Check max regions limit 47 51 if (regionStrings.length > limits["max-regions"]) {
+66
apps/server/src/routes/rpc/services/monitor/validators.ts
··· 96 96 otelHeaders: otelConfig.otelHeaders, 97 97 }; 98 98 } 99 + 100 + /** 101 + * Extract common database values for update operations. 102 + * Only includes fields that are explicitly provided (not undefined). 103 + * This enables partial updates where only specified fields are changed. 104 + */ 105 + export function getCommonDbValuesForUpdate(mon: { 106 + name?: string; 107 + periodicity?: Periodicity; 108 + timeout?: bigint; 109 + degradedAt?: bigint; 110 + active?: boolean; 111 + description?: string; 112 + public?: boolean; 113 + regions?: Region[]; 114 + retry?: bigint; 115 + openTelemetry?: Parameters<typeof openTelemetryToDb>[0]; 116 + }) { 117 + const result: Record<string, unknown> = {}; 118 + 119 + if (mon.name !== undefined && mon.name !== "") { 120 + result.name = mon.name; 121 + } 122 + 123 + if (mon.periodicity !== undefined && mon.periodicity !== 0) { 124 + const periodicityStr = periodicityToString(mon.periodicity); 125 + result.periodicity = toValidPeriodicity(periodicityStr); 126 + } 127 + 128 + if (mon.timeout !== undefined && mon.timeout !== BigInt(0)) { 129 + result.timeout = Number(mon.timeout); 130 + } 131 + 132 + if (mon.degradedAt !== undefined) { 133 + result.degradedAfter = Number(mon.degradedAt); 134 + } 135 + 136 + if (mon.active !== undefined) { 137 + result.active = mon.active; 138 + } 139 + 140 + if (mon.description !== undefined) { 141 + result.description = mon.description; 142 + } 143 + 144 + if (mon.public !== undefined) { 145 + result.public = mon.public; 146 + } 147 + 148 + if (mon.regions !== undefined && mon.regions.length > 0) { 149 + const regionStrings = regionsToStrings(mon.regions); 150 + result.regions = regionsToDbString(regionStrings); 151 + } 152 + 153 + if (mon.retry !== undefined && mon.retry !== BigInt(0)) { 154 + result.retry = Number(mon.retry); 155 + } 156 + 157 + if (mon.openTelemetry !== undefined) { 158 + const otelConfig = openTelemetryToDb(mon.openTelemetry); 159 + result.otelEndpoint = otelConfig.otelEndpoint; 160 + result.otelHeaders = otelConfig.otelHeaders; 161 + } 162 + 163 + return result; 164 + }
+54
packages/proto/api/openstatus/monitor/v1/service.proto
··· 33 33 // CreateDNSMonitor creates a new DNS monitor. 34 34 rpc CreateDNSMonitor(CreateDNSMonitorRequest) returns (CreateDNSMonitorResponse); 35 35 36 + // UpdateHTTPMonitor updates an existing HTTP monitor. 37 + rpc UpdateHTTPMonitor(UpdateHTTPMonitorRequest) returns (UpdateHTTPMonitorResponse); 38 + 39 + // UpdateTCPMonitor updates an existing TCP monitor. 40 + rpc UpdateTCPMonitor(UpdateTCPMonitorRequest) returns (UpdateTCPMonitorResponse); 41 + 42 + // UpdateDNSMonitor updates an existing DNS monitor. 43 + rpc UpdateDNSMonitor(UpdateDNSMonitorRequest) returns (UpdateDNSMonitorResponse); 44 + 36 45 // TriggerMonitor initiates an immediate check for a monitor. 37 46 rpc TriggerMonitor(TriggerMonitorRequest) returns (TriggerMonitorResponse); 38 47 ··· 82 91 // CreateDNSMonitorResponse is the response after creating a DNS monitor. 83 92 message CreateDNSMonitorResponse { 84 93 // The created monitor with assigned ID. 94 + DNSMonitor monitor = 1; 95 + } 96 + 97 + // UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor. 98 + message UpdateHTTPMonitorRequest { 99 + // Monitor ID to update (required). 100 + string id = 1 [(buf.validate.field).string.min_len = 1]; 101 + 102 + // Updated monitor configuration (all fields optional for partial updates). 103 + optional HTTPMonitor monitor = 2; 104 + } 105 + 106 + // UpdateHTTPMonitorResponse is the response after updating an HTTP monitor. 107 + message UpdateHTTPMonitorResponse { 108 + // The updated monitor. 109 + HTTPMonitor monitor = 1; 110 + } 111 + 112 + // UpdateTCPMonitorRequest is the request to update an existing TCP monitor. 113 + message UpdateTCPMonitorRequest { 114 + // Monitor ID to update (required). 115 + string id = 1 [(buf.validate.field).string.min_len = 1]; 116 + 117 + // Updated monitor configuration (all fields optional for partial updates). 118 + optional TCPMonitor monitor = 2; 119 + } 120 + 121 + // UpdateTCPMonitorResponse is the response after updating a TCP monitor. 122 + message UpdateTCPMonitorResponse { 123 + // The updated monitor. 124 + TCPMonitor monitor = 1; 125 + } 126 + 127 + // UpdateDNSMonitorRequest is the request to update an existing DNS monitor. 128 + message UpdateDNSMonitorRequest { 129 + // Monitor ID to update (required). 130 + string id = 1 [(buf.validate.field).string.min_len = 1]; 131 + 132 + // Updated monitor configuration (all fields optional for partial updates). 133 + optional DNSMonitor monitor = 2; 134 + } 135 + 136 + // UpdateDNSMonitorResponse is the response after updating a DNS monitor. 137 + message UpdateDNSMonitorResponse { 138 + // The updated monitor. 85 139 DNSMonitor monitor = 1; 86 140 } 87 141
+1 -1
packages/proto/gen/ts/openstatus/monitor/v1/monitor_pb.ts
··· 32 32 ACTIVE = 1, 33 33 34 34 /** 35 - * MONITOR_STATUS_DEGRADED indicates the monitor is paused. 35 + * MONITOR_STATUS_DEGRADED indicates the monitor is degraded. 36 36 * 37 37 * @generated from enum value: MONITOR_STATUS_DEGRADED = 2; 38 38 */
+190 -13
packages/proto/gen/ts/openstatus/monitor/v1/service_pb.ts
··· 19 19 * Describes the file openstatus/monitor/v1/service.proto. 20 20 */ 21 21 export const file_openstatus_monitor_v1_service: GenFile = /*@__PURE__*/ 22 - fileDesc("CiNvcGVuc3RhdHVzL21vbml0b3IvdjEvc2VydmljZS5wcm90bxIVb3BlbnN0YXR1cy5tb25pdG9yLnYxIlcKGENyZWF0ZUhUVFBNb25pdG9yUmVxdWVzdBI7Cgdtb25pdG9yGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9yQga6SAPIAQEiUAoZQ3JlYXRlSFRUUE1vbml0b3JSZXNwb25zZRIzCgdtb25pdG9yGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9yIlUKF0NyZWF0ZVRDUE1vbml0b3JSZXF1ZXN0EjoKB21vbml0b3IYASABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVENQTW9uaXRvckIGukgDyAEBIk4KGENyZWF0ZVRDUE1vbml0b3JSZXNwb25zZRIyCgdtb25pdG9yGAEgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3IiVQoXQ3JlYXRlRE5TTW9uaXRvclJlcXVlc3QSOgoHbW9uaXRvchgBIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5ETlNNb25pdG9yQga6SAPIAQEiTgoYQ3JlYXRlRE5TTW9uaXRvclJlc3BvbnNlEjIKB21vbml0b3IYASABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvciIsChVUcmlnZ2VyTW9uaXRvclJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEiKQoWVHJpZ2dlck1vbml0b3JSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIisKFERlbGV0ZU1vbml0b3JSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIigKFURlbGV0ZU1vbml0b3JSZXNwb25zZRIPCgdzdWNjZXNzGAEgASgIIm4KE0xpc3RNb25pdG9yc1JlcXVlc3QSIQoJcGFnZV9zaXplGAEgASgFQgm6SAYaBBhkKAFIAIgBARIXCgpwYWdlX3Rva2VuGAIgASgJSAGIAQFCDAoKX3BhZ2Vfc2l6ZUINCgtfcGFnZV90b2tlbiLwAQoUTGlzdE1vbml0b3JzUmVzcG9uc2USOQoNaHR0cF9tb25pdG9ycxgBIAMoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvchI3Cgx0Y3BfbW9uaXRvcnMYAiADKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVENQTW9uaXRvchI3CgxkbnNfbW9uaXRvcnMYAyADKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvchIXCg9uZXh0X3BhZ2VfdG9rZW4YBCABKAkSEgoKdG90YWxfc2l6ZRgFIAEoBSIuChdHZXRNb25pdG9yU3RhdHVzUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASJzCgxSZWdpb25TdGF0dXMSLQoGcmVnaW9uGAEgASgOMh0ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlJlZ2lvbhI0CgZzdGF0dXMYAiABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cyJcChhHZXRNb25pdG9yU3RhdHVzUmVzcG9uc2USCgoCaWQYASABKAkSNAoHcmVnaW9ucxgCIAMoCzIjLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25TdGF0dXMisQEKDU1vbml0b3JDb25maWcSMgoEaHR0cBgBIAEoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvckgAEjAKA3RjcBgCIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5UQ1BNb25pdG9ySAASMAoDZG5zGAMgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLkROU01vbml0b3JIAEIICgZjb25maWcinwEKGEdldE1vbml0b3JTdW1tYXJ5UmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQARI0Cgp0aW1lX3JhbmdlGAIgASgOMiAub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRpbWVSYW5nZRI4CgdyZWdpb25zGAMgAygOMh0ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlJlZ2lvbkIIukgFkgECEBwirAIKGUdldE1vbml0b3JTdW1tYXJ5UmVzcG9uc2USCgoCaWQYASABKAkSFAoMbGFzdF9waW5nX2F0GAIgASgJEhgKEHRvdGFsX3N1Y2Nlc3NmdWwYAyABKAMSFgoOdG90YWxfZGVncmFkZWQYBCABKAMSFAoMdG90YWxfZmFpbGVkGAUgASgDEgsKA3A1MBgGIAEoAxILCgNwNzUYByABKAMSCwoDcDkwGAggASgDEgsKA3A5NRgJIAEoAxILCgNwOTkYCiABKAMSNAoKdGltZV9yYW5nZRgLIAEoDjIgLm9wZW5zdGF0dXMubW9uaXRvci52MS5UaW1lUmFuZ2USLgoHcmVnaW9ucxgMIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb24qYQoJVGltZVJhbmdlEhoKFlRJTUVfUkFOR0VfVU5TUEVDSUZJRUQQABIRCg1USU1FX1JBTkdFXzFEEAESEQoNVElNRV9SQU5HRV83RBACEhIKDlRJTUVfUkFOR0VfMTREEAMyowcKDk1vbml0b3JTZXJ2aWNlEnYKEUNyZWF0ZUhUVFBNb25pdG9yEi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZUhUVFBNb25pdG9yUmVxdWVzdBowLm9wZW5zdGF0dXMubW9uaXRvci52MS5DcmVhdGVIVFRQTW9uaXRvclJlc3BvbnNlEnMKEENyZWF0ZVRDUE1vbml0b3ISLi5vcGVuc3RhdHVzLm1vbml0b3IudjEuQ3JlYXRlVENQTW9uaXRvclJlcXVlc3QaLy5vcGVuc3RhdHVzLm1vbml0b3IudjEuQ3JlYXRlVENQTW9uaXRvclJlc3BvbnNlEnMKEENyZWF0ZUROU01vbml0b3ISLi5vcGVuc3RhdHVzLm1vbml0b3IudjEuQ3JlYXRlRE5TTW9uaXRvclJlcXVlc3QaLy5vcGVuc3RhdHVzLm1vbml0b3IudjEuQ3JlYXRlRE5TTW9uaXRvclJlc3BvbnNlEm0KDlRyaWdnZXJNb25pdG9yEiwub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRyaWdnZXJNb25pdG9yUmVxdWVzdBotLm9wZW5zdGF0dXMubW9uaXRvci52MS5UcmlnZ2VyTW9uaXRvclJlc3BvbnNlEmoKDURlbGV0ZU1vbml0b3ISKy5vcGVuc3RhdHVzLm1vbml0b3IudjEuRGVsZXRlTW9uaXRvclJlcXVlc3QaLC5vcGVuc3RhdHVzLm1vbml0b3IudjEuRGVsZXRlTW9uaXRvclJlc3BvbnNlEmcKDExpc3RNb25pdG9ycxIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5MaXN0TW9uaXRvcnNSZXF1ZXN0Gisub3BlbnN0YXR1cy5tb25pdG9yLnYxLkxpc3RNb25pdG9yc1Jlc3BvbnNlEnMKEEdldE1vbml0b3JTdGF0dXMSLi5vcGVuc3RhdHVzLm1vbml0b3IudjEuR2V0TW9uaXRvclN0YXR1c1JlcXVlc3QaLy5vcGVuc3RhdHVzLm1vbml0b3IudjEuR2V0TW9uaXRvclN0YXR1c1Jlc3BvbnNlEnYKEUdldE1vbml0b3JTdW1tYXJ5Ei8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkdldE1vbml0b3JTdW1tYXJ5UmVxdWVzdBowLm9wZW5zdGF0dXMubW9uaXRvci52MS5HZXRNb25pdG9yU3VtbWFyeVJlc3BvbnNlQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_openstatus_monitor_v1_dns_monitor, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor, file_openstatus_monitor_v1_tcp_monitor]); 22 + fileDesc("CiNvcGVuc3RhdHVzL21vbml0b3IvdjEvc2VydmljZS5wcm90bxIVb3BlbnN0YXR1cy5tb25pdG9yLnYxIlcKGENyZWF0ZUhUVFBNb25pdG9yUmVxdWVzdBI7Cgdtb25pdG9yGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9yQga6SAPIAQEiUAoZQ3JlYXRlSFRUUE1vbml0b3JSZXNwb25zZRIzCgdtb25pdG9yGAEgASgLMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhUVFBNb25pdG9yIlUKF0NyZWF0ZVRDUE1vbml0b3JSZXF1ZXN0EjoKB21vbml0b3IYASABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVENQTW9uaXRvckIGukgDyAEBIk4KGENyZWF0ZVRDUE1vbml0b3JSZXNwb25zZRIyCgdtb25pdG9yGAEgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3IiVQoXQ3JlYXRlRE5TTW9uaXRvclJlcXVlc3QSOgoHbW9uaXRvchgBIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5ETlNNb25pdG9yQga6SAPIAQEiTgoYQ3JlYXRlRE5TTW9uaXRvclJlc3BvbnNlEjIKB21vbml0b3IYASABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvciJ1ChhVcGRhdGVIVFRQTW9uaXRvclJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESOAoHbW9uaXRvchgCIAEoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvckgAiAEBQgoKCF9tb25pdG9yIlAKGVVwZGF0ZUhUVFBNb25pdG9yUmVzcG9uc2USMwoHbW9uaXRvchgBIAEoCzIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5IVFRQTW9uaXRvciJzChdVcGRhdGVUQ1BNb25pdG9yUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQARI3Cgdtb25pdG9yGAIgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3JIAIgBAUIKCghfbW9uaXRvciJOChhVcGRhdGVUQ1BNb25pdG9yUmVzcG9uc2USMgoHbW9uaXRvchgBIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5UQ1BNb25pdG9yInMKF1VwZGF0ZUROU01vbml0b3JSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABEjcKB21vbml0b3IYAiABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuRE5TTW9uaXRvckgAiAEBQgoKCF9tb25pdG9yIk4KGFVwZGF0ZUROU01vbml0b3JSZXNwb25zZRIyCgdtb25pdG9yGAEgASgLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLkROU01vbml0b3IiLAoVVHJpZ2dlck1vbml0b3JSZXF1ZXN0EhMKAmlkGAEgASgJQge6SARyAhABIikKFlRyaWdnZXJNb25pdG9yUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCIrChREZWxldGVNb25pdG9yUmVxdWVzdBITCgJpZBgBIAEoCUIHukgEcgIQASIoChVEZWxldGVNb25pdG9yUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCJuChNMaXN0TW9uaXRvcnNSZXF1ZXN0EiEKCXBhZ2Vfc2l6ZRgBIAEoBUIJukgGGgQYZCgBSACIAQESFwoKcGFnZV90b2tlbhgCIAEoCUgBiAEBQgwKCl9wYWdlX3NpemVCDQoLX3BhZ2VfdG9rZW4i8AEKFExpc3RNb25pdG9yc1Jlc3BvbnNlEjkKDWh0dHBfbW9uaXRvcnMYASADKAsyIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1vbml0b3ISNwoMdGNwX21vbml0b3JzGAIgAygLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLlRDUE1vbml0b3ISNwoMZG5zX21vbml0b3JzGAMgAygLMiEub3BlbnN0YXR1cy5tb25pdG9yLnYxLkROU01vbml0b3ISFwoPbmV4dF9wYWdlX3Rva2VuGAQgASgJEhIKCnRvdGFsX3NpemUYBSABKAUiLgoXR2V0TW9uaXRvclN0YXR1c1JlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAEicwoMUmVnaW9uU3RhdHVzEi0KBnJlZ2lvbhgBIAEoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb24SNAoGc3RhdHVzGAIgASgOMiQub3BlbnN0YXR1cy5tb25pdG9yLnYxLk1vbml0b3JTdGF0dXMiXAoYR2V0TW9uaXRvclN0YXR1c1Jlc3BvbnNlEgoKAmlkGAEgASgJEjQKB3JlZ2lvbnMYAiADKAsyIy5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uU3RhdHVzIrEBCg1Nb25pdG9yQ29uZmlnEjIKBGh0dHAYASABKAsyIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1vbml0b3JIABIwCgN0Y3AYAiABKAsyIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVENQTW9uaXRvckgAEjAKA2RucxgDIAEoCzIhLm9wZW5zdGF0dXMubW9uaXRvci52MS5ETlNNb25pdG9ySABCCAoGY29uZmlnIp8BChhHZXRNb25pdG9yU3VtbWFyeVJlcXVlc3QSEwoCaWQYASABKAlCB7pIBHICEAESNAoKdGltZV9yYW5nZRgCIAEoDjIgLm9wZW5zdGF0dXMubW9uaXRvci52MS5UaW1lUmFuZ2USOAoHcmVnaW9ucxgDIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CCLpIBZIBAhAcIqwCChlHZXRNb25pdG9yU3VtbWFyeVJlc3BvbnNlEgoKAmlkGAEgASgJEhQKDGxhc3RfcGluZ19hdBgCIAEoCRIYChB0b3RhbF9zdWNjZXNzZnVsGAMgASgDEhYKDnRvdGFsX2RlZ3JhZGVkGAQgASgDEhQKDHRvdGFsX2ZhaWxlZBgFIAEoAxILCgNwNTAYBiABKAMSCwoDcDc1GAcgASgDEgsKA3A5MBgIIAEoAxILCgNwOTUYCSABKAMSCwoDcDk5GAogASgDEjQKCnRpbWVfcmFuZ2UYCyABKA4yIC5vcGVuc3RhdHVzLm1vbml0b3IudjEuVGltZVJhbmdlEi4KB3JlZ2lvbnMYDCADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uKmEKCVRpbWVSYW5nZRIaChZUSU1FX1JBTkdFX1VOU1BFQ0lGSUVEEAASEQoNVElNRV9SQU5HRV8xRBABEhEKDVRJTUVfUkFOR0VfN0QQAhISCg5USU1FX1JBTkdFXzE0RBADMoUKCg5Nb25pdG9yU2VydmljZRJ2ChFDcmVhdGVIVFRQTW9uaXRvchIvLm9wZW5zdGF0dXMubW9uaXRvci52MS5DcmVhdGVIVFRQTW9uaXRvclJlcXVlc3QaMC5vcGVuc3RhdHVzLm1vbml0b3IudjEuQ3JlYXRlSFRUUE1vbml0b3JSZXNwb25zZRJzChBDcmVhdGVUQ1BNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZVRDUE1vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZVRDUE1vbml0b3JSZXNwb25zZRJzChBDcmVhdGVETlNNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZUROU01vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkNyZWF0ZUROU01vbml0b3JSZXNwb25zZRJ2ChFVcGRhdGVIVFRQTW9uaXRvchIvLm9wZW5zdGF0dXMubW9uaXRvci52MS5VcGRhdGVIVFRQTW9uaXRvclJlcXVlc3QaMC5vcGVuc3RhdHVzLm1vbml0b3IudjEuVXBkYXRlSFRUUE1vbml0b3JSZXNwb25zZRJzChBVcGRhdGVUQ1BNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZVRDUE1vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZVRDUE1vbml0b3JSZXNwb25zZRJzChBVcGRhdGVETlNNb25pdG9yEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZUROU01vbml0b3JSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLlVwZGF0ZUROU01vbml0b3JSZXNwb25zZRJtCg5UcmlnZ2VyTW9uaXRvchIsLm9wZW5zdGF0dXMubW9uaXRvci52MS5UcmlnZ2VyTW9uaXRvclJlcXVlc3QaLS5vcGVuc3RhdHVzLm1vbml0b3IudjEuVHJpZ2dlck1vbml0b3JSZXNwb25zZRJqCg1EZWxldGVNb25pdG9yEisub3BlbnN0YXR1cy5tb25pdG9yLnYxLkRlbGV0ZU1vbml0b3JSZXF1ZXN0Giwub3BlbnN0YXR1cy5tb25pdG9yLnYxLkRlbGV0ZU1vbml0b3JSZXNwb25zZRJnCgxMaXN0TW9uaXRvcnMSKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuTGlzdE1vbml0b3JzUmVxdWVzdBorLm9wZW5zdGF0dXMubW9uaXRvci52MS5MaXN0TW9uaXRvcnNSZXNwb25zZRJzChBHZXRNb25pdG9yU3RhdHVzEi4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkdldE1vbml0b3JTdGF0dXNSZXF1ZXN0Gi8ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkdldE1vbml0b3JTdGF0dXNSZXNwb25zZRJ2ChFHZXRNb25pdG9yU3VtbWFyeRIvLm9wZW5zdGF0dXMubW9uaXRvci52MS5HZXRNb25pdG9yU3VtbWFyeVJlcXVlc3QaMC5vcGVuc3RhdHVzLm1vbml0b3IudjEuR2V0TW9uaXRvclN1bW1hcnlSZXNwb25zZUJTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw", [file_buf_validate_validate, file_openstatus_monitor_v1_dns_monitor, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor, file_openstatus_monitor_v1_tcp_monitor]); 23 23 24 24 /** 25 25 * CreateHTTPMonitorRequest is the request to create a new HTTP monitor. ··· 148 148 messageDesc(file_openstatus_monitor_v1_service, 5); 149 149 150 150 /** 151 + * UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor. 152 + * 153 + * @generated from message openstatus.monitor.v1.UpdateHTTPMonitorRequest 154 + */ 155 + export type UpdateHTTPMonitorRequest = Message<"openstatus.monitor.v1.UpdateHTTPMonitorRequest"> & { 156 + /** 157 + * Monitor ID to update (required). 158 + * 159 + * @generated from field: string id = 1; 160 + */ 161 + id: string; 162 + 163 + /** 164 + * Updated monitor configuration (all fields optional for partial updates). 165 + * 166 + * @generated from field: optional openstatus.monitor.v1.HTTPMonitor monitor = 2; 167 + */ 168 + monitor?: HTTPMonitor; 169 + }; 170 + 171 + /** 172 + * Describes the message openstatus.monitor.v1.UpdateHTTPMonitorRequest. 173 + * Use `create(UpdateHTTPMonitorRequestSchema)` to create a new message. 174 + */ 175 + export const UpdateHTTPMonitorRequestSchema: GenMessage<UpdateHTTPMonitorRequest> = /*@__PURE__*/ 176 + messageDesc(file_openstatus_monitor_v1_service, 6); 177 + 178 + /** 179 + * UpdateHTTPMonitorResponse is the response after updating an HTTP monitor. 180 + * 181 + * @generated from message openstatus.monitor.v1.UpdateHTTPMonitorResponse 182 + */ 183 + export type UpdateHTTPMonitorResponse = Message<"openstatus.monitor.v1.UpdateHTTPMonitorResponse"> & { 184 + /** 185 + * The updated monitor. 186 + * 187 + * @generated from field: openstatus.monitor.v1.HTTPMonitor monitor = 1; 188 + */ 189 + monitor?: HTTPMonitor; 190 + }; 191 + 192 + /** 193 + * Describes the message openstatus.monitor.v1.UpdateHTTPMonitorResponse. 194 + * Use `create(UpdateHTTPMonitorResponseSchema)` to create a new message. 195 + */ 196 + export const UpdateHTTPMonitorResponseSchema: GenMessage<UpdateHTTPMonitorResponse> = /*@__PURE__*/ 197 + messageDesc(file_openstatus_monitor_v1_service, 7); 198 + 199 + /** 200 + * UpdateTCPMonitorRequest is the request to update an existing TCP monitor. 201 + * 202 + * @generated from message openstatus.monitor.v1.UpdateTCPMonitorRequest 203 + */ 204 + export type UpdateTCPMonitorRequest = Message<"openstatus.monitor.v1.UpdateTCPMonitorRequest"> & { 205 + /** 206 + * Monitor ID to update (required). 207 + * 208 + * @generated from field: string id = 1; 209 + */ 210 + id: string; 211 + 212 + /** 213 + * Updated monitor configuration (all fields optional for partial updates). 214 + * 215 + * @generated from field: optional openstatus.monitor.v1.TCPMonitor monitor = 2; 216 + */ 217 + monitor?: TCPMonitor; 218 + }; 219 + 220 + /** 221 + * Describes the message openstatus.monitor.v1.UpdateTCPMonitorRequest. 222 + * Use `create(UpdateTCPMonitorRequestSchema)` to create a new message. 223 + */ 224 + export const UpdateTCPMonitorRequestSchema: GenMessage<UpdateTCPMonitorRequest> = /*@__PURE__*/ 225 + messageDesc(file_openstatus_monitor_v1_service, 8); 226 + 227 + /** 228 + * UpdateTCPMonitorResponse is the response after updating a TCP monitor. 229 + * 230 + * @generated from message openstatus.monitor.v1.UpdateTCPMonitorResponse 231 + */ 232 + export type UpdateTCPMonitorResponse = Message<"openstatus.monitor.v1.UpdateTCPMonitorResponse"> & { 233 + /** 234 + * The updated monitor. 235 + * 236 + * @generated from field: openstatus.monitor.v1.TCPMonitor monitor = 1; 237 + */ 238 + monitor?: TCPMonitor; 239 + }; 240 + 241 + /** 242 + * Describes the message openstatus.monitor.v1.UpdateTCPMonitorResponse. 243 + * Use `create(UpdateTCPMonitorResponseSchema)` to create a new message. 244 + */ 245 + export const UpdateTCPMonitorResponseSchema: GenMessage<UpdateTCPMonitorResponse> = /*@__PURE__*/ 246 + messageDesc(file_openstatus_monitor_v1_service, 9); 247 + 248 + /** 249 + * UpdateDNSMonitorRequest is the request to update an existing DNS monitor. 250 + * 251 + * @generated from message openstatus.monitor.v1.UpdateDNSMonitorRequest 252 + */ 253 + export type UpdateDNSMonitorRequest = Message<"openstatus.monitor.v1.UpdateDNSMonitorRequest"> & { 254 + /** 255 + * Monitor ID to update (required). 256 + * 257 + * @generated from field: string id = 1; 258 + */ 259 + id: string; 260 + 261 + /** 262 + * Updated monitor configuration (all fields optional for partial updates). 263 + * 264 + * @generated from field: optional openstatus.monitor.v1.DNSMonitor monitor = 2; 265 + */ 266 + monitor?: DNSMonitor; 267 + }; 268 + 269 + /** 270 + * Describes the message openstatus.monitor.v1.UpdateDNSMonitorRequest. 271 + * Use `create(UpdateDNSMonitorRequestSchema)` to create a new message. 272 + */ 273 + export const UpdateDNSMonitorRequestSchema: GenMessage<UpdateDNSMonitorRequest> = /*@__PURE__*/ 274 + messageDesc(file_openstatus_monitor_v1_service, 10); 275 + 276 + /** 277 + * UpdateDNSMonitorResponse is the response after updating a DNS monitor. 278 + * 279 + * @generated from message openstatus.monitor.v1.UpdateDNSMonitorResponse 280 + */ 281 + export type UpdateDNSMonitorResponse = Message<"openstatus.monitor.v1.UpdateDNSMonitorResponse"> & { 282 + /** 283 + * The updated monitor. 284 + * 285 + * @generated from field: openstatus.monitor.v1.DNSMonitor monitor = 1; 286 + */ 287 + monitor?: DNSMonitor; 288 + }; 289 + 290 + /** 291 + * Describes the message openstatus.monitor.v1.UpdateDNSMonitorResponse. 292 + * Use `create(UpdateDNSMonitorResponseSchema)` to create a new message. 293 + */ 294 + export const UpdateDNSMonitorResponseSchema: GenMessage<UpdateDNSMonitorResponse> = /*@__PURE__*/ 295 + messageDesc(file_openstatus_monitor_v1_service, 11); 296 + 297 + /** 151 298 * TriggerMonitorRequest is the request to trigger a monitor check. 152 299 * 153 300 * @generated from message openstatus.monitor.v1.TriggerMonitorRequest ··· 166 313 * Use `create(TriggerMonitorRequestSchema)` to create a new message. 167 314 */ 168 315 export const TriggerMonitorRequestSchema: GenMessage<TriggerMonitorRequest> = /*@__PURE__*/ 169 - messageDesc(file_openstatus_monitor_v1_service, 6); 316 + messageDesc(file_openstatus_monitor_v1_service, 12); 170 317 171 318 /** 172 319 * TriggerMonitorResponse is the response after triggering a monitor. ··· 187 334 * Use `create(TriggerMonitorResponseSchema)` to create a new message. 188 335 */ 189 336 export const TriggerMonitorResponseSchema: GenMessage<TriggerMonitorResponse> = /*@__PURE__*/ 190 - messageDesc(file_openstatus_monitor_v1_service, 7); 337 + messageDesc(file_openstatus_monitor_v1_service, 13); 191 338 192 339 /** 193 340 * DeleteMonitorRequest is the request to delete a monitor. ··· 208 355 * Use `create(DeleteMonitorRequestSchema)` to create a new message. 209 356 */ 210 357 export const DeleteMonitorRequestSchema: GenMessage<DeleteMonitorRequest> = /*@__PURE__*/ 211 - messageDesc(file_openstatus_monitor_v1_service, 8); 358 + messageDesc(file_openstatus_monitor_v1_service, 14); 212 359 213 360 /** 214 361 * DeleteMonitorResponse is the response after deleting a monitor. ··· 229 376 * Use `create(DeleteMonitorResponseSchema)` to create a new message. 230 377 */ 231 378 export const DeleteMonitorResponseSchema: GenMessage<DeleteMonitorResponse> = /*@__PURE__*/ 232 - messageDesc(file_openstatus_monitor_v1_service, 9); 379 + messageDesc(file_openstatus_monitor_v1_service, 15); 233 380 234 381 /** 235 382 * ListMonitorsRequest is the request to list monitors. ··· 257 404 * Use `create(ListMonitorsRequestSchema)` to create a new message. 258 405 */ 259 406 export const ListMonitorsRequestSchema: GenMessage<ListMonitorsRequest> = /*@__PURE__*/ 260 - messageDesc(file_openstatus_monitor_v1_service, 10); 407 + messageDesc(file_openstatus_monitor_v1_service, 16); 261 408 262 409 /** 263 410 * ListMonitorsResponse is the response containing a list of monitors. ··· 306 453 * Use `create(ListMonitorsResponseSchema)` to create a new message. 307 454 */ 308 455 export const ListMonitorsResponseSchema: GenMessage<ListMonitorsResponse> = /*@__PURE__*/ 309 - messageDesc(file_openstatus_monitor_v1_service, 11); 456 + messageDesc(file_openstatus_monitor_v1_service, 17); 310 457 311 458 /** 312 459 * GetMonitorStatusRequest is the request to get the status of all regions for a monitor. ··· 327 474 * Use `create(GetMonitorStatusRequestSchema)` to create a new message. 328 475 */ 329 476 export const GetMonitorStatusRequestSchema: GenMessage<GetMonitorStatusRequest> = /*@__PURE__*/ 330 - messageDesc(file_openstatus_monitor_v1_service, 12); 477 + messageDesc(file_openstatus_monitor_v1_service, 18); 331 478 332 479 /** 333 480 * RegionStatus represents the status of a monitor in a specific region. ··· 355 502 * Use `create(RegionStatusSchema)` to create a new message. 356 503 */ 357 504 export const RegionStatusSchema: GenMessage<RegionStatus> = /*@__PURE__*/ 358 - messageDesc(file_openstatus_monitor_v1_service, 13); 505 + messageDesc(file_openstatus_monitor_v1_service, 19); 359 506 360 507 /** 361 508 * GetMonitorStatusResponse is the response containing the status of all regions for a monitor. ··· 383 530 * Use `create(GetMonitorStatusResponseSchema)` to create a new message. 384 531 */ 385 532 export const GetMonitorStatusResponseSchema: GenMessage<GetMonitorStatusResponse> = /*@__PURE__*/ 386 - messageDesc(file_openstatus_monitor_v1_service, 14); 533 + messageDesc(file_openstatus_monitor_v1_service, 20); 387 534 388 535 /** 389 536 * MonitorConfig represents the type-specific configuration for a monitor. ··· 426 573 * Use `create(MonitorConfigSchema)` to create a new message. 427 574 */ 428 575 export const MonitorConfigSchema: GenMessage<MonitorConfig> = /*@__PURE__*/ 429 - messageDesc(file_openstatus_monitor_v1_service, 15); 576 + messageDesc(file_openstatus_monitor_v1_service, 21); 430 577 431 578 /** 432 579 * GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor. ··· 461 608 * Use `create(GetMonitorSummaryRequestSchema)` to create a new message. 462 609 */ 463 610 export const GetMonitorSummaryRequestSchema: GenMessage<GetMonitorSummaryRequest> = /*@__PURE__*/ 464 - messageDesc(file_openstatus_monitor_v1_service, 16); 611 + messageDesc(file_openstatus_monitor_v1_service, 22); 465 612 466 613 /** 467 614 * GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor. ··· 559 706 * Use `create(GetMonitorSummaryResponseSchema)` to create a new message. 560 707 */ 561 708 export const GetMonitorSummaryResponseSchema: GenMessage<GetMonitorSummaryResponse> = /*@__PURE__*/ 562 - messageDesc(file_openstatus_monitor_v1_service, 17); 709 + messageDesc(file_openstatus_monitor_v1_service, 23); 563 710 564 711 /** 565 712 * TimeRange represents the time period for metrics aggregation. ··· 637 784 methodKind: "unary"; 638 785 input: typeof CreateDNSMonitorRequestSchema; 639 786 output: typeof CreateDNSMonitorResponseSchema; 787 + }, 788 + /** 789 + * UpdateHTTPMonitor updates an existing HTTP monitor. 790 + * 791 + * @generated from rpc openstatus.monitor.v1.MonitorService.UpdateHTTPMonitor 792 + */ 793 + updateHTTPMonitor: { 794 + methodKind: "unary"; 795 + input: typeof UpdateHTTPMonitorRequestSchema; 796 + output: typeof UpdateHTTPMonitorResponseSchema; 797 + }, 798 + /** 799 + * UpdateTCPMonitor updates an existing TCP monitor. 800 + * 801 + * @generated from rpc openstatus.monitor.v1.MonitorService.UpdateTCPMonitor 802 + */ 803 + updateTCPMonitor: { 804 + methodKind: "unary"; 805 + input: typeof UpdateTCPMonitorRequestSchema; 806 + output: typeof UpdateTCPMonitorResponseSchema; 807 + }, 808 + /** 809 + * UpdateDNSMonitor updates an existing DNS monitor. 810 + * 811 + * @generated from rpc openstatus.monitor.v1.MonitorService.UpdateDNSMonitor 812 + */ 813 + updateDNSMonitor: { 814 + methodKind: "unary"; 815 + input: typeof UpdateDNSMonitorRequestSchema; 816 + output: typeof UpdateDNSMonitorResponseSchema; 640 817 }, 641 818 /** 642 819 * TriggerMonitor initiates an immediate check for a monitor.