import PostgresDatabase from '../Infrastructure/PostgresDatabase'; import Model from 'Model/Models/ScheduledMaintenance'; import DatabaseService from './DatabaseService'; import { OnCreate, OnDelete, OnUpdate } from '../Types/Database/Hooks'; import ObjectID from 'Common/Types/ObjectID'; import Monitor from 'Model/Models/Monitor'; import MonitorService from './MonitorService'; import DatabaseCommonInteractionProps from 'Common/Types/BaseDatabase/DatabaseCommonInteractionProps'; import ScheduledMaintenanceStateTimeline from 'Model/Models/ScheduledMaintenanceStateTimeline'; import ScheduledMaintenanceStateTimelineService from './ScheduledMaintenanceStateTimelineService'; import CreateBy from '../Types/Database/CreateBy'; import BadDataException from 'Common/Types/Exception/BadDataException'; import ScheduledMaintenanceState from 'Model/Models/ScheduledMaintenanceState'; import ScheduledMaintenanceStateService from './ScheduledMaintenanceStateService'; import LIMIT_MAX, { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax'; import ScheduledMaintenanceOwnerUserService from './ScheduledMaintenanceOwnerUserService'; import ScheduledMaintenanceOwnerUser from 'Model/Models/ScheduledMaintenanceOwnerUser'; import Typeof from 'Common/Types/Typeof'; import ScheduledMaintenanceOwnerTeamService from './ScheduledMaintenanceOwnerTeamService'; import ScheduledMaintenanceOwnerTeam from 'Model/Models/ScheduledMaintenanceOwnerTeam'; import TeamMemberService from './TeamMemberService'; import User from 'Model/Models/User'; import URL from 'Common/Types/API/URL'; import SortOrder from 'Common/Types/BaseDatabase/SortOrder'; import DatabaseConfig from '../DatabaseConfig'; import DeleteBy from '../Types/Database/DeleteBy'; export class Service extends DatabaseService { public constructor(postgresDatabase?: PostgresDatabase) { super(Model, postgresDatabase); this.hardDeleteItemsOlderThanInDays('createdAt', 120); } protected override async onBeforeDelete( deleteBy: DeleteBy ): Promise> { const scheduledMaintenanceEvents: Array = await this.findBy({ query: deleteBy.query, limit: LIMIT_MAX, skip: 0, select: { _id: true, projectId: true, monitors: { _id: true, }, }, props: { isRoot: true, }, }); return { carryForward: { scheduledMaintenanceEvents: scheduledMaintenanceEvents, }, deleteBy: deleteBy, }; } protected override async onDeleteSuccess( onDelete: OnDelete, _deletedItemIds: ObjectID[] ): Promise> { if (onDelete.carryForward?.scheduledMaintenanceEvents) { for (const scheduledMaintenanceEvent of onDelete?.carryForward ?.scheduledMaintenanceEvents || []) { await ScheduledMaintenanceStateTimelineService.enableActiveMonitoringForMonitors( scheduledMaintenanceEvent ); } } return onDelete; } protected override async onBeforeCreate( createBy: CreateBy ): Promise> { if (!createBy.props.tenantId) { throw new BadDataException('ProjectId required to create monitor.'); } const scheduledMaintenanceState: ScheduledMaintenanceState | null = await ScheduledMaintenanceStateService.findOneBy({ query: { projectId: createBy.props.tenantId, isScheduledState: true, }, select: { _id: true, }, props: { isRoot: true, }, }); if (!scheduledMaintenanceState || !scheduledMaintenanceState.id) { throw new BadDataException( 'Scheduled state not found for this project. Please add an scheduled event state from settings.' ); } createBy.data.currentScheduledMaintenanceStateId = scheduledMaintenanceState.id; return { createBy, carryForward: null }; } protected override async onCreateSuccess( onCreate: OnCreate, createdItem: Model ): Promise { // create new scheduled maintenance state timeline. const timeline: ScheduledMaintenanceStateTimeline = new ScheduledMaintenanceStateTimeline(); timeline.projectId = createdItem.projectId!; timeline.scheduledMaintenanceId = createdItem.id!; timeline.isOwnerNotified = true; // ignore notifying owners because you already notify for Scheduled Event, you don't have to notify them for timeline event. timeline.shouldStatusPageSubscribersBeNotified = Boolean( createdItem.shouldStatusPageSubscribersBeNotifiedOnEventCreated ); timeline.isStatusPageSubscribersNotified = Boolean( createdItem.shouldStatusPageSubscribersBeNotifiedOnEventCreated ); // ignore notifying subscribers because you already notify for Scheduled Event, you don't have to notify them for timeline event. timeline.scheduledMaintenanceStateId = createdItem.currentScheduledMaintenanceStateId!; await ScheduledMaintenanceStateTimelineService.create({ data: timeline, props: { isRoot: true, }, }); if ( createdItem.projectId && createdItem.id && onCreate.createBy.miscDataProps && (onCreate.createBy.miscDataProps['ownerTeams'] || onCreate.createBy.miscDataProps['ownerUsers']) ) { await this.addOwners( createdItem.projectId!, createdItem.id!, (onCreate.createBy.miscDataProps[ 'ownerUsers' ] as Array) || [], (onCreate.createBy.miscDataProps[ 'ownerTeams' ] as Array) || [], false, onCreate.createBy.props ); } return createdItem; } public async addOwners( projectId: ObjectID, scheduledMaintenanceId: ObjectID, userIds: Array, teamIds: Array, notifyOwners: boolean, props: DatabaseCommonInteractionProps ): Promise { for (let teamId of teamIds) { if (typeof teamId === Typeof.String) { teamId = new ObjectID(teamId.toString()); } const teamOwner: ScheduledMaintenanceOwnerTeam = new ScheduledMaintenanceOwnerTeam(); teamOwner.scheduledMaintenanceId = scheduledMaintenanceId; teamOwner.projectId = projectId; teamOwner.teamId = teamId; teamOwner.isOwnerNotified = !notifyOwners; await ScheduledMaintenanceOwnerTeamService.create({ data: teamOwner, props: props, }); } for (let userId of userIds) { if (typeof userId === Typeof.String) { userId = new ObjectID(userId.toString()); } const teamOwner: ScheduledMaintenanceOwnerUser = new ScheduledMaintenanceOwnerUser(); teamOwner.scheduledMaintenanceId = scheduledMaintenanceId; teamOwner.projectId = projectId; teamOwner.isOwnerNotified = !notifyOwners; teamOwner.userId = userId; await ScheduledMaintenanceOwnerUserService.create({ data: teamOwner, props: props, }); } } public async getScheduledMaintenanceLinkInDashboard( projectId: ObjectID, scheduledMaintenanceId: ObjectID ): Promise { const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl(); return URL.fromString(dashboardUrl.toString()).addRoute( `/${projectId.toString()}/scheduled-maintenance-events/${scheduledMaintenanceId.toString()}` ); } public async findOwners( scheduledMaintenanceId: ObjectID ): Promise> { if (!scheduledMaintenanceId) { throw new BadDataException('scheduledMaintenanceId is required'); } const ownerUsers: Array = await ScheduledMaintenanceOwnerUserService.findBy({ query: { scheduledMaintenanceId: scheduledMaintenanceId, }, select: { _id: true, user: { _id: true, email: true, name: true, }, }, props: { isRoot: true, }, limit: LIMIT_PER_PROJECT, skip: 0, }); const ownerTeams: Array = await ScheduledMaintenanceOwnerTeamService.findBy({ query: { scheduledMaintenanceId: scheduledMaintenanceId, }, select: { _id: true, teamId: true, }, skip: 0, limit: LIMIT_PER_PROJECT, props: { isRoot: true, }, }); const users: Array = ownerUsers.map((ownerUser: ScheduledMaintenanceOwnerUser) => { return ownerUser.user!; }) || []; if (ownerTeams.length > 0) { const teamIds: Array = ownerTeams.map((ownerTeam: ScheduledMaintenanceOwnerTeam) => { return ownerTeam.teamId!; }) || []; const teamUsers: Array = await TeamMemberService.getUsersInTeams(teamIds); for (const teamUser of teamUsers) { //check if the user is already added. const isUserAlreadyAdded: User | undefined = users.find( (user: User) => { return user.id!.toString() === teamUser.id!.toString(); } ); if (!isUserAlreadyAdded) { users.push(teamUser); } } } return users; } public async changeAttachedMonitorStates( item: Model, props: DatabaseCommonInteractionProps ): Promise { if (!item.projectId) { throw new BadDataException('projectId is required'); } if (!item.id) { throw new BadDataException('id is required'); } if (item.changeMonitorStatusToId && item.projectId) { // change status of all the monitors. await MonitorService.changeMonitorStatus( item.projectId, item.monitors?.map((monitor: Monitor) => { return new ObjectID(monitor._id || ''); }) || [], item.changeMonitorStatusToId, true, // notify owners 'Changed because of scheduled maintenance event: ' + item.id.toString(), undefined, props ); } } protected override async onUpdateSuccess( onUpdate: OnUpdate, updatedItemIds: ObjectID[] ): Promise> { if ( onUpdate.updateBy.data.currentScheduledMaintenanceStateId && onUpdate.updateBy.props.tenantId ) { for (const itemId of updatedItemIds) { await this.changeScheduledMaintenanceState({ projectId: onUpdate.updateBy.props.tenantId as ObjectID, scheduledMaintenanceId: itemId, scheduledMaintenanceStateId: onUpdate.updateBy.data .currentScheduledMaintenanceStateId as ObjectID, shouldNotifyStatusPageSubscribers: true, isSubscribersNotified: false, notifyOwners: true, // notifyOwners = true props: { isRoot: true, }, }); } } return onUpdate; } public async changeScheduledMaintenanceState(data: { projectId: ObjectID; scheduledMaintenanceId: ObjectID; scheduledMaintenanceStateId: ObjectID; shouldNotifyStatusPageSubscribers: boolean; isSubscribersNotified: boolean; notifyOwners: boolean; props: DatabaseCommonInteractionProps; }): Promise { const { projectId, scheduledMaintenanceId, scheduledMaintenanceStateId, notifyOwners, shouldNotifyStatusPageSubscribers, isSubscribersNotified, props, } = data; if (!projectId) { throw new BadDataException('projectId is required'); } if (!scheduledMaintenanceId) { throw new BadDataException('scheduledMaintenanceId is required'); } if (!scheduledMaintenanceStateId) { throw new BadDataException( 'scheduledMaintenanceStateId is required' ); } // get last scheduled status timeline. const lastState: ScheduledMaintenanceStateTimeline | null = await ScheduledMaintenanceStateTimelineService.findOneBy({ query: { scheduledMaintenanceId: scheduledMaintenanceId, projectId: projectId, }, select: { _id: true, scheduledMaintenanceStateId: true, }, sort: { createdAt: SortOrder.Descending, }, props: { isRoot: true, }, }); if ( lastState && lastState.scheduledMaintenanceStateId && lastState.scheduledMaintenanceStateId.toString() === scheduledMaintenanceStateId.toString() ) { return; } const statusTimeline: ScheduledMaintenanceStateTimeline = new ScheduledMaintenanceStateTimeline(); statusTimeline.scheduledMaintenanceId = scheduledMaintenanceId; statusTimeline.scheduledMaintenanceStateId = scheduledMaintenanceStateId; statusTimeline.projectId = projectId; statusTimeline.isOwnerNotified = !notifyOwners; statusTimeline.isStatusPageSubscribersNotified = isSubscribersNotified; statusTimeline.shouldStatusPageSubscribersBeNotified = shouldNotifyStatusPageSubscribers; await ScheduledMaintenanceStateTimelineService.create({ data: statusTimeline, props: props, }); await this.updateBy({ data: { currentScheduledMaintenanceStateId: scheduledMaintenanceStateId.id, }, skip: 0, limit: LIMIT_PER_PROJECT, query: { _id: scheduledMaintenanceId.toString()!, }, props: { isRoot: true, }, }); } } export default new Service();