From 065f1c8a5196c2a2cfe2e94b8aecaeea338a0ea1 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:26:28 -0700 Subject: [PATCH 1/2] Fetch connected user's info --- .../participant_connections_controller.ts | 236 +++++++++++++++--- .../src/models/participant_model.ts | 2 +- .../routes/participant_connections_routes.ts | 4 + 3 files changed, 212 insertions(+), 30 deletions(-) diff --git a/shatter-backend/src/controllers/participant_connections_controller.ts b/shatter-backend/src/controllers/participant_connections_controller.ts index d206b22..b2b5a9e 100644 --- a/shatter-backend/src/controllers/participant_connections_controller.ts +++ b/shatter-backend/src/controllers/participant_connections_controller.ts @@ -31,15 +31,28 @@ import { ParticipantConnection } from "../models/participant_connection_model"; */ export async function createParticipantConnection(req: Request, res: Response) { try { - const requiredFields = ["_eventId", "primaryParticipantId", "secondaryParticipantId"]; + const requiredFields = [ + "_eventId", + "primaryParticipantId", + "secondaryParticipantId", + ]; if (!check_req_fields(req, requiredFields)) { return res.status(400).json({ error: "Missing required fields" }); } - const { _eventId, primaryParticipantId, secondaryParticipantId, description } = req.body; + const { + _eventId, + primaryParticipantId, + secondaryParticipantId, + description, + } = req.body; // Validate ObjectId format before hitting the DB - const idsToValidate = { _eventId, primaryParticipantId, secondaryParticipantId }; + const idsToValidate = { + _eventId, + primaryParticipantId, + secondaryParticipantId, + }; for (const [key, value] of Object.entries(idsToValidate)) { if (!Types.ObjectId.isValid(value)) { return res.status(400).json({ error: `Invalid ${key}` }); @@ -48,15 +61,25 @@ export async function createParticipantConnection(req: Request, res: Response) { // Ensure both participants exist AND belong to the event const [primaryParticipant, secondaryParticipant] = await Promise.all([ - Participant.findOne({ _id: primaryParticipantId, eventId: _eventId }).select("_id"), - Participant.findOne({ _id: secondaryParticipantId, eventId: _eventId }).select("_id"), + Participant.findOne({ + _id: primaryParticipantId, + eventId: _eventId, + }).select("_id"), + Participant.findOne({ + _id: secondaryParticipantId, + eventId: _eventId, + }).select("_id"), ]); if (!primaryParticipant) { - return res.status(404).json({ error: "Primary participant not found for this event" }); + return res + .status(404) + .json({ error: "Primary participant not found for this event" }); } if (!secondaryParticipant) { - return res.status(404).json({ error: "Secondary participant not found for this event" }); + return res + .status(404) + .json({ error: "Secondary participant not found for this event" }); } // Prevent duplicates with exact same (_eventId, primaryParticipantId, secondaryParticipantId) @@ -68,7 +91,8 @@ export async function createParticipantConnection(req: Request, res: Response) { if (existing) { return res.status(409).json({ - error: "ParticipantConnection already exists for this event and participants", + error: + "ParticipantConnection already exists for this event and participants", existingConnection: existing, }); } @@ -111,14 +135,22 @@ export async function createParticipantConnection(req: Request, res: Response) { * @returns 409 - ParticipantConnection already exists for this event and participants * @returns 500 - Internal server error */ -export async function createParticipantConnectionByEmails(req: Request, res: Response) { +export async function createParticipantConnectionByEmails( + req: Request, + res: Response, +) { try { - const requiredFields = ["_eventId", "primaryUserEmail", "secondaryUserEmail"]; + const requiredFields = [ + "_eventId", + "primaryUserEmail", + "secondaryUserEmail", + ]; if (!check_req_fields(req, requiredFields)) { return res.status(400).json({ error: "Missing required fields" }); } - const { _eventId, primaryUserEmail, secondaryUserEmail, description } = req.body; + const { _eventId, primaryUserEmail, secondaryUserEmail, description } = + req.body; if (!Types.ObjectId.isValid(_eventId)) { return res.status(400).json({ error: "Invalid _eventId" }); @@ -136,7 +168,9 @@ export async function createParticipantConnectionByEmails(req: Request, res: Res } if (primaryEmail === secondaryEmail) { - return res.status(400).json({ error: "primaryUserEmail and secondaryUserEmail must be different" }); + return res.status(400).json({ + error: "primaryUserEmail and secondaryUserEmail must be different", + }); } // Find users by email @@ -145,20 +179,32 @@ export async function createParticipantConnectionByEmails(req: Request, res: Res User.findOne({ email: secondaryEmail }).select("_id"), ]); - if (!primaryUser) return res.status(404).json({ error: "Primary user not found" }); - if (!secondaryUser) return res.status(404).json({ error: "Secondary user not found" }); + if (!primaryUser) + return res.status(404).json({ error: "Primary user not found" }); + if (!secondaryUser) + return res.status(404).json({ error: "Secondary user not found" }); // Map User -> Participant (for the event) const [primaryParticipant, secondaryParticipant] = await Promise.all([ - Participant.findOne({ eventId: _eventId, userId: primaryUser._id }).select("_id"), - Participant.findOne({ eventId: _eventId, userId: secondaryUser._id }).select("_id"), + Participant.findOne({ + eventId: _eventId, + userId: primaryUser._id, + }).select("_id"), + Participant.findOne({ + eventId: _eventId, + userId: secondaryUser._id, + }).select("_id"), ]); if (!primaryParticipant) { - return res.status(404).json({ error: "Primary participant not found for this event (by user email)" }); + return res.status(404).json({ + error: "Primary participant not found for this event (by user email)", + }); } if (!secondaryParticipant) { - return res.status(404).json({ error: "Secondary participant not found for this event (by user email)" }); + return res.status(404).json({ + error: "Secondary participant not found for this event (by user email)", + }); } // Prevent duplicates with exact same (_eventId, primaryParticipantId, secondaryParticipantId) @@ -170,7 +216,8 @@ export async function createParticipantConnectionByEmails(req: Request, res: Res if (existing) { return res.status(409).json({ - error: "ParticipantConnection already exists for this event and participants", + error: + "ParticipantConnection already exists for this event and participants", existingConnection: existing, }); } @@ -219,7 +266,9 @@ export async function deleteParticipantConnection(req: Request, res: Response) { }); if (!deleted) { - return res.status(404).json({ error: "ParticipantConnection not found for this event" }); + return res + .status(404) + .json({ error: "ParticipantConnection not found for this event" }); } return res.status(200).json({ @@ -231,15 +280,26 @@ export async function deleteParticipantConnection(req: Request, res: Response) { } } -export async function getConnectionsByParticipantAndEvent(req: Request, res: Response) { +export async function getConnectionsByParticipantAndEvent( + req: Request, + res: Response, +) { try { const { eventId, participantId } = req.query; - if (!eventId || typeof eventId !== "string" || !Types.ObjectId.isValid(eventId)) { + if ( + !eventId || + typeof eventId !== "string" || + !Types.ObjectId.isValid(eventId) + ) { return res.status(400).json({ error: "Invalid eventId" }); } - if (!participantId || typeof participantId !== "string" || !Types.ObjectId.isValid(participantId)) { + if ( + !participantId || + typeof participantId !== "string" || + !Types.ObjectId.isValid(participantId) + ) { return res.status(400).json({ error: "Invalid participantId" }); } @@ -257,11 +317,18 @@ export async function getConnectionsByParticipantAndEvent(req: Request, res: Res } } -export async function getConnectionsByUserEmailAndEvent(req: Request, res: Response) { +export async function getConnectionsByUserEmailAndEvent( + req: Request, + res: Response, +) { try { const { eventId, userEmail } = req.query; - if (!eventId || typeof eventId !== "string" || !Types.ObjectId.isValid(eventId)) { + if ( + !eventId || + typeof eventId !== "string" || + !Types.ObjectId.isValid(eventId) + ) { return res.status(400).json({ error: "Invalid eventId" }); } @@ -276,17 +343,22 @@ export async function getConnectionsByUserEmailAndEvent(req: Request, res: Respo return res.status(400).json({ error: "Invalid userEmail" }); } - const participant = await Participant.findOne({ eventId, userId: user._id }).select("_id"); + const participant = await Participant.findOne({ + eventId, + userId: user._id, + }).select("_id"); if (!participant) { - return res.status(404).json({ error: "Participant not found for this event (by user email)" }); + return res.status(404).json({ + error: "Participant not found for this event (by user email)", + }); } const connections = await ParticipantConnection.find({ _eventId: eventId, $or: [ { primaryParticipantId: participant._id }, - { secondaryParticipantId: participant._id } + { secondaryParticipantId: participant._id }, ], }); @@ -294,4 +366,110 @@ export async function getConnectionsByUserEmailAndEvent(req: Request, res: Respo } catch (_error) { return res.status(500).json({ error: "Internal server error" }); } -} \ No newline at end of file +} + +/** + * GET /api/participantConnections/getParticipantConnections/connected-users + * + * Get all users connected with a given participant in an event, + * including the description of the connection. + * + * @param req.query.eventId - MongoDB ObjectId of the event (required) + * @param req.query.participantId - MongoDB ObjectId of the participant (required) + * + * @returns 200 - Array of connected users with connection descriptions + * @returns 400 - Missing/invalid params + * @returns 404 - Participant not found or no connections + * @returns 500 - Internal server error + */ +export async function getConnectedUsersInfo(req: Request, res: Response) { + try { + const { eventId, participantId } = req.query; + + if ( + !eventId || + typeof eventId !== "string" || + !Types.ObjectId.isValid(eventId) + ) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + if ( + !participantId || + typeof participantId !== "string" || + !Types.ObjectId.isValid(participantId) + ) { + return res.status(400).json({ error: "Invalid participantId" }); + } + + const connections = await ParticipantConnection.find({ + _eventId: eventId, + $or: [ + { primaryParticipantId: participantId }, + { secondaryParticipantId: participantId }, + ], + }); + + if (!connections.length) { + return res + .status(404) + .json({ error: "No connections found for this participant" }); + } + + const connectedMap = connections.map((conn) => { + const otherParticipantId = + conn.primaryParticipantId.toString() === participantId + ? conn.secondaryParticipantId + : conn.primaryParticipantId; + return { + participantId: otherParticipantId, + description: conn.description || null, + }; + }); + + // Remove duplicate connections for the same participant + const uniqueMap = Array.from( + new Map( + connectedMap.map((item) => [item.participantId.toString(), item]), + ).values(), + ); + + const participantIds = uniqueMap.map((item) => item.participantId); + + const participants = await Participant.find({ + _id: { $in: participantIds }, + }).select("userId name"); + const userIds = participants.map((p) => p.userId); + + const users = await User.find({ _id: { $in: userIds } }).select( + "name email linkedinUrl bio profilePhoto socialLinks", + ); + + const result = uniqueMap + .map((item) => { + const participant = participants.find( + (p) => p._id && p._id.toString() === item.participantId.toString(), + ); + + if (!participant || !participant.userId) return null; + + const user = users.find( + (u) => u._id.toString() === participant.userId.toString(), + ); + + if (!user) return null; + + return { + user, + participantId: participant._id, + participantName: participant.name, + connectionDescription: item.description, + }; + }) + .filter(Boolean); + + return res.status(200).json(result); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts index f26a574..67f10bb 100644 --- a/shatter-backend/src/models/participant_model.ts +++ b/shatter-backend/src/models/participant_model.ts @@ -1,7 +1,7 @@ import { Schema, model, Document } from "mongoose"; export interface IParticipant extends Document { - userId: Schema.Types.ObjectId | null; + userId: Schema.Types.ObjectId; name: string; eventId: Schema.Types.ObjectId; } diff --git a/shatter-backend/src/routes/participant_connections_routes.ts b/shatter-backend/src/routes/participant_connections_routes.ts index 383168e..0cea1a6 100644 --- a/shatter-backend/src/routes/participant_connections_routes.ts +++ b/shatter-backend/src/routes/participant_connections_routes.ts @@ -5,6 +5,7 @@ import { createParticipantConnection, createParticipantConnectionByEmails, deleteParticipantConnection, + getConnectedUsersInfo, getConnectionsByParticipantAndEvent, getConnectionsByUserEmailAndEvent, } from "../controllers/participant_connections_controller"; @@ -57,4 +58,7 @@ router.get("/getByParticipantAndEvent", authMiddleware, getConnectionsByParticip // GET /api/participantConnections/getByUserEmailAndEvent router.get("/getByUserEmailAndEvent", authMiddleware, getConnectionsByUserEmailAndEvent); +// Get all user's information that connected with the participant +router.get("/getParticipantConnections/connected-users", authMiddleware, getConnectedUsersInfo); + export default router; \ No newline at end of file From f1433b3fb989020335b7424c47e6276947c5eb44 Mon Sep 17 00:00:00 2001 From: rxmox Date: Fri, 6 Mar 2026 21:28:48 -0700 Subject: [PATCH 2/2] Improve getConnectedUsersInfo endpoint and update docs Simplify route path to /connected-users, return 200 with empty array instead of 404 when no connections exist, remove dedup to preserve all connections with different descriptions, use populate to reduce DB queries, handle null userId gracefully, and restore nullable userId type on Participant interface. --- shatter-backend/docs/API_REFERENCE.md | 47 +++++++++++++ .../participant_connections_controller.ts | 66 ++++++++----------- .../src/models/participant_model.ts | 2 +- .../routes/participant_connections_routes.ts | 3 +- 4 files changed, 79 insertions(+), 39 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index b1a11a4..f1f50a8 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -40,6 +40,7 @@ - [DELETE /api/participantConnections/delete](#delete-apiparticipantconnectionsdelete) - [GET /api/participantConnections/getByParticipantAndEvent](#get-apiparticipantconnectionsgetbyparticipantandevent) - [GET /api/participantConnections/getByUserEmailAndEvent](#get-apiparticipantconnectionsgetbyuseremailandevent) + - [GET /api/participantConnections/connected-users](#get-apiparticipantconnectionsconnected-users) - [Planned Endpoints](#planned-endpoints-) - [Quick Start Examples](#quick-start-examples) @@ -77,6 +78,7 @@ Quick reference of all implemented endpoints. See detailed sections below for re | DELETE | `/api/participantConnections/delete` | Protected | Delete a connection | | GET | `/api/participantConnections/getByParticipantAndEvent` | Protected | Get connections by participant + event | | GET | `/api/participantConnections/getByUserEmailAndEvent` | Protected | Get connections by email + event | +| GET | `/api/participantConnections/connected-users` | Protected | Get connected users' info by participant + event | --- @@ -1119,6 +1121,51 @@ Get all connections for a user (by email) in an event. --- +### GET `/api/participantConnections/connected-users` + +Get all users connected with a given participant in an event, including connection descriptions. Returns all connections (including multiple connections to the same user with different descriptions). + +- **Auth:** Protected + +**Query Params:** + +| Param | Type | Required | +|-----------------|----------|----------| +| `eventId` | ObjectId | Yes | +| `participantId` | ObjectId | Yes | + +**Success Response (200):** + +```json +[ + { + "user": { + "_id": "664f...", + "name": "John Doe", + "email": "john@example.com", + "linkedinUrl": "https://linkedin.com/in/johndoe", + "bio": "Software developer", + "profilePhoto": "https://example.com/photo.jpg", + "socialLinks": { "github": "https://github.com/johndoe" } + }, + "participantId": "666b...", + "participantName": "John Doe", + "connectionDescription": "Both love hiking" + } +] +``` + +Returns an empty array `[]` if no connections exist. If a participant has no linked user (guest with null `userId`), the `user` field is `null` while `participantId` and `participantName` are still returned. + +**Error Responses:** + +| Status | Error | +|--------|-------| +| 400 | `"Invalid eventId"` | +| 400 | `"Invalid participantId"` | + +--- + ## Planned Endpoints ⏳ These endpoints are **not yet implemented**. Do not depend on them. diff --git a/shatter-backend/src/controllers/participant_connections_controller.ts b/shatter-backend/src/controllers/participant_connections_controller.ts index b2b5a9e..e66874e 100644 --- a/shatter-backend/src/controllers/participant_connections_controller.ts +++ b/shatter-backend/src/controllers/participant_connections_controller.ts @@ -369,7 +369,7 @@ export async function getConnectionsByUserEmailAndEvent( } /** - * GET /api/participantConnections/getParticipantConnections/connected-users + * GET /api/participantConnections/connected-users * * Get all users connected with a given participant in an event, * including the description of the connection. @@ -377,9 +377,8 @@ export async function getConnectionsByUserEmailAndEvent( * @param req.query.eventId - MongoDB ObjectId of the event (required) * @param req.query.participantId - MongoDB ObjectId of the participant (required) * - * @returns 200 - Array of connected users with connection descriptions + * @returns 200 - Array of connected users with connection descriptions (empty array if none) * @returns 400 - Missing/invalid params - * @returns 404 - Participant not found or no connections * @returns 500 - Internal server error */ export async function getConnectedUsersInfo(req: Request, res: Response) { @@ -411,12 +410,11 @@ export async function getConnectedUsersInfo(req: Request, res: Response) { }); if (!connections.length) { - return res - .status(404) - .json({ error: "No connections found for this participant" }); + return res.status(200).json([]); } - const connectedMap = connections.map((conn) => { + // Map each connection to the other participant's ID + description + const connectedItems = connections.map((conn) => { const otherParticipantId = conn.primaryParticipantId.toString() === participantId ? conn.secondaryParticipantId @@ -427,46 +425,40 @@ export async function getConnectedUsersInfo(req: Request, res: Response) { }; }); - // Remove duplicate connections for the same participant - const uniqueMap = Array.from( - new Map( - connectedMap.map((item) => [item.participantId.toString(), item]), - ).values(), - ); - - const participantIds = uniqueMap.map((item) => item.participantId); + const participantIds = [ + ...new Set(connectedItems.map((item) => item.participantId.toString())), + ]; + // Single query with populate to get participants + their users const participants = await Participant.find({ _id: { $in: participantIds }, - }).select("userId name"); - const userIds = participants.map((p) => p.userId); + }) + .select("userId name") + .populate("userId", "name email linkedinUrl bio profilePhoto socialLinks"); - const users = await User.find({ _id: { $in: userIds } }).select( - "name email linkedinUrl bio profilePhoto socialLinks", + const participantMap = new Map( + participants.map((p) => [(p._id as Types.ObjectId).toString(), p]), ); - const result = uniqueMap - .map((item) => { - const participant = participants.find( - (p) => p._id && p._id.toString() === item.participantId.toString(), - ); - - if (!participant || !participant.userId) return null; - - const user = users.find( - (u) => u._id.toString() === participant.userId.toString(), - ); - - if (!user) return null; + const result = connectedItems.map((item) => { + const participant = participantMap.get(item.participantId.toString()); + if (!participant) { return { - user, - participantId: participant._id, - participantName: participant.name, + user: null, + participantId: item.participantId, + participantName: null, connectionDescription: item.description, }; - }) - .filter(Boolean); + } + + return { + user: participant.userId || null, + participantId: participant._id, + participantName: participant.name, + connectionDescription: item.description, + }; + }); return res.status(200).json(result); } catch (_error) { diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts index 67f10bb..f26a574 100644 --- a/shatter-backend/src/models/participant_model.ts +++ b/shatter-backend/src/models/participant_model.ts @@ -1,7 +1,7 @@ import { Schema, model, Document } from "mongoose"; export interface IParticipant extends Document { - userId: Schema.Types.ObjectId; + userId: Schema.Types.ObjectId | null; name: string; eventId: Schema.Types.ObjectId; } diff --git a/shatter-backend/src/routes/participant_connections_routes.ts b/shatter-backend/src/routes/participant_connections_routes.ts index 0cea1a6..8bb6dc8 100644 --- a/shatter-backend/src/routes/participant_connections_routes.ts +++ b/shatter-backend/src/routes/participant_connections_routes.ts @@ -59,6 +59,7 @@ router.get("/getByParticipantAndEvent", authMiddleware, getConnectionsByParticip router.get("/getByUserEmailAndEvent", authMiddleware, getConnectionsByUserEmailAndEvent); // Get all user's information that connected with the participant -router.get("/getParticipantConnections/connected-users", authMiddleware, getConnectedUsersInfo); +// GET /api/participantConnections/connected-users +router.get("/connected-users", authMiddleware, getConnectedUsersInfo); export default router; \ No newline at end of file