Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Db, ObjectId } from "mongodb";

const HEX24 = /^[0-9a-fA-F]{24}$/;

export async function up(db: Db) {
try {
const historiesToChange = await db
.collection("histories")
.find({ user: { $type: "string", $regex: HEX24 } })
.toArray();

if (historiesToChange.length === 0) {
console.log("No histories with string users to change.");
} else {
const bulkOps = historiesToChange.map((history) => ({
updateOne: {
filter: { _id: history._id },
update: { $set: { user: new ObjectId(history.user as string) } },
},
}));

const result = await db.collection("histories").bulkWrite(bulkOps);
console.log(`Converted ${result.modifiedCount} history.user fields to ObjectId.`);
}
} catch (error) {
console.error("Migration UP failed:", error);
throw error;
}
}

export async function down(db: Db) {
try {
const historiesToRedefine = await db
.collection("histories")
.find({ user: { $type: "objectId" } })
.toArray();

if (historiesToRedefine.length === 0) return;

const bulkOps = historiesToRedefine.map((history) => {
const userIdString = history.user ? String(history.user) : null;

return {
updateOne: {
filter: { _id: history._id },
update: { $set: { user: userIdString } },
},
};
}).filter(op => op.updateOne.update.$set.user !== null);

const result = await db.collection("histories").bulkWrite(bulkOps);
console.log(`Reverted ${result.modifiedCount} fields back to strings.`);

} catch (error) {
console.error("Migration DOWN failed:", error);
throw error;
}
}
4 changes: 3 additions & 1 deletion public/locales/en/history.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"anonymousUserName": "Anonymous",
"automatedMonitoring": "Automated Monitoring",
"virtualAssistant": "Virtual Assistant",
"create": "created",
"update": "updated",
"delete": "deleted",
Expand All @@ -8,4 +10,4 @@
"personality": "profile",
"claim": "claim",
"historyItem": "{{username}} {{type}} {{title}} {{targetModel}}"
}
}
4 changes: 3 additions & 1 deletion public/locales/pt/history.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"anonymousUserName": "Anônimo",
"automatedMonitoring": "Monitoramento Automatizado",
"virtualAssistant": "Assistente Virtual",
"create": "criou",
"update": "atualizou",
"delete": "deletou",
Expand All @@ -8,4 +10,4 @@
"personality": "o perfil de",
"claim": "a afirmação",
"historyItem": "{{username}} {{type}} {{targetModel}} {{title}}"
}
}
50 changes: 27 additions & 23 deletions server/history/history.controller.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
import { Controller, Get, Logger, Param, Query } from "@nestjs/common";
import { HistoryService } from "./history.service";
import { ApiTags } from "@nestjs/swagger";
import type {
HistoryParams,
HistoryQuery,
HistoryResponse,
} from "./types/history.interfaces";

@Controller()
@ApiTags("history")
@Controller("api/history")
export class HistoryController {
private readonly logger = new Logger("HistoryController");
constructor(private historyService: HistoryService) {}
private readonly logger = new Logger(HistoryController.name);
constructor(private historyService: HistoryService) { }

@ApiTags("history")
@Get("api/history/:targetModel/:targetId")
public async getHistory(@Param() param, @Query() getHistory: any) {
@Get(":targetModel/:targetId")
public async getHistory(
@Param() param: HistoryParams,
@Query() query: HistoryQuery
): Promise<HistoryResponse> {
const { targetId, targetModel } = param;
const { page, order } = getHistory;
const pageSize = parseInt(getHistory.pageSize, 10);
return this.historyService
.getByTargetIdModelAndType(
try {
const response = await this.historyService.getHistoryForTarget(
targetId,
targetModel,
page,
pageSize,
order
)
.then((history) => {
const totalChanges = history.length;
const totalPages = Math.ceil(totalChanges / pageSize);
this.logger.log(
`Found ${totalChanges} changes for targetId ${targetId}. Page ${page} of ${totalPages}`
);
return { history, totalChanges, totalPages, page, pageSize };
})
.catch();
query
);
this.logger.log(
`Found ${response.totalChanges} changes for targetId ${targetId}. Page ${response.page} of ${response.totalPages}`
);

return response;
} catch (err) {
this.logger.error(`Error fetching history: ${err.message}`);
throw err;
}
}
}
160 changes: 106 additions & 54 deletions server/history/history.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { Injectable, Logger } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model, Types } from "mongoose";
import { Model, Types, isValidObjectId } from "mongoose";
import { History, HistoryDocument, HistoryType } from "./schema/history.schema";
import {
HistoryItem,
HistoryQuery,
HistoryResponse,
} from "./types/history.interfaces";

