add v2 models

This commit is contained in:
Alex Holliday committed 2025-09-23 14:40:57 -07:00
1 parent b118a321b9
commit 059408f098
9 files changed
+797

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IRole extends Document {
_id: Types.ObjectId;
name: string;
description?: string;
permissions: string[];
isSystem: boolean;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
const roleSchema = new Schema<IRole>(
{
name: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
description: {
type: String,
trim: true,
maxlength: 200,
},
permissions: [
{
type: String,
required: true,
},
],
isSystem: {
type: Boolean,
default: false,
},
isActive: {
type: Boolean,
default: true,
},
},
{
timestamps: true,
}
);
roleSchema.index({ name: 1 }, { unique: true });
export const Role = mongoose.model<IRole>("Role_v2", roleSchema);
+108
View File
@@ -0,0 +1,108 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface ITokenizedUser {
sub: string;
roles: string[];
}
export interface IUser extends Document {
_id: Types.ObjectId;
email: string;
firstName: string;
lastName: string;
passwordHash: string;
roles: Types.ObjectId[];
profile: {
avatar?: string;
bio?: string;
timezone: string;
locale: string;
};
preferences: {
theme: "light" | "dark" | "system";
};
lastLoginAt?: Date;
isActive: boolean;
isVerified: boolean;
createdAt: Date;
updatedAt: Date;
}
const userSchema = new Schema<IUser>(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
firstName: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
lastName: {
type: String,
required: true,
trim: true,
maxlength: 50,
},
passwordHash: {
type: String,
required: true,
},
roles: [
{
type: Schema.Types.ObjectId,
ref: "Role_v2",
},
],
profile: {
avatar: {
type: String,
},
bio: {
type: String,
maxlength: 200,
},
timezone: {
type: String,
default: "UTC",
},
locale: {
type: String,
default: "en",
},
},
preferences: {
theme: {
type: String,
enum: ["light", "dark", "system"],
default: "system",
},
},
lastLoginAt: {
type: Date,
},
isActive: {
type: Boolean,
default: true,
},
isVerified: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
toJSON: {
transform: function (_doc, ret) {
delete ret.passwordHash;
return ret;
},
},
}
);
export const User = mongoose.model<IUser>("User_v2", userSchema);
+297
View File
@@ -0,0 +1,297 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js";
import type { Response } from "got";
export type GotTimings = Response["timings"];
export interface ITimingPhases {
wait: number;
dns: number;
tcp: number;
tls: number;
request: number;
firstByte: number;
download: number;
total: number;
}
export interface ICpuInfo {
physical_core: number;
logical_core: number;
frequency: number;
current_frequency: number;
temperature: number[]; // per-core temps
free_percent: number;
usage_percent: number;
}
export interface IMemoryInfo {
total_bytes: number;
available_bytes: number;
used_bytes: number;
usage_percent: number;
}
export interface IHostInfo {
os?: string;
platform?: string;
kernel_version?: string;
pretty_name?: string;
}
export interface IDiskInfo {
device: string;
total_bytes: number;
free_bytes: number;
used_bytes: number;
usage_percent: number;
total_inodes?: number;
free_inodes?: number;
used_inodes?: number;
inodes_usage_percent?: number;
read_bytes?: number;
write_bytes?: number;
read_time?: number;
write_time?: number;
}
export interface INetInfo {
name: string;
bytes_sent: number;
bytes_recv: number;
packets_sent: number;
packets_recv: number;
err_in: number;
err_out: number;
drop_in: number;
drop_out: number;
fifo_in: number;
fifo_out: number;
}
export interface ICaptureInfo {
version?: string;
mode?: string;
}
export interface ISystemInfo {
cpu: ICpuInfo;
memory: IMemoryInfo;
disk: IDiskInfo[];
host: IHostInfo;
net: INetInfo[];
}
export interface ILighthouseAudit {
id?: string;
title?: string;
score?: number | null;
displayValue?: string;
numericValue?: number;
numericUnit?: string;
}
export interface ILighthouseCategories {
accessibility?: { score?: number | null };
"best-practices"?: { score?: number | null };
seo?: { score?: number | null };
performance?: { score?: number | null };
}
export interface ILighthouseResult {
categories?: ILighthouseCategories;
audits?: Record<string, ILighthouseAudit>;
}
export interface ICheckLighthouseFields {
accessibility: number;
bestPractices: number;
seo: number;
performance: number;
audits: {
cls: ILighthouseAudit;
si: ILighthouseAudit;
fcp: ILighthouseAudit;
lcp: ILighthouseAudit;
tbt: ILighthouseAudit;
};
}
export interface ICheck extends Document {
_id: Types.ObjectId;
monitorId: Types.ObjectId;
type: MonitorType;
status: MonitorStatus;
message: string;
responseTime?: number; // in ms
timings?: GotTimings;
httpStatusCode?: number;
errorMessage?: string;
ack: boolean;
ackAt?: Date;
ackBy?: Types.ObjectId;
expiry: Date;
createdAt: Date;
updatedAt: Date;
system?: ISystemInfo;
capture?: ICaptureInfo;
lighthouse?: ICheckLighthouseFields;
}
const CheckSchema = new Schema<ICheck>(
{
monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true },
type: {
type: String,
required: true,
enum: MonitorTypes,
},
status: {
type: String,
required: true,
enum: MonitorStatuses,
},
message: { type: String, trim: true },
responseTime: { type: Number },
timings: {
start: { type: Date },
socket: { type: Date },
lookup: { type: Date },
connect: { type: Date },
secureConnect: { type: Date },
response: { type: Date },
end: { type: Date },
phases: {
wait: { type: Number },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
request: { type: Number },
firstByte: { type: Number },
download: { type: Number },
total: { type: Number },
},
},
system: {
type: {
cpu: {
physical_core: { type: Number },
logical_core: { type: Number },
frequency: { type: Number },
current_frequency: { type: Number },
temperature: [{ type: Number }],
free_percent: { type: Number },
usage_percent: { type: Number },
},
memory: {
total_bytes: { type: Number },
available_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
},
disk: [
{
device: { type: String },
total_bytes: { type: Number },
free_bytes: { type: Number },
used_bytes: { type: Number },
usage_percent: { type: Number },
total_inodes: { type: Number },
free_inodes: { type: Number },
used_inodes: { type: Number },
inodes_usage_percent: { type: Number },
read_bytes: { type: Number },
write_bytes: { type: Number },
read_time: { type: Number },
write_time: { type: Number },
},
],
host: {
os: { type: String },
platform: { type: String },
kernel_version: { type: String },
pretty_name: { type: String },
},
net: [
{
name: { type: String },
bytes_sent: { type: Number },
bytes_recv: { type: Number },
packets_sent: { type: Number },
packets_recv: { type: Number },
err_in: { type: Number },
err_out: { type: Number },
drop_in: { type: Number },
drop_out: { type: Number },
fifo_in: { type: Number },
fifo_out: { type: Number },
},
],
},
required: false,
},
capture: {
type: {
version: { type: String },
mode: { type: String },
},
required: false,
},
lighthouse: {
accessibility: { type: Number, required: false },
bestPractices: { type: Number, required: false },
seo: { type: Number, required: false },
performance: { type: Number, required: false },
audits: {
cls: {
type: Object,
},
si: {
type: Object,
},
fcp: {
type: Object,
},
lcp: {
type: Object,
},
tbt: {
type: Object,
},
},
type: {
accessibility: { type: Number },
bestPractices: { type: Number },
seo: { type: Number },
performance: { type: Number },
audits: {
cls: { type: Object },
si: { type: Object },
fcp: { type: Object },
lcp: { type: Object },
tbt: { type: Object },
},
},
required: false,
},
httpStatusCode: { type: Number },
errorMessage: { type: String, trim: true },
ack: { type: Boolean, required: true, default: false },
ackAt: { type: Date },
ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" },
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24 * 30,
},
},
{ timestamps: true }
);
CheckSchema.index({ monitorId: 1, createdAt: -1 });
CheckSchema.index({ status: 1 });
CheckSchema.index({ status: 1, ack: 1 });
CheckSchema.index({ ack: 1, ackAt: 1 });
CheckSchema.index({ createdAt: -1 });
export const Check = mongoose.model<ICheck>("Check_v2", CheckSchema);
+33
View File
@@ -0,0 +1,33 @@
export { User } from "./auth/User.js";
export type { IUser } from "./auth/User.js";
export type { ITokenizedUser } from "./auth/User.js";
export { Role } from "./auth/Role.js";
export type { IRole } from "./auth/Role.js";
export { connectDatabase, disconnectDatabase } from "../index.js";
export { Monitor } from "./monitors/Monitor.js";
export { MonitorStatuses } from "./monitors/Monitor.js";
export type { IMonitor } from "./monitors/Monitor.js";
export { Check } from "./checks/Check.js";
export type {
ICheck,
ISystemInfo,
ICaptureInfo,
INetInfo,
IDiskInfo,
IHostInfo,
IMemoryInfo,
ICpuInfo,
ILighthouseAudit,
ITimingPhases,
ILighthouseCategories,
ILighthouseResult,
ICheckLighthouseFields,
} from "./checks/Check.js";
export type { IMonitorStats } from "./monitors/MonitorStats.js";
export { MonitorStats } from "./monitors/MonitorStats.js";
export type { INotificationChannel } from "./notification-channel/NotificationChannel.js";
export { NotificationChannel } from "./notification-channel/NotificationChannel.js";
export type { IMaintenance } from "./maintenance/Maintenance.js";
export { Maintenance } from "./maintenance/Maintenance.js";
export type { IInvite } from "./invite/Invite.js";
export { Invite } from "./invite/Invite.js";
+43
View File
@@ -0,0 +1,43 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IInvite extends Document {
_id: Types.ObjectId;
email: string;
tokenHash: string;
roles: Types.ObjectId[];
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
expiry: Date;
createdAt: Date;
updatedAt: Date;
}
const InviteSchema = new Schema<IInvite>(
{
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
},
roles: [
{
type: Schema.Types.ObjectId,
ref: "Role_v2",
required: true,
},
],
tokenHash: { type: String, required: true, unique: true },
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24,
},
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
export const Invite = mongoose.model<IInvite>("Invite_v2", InviteSchema);
@@ -0,0 +1,39 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IMaintenance extends Document {
_id: Types.ObjectId;
name: string;
isActive: boolean;
monitors: Types.ObjectId[];
startTime: Date;
endTime: Date;
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const MaintenanceSchema = new Schema<IMaintenance>(
{
name: { type: String, required: true, trim: true },
isActive: { type: Boolean, required: true, default: true },
monitors: [
{
type: Schema.Types.ObjectId,
ref: "Monitor_v2",
required: true,
},
],
startTime: { type: Date, required: true },
endTime: { type: Date, required: true },
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
MaintenanceSchema.index({ isActive: 1 });
MaintenanceSchema.index({ startTime: 1 });
MaintenanceSchema.index({ endTime: 1 });
export const Maintenance = mongoose.model<IMaintenance>("Maintenance_v2", MaintenanceSchema);
+100
View File
@@ -0,0 +1,100 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import { Check, MonitorStats } from "../index.js";
export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number];
export interface IMonitor extends Document {
_id: Types.ObjectId;
name: string;
url: string;
secret?: string;
type: MonitorType;
interval: number; // in ms
isActive: boolean;
status: MonitorStatus;
n: number; // Number of consecutive successes required to change status
lastCheckedAt?: Date;
latestChecks: {
status: MonitorStatus;
responseTime: number;
checkedAt: Date;
}[];
notificationChannels?: Types.ObjectId[];
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const MonitorSchema = new Schema<IMonitor>(
{
name: { type: String, required: true, trim: true, maxlength: 100 },
url: { type: String, required: true, trim: true },
secret: { type: String, required: false },
type: {
type: String,
required: true,
enum: MonitorTypes,
},
interval: { type: Number, required: true, default: 60000 },
isActive: { type: Boolean, required: true, default: true },
status: {
type: String,
required: true,
default: "initializing",
enum: MonitorStatuses,
},
n: { type: Number, required: true, default: 1 },
lastCheckedAt: { type: Date },
latestChecks: {
type: [
{
status: {
type: String,
required: true,
enum: MonitorStatuses,
},
responseTime: { type: Number, required: true },
checkedAt: { type: Date, required: true },
},
],
default: [],
},
notificationChannels: [
{
type: Schema.Types.ObjectId,
ref: "NotificationChannel_v2",
required: false,
default: [],
},
],
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) {
try {
const monitorId = this._id;
await Check.deleteMany({ monitorId });
await MonitorStats.deleteMany({ monitorId });
next();
} catch (error: any) {
next(error);
}
});
MonitorSchema.index({ isActive: 1 });
MonitorSchema.index({ status: 1 });
MonitorSchema.index({ type: 1 });
MonitorSchema.index({ lastCheckedAt: 1 });
MonitorSchema.index({ isActive: 1, status: 1 });
MonitorSchema.index({ createdBy: 1 });
MonitorSchema.index({ updatedBy: 1 });
export const Monitor = mongoose.model<IMonitor>("Monitor_v2", MonitorSchema);
@@ -0,0 +1,77 @@
import mongoose, { Schema, Document, Types } from "mongoose";
import { MonitorStatus, MonitorStatuses } from "./Monitor.js";
export interface IMonitorStats extends mongoose.Document {
monitorId: mongoose.Types.ObjectId;
avgResponseTime: number;
maxResponseTime: number;
totalChecks: number;
totalUpChecks: number;
totalDownChecks: number;
uptimePercentage: number;
lastCheckTimestamp: number;
lastResponseTime: number;
timeOfLastFailure: number;
currentStreak: number;
currentStreakStatus: MonitorStatus;
currentStreakStartedAt: number;
createdAt: Date;
updatedAt: Date;
}
const MonitorStatsSchema = new Schema<IMonitorStats>(
{
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor_v2",
immutable: true,
index: true,
},
avgResponseTime: {
type: Number,
default: 0,
},
maxResponseTime: {
type: Number,
default: 0,
},
lastResponseTime: {
type: Number,
default: 0,
},
totalChecks: {
type: Number,
default: 0,
},
totalUpChecks: {
type: Number,
default: 0,
},
totalDownChecks: {
type: Number,
default: 0,
},
uptimePercentage: {
type: Number,
default: 0,
},
lastCheckTimestamp: {
type: Number,
default: 0,
},
timeOfLastFailure: {
type: Number,
default: 0,
},
currentStreak: { type: Number, required: false, default: 0 },
currentStreakStatus: {
type: String,
required: false,
enum: MonitorStatuses,
},
currentStreakStartedAt: { type: Number, required: false },
},
{ timestamps: true }
);
export const MonitorStats = mongoose.model<IMonitorStats>("MonitorStats_v2", MonitorStatsSchema);
@@ -0,0 +1,50 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
export type ChannelType = (typeof ChannelTypes)[number];
export interface INotificationChannelConfig {
url?: string; // For webhook, slack, discord
emailAddress?: string; // For email
}
export interface INotificationChannel {
_id: Types.ObjectId;
name: string;
type: ChannelType;
config: INotificationChannelConfig;
isActive: boolean;
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const NotificationChannelConfigSchema = new Schema<INotificationChannelConfig>(
{
url: { type: String, required: false },
emailAddress: { type: String, required: false },
},
{ _id: false, strict: "throw" }
);
const NotificationChannelSchema = new Schema<INotificationChannel>(
{
name: { type: String, required: true, trim: true },
type: {
type: String,
required: true,
enum: ChannelTypes,
},
config: { type: NotificationChannelConfigSchema, required: true },
isActive: { type: Boolean, required: true, default: true },
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
},
{ timestamps: true }
);
NotificationChannelSchema.index({ isActive: 1 });
NotificationChannelSchema.index({ type: 1 });
NotificationChannelSchema.index({ type: 1, isActive: 1 });
export const NotificationChannel = mongoose.model<INotificationChannel>("NotificationChannel_v2", NotificationChannelSchema);