From 868cd72927b969e0ba513029d368964d350edb27 Mon Sep 17 00:00:00 2001 From: gf-rog Date: Fri, 15 Mar 2024 10:53:18 +0100 Subject: [PATCH] Separate user functions from routes --- backend/src/routes/usersRoute.ts | 250 +++++++------------------------ backend/src/users.ts | 171 +++++++++++++++++++++ 2 files changed, 227 insertions(+), 194 deletions(-) diff --git a/backend/src/routes/usersRoute.ts b/backend/src/routes/usersRoute.ts index 654133d0..0efbc231 100644 --- a/backend/src/routes/usersRoute.ts +++ b/backend/src/routes/usersRoute.ts @@ -1,10 +1,5 @@ import { Router, Request, Response } from "express"; -import { v4 as uuidv4 } from "uuid"; -import { Session } from "neo4j-driver"; import driver from "../driver/driver"; -import bcrypt from "bcrypt"; -import wordToVec from "../misc/wordToVec"; -import User from "../models/User"; import { JWTRequest, authenticateToken } from "../misc/jwt"; import { FriendsErrorResponse, @@ -14,54 +9,35 @@ import { UsersSearchErrorResponse, } from "../types/userResponse"; import usersFriendsRoute from "./usersFriendsRoute"; - -import removeKeys from "../misc/removeKeys"; - -const filterUser = (user: User) => removeKeys({ ...user }, ["name_embedding"]); +import { + getAllUsers, + searchUser as searchUsers, + getUser as getUser, + getFriends, + createUser, + updateUser, + deleteUser, +} from "../users"; const usersRouter = Router(); usersRouter.use("/", usersFriendsRoute); -export async function userExists( - session: Session, - props: Partial, -): Promise { - const propsStr = Object.keys(props) - .map((k) => `${k}: $${k}`) - .join(", "); - - const userExistsResult = await session.run( - `MATCH (u:User {${propsStr}}) RETURN u`, - props, - ); - - if (userExistsResult.records.length === 0) { - return null; - } - - const user = userExistsResult.records[0].get("u").properties as User; - return filterUser(user); -} - function userNotFoundRes(res: Response) { const json = { status: "error", errors: { id: "not found" } } as const; return res.status(404).json(json); } usersRouter.get("/", async (_req: Request, res: UsersErrorResponse) => { + const session = driver.session(); try { - const session = driver.session(); - const usersRequest = await session.run(`MATCH (u:User) RETURN u`); - const users = usersRequest.records.map((r) => - filterUser(r.get("u").properties), - ); - - await session.close(); + const users = await getAllUsers(session); return res.json({ status: "ok", users }); } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }); @@ -91,45 +67,31 @@ usersRouter.get( .json({ status: "error", errors: { searchTerm: "is empty" } }); } + const session = driver.session(); try { - const session = driver.session(); - const wordVec = wordToVec(searchTerm); - - if (wordVec.length == 0) { + const users = await searchUsers(session, searchTerm); + if (users === null) { return res .status(400) .json({ status: "error", errors: { searchTerm: "incorrect" } }); } - const userRequest = await session.run( - `CALL db.index.vector.queryNodes('user-names', 10, $wordVec) - YIELD node AS similarUser, score - RETURN similarUser, score`, - { wordVec }, - ); - const users = userRequest.records.map((r) => { - return [ - filterUser(r.get("similarUser").properties), - Number(r.get("score")), - ] as [User, number]; - }); - await session.close(); return res.json({ status: "ok", users }); } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }, ); usersRouter.get("/:userId", async (req: Request, res: UserErrorResponse) => { - try { - const userId = req.params.userId; - - const session = driver.session(); - const user = await userExists(session, { id: userId }); - await session.close(); + const userId = req.params.userId; + const session = driver.session(); + try { + const user = await getUser(session, { id: userId }); if (!user) { return userNotFoundRes(res); } @@ -138,192 +100,92 @@ usersRouter.get("/:userId", async (req: Request, res: UserErrorResponse) => { } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }); usersRouter.get( "/:userId/friends", async (req: Request, res: FriendsErrorResponse) => { - try { - const userId = req.params.userId; - - const session = driver.session(); - const user = await userExists(session, { id: userId }); + const userId = req.params.userId; - if (!user) { + const session = driver.session(); + try { + const friends = await getFriends(session, userId); + if (friends === null) { return userNotFoundRes(res); } - const friendRequest = await session.run( - `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User) RETURN f`, - { userId }, - ); - await session.close(); - - const friends = friendRequest.records.map((f) => - filterUser(f.get("f").properties), - ); return res.json({ status: "ok", friends }); } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }, ); -usersRouter.get("/meetings/:userId", async (req: Request, res) => { - try { - const session = driver.session(); - const userId = req.params.userId; - - const user = await userExists(session, { id: userId }); - if (!user) { - await session.close(); - return res; - } - - const meetingsRequest = await session.run( - `MATCH (u1:User {id: $userId})-[m:MEETING]-(u2:User) RETURN m, u2`, - { userId }, - ); - await session.close(); - try { - const meetings = meetingsRequest.records.map((meeting) => { - const { meetingId, waiting } = meeting.get(0).properties; - const { id, first_name, last_name } = meeting.get(1).properties; - return { meetingId, id, first_name, last_name, waiting }; - }); - return res.json({ status: "ok", meetings }); - } catch (_err) { - return res.json({ status: "ok", meetings: [] }); - } - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } -}); - -usersRouter.put("/meetings/:meetingId", async (req: Request, res) => { - try { - const session = driver.session(); - const meetingId = req.params.meetingId; - - await session.run( - `MATCH (u1:User)-[m:MEETING]-(u2:User) WHERE m.meetingId=$meetingId SET m.waiting = true`, - { meetingId }, - ); - await session.close(); - return res.json({ status: "ok" }); - } catch (err) { - console.log("Error:", err); - return res.status(404).json({ status: "error", errors: err as object }); - } -}); - usersRouter.post("/", async (req: Request, res: UserErrorResponse) => { - try { - const newUserProps = req.body; - // TODO: verify user fields - - const session = driver.session(); - const existsUser = await userExists(session, { mail: newUserProps.mail }); - - if (existsUser) { - await session.close(); - return res - .status(400) - .json({ status: "error", errors: { id: "already exists" } }); - } - - newUserProps.id = uuidv4(); - - const firstNameEmbedding = wordToVec(newUserProps.first_name); - const lastNameEmbedding = wordToVec(newUserProps.last_name); - - const errors: Record = {}; - - if (firstNameEmbedding.length == 0) { - errors["first_name"] = "incorrect"; - } + // TODO: verify user fields + const newUserProps = req.body; - if (lastNameEmbedding.length == 0) { - errors["last_name"] = "incorrect"; - } + const session = driver.session(); + try { + const user = await createUser(session, newUserProps); - for (const _ in errors) { + if ("errors" in user) { + const errors = user["errors"]; return res.status(400).json({ status: "error", errors }); } - newUserProps.name_embedding = firstNameEmbedding.map((e1, i) => { - const e2 = lastNameEmbedding[i]; - return (e1 + e2) / 2; - }); - - const { password } = newUserProps; - const passwordHashed = await bcrypt.hash(password, 10); - newUserProps.password = passwordHashed; - - const newUserResult = await session.run(`CREATE (u:User $user) RETURN u`, { - user: newUserProps, - }); - - const user = filterUser(newUserResult.records[0].get("u").properties); - await session.close(); - return res.json({ status: "ok", user }); } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }); usersRouter.put("/:userId", async (req: Request, res: OkErrorResponse) => { - try { - const userId = req.params.userId; - const newUserProps = req.body; - - // TODO: verify user fields - const session = driver.session(); - const user = await userExists(session, { id: userId }); + // TODO: verify user fields + const userId = req.params.userId; + const newUserProps = req.body; - if (!user) { + const session = driver.session(); + try { + const newUser = await updateUser(session, userId, newUserProps); + if (!newUser) { return userNotFoundRes(res); } - const newUser = { ...user, ...newUserProps }; - await session.run(`MATCH (u:User {id: $userId}) SET u=$user`, { - userId, - user: newUser, - }); - await session.close(); - return res.json({ status: "ok" }); } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }); usersRouter.delete("/:userId", async (req: Request, res: OkErrorResponse) => { - try { - const userId = req.params.userId; + const userId = req.params.userId; - const session = driver.session(); - const user = await userExists(session, { id: userId }); - - if (!user) { + const session = driver.session(); + try { + const isDeleted = await deleteUser(session, userId); + if (!isDeleted) { return userNotFoundRes(res); } - await session.run(`MATCH (u:User {id: $userId}) DETACH DELETE u`, { - userId, - }); - await session.close(); - return res.json({ status: "ok" }); } catch (err) { console.log("Error:", err); return res.status(404).json({ status: "error", errors: err as object }); + } finally { + session.close(); } }); diff --git a/backend/src/users.ts b/backend/src/users.ts index b8c05279..6fe37330 100644 --- a/backend/src/users.ts +++ b/backend/src/users.ts @@ -1,4 +1,175 @@ import { Session } from "neo4j-driver"; +import { v4 as uuidv4 } from "uuid"; +import bcrypt from "bcrypt"; +import User from "./models/User"; +import removeKeys from "./misc/removeKeys"; +import wordToVec from "./misc/wordToVec"; +import DbUser from "./models/DbUser"; + +export const filterUser = (user: DbUser): User => + removeKeys({ ...user }, ["name_embedding"]); + +type UserCreate = User | { errors: Record }; + +export async function createUser( + session: Session, + userData: DbUser, +): Promise { + const existsUser = await getUser(session, { mail: userData.mail }); + + if (existsUser) { + return { errors: { id: "already exists" } }; + } + + const firstNameEmbedding = wordToVec(userData.first_name); + const lastNameEmbedding = wordToVec(userData.last_name); + + const errors: Record = {}; + + if (firstNameEmbedding.length == 0) { + errors["first_name"] = "incorrect"; + } + + if (lastNameEmbedding.length == 0) { + errors["last_name"] = "incorrect"; + } + + for (const _ in errors) { + return { errors }; + } + + const { password } = userData; + const passwordHashed = await bcrypt.hash(password, 10); + + const newUserData = { ...userData } as DbUser + newUserData.id = uuidv4(); + newUserData.name_embedding = firstNameEmbedding.map((e1, i) => { + const e2 = lastNameEmbedding[i]; + return (e1 + e2) / 2; + }); + newUserData.password = passwordHashed; + + const userResult = await session.run(`CREATE (u:User $user) RETURN u`, { + user: userData, + }); + + const user = filterUser(userResult.records[0].get("u").properties); + return user; +} + +export async function getUser( + session: Session, + props: Partial, +): Promise { + const propsStr = Object.keys(props) + .map((k) => `${k}: $${k}`) + .join(", "); + + const userExistsResult = await session.run( + `MATCH (u:User {${propsStr}}) RETURN u`, + props, + ); + + if (userExistsResult.records.length === 0) { + return null; + } + + const user = userExistsResult.records[0].get("u").properties as DbUser; + return filterUser(user); +} + +export async function getAllUsers(session: Session) { + const usersRequest = await session.run(`MATCH (u:User) RETURN u`); + const users = usersRequest.records.map((r) => + filterUser(r.get("u").properties), + ); + return users; +} + +type UserScore = [User, number]; + +export async function searchUser( + session: Session, + searchTerm: string, +): Promise { + const wordVec = wordToVec(searchTerm); + + if (wordVec.length == 0) { + return null; + } + + const userRequest = await session.run( + `CALL db.index.vector.queryNodes('user-names', 10, $wordVec) + YIELD node AS similarUser, score + RETURN similarUser, score`, + { wordVec }, + ); + + const users = userRequest.records.map((r) => { + return [ + filterUser(r.get("similarUser").properties), + Number(r.get("score")), + ] as UserScore; + }); + + return users; +} + +export async function updateUser( + session: Session, + userId: string, + newUserProps: Partial, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return false; + } + + const newUser = { ...user, ...newUserProps }; + await session.run(`MATCH (u:User {id: $userId}) SET u=$user`, { + userId, + user: newUser, + }); + + return true +} + +export async function deleteUser( + session: Session, + userId: string +): Promise { + const user = await getUser(session, { id: userId }); + + if (!user) { + return false; + } + + await session.run(`MATCH (u:User {id: $userId}) DETACH DELETE u`, { + userId, + }); + + return true; +} + +export async function getFriends( + session: Session, + userId: string, +): Promise { + const user = await getUser(session, { id: userId }); + if (!user) { + return null; + } + + const friendRequest = await session.run( + `MATCH (u:User {id: $userId})-[:IS_FRIENDS_WITH]-(f:User) RETURN f`, + { userId }, + ); + + const friends = friendRequest.records.map((f) => + filterUser(f.get("f").properties), + ); + return friends; +} export async function isFriend( session: Session,