Files
oneuptime/Common/Server/API/IncidentAPI.ts

365 lines
11 KiB
TypeScript

import Incident from "../../Models/DatabaseModels/Incident";
import File from "../../Models/DatabaseModels/File";
import NotFoundException from "../../Types/Exception/NotFoundException";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import IncidentService, {
Service as IncidentServiceType,
} from "../Services/IncidentService";
import UserMiddleware from "../Middleware/UserAuthorization";
import Response from "../Utils/Response";
import BaseAPI from "./BaseAPI";
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from "../Utils/Express";
import CommonAPI from "./CommonAPI";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import AIService, { AILogRequest, AILogResponse } from "../Services/AIService";
import IncidentAIContextBuilder, {
AIGenerationContext,
IncidentContextData,
} from "../Utils/AI/IncidentAIContextBuilder";
import JSONFunctions from "../../Types/JSONFunctions";
import Permission from "../../Types/Permission";
export default class IncidentAPI extends BaseAPI<
Incident,
IncidentServiceType
> {
public constructor() {
super(Incident, IncidentService);
this.router.get(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/postmortem/attachment/:projectId/:incidentId/:fileId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.getPostmortemAttachment(req, res);
} catch (err) {
next(err);
}
},
);
// Generate postmortem from AI
this.router.post(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/generate-postmortem-from-ai/:incidentId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.generatePostmortemFromAI(req, res);
} catch (err) {
next(err);
}
},
);
// Generate note from AI
this.router.post(
`${new this.entityType()
.getCrudApiPath()
?.toString()}/generate-note-from-ai/:incidentId`,
UserMiddleware.getUserMiddleware,
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
await this.generateNoteFromAI(req, res);
} catch (err) {
next(err);
}
},
);
}
private async getPostmortemAttachment(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const projectIdParam: string | undefined = req.params["projectId"];
const incidentIdParam: string | undefined = req.params["incidentId"];
const fileIdParam: string | undefined = req.params["fileId"];
if (!projectIdParam || !incidentIdParam || !fileIdParam) {
throw new NotFoundException("Attachment not found");
}
let incidentId: ObjectID;
let fileId: ObjectID;
let projectId: ObjectID;
try {
incidentId = new ObjectID(incidentIdParam);
fileId = new ObjectID(fileIdParam);
projectId = new ObjectID(projectIdParam);
} catch {
throw new NotFoundException("Attachment not found");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
const incident: Incident | null = await this.service.findOneBy({
query: {
_id: incidentId,
projectId,
},
select: {
postmortemAttachments: {
_id: true,
file: true,
fileType: true,
name: true,
},
},
props,
});
if (!incident) {
throw new NotFoundException("Attachment not found");
}
const attachment: File | undefined = incident.postmortemAttachments?.find(
(file: File) => {
const attachmentId: string | null = file._id
? file._id.toString()
: file.id
? file.id.toString()
: null;
return attachmentId === fileId.toString();
},
);
if (!attachment || !attachment.file) {
throw new NotFoundException("Attachment not found");
}
Response.setNoCacheHeaders(res);
return Response.sendFileResponse(req, res, attachment);
}
private async generatePostmortemFromAI(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const incidentIdParam: string | undefined = req.params["incidentId"];
if (!incidentIdParam) {
throw new BadDataException("Incident ID is required");
}
let incidentId: ObjectID;
try {
incidentId = new ObjectID(incidentIdParam);
} catch {
throw new BadDataException("Invalid Incident ID");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
// Verify user has permission to edit the incident
const permissions: Array<Permission> | undefined = props
.userTenantAccessPermission?.["permissions"] as
| Array<Permission>
| undefined;
const hasPermission: boolean = permissions
? permissions.some((p: Permission) => {
return (
p === Permission.ProjectOwner ||
p === Permission.ProjectAdmin ||
p === Permission.EditProjectIncident
);
})
: false;
if (!hasPermission && !props.isMasterAdmin) {
throw new BadDataException(
"You do not have permission to generate postmortem for this incident. You need to have one of these permissions: Project Owner, Project Admin, Edit Project Incident.",
);
}
// Get the template from request body if provided
const template: string | undefined = JSONFunctions.getJSONValueInPath(
req.body,
"template",
) as string | undefined;
// Always include workspace messages for comprehensive context
const includeWorkspaceMessages: boolean = true;
// Get the incident to verify it exists and get the project ID
const incident: Incident | null = await this.service.findOneById({
id: incidentId,
select: {
_id: true,
projectId: true,
},
props,
});
if (!incident || !incident.projectId) {
throw new NotFoundException("Incident not found");
}
// Build incident context
const contextData: IncidentContextData =
await IncidentAIContextBuilder.buildIncidentContext({
incidentId,
includeWorkspaceMessages,
workspaceMessageLimit: 500,
});
// Format context for postmortem generation
const aiContext: AIGenerationContext =
IncidentAIContextBuilder.formatIncidentContextForPostmortem(
contextData,
template,
);
// Generate postmortem using AIService (handles billing and logging)
const aiLogRequest: AILogRequest = {
projectId: incident.projectId,
feature: "Incident Postmortem",
incidentId: incidentId,
messages: aiContext.messages,
maxTokens: 8192,
temperature: 0.7,
};
if (props.userId) {
aiLogRequest.userId = props.userId;
}
const response: AILogResponse =
await AIService.executeWithLogging(aiLogRequest);
return Response.sendJsonObjectResponse(req, res, {
postmortemNote: response.content,
});
}
private async generateNoteFromAI(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const incidentIdParam: string | undefined = req.params["incidentId"];
if (!incidentIdParam) {
throw new BadDataException("Incident ID is required");
}
let incidentId: ObjectID;
try {
incidentId = new ObjectID(incidentIdParam);
} catch {
throw new BadDataException("Invalid Incident ID");
}
const props: DatabaseCommonInteractionProps =
await CommonAPI.getDatabaseCommonInteractionProps(req);
// Verify user has permission to edit the incident
const permissions: Array<Permission> | undefined = props
.userTenantAccessPermission?.["permissions"] as
| Array<Permission>
| undefined;
const hasPermission: boolean = permissions
? permissions.some((p: Permission) => {
return (
p === Permission.ProjectOwner ||
p === Permission.ProjectAdmin ||
p === Permission.EditProjectIncident ||
p === Permission.CreateIncidentInternalNote ||
p === Permission.CreateIncidentPublicNote
);
})
: false;
if (!hasPermission && !props.isMasterAdmin) {
throw new BadDataException(
"You do not have permission to generate notes for this incident.",
);
}
// Get the template and note type from request body
const template: string | undefined = JSONFunctions.getJSONValueInPath(
req.body,
"template",
) as string | undefined;
const noteType: string =
(JSONFunctions.getJSONValueInPath(req.body, "noteType") as string) ||
"internal";
if (noteType !== "public" && noteType !== "internal") {
throw new BadDataException("Note type must be 'public' or 'internal'");
}
// Always include workspace messages for comprehensive context
const includeWorkspaceMessages: boolean = true;
// Get the incident to verify it exists and get the project ID
const incident: Incident | null = await this.service.findOneById({
id: incidentId,
select: {
_id: true,
projectId: true,
},
props,
});
if (!incident || !incident.projectId) {
throw new NotFoundException("Incident not found");
}
// Build incident context
const contextData: IncidentContextData =
await IncidentAIContextBuilder.buildIncidentContext({
incidentId,
includeWorkspaceMessages,
workspaceMessageLimit: 300,
});
// Format context for note generation
const aiContext: AIGenerationContext =
IncidentAIContextBuilder.formatIncidentContextForNote(
contextData,
noteType as "public" | "internal",
template,
);
// Generate note using AIService (handles billing and logging)
const aiLogRequest: AILogRequest = {
projectId: incident.projectId,
feature:
noteType === "public"
? "Incident Public Note"
: "Incident Internal Note",
incidentId: incidentId,
messages: aiContext.messages,
maxTokens: 4096,
temperature: 0.7,
};
if (props.userId) {
aiLogRequest.userId = props.userId;
}
const response: AILogResponse =
await AIService.executeWithLogging(aiLogRequest);
return Response.sendJsonObjectResponse(req, res, {
note: response.content,
});
}
}