use repos

This commit is contained in:
Alex Holliday committed 2026-01-06 22:53:54 +00:00
1 parent 052b9377bc
commit 61bf2a0dfb
28 files changed
+589 -113

No files matched your search

@@ -1,6 +1,8 @@
import mongoose, { Schema, Document, Types } from "mongoose";
export const MaintenanceRepeats = ["no repeat", "daily", "weekly"] as const;
export type MaintenanceRepeat = (typeof MaintenanceRepeats)[number];
import {
type MaintenanceRepeat,
MaintenanceRepeats,
} from "@/types/domain/index.js";
export interface IMaintenance extends Document {
_id: Types.ObjectId;
orgId: Types.ObjectId;
+3 -2
View File
@@ -1,4 +1,6 @@
import mongoose, { Schema } from "mongoose";
import type { IStatsBase } from "./StatsHourly.js";
import type {
IInfraCpu,
IInfraDiskEntry,
@@ -6,8 +8,7 @@ import type {
IInfraMemory,
IInfraNetEntry,
IPageSpeedMetrics,
IStatsBase,
} from "./StatsHourly.js";
} from "@/types/domain/index.js";
export interface IStatsDaily
extends mongoose.Document,
+9 -69
View File
@@ -1,5 +1,13 @@
import mongoose, { Schema, Types } from "mongoose";
import type { MonitorType } from "@/types/domain/index.js";
import type {
IInfraCpu,
IPageSpeedMetrics,
IInfraMemory,
IInfraDiskEntry,
IInfraHost,
IInfraNetEntry,
MonitorType,
} from "@/types/domain/index.js";
// Common/base metrics across monitor types
export interface IStatsBase {
@@ -19,74 +27,6 @@ export interface IStatsBase {
updatedAt: Date;
}
// Pagespeed rollups (averages per window)
export interface IPageSpeedMetrics {
accessibility?: number;
bestPractices?: number;
seo?: number;
performance?: number;
cls?: number;
si?: number;
fcp?: number;
lcp?: number;
tbt?: number;
}
// Infrastructure rollups
export interface IInfraCpu {
physical_core?: number;
logical_core?: number;
frequency?: number;
current_frequency?: number;
temperature?: number[]; // averaged per sensor index
free_percent?: number;
used_percent?: number;
}
export interface IInfraMemory {
total_bytes?: number;
available_bytes?: number;
used_bytes?: number;
usage_percent?: number;
}
export interface IInfraDiskEntry {
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 IInfraHost {
os?: string;
platform?: string;
kernel_version?: string;
pretty_name?: string;
}
export interface IInfraNetEntry {
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 IStatsHourly
extends mongoose.Document,
IStatsBase,
+14 -2
View File
@@ -38,17 +38,29 @@ import {
MongoChecksRepository,
MongoMonitorStatsRepository,
MongoIncidentsRepository,
MongoUserRepository,
MongoRoleRepository,
MongoTeamRepository,
MongoInviteRepository,
} from "@/repositories/index.js";
import type { CheckEntity } from "@/types/domain/index.js";
export const initServices = async () => {
const monitorRepository = new MongoMonitorRepository();
const checksRepository = new MongoChecksRepository();
const monitorStatsRepository = new MongoMonitorStatsRepository();
const incidentsRepository = new MongoIncidentsRepository();
const userRepository = new MongoUserRepository();
const roleRepository = new MongoRoleRepository();
const teamRepository = new MongoTeamRepository();
const inviteRepository = new MongoInviteRepository();
const checkService = new CheckService(checksRepository, monitorRepository);
const inviteService = new InviteService();
const inviteService = new InviteService(
userRepository,
roleRepository,
teamRepository,
inviteRepository
);
const maintenanceService = new MaintenanceService();
const monitorStatsService = new MonitorStatsService(
monitorRepository,
+15
View File
@@ -12,3 +12,18 @@ export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-s
export type { IIncidentsRepository } from "@/repositories/incidents/IIncidentsRepository.js";
export { default as MongoIncidentsRepository } from "@/repositories/incidents/MongoIncidentsRepository.js";
export type { IUserRepository } from "@/repositories/user/IUserRepository.js";
export { default as MongoUserRepository } from "@/repositories/user/MongoUserRepository.js";
export type { IInviteRepository } from "@/repositories/invites/IInviteRepository.js";
export { default as MongoInviteRepository } from "@/repositories/invites/MongoInviteRepository.js";
export type { IRoleRepository } from "@/repositories/role/IRoleRepository.js";
export { default as MongoRoleRepository } from "@/repositories/role/MongoRoleRepository.js";
export type { ITeamRepository } from "@/repositories/team/ITeamRepository.js";
export { default as MongoTeamRepository } from "@/repositories/team/MongoTeamRepository.js";
export type { ITeamMembershipRepository } from "@/repositories/team-membership/ITeamMembershipRepository.js";
export { default as MongoTeamMembershipRepository } from "@/repositories/team-membership/MongoTeamMembershipRepository.js";
@@ -0,0 +1,14 @@
import { Invite } from "@/types/domain/index.js";
export interface IInviteRepository {
// create
create(inviteData: Partial<Invite>): Promise<Invite | null>;
// single fetch
findByEmail(email: string, teamId: string): Promise<Invite | null>;
findByHash(tokenHash: string): Promise<Invite | null>;
// collection fetch
findAll(): Promise<Invite[]>;
// update
// delete
deleteById(id: string): Promise<boolean>;
}
@@ -0,0 +1,82 @@
import { type IInvite, Invite } from "@/db/models/index.js";
import type { IInviteRepository } from "@/repositories/index.js";
import { Invite as InviteEntity } from "@/types/domain/index.js";
import ApiError from "@/utils/ApiError.js";
class MongoInviteRepository implements IInviteRepository {
private toEntity = (doc: IInvite): InviteEntity => {
return {
id: doc._id.toString(),
orgId: doc.orgId.toString(),
orgRoleId: doc.orgRoleId?.toString(),
teamId: doc.teamId.toString(),
teamRoleId: doc.teamRoleId.toString(),
email: doc.email,
tokenHash: doc.tokenHash,
createdBy: doc.createdBy.toString(),
updatedBy: doc.updatedBy.toString(),
expiry: doc.expiry,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
};
create = async (inviteData: Partial<InviteEntity>) => {
const {
orgId,
orgRoleId,
teamId,
teamRoleId,
email,
tokenHash,
createdBy,
updatedBy,
} = inviteData;
const invite = await Invite.create({
orgId,
...(orgRoleId && { orgRoleId }),
teamId,
teamRoleId,
email,
tokenHash,
createdBy,
updatedBy,
});
if (!invite) {
return null;
}
return this.toEntity(invite);
};
findByEmail = async (email: string, teamId: string) => {
const invite = await Invite.findOne({
email,
teamId,
});
if (!invite) {
return null;
}
return this.toEntity(invite);
};
findByHash = async (tokenHash: string) => {
const invite = await Invite.findOne({ tokenHash });
if (!invite) return null;
return this.toEntity(invite);
};
findAll = async () => {
const invites = await Invite.find();
return invites.map(this.toEntity);
};
deleteById = async (id: string) => {
const result = await Invite.deleteOne({ _id: id });
if (!result.deletedCount) {
throw new ApiError("Invite not found", 404);
}
return result.deletedCount === 1;
};
}
export default MongoInviteRepository;
@@ -0,0 +1,10 @@
import { Role } from "@/types/domain/index.js";
export interface IRoleRepository {
// create
// single fetch
findById(roleId: string, orgId: string): Promise<Role | null>;
// collection fetch
// update
// delete
}
@@ -0,0 +1,29 @@
import type { IRoleRepository } from "./IRoleRepository.js";
import { Role } from "@/db/models/index.js";
import type { IRole } from "@/db/models/index.js";
import { Role as RoleEntity } from "@/types/domain/index.js";
class MongoRoleRepository implements IRoleRepository {
private toEntity = (doc: IRole): RoleEntity => {
return {
id: doc._id.toString(),
organizationId: doc.organizationId.toString(),
name: doc.name,
scope: doc.scope,
permissions: doc.permissions,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
};
findById = async (roleId: string, orgId: string) => {
const role = await Role.findOne({
_id: roleId,
organizationId: orgId,
});
if (!role) {
return null;
}
return this.toEntity(role);
};
}
export default MongoRoleRepository;
@@ -0,0 +1,9 @@
import { TeamMembership } from "@/types/domain/index.js";
export interface ITeamMembershipRepository {
// create
// single fetch
findByUserId(userId: string, teamId: string): Promise<TeamMembership | null>;
// collection fetch
// update
// delete
}
@@ -0,0 +1,26 @@
import { type ITeamMembership, TeamMembership } from "@/db/models/index.js";
import type { ITeamMembershipRepository } from "@/repositories/index.js";
import { TeamMembership as TeamMembershipEntity } from "@/types/domain/index.js";
class MongoTeamMembershipRepository implements ITeamMembershipRepository {
private toEntity = (doc: ITeamMembership): TeamMembershipEntity => {
return {
id: doc._id.toString(),
orgId: doc.orgId.toString(),
teamId: doc.teamId.toString(),
userId: doc.userId.toString(),
roleId: doc.roleId.toString(),
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
};
findByUserId = async (userId: string, teamId: string) => {
const membership = await TeamMembership.findOne({
userId,
teamId,
});
if (!membership) return null;
return this.toEntity(membership);
};
}
export default MongoTeamMembershipRepository;
@@ -0,0 +1,10 @@
import { Team } from "@/types/domain/index.js";
export interface ITeamRepository {
// create
// single fetch
findById(teamId: string, orgId: string): Promise<Team | null>;
// collection fetch
// update
// delete
}
@@ -0,0 +1,29 @@
import type { ITeamRepository } from "@/repositories/index.js";
import { type ITeam, Team } from "@/db/models/index.js";
import { Team as TeamEntity } from "@/types/domain/index.js";
class MongoTeamRepository implements ITeamRepository {
private toEntity = (doc: ITeam): TeamEntity => {
return {
id: doc._id.toString(),
orgId: doc.orgId.toString(),
name: doc.name,
description: doc.description,
isSystem: doc.isSystem,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
};
findById = async (teamId: string, orgId: string) => {
const team = await Team.findOne({
_id: teamId,
orgId,
});
if (!team) {
return null;
}
return this.toEntity(team);
};
}
export default MongoTeamRepository;
@@ -0,0 +1,9 @@
import type { User } from "@/types/domain/index.js";
export interface IUserRepository {
// create
// single fetch
findByEmail: (email: string) => Promise<User | null>;
// collection fetch
// update
// delete
}
@@ -0,0 +1,27 @@
import type { User as UserEntity } from "@/types/domain/index.js";
import { IUserRepository } from "@/repositories/index.js";
import { User } from "@/db/models/index.js";
import type { IUser } from "@/db/models/index.js";
class MongoUserRepository implements IUserRepository {
private toEntity = (user: IUser): UserEntity => {
return {
id: user._id.toString(),
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
passwordHash: user.passwordHash,
lastLoginAt: user.lastLoginAt,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
};
findByEmail = async (email: string) => {
const user = await User.findOne({ email });
if (!user) return null;
return this.toEntity(user);
};
}
export default MongoUserRepository;
@@ -1,4 +1,3 @@
import { Incident, Monitor } from "@/db/models/index.js";
import type { ResolutionType } from "@/types/domain/index.js";
import { getChildLogger } from "@/logger/Logger.js";
import { ThresholdEvaluationResult } from "@/services/infrastructure/StatusService.js";
+46 -37
View File
@@ -1,14 +1,14 @@
import crypto from "node:crypto";
import {
IInvite,
Invite,
User,
IUser,
Role,
Team,
TeamMembership,
} from "@/db/models/index.js";
import ApiError from "@/utils/ApiError.js";
import type {
IUserRepository,
IRoleRepository,
ITeamRepository,
IInviteRepository,
ITeamMembershipRepository,
} from "@/repositories/index.js";
import { User } from "@/types/domain/index.js";
import { Invite } from "@/types/domain/index.js";
const SERVICE_NAME = "InviteService";
export interface IInviteService {
@@ -20,15 +20,31 @@ export interface IInviteService {
teamId: string,
teamRoleId: string
) => Promise<string>;
getAll: () => Promise<IInvite[]>;
get: (tokenHash: string) => Promise<{ user: IUser | null; invite: IInvite }>;
getAll: () => Promise<Invite[]>;
get: (tokenHash: string) => Promise<{ user: User | null; invite: Invite }>;
delete: (id: string) => Promise<boolean>;
}
class InviteService implements IInviteService {
public SERVICE_NAME: string;
constructor() {
private userRepository: IUserRepository;
private roleRepository: IRoleRepository;
private teamRepository: ITeamRepository;
private inviteRepository: IInviteRepository;
private teamMembershipRepository: ITeamMembershipRepository;
constructor(
userRepository: IUserRepository,
roleRepository: IRoleRepository,
teamRepository: ITeamRepository,
inviteRepository: IInviteRepository,
teamMembershipRepository: ITeamMembershipRepository
) {
this.SERVICE_NAME = SERVICE_NAME;
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.teamRepository = teamRepository;
this.inviteRepository = inviteRepository;
this.teamMembershipRepository = teamMembershipRepository;
}
create = async (
@@ -40,40 +56,36 @@ class InviteService implements IInviteService {
teamRoleId: string
) => {
try {
const role = await Role.findOne({
_id: teamRoleId,
organizationId: orgId,
});
const role = await this.roleRepository.findById(teamRoleId, orgId);
if (!role) {
throw new ApiError("Role not found", 404);
}
const team = await Team.findOne({
_id: teamId,
orgId,
});
const team = await this.teamRepository.findById(teamId, orgId);
if (!team) {
throw new ApiError("Team not found", 404);
}
// Check if already a team member
const user = await User.findOne({ email });
const existingMembership = await TeamMembership.findOne({
userId: user?._id,
teamId,
});
const user = await this.userRepository.findByEmail(email);
const existingMembership =
await this.teamMembershipRepository.findByUserId(
user?.id || "",
teamId
);
if (existingMembership) {
throw new ApiError("User is already a team member", 409);
}
// Check if inivite already exists
const existingInvite = await Invite.findOne({
const existingInvite = await this.inviteRepository.findByEmail(
email,
teamId,
});
teamId
);
if (existingInvite) {
throw new ApiError("An invite already exists for this email", 409);
@@ -82,7 +94,7 @@ class InviteService implements IInviteService {
const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
const invite = await Invite.create({
const invite = await this.inviteRepository.create({
orgId,
...(orgRoleId && { orgRoleId }),
teamId,
@@ -104,24 +116,21 @@ class InviteService implements IInviteService {
get = async (token: string) => {
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
const invite = await Invite.findOne({ tokenHash });
const invite = await this.inviteRepository.findByHash(tokenHash);
if (!invite) {
throw new ApiError("Invite not found", 404);
}
const user = await User.findOne({ email: invite.email });
const user = await this.userRepository.findByEmail(invite.email);
return { user, invite };
};
getAll = async () => {
return Invite.find();
return await this.inviteRepository.findAll();
};
delete = async (id: string) => {
const result = await Invite.deleteOne({ _id: id });
if (!result.deletedCount) {
throw new ApiError("Invite not found", 404);
}
return result.deletedCount === 1;
return await this.inviteRepository.deleteById(id);
};
}
+9
View File
@@ -2,3 +2,12 @@ export * from "@/types/domain/monitor.js";
export * from "@/types/domain/check.js";
export * from "@/types/domain/monitorStats.js";
export * from "@/types/domain/incident.js";
export * from "@/types/domain/user.js";
export * from "@/types/domain/role.js";
export * from "@/types/domain/team.js";
export * from "@/types/domain/teamMembership.js";
export * from "@/types/domain/invite.js";
export * from "@/types/domain/org.js";
export * from "@/types/domain/orgMembership.js";
export * from "@/types/domain/maintenance.js";
export * from "@/types/domain/statsHourly.js";
+14
View File
@@ -0,0 +1,14 @@
export interface Invite {
id: string;
orgId: string;
orgRoleId?: string;
teamId: string;
teamRoleId: string;
email: string;
tokenHash: string;
createdBy: string;
updatedBy: string;
expiry: Date;
createdAt: Date;
updatedAt: Date;
}
+18
View File
@@ -0,0 +1,18 @@
export const MaintenanceRepeats = ["no repeat", "daily", "weekly"] as const;
export type MaintenanceRepeat = (typeof MaintenanceRepeats)[number];
export interface Maintenance {
id: string;
orgId: string;
teamId: string;
name: string;
isActive: boolean;
repeat: MaintenanceRepeat;
monitors: string[];
startTime: Date;
endTime: Date;
createdBy: string;
updatedBy: string;
createdAt: Date;
updatedAt: Date;
}
+39
View File
@@ -0,0 +1,39 @@
import { PlanKey } from "@/types/entitlements.js";
export type SubscriptionStatus =
| "active"
| "trialing"
| "past_due"
| "canceled"
| "incomplete";
export interface Org {
id: string;
name: string;
ownerId: string;
// Entitlements & plan
planKey: "free" | "pro" | "business" | "enterprise";
// Billing
billingCustomerId?: string;
subscriptionId?: string;
priceId: string;
priceIds?: string[];
subscriptionStatus?: SubscriptionStatus;
currentPeriodStart?: Date;
currentPeriodEnd?: Date;
// Cached entitlements
entitlements: {
plan: PlanKey;
monitorsMax: number;
notificationChannelsMax: number;
statusPagesMax: number;
checksIntervalMsMin: number;
teamsMax: number;
notificationsEnabled: boolean;
retentionDays: number;
};
createdAt: Date;
updatedAt: Date;
}
+9
View File
@@ -0,0 +1,9 @@
export interface OrgMembership extends Document {
id: string;
orgId: string;
userId: string;
roleId?: string;
role: string;
createdAt: Date;
updatedAt: Date;
}
+12
View File
@@ -0,0 +1,12 @@
export const ScopeTypes = ["organization", "team"] as const;
export type ScopeType = (typeof ScopeTypes)[number];
export interface Role {
id: string;
organizationId: string;
name: string;
scope: ScopeType;
permissions: string[];
createdAt: Date;
updatedAt: Date;
}
+24
View File
@@ -0,0 +1,24 @@
import type { IStatsBase } from "@/types/domain/index.js";
import type {
IInfraCpu,
IInfraDiskEntry,
IInfraHost,
IInfraMemory,
IInfraNetEntry,
IPageSpeedMetrics,
} from "@/types/domain/index.js";
export interface IStatsDaily extends IStatsBase, IPageSpeedMetrics {
cpu?: IInfraCpu;
memory?: IInfraMemory;
disk?: IInfraDiskEntry[];
host?: IInfraHost;
net?: IInfraNetEntry[];
// Docker rollups
dockerRunningPercent?: number;
dockerHealthyPercent?: number;
dockerRunningContainers?: number;
dockerHealthyContainers?: number;
dockerTotalContainers?: number;
}
+100
View File
@@ -0,0 +1,100 @@
import type { MonitorType } from "@/types/domain/index.js";
// Common/base metrics across monitor types
export interface IStatsBase {
monitorId: string;
teamId: string;
type: MonitorType;
windowStart: Date; // inclusive
windowEnd: Date; // exclusive
finalized: boolean;
count: number; // number of checks in window
avgResponseTime: number; // ms
upChecks?: number;
downChecks?: number;
avgResponseTimeUp?: number;
avgResponseTimeDown?: number;
createdAt: Date;
updatedAt: Date;
}
// Pagespeed rollups (averages per window)
export interface IPageSpeedMetrics {
accessibility?: number;
bestPractices?: number;
seo?: number;
performance?: number;
cls?: number;
si?: number;
fcp?: number;
lcp?: number;
tbt?: number;
}
// Infrastructure rollups
export interface IInfraCpu {
physical_core?: number;
logical_core?: number;
frequency?: number;
current_frequency?: number;
temperature?: number[]; // averaged per sensor index
free_percent?: number;
used_percent?: number;
}
export interface IInfraMemory {
total_bytes?: number;
available_bytes?: number;
used_bytes?: number;
usage_percent?: number;
}
export interface IInfraDiskEntry {
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 IInfraHost {
os?: string;
platform?: string;
kernel_version?: string;
pretty_name?: string;
}
export interface IInfraNetEntry {
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 IStatsHourly extends IStatsBase, IPageSpeedMetrics {
cpu?: IInfraCpu;
memory?: IInfraMemory;
disk?: IInfraDiskEntry[];
host?: IInfraHost;
net?: IInfraNetEntry[];
dockerRunningPercent?: number;
dockerHealthyPercent?: number;
dockerRunningContainers?: number;
dockerHealthyContainers?: number;
dockerTotalContainers?: number;
}
+9
View File
@@ -0,0 +1,9 @@
export interface Team {
id: string;
orgId: string;
name: string;
description?: string;
isSystem?: boolean;
createdAt: Date;
updatedAt: Date;
}
@@ -0,0 +1,9 @@
export interface TeamMembership {
id: string;
orgId: string;
teamId: string;
userId: string;
roleId: string;
createdAt: Date;
updatedAt: Date;
}
+10
View File
@@ -0,0 +1,10 @@
export interface User {
id: string;
firstName: string;
lastName: string;
email: string;
passwordHash: string;
lastLoginAt?: Date;
createdAt: Date;
updatedAt: Date;
}