diff --git a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts index 2a6c33c71..9c54839af 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts @@ -2246,6 +2246,124 @@ describe("MonitorService.GetMonitor", () => { }); }); +describe("MonitorService - Private Locations", () => { + async function seedMonitorWithPrivateLocation(suffix: string) { + const mon = await db + .insert(monitor) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-pl-${suffix}`, + url: `https://pl-${suffix}.example.com`, + periodicity: "1m", + active: true, + regions: "ams", + jobType: "http", + }) + .returning() + .get(); + const pl = await db + .insert(privateLocation) + .values({ + workspaceId: 1, + name: `${TEST_PREFIX}-pl-loc-${suffix}`, + token: `${TEST_PREFIX}-pl-token-${suffix}-${mon.id}`, + }) + .returning() + .get(); + await db + .insert(privateLocationToMonitors) + .values({ privateLocationId: pl.id, monitorId: mon.id }); + return { mon, pl }; + } + + async function cleanupMonitorWithPrivateLocation( + monitorId: number, + privateLocationId: number, + ) { + await db + .delete(privateLocationToMonitors) + .where(eq(privateLocationToMonitors.privateLocationId, privateLocationId)); + await db + .delete(privateLocation) + .where(eq(privateLocation.id, privateLocationId)); + await db.delete(monitor).where(eq(monitor.id, monitorId)); + } + + test("GetMonitor returns the attached private location id", async () => { + const { mon, pl } = await seedMonitorWithPrivateLocation("get"); + try { + const res = await connectRequest( + "GetMonitor", + { id: String(mon.id) }, + { "x-openstatus-key": "1" }, + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.monitor.http.privateLocationIds).toEqual([String(pl.id)]); + } finally { + await cleanupMonitorWithPrivateLocation(mon.id, pl.id); + } + }); + + test("GetMonitor omits private_location_ids when none attached", async () => { + const res = await connectRequest( + "GetMonitor", + { id: String(testHttpMonitorId) }, + { "x-openstatus-key": "1" }, + ); + expect(res.status).toBe(200); + const data = await res.json(); + // proto3 JSON omits empty repeated fields + expect(data.monitor.http.privateLocationIds ?? []).toEqual([]); + }); + + test("ListMonitors returns private_location_ids per monitor", async () => { + const { mon, pl } = await seedMonitorWithPrivateLocation("list"); + try { + const res = await connectRequest( + "ListMonitors", + { limit: 100 }, + { "x-openstatus-key": "1" }, + ); + expect(res.status).toBe(200); + const data = await res.json(); + const httpMonitors = (data.httpMonitors ?? []) as Array<{ + id: string; + privateLocationIds?: string[]; + }>; + + const attached = httpMonitors.find((m) => m.id === String(mon.id)); + expect(attached?.privateLocationIds).toEqual([String(pl.id)]); + + const unattached = httpMonitors.find( + (m) => m.id === String(testHttpMonitorId), + ); + expect(unattached?.privateLocationIds ?? []).toEqual([]); + } finally { + await cleanupMonitorWithPrivateLocation(mon.id, pl.id); + } + }); + + test("UpdateHTTPMonitor keeps the attached private location id", async () => { + const { mon, pl } = await seedMonitorWithPrivateLocation("update"); + try { + const res = await connectRequest( + "UpdateHTTPMonitor", + { + id: String(mon.id), + monitor: { name: `${TEST_PREFIX}-pl-update-renamed` }, + }, + { "x-openstatus-key": "1" }, + ); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.monitor.privateLocationIds).toEqual([String(pl.id)]); + } finally { + await cleanupMonitorWithPrivateLocation(mon.id, pl.id); + } + }); +}); + describe("MonitorService.GetMonitorSummary", () => { test("returns summary for HTTP monitor with correct structure", async () => { const res = await connectRequest( diff --git a/apps/server/src/routes/rpc/handlers/monitor/converters/monitors.ts b/apps/server/src/routes/rpc/handlers/monitor/converters/monitors.ts index 9fbca6f7c..a7cc9a330 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/converters/monitors.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/converters/monitors.ts @@ -18,7 +18,10 @@ import { stringsToRegions } from "./regions"; /** * Transform database HTTP monitor to proto HTTPMonitor. */ -export function dbMonitorToHttpProto(dbMon: Monitor): HTTPMonitor { +export function dbMonitorToHttpProto( + dbMon: Monitor, + privateLocationIds: string[] = [], +): HTTPMonitor { const assertions = parseHttpAssertions(dbMon.assertions); return { @@ -43,13 +46,17 @@ export function dbMonitorToHttpProto(dbMon: Monitor): HTTPMonitor { regions: stringsToRegions(dbMon.regions), openTelemetry: parseOpenTelemetry(dbMon.otelEndpoint, dbMon.otelHeaders), status: stringToMonitorStatus(dbMon.status), + privateLocationIds, }; } /** * Transform database TCP monitor to proto TCPMonitor. */ -export function dbMonitorToTcpProto(dbMon: Monitor): TCPMonitor { +export function dbMonitorToTcpProto( + dbMon: Monitor, + privateLocationIds: string[] = [], +): TCPMonitor { return { $typeName: "openstatus.monitor.v1.TCPMonitor", id: String(dbMon.id), @@ -65,13 +72,17 @@ export function dbMonitorToTcpProto(dbMon: Monitor): TCPMonitor { regions: stringsToRegions(dbMon.regions), openTelemetry: parseOpenTelemetry(dbMon.otelEndpoint, dbMon.otelHeaders), status: stringToMonitorStatus(dbMon.status), + privateLocationIds, }; } /** * Transform database DNS monitor to proto DNSMonitor. */ -export function dbMonitorToDnsProto(dbMon: Monitor): DNSMonitor { +export function dbMonitorToDnsProto( + dbMon: Monitor, + privateLocationIds: string[] = [], +): DNSMonitor { return { $typeName: "openstatus.monitor.v1.DNSMonitor", id: String(dbMon.id), @@ -88,5 +99,6 @@ export function dbMonitorToDnsProto(dbMon: Monitor): DNSMonitor { regions: stringsToRegions(dbMon.regions), openTelemetry: parseOpenTelemetry(dbMon.otelEndpoint, dbMon.otelHeaders), status: stringToMonitorStatus(dbMon.status), + privateLocationIds, }; } diff --git a/apps/server/src/routes/rpc/handlers/monitor/index.ts b/apps/server/src/routes/rpc/handlers/monitor/index.ts index 2151e55d8..885e30570 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/index.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/index.ts @@ -26,6 +26,7 @@ import { deleteMonitor, getMonitorStatus, getMonitorSummary, + getPrivateLocationIdsByMonitor, getResponseLog, listResponseLogs, } from "@openstatus/services/monitor"; @@ -323,13 +324,21 @@ export const monitorServiceImpl: ServiceImpl = { const dbMon = await validateAndGetMonitor(req.id, workspaceId, "http"); + const plMap = await getPrivateLocationIdsByMonitor({ + ctx: toServiceCtx(rpcCtx), + input: { monitorIds: [dbMon.id] }, + }); + const privateLocationIds = plMap.get(dbMon.id) ?? []; + // If no monitor data provided, return current monitor if (!req.monitor) { const parsed = selectMonitorSchema.safeParse(dbMon); if (!parsed.success) { throw monitorParseFailedError(req.id); } - return { monitor: dbMonitorToHttpProto(parsed.data) }; + return { + monitor: dbMonitorToHttpProto(parsed.data, privateLocationIds), + }; } const mon = req.monitor; @@ -385,11 +394,8 @@ export const monitorServiceImpl: ServiceImpl = { ); } - return performUpdateAndReturn( - dbMon.id, - req.id, - updateValues, - dbMonitorToHttpProto, + return performUpdateAndReturn(dbMon.id, req.id, updateValues, (data) => + dbMonitorToHttpProto(data, privateLocationIds), ); }, @@ -400,13 +406,21 @@ export const monitorServiceImpl: ServiceImpl = { const dbMon = await validateAndGetMonitor(req.id, workspaceId, "tcp"); + const plMap = await getPrivateLocationIdsByMonitor({ + ctx: toServiceCtx(rpcCtx), + input: { monitorIds: [dbMon.id] }, + }); + const privateLocationIds = plMap.get(dbMon.id) ?? []; + // If no monitor data provided, return current monitor if (!req.monitor) { const parsed = selectMonitorSchema.safeParse(dbMon); if (!parsed.success) { throw monitorParseFailedError(req.id); } - return { monitor: dbMonitorToTcpProto(parsed.data) }; + return { + monitor: dbMonitorToTcpProto(parsed.data, privateLocationIds), + }; } const mon = req.monitor; @@ -433,11 +447,8 @@ export const monitorServiceImpl: ServiceImpl = { updateValues.url = mon.uri; } - return performUpdateAndReturn( - dbMon.id, - req.id, - updateValues, - dbMonitorToTcpProto, + return performUpdateAndReturn(dbMon.id, req.id, updateValues, (data) => + dbMonitorToTcpProto(data, privateLocationIds), ); }, @@ -448,13 +459,21 @@ export const monitorServiceImpl: ServiceImpl = { const dbMon = await validateAndGetMonitor(req.id, workspaceId, "dns"); + const plMap = await getPrivateLocationIdsByMonitor({ + ctx: toServiceCtx(rpcCtx), + input: { monitorIds: [dbMon.id] }, + }); + const privateLocationIds = plMap.get(dbMon.id) ?? []; + // If no monitor data provided, return current monitor if (!req.monitor) { const parsed = selectMonitorSchema.safeParse(dbMon); if (!parsed.success) { throw monitorParseFailedError(req.id); } - return { monitor: dbMonitorToDnsProto(parsed.data) }; + return { + monitor: dbMonitorToDnsProto(parsed.data, privateLocationIds), + }; } const mon = req.monitor; @@ -486,11 +505,8 @@ export const monitorServiceImpl: ServiceImpl = { updateValues.assertions = dnsAssertionsToDbJson(mon.recordAssertions); } - return performUpdateAndReturn( - dbMon.id, - req.id, - updateValues, - dbMonitorToDnsProto, + return performUpdateAndReturn(dbMon.id, req.id, updateValues, (data) => + dbMonitorToDnsProto(data, privateLocationIds), ); }, @@ -627,27 +643,34 @@ export const monitorServiceImpl: ServiceImpl = { .offset(offset) .all(); + // Parse first so private-location ids can be resolved in one batched query + const parsedMonitors: ParsedMonitor[] = []; + for (const m of monitors) { + const parsed = selectMonitorSchema.safeParse(m); + if (parsed.success) parsedMonitors.push(parsed.data); // Skip invalid monitors + } + + const plMap = await getPrivateLocationIdsByMonitor({ + ctx: toServiceCtx(rpcCtx), + input: { monitorIds: parsedMonitors.map((m) => m.id) }, + }); + // Group monitors by type const httpMonitors: HTTPMonitor[] = []; const tcpMonitors: TCPMonitor[] = []; const dnsMonitors: DNSMonitor[] = []; - for (const m of monitors) { - // Parse through schema to transform fields - const parsed = selectMonitorSchema.safeParse(m); - if (!parsed.success) { - continue; // Skip invalid monitors - } - - switch (parsed.data.jobType) { + for (const data of parsedMonitors) { + const privateLocationIds = plMap.get(data.id) ?? []; + switch (data.jobType) { case "http": - httpMonitors.push(dbMonitorToHttpProto(parsed.data)); + httpMonitors.push(dbMonitorToHttpProto(data, privateLocationIds)); break; case "tcp": - tcpMonitors.push(dbMonitorToTcpProto(parsed.data)); + tcpMonitors.push(dbMonitorToTcpProto(data, privateLocationIds)); break; case "dns": - dnsMonitors.push(dbMonitorToDnsProto(parsed.data)); + dnsMonitors.push(dbMonitorToDnsProto(data, privateLocationIds)); break; } } @@ -699,6 +722,12 @@ export const monitorServiceImpl: ServiceImpl = { const monitorData = parsed.data; + const plMap = await getPrivateLocationIdsByMonitor({ + ctx: toServiceCtx(rpcCtx), + input: { monitorIds: [monitorData.id] }, + }); + const privateLocationIds = plMap.get(monitorData.id) ?? []; + // Convert to appropriate proto type based on jobType let monitorConfig: MonitorConfig; @@ -706,19 +735,28 @@ export const monitorServiceImpl: ServiceImpl = { case "http": monitorConfig = { $typeName: "openstatus.monitor.v1.MonitorConfig", - config: { case: "http", value: dbMonitorToHttpProto(monitorData) }, + config: { + case: "http", + value: dbMonitorToHttpProto(monitorData, privateLocationIds), + }, }; break; case "tcp": monitorConfig = { $typeName: "openstatus.monitor.v1.MonitorConfig", - config: { case: "tcp", value: dbMonitorToTcpProto(monitorData) }, + config: { + case: "tcp", + value: dbMonitorToTcpProto(monitorData, privateLocationIds), + }, }; break; case "dns": monitorConfig = { $typeName: "openstatus.monitor.v1.MonitorConfig", - config: { case: "dns", value: dbMonitorToDnsProto(monitorData) }, + config: { + case: "dns", + value: dbMonitorToDnsProto(monitorData, privateLocationIds), + }, }; break; default: diff --git a/apps/server/static/openapi.yaml b/apps/server/static/openapi.yaml index 2099c29f6..1a0dd4bde 100644 --- a/apps/server/static/openapi.yaml +++ b/apps/server/static/openapi.yaml @@ -585,6 +585,12 @@ components: title: status description: Current operational status of the monitor. $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus' + privateLocationIds: + type: array + items: + type: string + title: private_location_ids + description: IDs of private locations that run this monitor. Read-only. title: DNSMonitor additionalProperties: false description: DNSMonitor defines the configuration for a DNS monitor. @@ -934,6 +940,12 @@ components: title: status description: Current operational status of the monitor. $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus' + privateLocationIds: + type: array + items: + type: string + title: private_location_ids + description: IDs of private locations that run this monitor. Read-only. title: HTTPMonitor additionalProperties: false description: HTTPMonitor defines the configuration for an HTTP monitor. @@ -1566,6 +1578,12 @@ components: title: status description: Current operational status of the monitor. $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus' + privateLocationIds: + type: array + items: + type: string + title: private_location_ids + description: IDs of private locations that run this monitor. Read-only. title: TCPMonitor additionalProperties: false description: TCPMonitor defines the configuration for a TCP monitor. diff --git a/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto b/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto index c2cf0519d..d1367e3db 100644 --- a/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto @@ -83,4 +83,7 @@ message DNSMonitor { // Current operational status of the monitor. MonitorStatus status = 14; + + // IDs of private locations that run this monitor. Read-only. + repeated string private_location_ids = 15; } diff --git a/packages/proto/api/openstatus/monitor/v1/http_monitor.proto b/packages/proto/api/openstatus/monitor/v1/http_monitor.proto index 0b9ad62db..672feba0e 100644 --- a/packages/proto/api/openstatus/monitor/v1/http_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/http_monitor.proto @@ -141,4 +141,7 @@ message HTTPMonitor { // Current operational status of the monitor. MonitorStatus status = 20; + + // IDs of private locations that run this monitor. Read-only. + repeated string private_location_ids = 21; } diff --git a/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto b/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto index 99a75eb34..840006adc 100644 --- a/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto @@ -79,4 +79,7 @@ message TCPMonitor { // Current operational status of the monitor. MonitorStatus status = 13; + + // IDs of private locations that run this monitor. Read-only. + repeated string private_location_ids = 14; } diff --git a/packages/proto/gen/openapi.yaml b/packages/proto/gen/openapi.yaml index 2099c29f6..1a0dd4bde 100644 --- a/packages/proto/gen/openapi.yaml +++ b/packages/proto/gen/openapi.yaml @@ -585,6 +585,12 @@ components: title: status description: Current operational status of the monitor. $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus' + privateLocationIds: + type: array + items: + type: string + title: private_location_ids + description: IDs of private locations that run this monitor. Read-only. title: DNSMonitor additionalProperties: false description: DNSMonitor defines the configuration for a DNS monitor. @@ -934,6 +940,12 @@ components: title: status description: Current operational status of the monitor. $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus' + privateLocationIds: + type: array + items: + type: string + title: private_location_ids + description: IDs of private locations that run this monitor. Read-only. title: HTTPMonitor additionalProperties: false description: HTTPMonitor defines the configuration for an HTTP monitor. @@ -1566,6 +1578,12 @@ components: title: status description: Current operational status of the monitor. $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus' + privateLocationIds: + type: array + items: + type: string + title: private_location_ids + description: IDs of private locations that run this monitor. Read-only. title: TCPMonitor additionalProperties: false description: TCPMonitor defines the configuration for a TCP monitor. diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts index c38cc8653..786ff4916 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/dns_monitor.proto. */ export const file_openstatus_monitor_v1_dns_monitor: GenFile = /*@__PURE__*/ - fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvZG5zX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSLxBAoKRE5TTW9uaXRvchIKCgJpZBgBIAEoCRIzCgRuYW1lGAIgASgJQiW6Rxg6FhIURE5TIFJlc29sdXRpb24gQ2hlY2u6SAdyBRABGIACEikKA3VyaRgDIAEoCUIcukcPOg0SC2V4YW1wbGUuY29tukgHcgUQARiAEBJBCgtwZXJpb2RpY2l0eRgEIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5QZXJpb2RpY2l0eUIIukgFggECIAASHAoHdGltZW91dBgFIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYBiABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYByABKANCCbpIBiIEGAooABJLChFyZWNvcmRfYXNzZXJ0aW9ucxgIIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWNvcmRBc3NlcnRpb25CCLpIBZIBAhAKEh0KC2Rlc2NyaXB0aW9uGAkgASgJQgi6SAVyAxiACBIOCgZhY3RpdmUYCiABKAgSDgoGcHVibGljGAsgASgIEj8KB3JlZ2lvbnMYDCADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDSABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDiABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1c0IOCgxfZGVncmFkZWRfYXRCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); + fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvZG5zX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSKPBQoKRE5TTW9uaXRvchIKCgJpZBgBIAEoCRIzCgRuYW1lGAIgASgJQiW6Rxg6FhIURE5TIFJlc29sdXRpb24gQ2hlY2u6SAdyBRABGIACEikKA3VyaRgDIAEoCUIcukcPOg0SC2V4YW1wbGUuY29tukgHcgUQARiAEBJBCgtwZXJpb2RpY2l0eRgEIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5QZXJpb2RpY2l0eUIIukgFggECIAASHAoHdGltZW91dBgFIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYBiABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYByABKANCCbpIBiIEGAooABJLChFyZWNvcmRfYXNzZXJ0aW9ucxgIIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWNvcmRBc3NlcnRpb25CCLpIBZIBAhAKEh0KC2Rlc2NyaXB0aW9uGAkgASgJQgi6SAVyAxiACBIOCgZhY3RpdmUYCiABKAgSDgoGcHVibGljGAsgASgIEj8KB3JlZ2lvbnMYDCADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDSABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDiABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIcChRwcml2YXRlX2xvY2F0aW9uX2lkcxgPIAMoCUIOCgxfZGVncmFkZWRfYXRCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); /** * DNSMonitor defines the configuration for a DNS monitor. @@ -123,6 +123,13 @@ export type DNSMonitor = Message<"openstatus.monitor.v1.DNSMonitor"> & { * @generated from field: openstatus.monitor.v1.MonitorStatus status = 14; */ status: MonitorStatus; + + /** + * IDs of private locations that run this monitor. Read-only. + * + * @generated from field: repeated string private_location_ids = 15; + */ + privateLocationIds: string[]; }; /** diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts index 6dfb257fe..c5f6da295 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/http_monitor.proto. */ export const file_openstatus_monitor_v1_http_monitor: GenFile = /*@__PURE__*/ - fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaHR0cF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEiWgoHSGVhZGVycxIoCgNrZXkYASABKAlCG7pHEToPEg1BdXRob3JpemF0aW9uukgEcgIQARIlCgV2YWx1ZRgCIAEoCUIWukcTOhESD0JlYXJlciB0b2tlbjEyMyJsChNPcGVuVGVsZW1ldHJ5Q29uZmlnEhoKCGVuZHBvaW50GAEgASgJQgi6SAVyAxiAEBI5CgdoZWFkZXJzGAIgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUIoEICgtIVFRQTW9uaXRvchIKCgJpZBgBIAEoCRI6CgRuYW1lGAIgASgJQiy6Rx86HRIbUHJvZHVjdGlvbiBBUEkgSGVhbHRoIENoZWNrukgHcgUQARiAAhI/CgN1cmwYAyABKAlCMrpHIjogEh5odHRwczovL2FwaS5leGFtcGxlLmNvbS9oZWFsdGi6SApyCBABGIAQiAEBEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABI7CgZtZXRob2QYBSABKA4yIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1ldGhvZEIIukgFggECIAASJQoEYm9keRgGIAEoCUIXukcUOhISEHsia2V5IjogInZhbHVlIn0SHAoHdGltZW91dBgHIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYCCABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYCSABKANCCbpIBiIEGAooABIdChBmb2xsb3dfcmVkaXJlY3RzGAogASgISAGIAQESOQoHaGVhZGVycxgLIAMoCzIeLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJzQgi6SAWSAQIQFBJUChZzdGF0dXNfY29kZV9hc3NlcnRpb25zGAwgAygLMioub3BlbnN0YXR1cy5tb25pdG9yLnYxLlN0YXR1c0NvZGVBc3NlcnRpb25CCLpIBZIBAhAKEkcKD2JvZHlfYXNzZXJ0aW9ucxgNIAMoCzIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Cb2R5QXNzZXJ0aW9uQgi6SAWSAQIQChJLChFoZWFkZXJfYXNzZXJ0aW9ucxgOIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJBc3NlcnRpb25CCLpIBZIBAhAKEh0KC2Rlc2NyaXB0aW9uGA8gASgJQgi6SAVyAxiACBIOCgZhY3RpdmUYECABKAgSDgoGcHVibGljGBEgASgIEj8KB3JlZ2lvbnMYEiADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYEyABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYFCABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1c0IOCgxfZGVncmFkZWRfYXRCEwoRX2ZvbGxvd19yZWRpcmVjdHMq9wEKCkhUVFBNZXRob2QSGwoXSFRUUF9NRVRIT0RfVU5TUEVDSUZJRUQQABITCg9IVFRQX01FVEhPRF9HRVQQARIUChBIVFRQX01FVEhPRF9QT1NUEAISFAoQSFRUUF9NRVRIT0RfSEVBRBADEhMKD0hUVFBfTUVUSE9EX1BVVBAEEhUKEUhUVFBfTUVUSE9EX1BBVENIEAUSFgoSSFRUUF9NRVRIT0RfREVMRVRFEAYSFQoRSFRUUF9NRVRIT0RfVFJBQ0UQBxIXChNIVFRQX01FVEhPRF9DT05ORUNUEAgSFwoTSFRUUF9NRVRIT0RfT1BUSU9OUxAJQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_monitor]); + fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaHR0cF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEiWgoHSGVhZGVycxIoCgNrZXkYASABKAlCG7pHEToPEg1BdXRob3JpemF0aW9uukgEcgIQARIlCgV2YWx1ZRgCIAEoCUIWukcTOhESD0JlYXJlciB0b2tlbjEyMyJsChNPcGVuVGVsZW1ldHJ5Q29uZmlnEhoKCGVuZHBvaW50GAEgASgJQgi6SAVyAxiAEBI5CgdoZWFkZXJzGAIgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUIp8ICgtIVFRQTW9uaXRvchIKCgJpZBgBIAEoCRI6CgRuYW1lGAIgASgJQiy6Rx86HRIbUHJvZHVjdGlvbiBBUEkgSGVhbHRoIENoZWNrukgHcgUQARiAAhI/CgN1cmwYAyABKAlCMrpHIjogEh5odHRwczovL2FwaS5leGFtcGxlLmNvbS9oZWFsdGi6SApyCBABGIAQiAEBEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABI7CgZtZXRob2QYBSABKA4yIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1ldGhvZEIIukgFggECIAASJQoEYm9keRgGIAEoCUIXukcUOhISEHsia2V5IjogInZhbHVlIn0SHAoHdGltZW91dBgHIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYCCABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYCSABKANCCbpIBiIEGAooABIdChBmb2xsb3dfcmVkaXJlY3RzGAogASgISAGIAQESOQoHaGVhZGVycxgLIAMoCzIeLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJzQgi6SAWSAQIQFBJUChZzdGF0dXNfY29kZV9hc3NlcnRpb25zGAwgAygLMioub3BlbnN0YXR1cy5tb25pdG9yLnYxLlN0YXR1c0NvZGVBc3NlcnRpb25CCLpIBZIBAhAKEkcKD2JvZHlfYXNzZXJ0aW9ucxgNIAMoCzIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Cb2R5QXNzZXJ0aW9uQgi6SAWSAQIQChJLChFoZWFkZXJfYXNzZXJ0aW9ucxgOIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJBc3NlcnRpb25CCLpIBZIBAhAKEh0KC2Rlc2NyaXB0aW9uGA8gASgJQgi6SAVyAxiACBIOCgZhY3RpdmUYECABKAgSDgoGcHVibGljGBEgASgIEj8KB3JlZ2lvbnMYEiADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYEyABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYFCABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIcChRwcml2YXRlX2xvY2F0aW9uX2lkcxgVIAMoCUIOCgxfZGVncmFkZWRfYXRCEwoRX2ZvbGxvd19yZWRpcmVjdHMq9wEKCkhUVFBNZXRob2QSGwoXSFRUUF9NRVRIT0RfVU5TUEVDSUZJRUQQABITCg9IVFRQX01FVEhPRF9HRVQQARIUChBIVFRQX01FVEhPRF9QT1NUEAISFAoQSFRUUF9NRVRIT0RfSEVBRBADEhMKD0hUVFBfTUVUSE9EX1BVVBAEEhUKEUhUVFBfTUVUSE9EX1BBVENIEAUSFgoSSFRUUF9NRVRIT0RfREVMRVRFEAYSFQoRSFRUUF9NRVRIT0RfVFJBQ0UQBxIXChNIVFRQX01FVEhPRF9DT05ORUNUEAgSFwoTSFRUUF9NRVRIT0RfT1BUSU9OUxAJQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_monitor]); /** * Headers represents a key-value pair for HTTP headers. @@ -219,6 +219,13 @@ export type HTTPMonitor = Message<"openstatus.monitor.v1.HTTPMonitor"> & { * @generated from field: openstatus.monitor.v1.MonitorStatus status = 20; */ status: MonitorStatus; + + /** + * IDs of private locations that run this monitor. Read-only. + * + * @generated from field: repeated string private_location_ids = 21; + */ + privateLocationIds: string[]; }; /** diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts index 461cd5efc..110ef0503 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/tcp_monitor.proto. */ export const file_openstatus_monitor_v1_tcp_monitor: GenFile = /*@__PURE__*/ - fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvdGNwX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSK3BAoKVENQTW9uaXRvchIKCgJpZBgBIAEoCRI4CgRuYW1lGAIgASgJQiq6Rx06GxIZRGF0YWJhc2UgQ29ubmVjdGlvbiBDaGVja7pIB3IFEAEYgAISNwoDdXJpGAMgASgJQiq6Rx06GxIZdGNwOi8vZGIuZXhhbXBsZS5jb206NTQzMrpIB3IFEAEYgBASQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGMCpBygAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjAqQcoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASHQoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAIEg4KBmFjdGl2ZRgJIAEoCBIOCgZwdWJsaWMYCiABKAgSPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzQg4KDF9kZWdyYWRlZF9hdEJTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); + fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvdGNwX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSLVBAoKVENQTW9uaXRvchIKCgJpZBgBIAEoCRI4CgRuYW1lGAIgASgJQiq6Rx06GxIZRGF0YWJhc2UgQ29ubmVjdGlvbiBDaGVja7pIB3IFEAEYgAISNwoDdXJpGAMgASgJQiq6Rx06GxIZdGNwOi8vZGIuZXhhbXBsZS5jb206NTQzMrpIB3IFEAEYgBASQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGMCpBygAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjAqQcoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASHQoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAIEg4KBmFjdGl2ZRgJIAEoCBIOCgZwdWJsaWMYCiABKAgSPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzEhwKFHByaXZhdGVfbG9jYXRpb25faWRzGA4gAygJQg4KDF9kZWdyYWRlZF9hdEJTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); /** * TCPMonitor defines the configuration for a TCP monitor. @@ -114,6 +114,13 @@ export type TCPMonitor = Message<"openstatus.monitor.v1.TCPMonitor"> & { * @generated from field: openstatus.monitor.v1.MonitorStatus status = 13; */ status: MonitorStatus; + + /** + * IDs of private locations that run this monitor. Read-only. + * + * @generated from field: repeated string private_location_ids = 14; + */ + privateLocationIds: string[]; }; /** diff --git a/packages/services/src/monitor/__tests__/reads.test.ts b/packages/services/src/monitor/__tests__/reads.test.ts index ba2b25f06..0a4725808 100644 --- a/packages/services/src/monitor/__tests__/reads.test.ts +++ b/packages/services/src/monitor/__tests__/reads.test.ts @@ -1,4 +1,8 @@ -import { monitorStatusTable } from "@openstatus/db/src/schema"; +import { + monitorStatusTable, + privateLocation, + privateLocationToMonitors, +} from "@openstatus/db/src/schema"; import { expect } from "@std/expect"; import { beforeAll, describe, test } from "@std/testing/bdd"; @@ -19,6 +23,7 @@ import { getMonitorStatus } from "../get-monitor-status"; import { getMonitorSummary } from "../get-monitor-summary"; import { getResponseLog } from "../get-response-log"; import { listResponseLogs } from "../list-response-logs"; +import { getPrivateLocationIdsByMonitor } from "../private-locations"; const TEST_PREFIX = "svc-monitor-reads-test"; @@ -410,3 +415,132 @@ describe("listResponseLogs", () => { }); }); }); + +describe("getPrivateLocationIdsByMonitor", () => { + async function createHttpMonitor(ctx: ServiceContext, name: string) { + return createMonitor({ + ctx, + input: { + name: `${TEST_PREFIX}-${name}`, + jobType: "http", + url: "https://example.com", + method: "GET", + headers: [], + assertions: [], + active: false, + regions: ["ams"], + }, + }); + } + + test("groups private location ids by monitor across the input", async () => { + await withTestTransaction(async (tx) => { + const ctx: ServiceContext = { ...teamCtx, db: tx }; + const mon1 = await createHttpMonitor(ctx, "group-mon1"); + const mon2 = await createHttpMonitor(ctx, "group-mon2"); + const mon3 = await createHttpMonitor(ctx, "group-mon3"); + + const [pl1] = await tx + .insert(privateLocation) + .values({ + workspaceId: teamCtx.workspace.id, + name: `${TEST_PREFIX}-group-pl1`, + token: `${TEST_PREFIX}-group-pl1-token`, + }) + .returning(); + const [pl2] = await tx + .insert(privateLocation) + .values({ + workspaceId: teamCtx.workspace.id, + name: `${TEST_PREFIX}-group-pl2`, + token: `${TEST_PREFIX}-group-pl2-token`, + }) + .returning(); + + await tx.insert(privateLocationToMonitors).values([ + { privateLocationId: pl1.id, monitorId: mon1.id }, + { privateLocationId: pl2.id, monitorId: mon1.id }, + { privateLocationId: pl1.id, monitorId: mon2.id }, + ]); + + const map = await getPrivateLocationIdsByMonitor({ + ctx, + input: { monitorIds: [mon1.id, mon2.id, mon3.id] }, + }); + + expect(map.get(mon1.id)).toEqual( + [pl1.id, pl2.id].sort((a, b) => a - b).map(String), + ); + expect(map.get(mon2.id)).toEqual([String(pl1.id)]); + // monitor with no associations is absent from the map + expect(map.has(mon3.id)).toBe(false); + }); + }); + + test("excludes private locations from another workspace", async () => { + await withTestTransaction(async (tx) => { + const teamTxCtx: ServiceContext = { ...teamCtx, db: tx }; + const mon = await createHttpMonitor(teamTxCtx, "cross-ws-mon"); + + // A private location owned by the free workspace, wired to a team + // monitor. The workspace-scoped join must not surface it. + const [foreignPl] = await tx + .insert(privateLocation) + .values({ + workspaceId: freeCtx.workspace.id, + name: `${TEST_PREFIX}-cross-ws-pl`, + token: `${TEST_PREFIX}-cross-ws-pl-token`, + }) + .returning(); + await tx + .insert(privateLocationToMonitors) + .values({ privateLocationId: foreignPl.id, monitorId: mon.id }); + + const map = await getPrivateLocationIdsByMonitor({ + ctx: teamTxCtx, + input: { monitorIds: [mon.id] }, + }); + + expect(map.has(mon.id)).toBe(false); + }); + }); + + test("excludes soft-deleted attachments", async () => { + await withTestTransaction(async (tx) => { + const ctx: ServiceContext = { ...teamCtx, db: tx }; + const mon = await createHttpMonitor(ctx, "soft-deleted"); + + const [pl] = await tx + .insert(privateLocation) + .values({ + workspaceId: teamCtx.workspace.id, + name: `${TEST_PREFIX}-soft-deleted-pl`, + token: `${TEST_PREFIX}-soft-deleted-pl-token`, + }) + .returning(); + // A soft-deleted join row: the location no longer runs the monitor. + await tx.insert(privateLocationToMonitors).values({ + privateLocationId: pl.id, + monitorId: mon.id, + deletedAt: new Date(), + }); + + const map = await getPrivateLocationIdsByMonitor({ + ctx, + input: { monitorIds: [mon.id] }, + }); + + expect(map.has(mon.id)).toBe(false); + }); + }); + + test("returns an empty map for empty input", async () => { + await withTestTransaction(async (tx) => { + const map = await getPrivateLocationIdsByMonitor({ + ctx: { ...teamCtx, db: tx }, + input: { monitorIds: [] }, + }); + expect(map.size).toBe(0); + }); + }); +}); diff --git a/packages/services/src/monitor/index.ts b/packages/services/src/monitor/index.ts index 32040998b..23903f389 100644 --- a/packages/services/src/monitor/index.ts +++ b/packages/services/src/monitor/index.ts @@ -12,6 +12,7 @@ export { type GetMonitorSummaryResult, } from "./get-monitor-summary"; export { getResponseLog, type ResponseLogDetail } from "./get-response-log"; +export { getPrivateLocationIdsByMonitor } from "./private-locations"; export { listResponseLogs, type ListResponseLogsResult, @@ -56,6 +57,7 @@ export { GetMonitorInput, GetMonitorStatusInput, GetMonitorSummaryInput, + GetPrivateLocationIdsByMonitorInput, GetResponseLogInput, ListMonitorsInput, ListResponseLogsInput, diff --git a/packages/services/src/monitor/private-locations.ts b/packages/services/src/monitor/private-locations.ts new file mode 100644 index 000000000..8aa59ccd9 --- /dev/null +++ b/packages/services/src/monitor/private-locations.ts @@ -0,0 +1,53 @@ +import { and, eq, inArray, isNull } from "@openstatus/db"; +import { privateLocation, privateLocationToMonitors } from "@openstatus/db/src/schema"; + +import { type ServiceContext, getReadDb } from "../context"; +import { GetPrivateLocationIdsByMonitorInput } from "./schemas"; + +/** + * Map each monitor id to the ids of the private locations that run it, in one + * batched workspace-scoped query. Monitors with no associations are absent from + * the map (callers default to `[]`). + */ +export async function getPrivateLocationIdsByMonitor(args: { + ctx: ServiceContext; + input: GetPrivateLocationIdsByMonitorInput; +}): Promise> { + const input = GetPrivateLocationIdsByMonitorInput.parse(args.input); + const db = getReadDb(args.ctx); + + const map = new Map(); + const ids = Array.from(new Set(input.monitorIds)); + if (ids.length === 0) return map; + + const rows = await db + .select({ + monitorId: privateLocationToMonitors.monitorId, + privateLocationId: privateLocationToMonitors.privateLocationId, + }) + .from(privateLocationToMonitors) + .innerJoin( + privateLocation, + eq(privateLocationToMonitors.privateLocationId, privateLocation.id), + ) + .where( + and( + inArray(privateLocationToMonitors.monitorId, ids), + eq(privateLocation.workspaceId, args.ctx.workspace.id), + // Match the checker: a soft-deleted attachment no longer runs the monitor. + isNull(privateLocationToMonitors.deletedAt), + ), + ) + .all(); + + for (const row of rows) { + if (row.monitorId == null || row.privateLocationId == null) continue; + const arr = map.get(row.monitorId) ?? []; + arr.push(String(row.privateLocationId)); + map.set(row.monitorId, arr); + } + + map.forEach((arr) => arr.sort((a, b) => Number(a) - Number(b))); + + return map; +} diff --git a/packages/services/src/monitor/schemas.ts b/packages/services/src/monitor/schemas.ts index c6ac70d77..fae7c0433 100644 --- a/packages/services/src/monitor/schemas.ts +++ b/packages/services/src/monitor/schemas.ts @@ -178,3 +178,10 @@ export const GetResponseLogInput = z.object({ logId: z.string().min(1), }); export type GetResponseLogInput = z.infer; + +export const GetPrivateLocationIdsByMonitorInput = z.object({ + monitorIds: z.array(z.number().int()), +}); +export type GetPrivateLocationIdsByMonitorInput = z.infer< + typeof GetPrivateLocationIdsByMonitorInput +>;