@Injectable()
export class HistoryService {
Expand All @@ -22,25 +27,42 @@ export class HistoryService {
* This function return an history object.
* @param dataId Target Id.
* @param targetModel The model of the target(claim or personality ).
* @param user User who made the change.
* @param performedBy The actor who performed the change. Can be:
* - a intern user object with _id field
* - a string representing chatbot ID
* - a Machine-to-Machine (M2M) object
* - null if unknown or invalid
* @param type Type of the change(create, personality or delete).
* @param latestChange Model latest change .
* @param previousChange Model previous change.
* @returns Returns an object with de params necessary to create an history.
*/
getHistoryParams(
dataId,
targetModel,
user,
type,
latestChange,
dataId: string,
targetModel: string,
performedBy: any,
type: string,
latestChange: any,
previousChange = null
) {
const date = new Date();
const targetId = Types.ObjectId(dataId);
let currentPerformedBy = null;

if (performedBy?._id) {
currentPerformedBy = Types.ObjectId(performedBy?._id);
} else {
currentPerformedBy = performedBy;
}

if (!isValidObjectId(dataId)) {
throw new Error(`Invalid dataId received: ${dataId}`);
}

return {
targetId: Types.ObjectId(dataId),
targetId: targetId,
targetModel,
user: user?._id || user || null, //I need to make it optional because we still need to do M2M for chatbot
user: currentPerformedBy,
type,
details: {
after: latestChange,
Expand All @@ -60,57 +82,87 @@ export class HistoryService {
return newHistory.save();
}

/**
* This function queries the database for the history of changes on a target.
* @param targetId The id of the target.
* @param targetModel The model of the target (claim or personality).
* @param page The page of results, used in combination with pageSize to paginate results.
* @param pageSize How many results per page.
* @param order asc or desc.
* @returns The paginated history of a target.
*/
async getByTargetIdModelAndType(
targetId,
targetModel,
page,
pageSize,
order = "asc",
type = ""
) {
let query;
if (type) {
query = {
targetId: Types.ObjectId(targetId),
targetModel,
type,
};
} else {
query = {
targetId: Types.ObjectId(targetId),
targetModel,
};
}
return this.HistoryModel.find(query)
.populate("user", "_id name")
.skip(page * pageSize)
.limit(pageSize)
.sort({ date: order });
async getHistoryForTarget(
targetId: string,
targetModel: string,
query: HistoryQuery
): Promise<HistoryResponse> {
const page = Math.max(Number(query.page) || 0, 0);
const pageSize = Math.max(Number(query.pageSize) || 10, 1);
const order = query.order === "desc" ? -1 : 1;
const type = query.type || "";

const mongoQuery: HistoryItem = {
targetId: Types.ObjectId(targetId),
targetModel,
};
if (type) mongoQuery.type = type;

const result = await this.HistoryModel.aggregate([
{ $match: mongoQuery },
{
$facet: {
data: [
{ $sort: { date: order } },
{ $skip: page * pageSize },
{ $limit: pageSize },
...this.getUserLookupStages(),
],
totalCount: [{ $count: "total" }],
},
},
]);

const totalChanges = result[0]?.totalCount[0]?.total || 0;

return {
history: result[0]?.data || [],
totalChanges,
totalPages: Math.ceil(totalChanges / pageSize),
page,
pageSize,
};
}

private getUserLookupStages() {
return [
{
$lookup: {
from: "users",
let: { userId: "$user" },
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$userId"] } } },
{ $project: { _id: 1, name: 1 } },
],
as: "userLookup",
},
},
{
$addFields: {
user: {
$cond: [
{ $gt: [{ $size: "$userLookup" }, 0] },
{ $arrayElemAt: ["$userLookup", 0] },
"$user",
],
},
},
},
{ $project: { userLookup: 0 } },
];
}

async getDescriptionForHide(content, target) {
async getDescriptionForHide(content: any, target: string) {
if (content?.isHidden) {
const history = await this.getByTargetIdModelAndType(
content._id,
target,
0,
1,
"desc",
HistoryType.Hide
);
const { history } = await this.getHistoryForTarget(content._id, target, {
page: 0,
pageSize: 1,
order: "desc",
type: HistoryType.Hide,
});

return history[0]?.details?.after?.description;
}

return "";
}
}
6 changes: 3 additions & 3 deletions server/history/schema/history.schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import * as mongoose from "mongoose";
import { User } from "../../users/schemas/user.schema";
import { M2M } from "../../entities/m2m.entity";

export type HistoryDocument = History & mongoose.Document;

Expand Down Expand Up @@ -47,11 +48,10 @@ export class History {
targetModel: TargetModel;

@Prop({
type: mongoose.Types.ObjectId,
type: mongoose.Schema.Types.Mixed,
required: false,
ref: "User",
})
user: User;
user: User | M2M | string;

@Prop({
required: true,
Expand Down
Loading
Loading