Skip to content

Commit 40eb783

Browse files
authored
feat: new context on from/to erep (#601)
* feat: new context on from/to erep * feat: clamp + page limit
1 parent 8e18a26 commit 40eb783

File tree

4 files changed

+147
-2
lines changed

4 files changed

+147
-2
lines changed

platforms/eReputation-api/src/controllers/ReferenceController.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,112 @@ export class ReferenceController {
100100
}
101101
};
102102

103+
/**
104+
* Maps reference status from DB format to display format.
105+
*/
106+
private mapStatus(status: string): string {
107+
switch (status?.toLowerCase()) {
108+
case "revoked":
109+
return "Revoked";
110+
case "signed":
111+
case "active":
112+
return "Signed";
113+
case "pending":
114+
return "Pending";
115+
default:
116+
return "Unknown";
117+
}
118+
}
119+
120+
/**
121+
* Combined references (sent + received) for the authenticated user with pagination.
122+
* Uses DB-level pagination to avoid loading all records into memory.
123+
*/
124+
getAllUserReferences = async (req: Request, res: Response) => {
125+
try {
126+
const userId = req.user!.id;
127+
const MAX_LIMIT = 100;
128+
const MAX_FETCH = 500; // Maximum records to fetch for merging
129+
130+
// Sanitize and clamp inputs
131+
let page = parseInt(req.query.page as string, 10) || 1;
132+
let limit = parseInt(req.query.limit as string, 10) || 10;
133+
page = Math.max(1, page);
134+
limit = Math.min(Math.max(1, limit), MAX_LIMIT);
135+
const offset = (page - 1) * limit;
136+
137+
// Calculate how many records we need to fetch to cover the requested page
138+
// We need at least (page * limit) records from each query to ensure we have enough
139+
// after merging, but cap it to avoid loading too much
140+
const recordsNeeded = page * limit;
141+
const fetchLimit = Math.min(Math.max(recordsNeeded, limit * 2), MAX_FETCH);
142+
143+
// Fetch paginated results from both queries using DB-level pagination
144+
const sentResult = await this.referenceService.getUserReferencesPaginated(userId, 1, fetchLimit);
145+
const receivedResult = await this.referenceService.getReferencesForTargetPaginated("user", userId, 1, fetchLimit, false);
146+
147+
// Format and merge results
148+
const formatted = [
149+
...sentResult.references.map((ref) => ({
150+
id: ref.id,
151+
type: "Sent" as const,
152+
forFrom: ref.targetName,
153+
targetType: ref.targetType,
154+
targetName: ref.targetName,
155+
referenceType: ref.referenceType,
156+
numericScore: ref.numericScore,
157+
content: ref.content,
158+
status: this.mapStatus(ref.status),
159+
date: ref.createdAt,
160+
})),
161+
...receivedResult.references.map((ref) => ({
162+
id: ref.id,
163+
type: "Received" as const,
164+
forFrom: ref.author?.name || ref.author?.ename || "Unknown",
165+
targetType: ref.targetType,
166+
targetName: ref.targetName,
167+
referenceType: ref.referenceType,
168+
numericScore: ref.numericScore,
169+
content: ref.content,
170+
status: this.mapStatus(ref.status),
171+
date: ref.createdAt,
172+
author: {
173+
id: ref.author?.id,
174+
ename: ref.author?.ename,
175+
name: ref.author?.name,
176+
},
177+
})),
178+
];
179+
180+
// Sort newest first (already sorted by DB, but merge may need re-sort)
181+
formatted.sort(
182+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
183+
);
184+
185+
// Calculate total from both queries
186+
const total = sentResult.total + receivedResult.total;
187+
188+
// Apply final pagination to merged results
189+
const totalPages = Math.max(1, Math.ceil(total / limit));
190+
const paginated = formatted.slice(offset, offset + limit);
191+
192+
res.json({
193+
references: paginated,
194+
pagination: {
195+
page,
196+
limit,
197+
total,
198+
totalPages,
199+
hasNext: page < totalPages,
200+
hasPrev: page > 1,
201+
},
202+
});
203+
} catch (error) {
204+
console.error("Error getting user references:", error);
205+
res.status(500).json({ error: "Internal server error" });
206+
}
207+
};
208+
103209
getUserReferences = async (req: Request, res: Response) => {
104210
try {
105211
const userId = req.user!.id;

platforms/eReputation-api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ app.get("/api/dashboard/activities", authGuard, dashboardController.getActivitie
106106
app.post("/api/references", authGuard, referenceController.createReference);
107107
app.get("/api/references/target/:targetType/:targetId", referenceController.getReferencesForTarget);
108108
app.get("/api/references/my", authGuard, referenceController.getUserReferences);
109+
app.get("/api/references", authGuard, referenceController.getAllUserReferences);
109110
app.patch("/api/references/:referenceId/revoke", authGuard, referenceController.revokeReference);
110111

111112
// Reference signing routes

platforms/eReputation-api/src/services/ReferenceService.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,42 @@ export class ReferenceService {
4646
});
4747
}
4848

49+
async getUserReferencesPaginated(
50+
authorId: string,
51+
page: number,
52+
limit: number
53+
): Promise<{ references: Reference[]; total: number }> {
54+
const [references, total] = await this.referenceRepository.findAndCount({
55+
where: { authorId },
56+
order: { createdAt: "DESC" },
57+
skip: (page - 1) * limit,
58+
take: limit,
59+
});
60+
return { references, total };
61+
}
62+
63+
async getReferencesForTargetPaginated(
64+
targetType: string,
65+
targetId: string,
66+
page: number,
67+
limit: number,
68+
onlySigned: boolean = true
69+
): Promise<{ references: Reference[]; total: number }> {
70+
const whereClause: any = { targetType, targetId };
71+
if (onlySigned) {
72+
whereClause.status = "signed";
73+
}
74+
75+
const [references, total] = await this.referenceRepository.findAndCount({
76+
where: whereClause,
77+
relations: ["author"],
78+
order: { createdAt: "DESC" },
79+
skip: (page - 1) * limit,
80+
take: limit,
81+
});
82+
return { references, total };
83+
}
84+
4985
async revokeReference(referenceId: string, authorId: string): Promise<Reference | null> {
5086
const reference = await this.referenceRepository.findOne({
5187
where: { id: referenceId, authorId }

platforms/eReputation/client/src/pages/references.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ export default function References() {
193193
<thead className="bg-gradient-to-r from-fig/5 to-apple-red/5">
194194
<tr>
195195
<th className="px-6 py-4 text-left text-xs font-black text-fig uppercase tracking-wider w-1/4">Type</th>
196-
<th className="px-6 py-4 text-left text-xs font-black text-fig uppercase tracking-wider w-1/3">Subject</th>
196+
<th className="px-6 py-4 text-left text-xs font-black text-fig uppercase tracking-wider w-1/3">To / From</th>
197197
<th className="px-6 py-4 text-left text-xs font-black text-fig uppercase tracking-wider hidden sm:table-cell w-1/6">Date</th>
198198
<th className="px-6 py-4 text-left text-xs font-black text-fig uppercase tracking-wider hidden lg:table-cell w-1/6">Status</th>
199199
<th className="px-6 py-4 w-16"></th>
@@ -374,7 +374,9 @@ export default function References() {
374374

375375
<div className="space-y-2">
376376
<div>
377-
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide">Subject</div>
377+
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide">
378+
{reference.type === 'Received' ? 'From' : 'To'}
379+
</div>
378380
<div className="text-sm font-medium text-gray-900">{reference.forFrom}</div>
379381
</div>
380382
<div className="flex justify-between items-center">

0 commit comments

Comments
 (0)