monitor import/export

This commit is contained in:
Alex Holliday committed 2025-11-20 23:27:30 +00:00
1 parent af6fede1b3
commit 6881be7555
12 files changed
+381 -154

No files matched your search

@@ -19,6 +19,8 @@ export interface IMonitorController {
res: Response,
next: NextFunction
): Promise<void>;
export(req: Request, res: Response, next: NextFunction): Promise<void>;
import(req: Request, res: Response, next: NextFunction): Promise<void>;
}
class MonitorController {
private monitorService: MonitorService;
@@ -315,6 +317,62 @@ class MonitorController {
next(error);
}
};
export = async (req: Request, res: Response, next: NextFunction) => {
try {
const userContext = req.user;
if (!userContext) {
return res.status(401).json({ message: "Unauthorized" });
}
const teamId = userContext.currentTeamId;
if (!teamId) {
throw new ApiError("No team ID", 400);
}
const monitors = await this.monitorService.export(teamId);
res.status(200).json({
message: "Monitors exported successfully",
data: monitors,
});
} catch (error) {
next(error);
}
};
import = async (req: Request, res: Response, next: NextFunction) => {
const userContext = req.user;
if (!userContext) {
return res.status(401).json({ message: "Unauthorized" });
}
const teamId = userContext.currentTeamId;
if (!teamId) {
throw new ApiError("No team ID", 400);
}
const orgId = userContext.orgId;
if (!orgId) {
throw new ApiError("No organization ID", 400);
}
const result = await this.monitorService.import(
orgId,
teamId,
userContext.sub,
req.body
);
res.status(200).json({
message: `Import request received. ${result.imported} successfully inserted, ${result.errors} failed`,
data: result,
});
try {
} catch (error) {
next(error);
}
};
}
export default MonitorController;
+1 -1
View File
@@ -6,7 +6,7 @@ export type {
ILighthouseResult,
} from "./checks/Check.js";
export { Monitor, MonitorStatuses } from "./monitors/Monitor.js";
export { Monitor, MonitorStatuses, MonitorTypes } from "./monitors/Monitor.js";
export type { IMonitor } from "./monitors/Monitor.js";
export { MonitorStats } from "./monitors/MonitorStats.js";
+17 -1
View File
@@ -10,8 +10,8 @@ import {
monitorIdChecksQuerySchema,
monitorPatchSchema,
monitorAllEmbedChecksQuerySchema,
monitorImportSchema,
} from "@/validation/index.js";
import { verify } from "node:crypto";
class MonitorRoutes {
private router;
@@ -41,6 +41,22 @@ class MonitorRoutes {
this.controller.getAll
);
this.router.get(
"/export",
verifyToken,
addUserContext,
verifyTeamPermission([PERMISSIONS.monitors.read]),
this.controller.export
);
this.router.post(
"/import",
verifyToken,
addUserContext,
validateBody(monitorImportSchema),
verifyTeamPermission([PERMISSIONS.monitors.write]),
this.controller.import
);
this.router.get(
"/:id/checks",
verifyToken,
@@ -6,6 +6,14 @@ import { IJobQueue } from "@/services/infrastructure/JobQueue.js";
import { MonitorWithChecksResponse } from "@/types/index.js";
import { MonitorStatus, MonitorType } from "@/db/models/monitors/Monitor.js";
export interface IImportedMonitor {
name: string;
type: MonitorType;
interval: number;
url: string;
n: number;
}
const SERVICE_NAME = "MonitorService";
export interface IMonitorService {
@@ -50,6 +58,13 @@ export interface IMonitorService {
updateData: Partial<IMonitor>
) => Promise<IMonitor>;
delete: (teamId: string, monitorId: string) => Promise<boolean>;
export: (teamId: string) => Promise<Array<Record<string, unknown>>>;
import: (
orgId: string,
teamId: string,
userId: string,
data: { monitors: Array<IImportedMonitor> }
) => Promise<{ imported: number; errors: number }>;
}
class MonitorService implements IMonitorService {
@@ -639,6 +654,43 @@ class MonitorService implements IMonitorService {
await monitor.deleteOne();
return true;
}
export = async (teamId: string) => {
return Monitor.find({ teamId })
.select("name url type interval n secret -_id")
.lean();
};
import = async (
orgId: string,
teamId: string,
userId: string,
data: { monitors: Array<IImportedMonitor> }
) => {
let result = [];
try {
result = await Monitor.insertMany(
data?.monitors.map((monitor) => ({
...monitor,
orgId: new mongoose.Types.ObjectId(orgId),
teamId: new mongoose.Types.ObjectId(teamId),
createdBy: new mongoose.Types.ObjectId(userId),
updatedBy: new mongoose.Types.ObjectId(userId),
}))
);
for (const monitor of result) {
await this.jobQueue.addJob(monitor);
}
} catch (error) {
console.error("Import error:", error);
}
return {
imported: result.length,
errors: data?.monitors?.length - result.length,
};
};
}
export default MonitorService;
@@ -96,9 +96,12 @@ class StatusService implements IStatusService {
statusResponse: StatusResponse,
statusChanged: boolean
) => {
const stats = await MonitorStats.findOne({ monitorId: monitor._id });
let stats = await MonitorStats.findOne({ monitorId: monitor._id });
if (!stats) {
throw new ApiError("MonitorStats not found", 500);
stats = await MonitorStats.create({
monitorId: monitor._id,
currentStreakStartedAt: Date.now(),
});
}
// Update check counts
+20
View File
@@ -1,6 +1,7 @@
import {
ChannelTypes,
MonitorStatuses,
MonitorTypes,
ResolutionTypes,
} from "@/db/models/index.js";
import { z } from "zod";
@@ -82,6 +83,25 @@ export const monitorSchema = z
}
});
export const monitorImportSchema = z.object({
monitors: z
.array(
z
.object({
name: z.string().trim().min(1, "Monitor name is required"),
url: z.string().trim().regex(urlRegex, "Invalid URL"),
secret: z.string().trim().optional(),
type: z.enum(MonitorTypes),
interval: z.number().min(1, "Interval must be greater than 0"),
n: z.number().min(1, "n must be at least 1"),
})
.strict()
)
.min(1, { message: "Provide at least one monitor" }),
});
export type MonitorImportPayload = z.infer<typeof monitorImportSchema>;
export const monitorPatchSchema = monitorSchema
.omit({
url: true,