diff --git a/bun.lock b/bun.lock index 96ec5f46..1f8b457e 100644 --- a/bun.lock +++ b/bun.lock @@ -30,6 +30,7 @@ "dotenv": "^16.4.5", "dotenv-cli": "^7.4.2", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "firebase-admin": "^12.2.0", "sharp": "^0.33.5", "socket.io": "^4.7.5", @@ -716,6 +717,8 @@ "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + "express-async-errors": ["express-async-errors@3.1.1", "", { "peerDependencies": { "express": "^4.16.2" } }, "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "farmhash-modern": ["farmhash-modern@1.1.0", "", {}, "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA=="], diff --git a/common/lib/panic.ts b/common/lib/panic.ts new file mode 100644 index 00000000..ae3cba1e --- /dev/null +++ b/common/lib/panic.ts @@ -0,0 +1,11 @@ +// unexpected error +export function panic(reason: string): never { + throw new Error(reason, { + cause: "panic", + }); +} + +// expected error +export function error(reason: string, code?: number): never { + throw new Error(reason, { cause: code }); +} diff --git a/common/lib/result.ts b/common/lib/result.ts deleted file mode 100644 index e7d7e369..00000000 --- a/common/lib/result.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** -# Result -Result allows you to handle results in safer and more type-safe way. -Result を使うと、失敗する可能性がある関数の結果をより安全で Type-safe な方法で扱うことができます。 - -basic use case: -```js -function fallible() { - if (Math.random() > 0.5) - throw new Error("something went wrong!"); - return "ok" -} - -const result = safeTry(() => fallible()); -if (!result.ok) { - // handle error - console.error(result.error); - return; -} - -// use value without worrying about thrown errors -console.log(result.value); // -> ok -``` - -or better, make it built in to your function. - -```js -// never throws. -function safeFallible(): Result { - if (Math.random() > 0.5) - return Err("something went wrong!"); - return Ok("ok"); -} - -const result = safeFallible(); -if (!result.ok) { - // handle error - console.error(result.error); - return; -} - -console.log(result.value); -``` -**/ - -// Core - -export type Result = Ok | Err; - -type Ok = { - ok: true; - value: T; -}; - -type Err = { - ok: false; - error: unknown; -}; - -// Core functions - -export function Ok(value: T): Ok { - return { - ok: true, - value, - }; -} - -export function Err(error: unknown): Err { - return { - ok: false, - error, - }; -} - -// Utility functions - -export function safeTry(fallible: () => T): Result { - try { - return Ok(fallible()); - } catch (e) { - return Err(e); - } -} - -export async function safeTryAsync( - fallible: () => Promise, -): Promise> { - try { - return Ok(await fallible()); - } catch (e) { - return Err(e); - } -} - -export function safify(fallible: (v: T) => U): (v: T) => Result { - return (v: T): Result => { - try { - return Ok(fallible(v)); - } catch (e) { - return Err(e); - } - }; -} - -export function safifyAsync( - fallible: (v: T) => Promise, -): (v: T) => Promise> { - return async (v: T): Promise> => { - try { - return Ok(await fallible(v)); - } catch (e) { - return Err(e); - } - }; -} diff --git a/common/lib/result/safeParseInt.ts b/common/lib/result/safeParseInt.ts deleted file mode 100644 index b9705598..00000000 --- a/common/lib/result/safeParseInt.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Err, Ok, type Result } from "../result"; - -export function safeParseInt(s: string | undefined): Result { - if (!s) return Err(new Error("empty string")); - const n = Number.parseInt(s); - if (Number.isNaN(n)) return Err(new Error(`invalid formatting: ${s}`)); - return Ok(n); -} diff --git a/flake.lock b/flake.lock index fd1545f5..8b9b19c0 100644 --- a/flake.lock +++ b/flake.lock @@ -68,26 +68,12 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1738703096, - "narHash": "sha256-1MABVDwNpAUrUDvyM6PlHlAB1eTWAX0eNYCzdsZ54NI=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "f7384aacd0ecd28681a99269ac0dff2c3a805d63", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "prisma-utils": { "inputs": { "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_2", + "nixpkgs": [ + "nixpkgs" + ], "treefmt-nix": "treefmt-nix" }, "locked": { diff --git a/flake.nix b/flake.nix index 35947e24..c5786b20 100644 --- a/flake.nix +++ b/flake.nix @@ -13,8 +13,7 @@ prisma-utils = { url = "github:VanCoding/nix-prisma-utils"; - # HACK: they have named nixpkgs pkgs. I'm submitting a fix PR soon, rename this to `inputs.nixpkgs.follows` when that gets merged. - inputs.pkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/server/package.json b/server/package.json index e0c75cc7..c08b3ae4 100644 --- a/server/package.json +++ b/server/package.json @@ -16,13 +16,14 @@ "author": "", "license": "ISC", "dependencies": { - "common": "workspace:common", "@prisma/client": "^5.20.0", + "common": "workspace:common", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.4.5", "dotenv-cli": "^7.4.2", "express": "^4.18.2", + "express-async-errors": "^3.1.1", "firebase-admin": "^12.2.0", "sharp": "^0.33.5", "socket.io": "^4.7.5", diff --git a/server/prisma.nix b/server/prisma.nix index 45008b1d..bdb062f5 100644 --- a/server/prisma.nix +++ b/server/prisma.nix @@ -5,7 +5,7 @@ prisma = (prisma-utils.lib.prisma-factory { - nixpkgs = pkgs; + inherit pkgs; prisma-fmt-hash = "sha256-atD5GZfmeU86mF1V6flAshxg4fFR2ews7EwaJWZZzbc="; query-engine-hash = "sha256-8FTZaKmQCf9lrDQvkF5yWPeZ7TSVfFjTbjdbWWEHgq4="; libquery-engine-hash = "sha256-USIdaum87ekGY6F6DaL/tKH0BAZvHBDK7zjmCLo//kM="; diff --git a/server/src/database/chat.ts b/server/src/database/chat.ts index f0d21864..2f07b0bb 100644 --- a/server/src/database/chat.ts +++ b/server/src/database/chat.ts @@ -1,4 +1,3 @@ -import { Err, Ok, type Result } from "common/lib/result"; import type { UserID } from "common/types"; import type { DMOverview, @@ -21,96 +20,74 @@ import { } from "./requests"; import { getUserByID } from "./users"; -export async function getOverview( - user: UserID, -): Promise> { - try { - const matched = await getMatchedUser(user); - if (!matched.ok) return Err(matched.error); +export async function getOverview(user: UserID): Promise { + const matched = await getMatchedUser(user); + const senders = await getPendingRequestsToUser(user); + const receivers = await getPendingRequestsFromUser(user); - const senders = await getPendingRequestsToUser(user); - if (!senders.ok) return Err(senders.error); + //マッチングしている人のオーバービュー + const matchingOverview = await Promise.all( + matched.map(async (m) => getOverviewBetween(user, m.id)), + ); - const receivers = await getPendingRequestsFromUser(user); - if (!receivers.ok) return Err(receivers.error); + //自分にリクエストを送ってきた人のオーバービュー + const senderOverview = await Promise.all( + senders.map((s) => getOverviewBetween(user, s.id)), + ); - //マッチングしている人のオーバービュー - const matchingOverview = await Promise.all( - matched.value.map(async (m) => getOverviewBetween(user, m.id)), - ); + //自分がリクエストを送った人のオーバービュー + const receiverOverview = await Promise.all( + receivers.map((r) => getOverviewBetween(user, r.id)), + ); - //自分にリクエストを送ってきた人のオーバービュー - const senderOverview = await Promise.all( - senders.value.map((s) => getOverviewBetween(user, s.id)), - ); - - //自分がリクエストを送った人のオーバービュー - const receiverOverview = await Promise.all( - receivers.value.map((r) => getOverviewBetween(user, r.id)), - ); - - const sharedRooms: { - id: number; - name: string; - thumbnail: string; - }[] = await prisma.sharedRoom.findMany({ - where: { - members: { - has: user, - }, + const sharedRooms: { + id: number; + name: string; + thumbnail: string; + }[] = await prisma.sharedRoom.findMany({ + where: { + members: { + has: user, }, - }); - const shared = sharedRooms.map((room) => { - const overview: SharedRoomOverview = { - roomId: room.id as ShareRoomID, - name: room.name, - thumbnail: room.thumbnail, - isDM: false, - }; - return overview; - }); + }, + }); + const shared = sharedRooms.map((room) => { + const overview: SharedRoomOverview = { + roomId: room.id as ShareRoomID, + name: room.name, + thumbnail: room.thumbnail, + isDM: false, + }; + return overview; + }); - const overview = [ - ...matchingOverview, - ...senderOverview, - ...receiverOverview, - ...shared, - ]; + const overview = [ + ...matchingOverview, + ...senderOverview, + ...receiverOverview, + ...shared, + ]; - const sortedOverviewByTime = overview.sort((a, b) => { - const dateA = a.lastMsg?.createdAt ? a.lastMsg.createdAt.getTime() : 0; - const dateB = b.lastMsg?.createdAt ? b.lastMsg.createdAt.getTime() : 0; - return dateB - dateA; - }); + const sortedOverviewByTime = overview.sort((a, b) => { + const dateA = a.lastMsg?.createdAt ? a.lastMsg.createdAt.getTime() : 0; + const dateB = b.lastMsg?.createdAt ? b.lastMsg.createdAt.getTime() : 0; + return dateB - dateA; + }); - return Ok([...sortedOverviewByTime]); - } catch (e) { - return Err(e); - } + return [...sortedOverviewByTime]; } async function getOverviewBetween( user: number, other: number, ): Promise { - const relR = await getRelation(user, other); - if (!relR.ok) throw relR.error; - const rel = relR.value; + const rel = await getRelation(user, other); const friendId = rel.receivingUserId === user ? rel.sendingUserId : rel.receivingUserId; - const lastMessage = getLastMessage(user, friendId).then((val) => { - if (val.ok) return val.value; - return undefined; - }); - const unreadCount = unreadMessages(user, rel.id).then((val) => { - if (val.ok) return val.value; - throw val.error; - }); - const friend = await getUserByID(friendId).then((val) => { - if (val.ok) return val.value; - throw val.error; - }); + const lastMessage = getLastMessage(user, friendId); + const unreadCount = unreadMessages(user, rel.id); + const friend = await getUserByID(friendId); const overview: DMOverview = { isDM: true, matchingStatus: "matched", @@ -152,21 +129,17 @@ export async function markAsRead( export async function sendDM( relation: RelationshipID, content: Omit, "isPicture">, -): Promise> { - try { - const message = await prisma.message.create({ - data: { - // isPicture: false, // todo: bring it back - relationId: relation, - isPicture: false, - read: false, - ...content, - }, - }); - return Ok(message); - } catch (e) { - return Err(e); - } +): Promise { + const message = await prisma.message.create({ + data: { + // isPicture: false, // todo: bring it back + relationId: relation, + isPicture: false, + read: false, + ...content, + }, + }); + return message; } /** this doesn't create the image. use uploadPic in database/picture.ts to create the image. @@ -176,258 +149,198 @@ export async function createImageMessage( relation: RelationshipID, url: string, ) { - return prisma.message - .create({ - data: { - creator: sender, - relationId: relation, - content: url, - isPicture: true, - }, - }) - .then((val) => Ok(val)) - .catch((err) => Err(err)); + return prisma.message.create({ + data: { + creator: sender, + relationId: relation, + content: url, + isPicture: true, + }, + }); } -export async function createSharedRoom( - room: InitRoom, -): Promise> { - try { - type CreateRoom = Omit, "messages">; - const created: CreateRoom = await prisma.sharedRoom.create({ - data: { - thumbnail: "todo", - name: room.name, - members: room.members, - }, - }); - return Ok({ - isDM: false, - messages: [], - ...created, - }); - } catch (e) { - return Err(e); - } +export async function createSharedRoom(room: InitRoom): Promise { + type CreateRoom = Omit, "messages">; + const created: CreateRoom = await prisma.sharedRoom.create({ + data: { + thumbnail: "todo", + name: room.name, + members: room.members, + }, + }); + return { + isDM: false, + messages: [], + ...created, + }; } export async function isUserInRoom( roomId: ShareRoomID, userId: UserID, -): Promise> { - try { - const room = await prisma.sharedRoom.findUnique({ - where: { - id: roomId, - members: { - has: userId, - }, +): Promise { + const room = await prisma.sharedRoom.findUnique({ + where: { + id: roomId, + members: { + has: userId, }, - }); + }, + }); - return Ok(room !== null); - } catch (e) { - return Err(e); - } + return room !== null; } export async function updateRoom( roomId: ShareRoomID, newRoom: Partial>, -): Promise>> { - try { - type UpdatedRoom = Omit, "messages">; - const updated: UpdatedRoom = await prisma.sharedRoom.update({ - where: { - id: roomId, - }, - data: newRoom, - }); - return Ok({ - isDM: false, - ...updated, - }); - } catch (e) { - return Err(e); - } +): Promise> { + type UpdatedRoom = Omit, "messages">; + const updated: UpdatedRoom = await prisma.sharedRoom.update({ + where: { + id: roomId, + }, + data: newRoom, + }); + return { + isDM: false, + ...updated, + }; } export async function inviteUserToSharedRoom( roomId: ShareRoomID, invite: UserID[], -): Promise>> { - try { - const update = await prisma.sharedRoom.update({ - where: { - id: roomId, - }, - data: { - members: { - push: invite, - }, +): Promise> { + const update = await prisma.sharedRoom.update({ + where: { + id: roomId, + }, + data: { + members: { + push: invite, }, - }); - return Ok({ - isDM: false, - ...update, - }); - } catch (e) { - return Err(e); - } + }, + }); + return { + isDM: false, + ...update, + }; } -export async function getDMbetween( - u1: UserID, - u2: UserID, -): Promise> { - try { - const rel = await getRelation(u1, u2); - if (!rel.ok) return Err("room not found"); - - return await findDM(rel.value.id); - } catch (e) { - return Err(e); - } +export async function getDMbetween(u1: UserID, u2: UserID): Promise { + const rel = await getRelation(u1, u2); + return await findDM(rel.id); } -export async function findDM(relID: RelationshipID): Promise> { - try { - const messages: Message[] = await prisma.message.findMany({ - where: { - relationId: relID, - }, - orderBy: { - createdAt: "asc", - }, - }); +export async function findDM(relID: RelationshipID): Promise { + const messages: Message[] = await prisma.message.findMany({ + where: { + relationId: relID, + }, + orderBy: { + createdAt: "asc", + }, + }); - return Ok({ - isDM: true, - id: relID, - messages: messages, - }); - } catch (e) { - return Err(e); - } + return { + isDM: true, + id: relID, + messages: messages, + }; } -export async function getSharedRoom( - roomId: ShareRoomID, -): Promise> { - try { - const room = await prisma.sharedRoom.findUnique({ - where: { - id: roomId, - }, - }); - if (!room) return Err("room not found"); +export async function getSharedRoom(roomId: ShareRoomID): Promise { + const room = await prisma.sharedRoom.findUnique({ + where: { + id: roomId, + }, + }); + if (!room) throw new Error("room not found"); - const messages = await prisma.message.findMany({ - where: { - sharedRoomId: room.id, - }, - }); - return Ok({ - isDM: false, - messages: messages, - ...room, - }); - } catch (e) { - return Err(e); - } + const messages = await prisma.message.findMany({ + where: { + sharedRoomId: room.id, + }, + }); + return { + isDM: false, + messages: messages, + ...room, + }; } -export async function getMessage(id: MessageID): Promise> { - try { - const message = await prisma.message.findUnique({ - where: { - id: id, - }, - }); - if (!message) return Err("message not found"); - return Ok(message); - } catch (e) { - return Err(e); - } +export async function getMessage(id: MessageID): Promise { + const message = await prisma.message.findUnique({ + where: { + id: id, + }, + }); + if (!message) throw new Error("not found"); + return message; } export async function updateMessage( id: MessageID, content: string, -): Promise> { - try { - const message = await prisma.message.update({ - where: { - id: id, - }, - data: { - content: content, - edited: true, - }, - }); - return Ok(message); - } catch (e) { - return Err(e); - } +): Promise { + const message = await prisma.message.update({ + where: { + id: id, + }, + data: { + content: content, + edited: true, + }, + }); + return message; } export async function deleteMessage( id: MessageID, creatorId: UserID | undefined, ): Promise { - try { - const message = await prisma.message.delete({ - where: { - id: id, - creator: creatorId, - }, - }); - return message; - } catch (e) { - return null; - } + const message = await prisma.message.delete({ + where: { + id: id, + creator: creatorId, + }, + }); + return message; } export async function getLastMessage( userId: UserID, friendId: UserID, -): Promise> { - try { - const rel = await getRelation(userId, friendId); - if (!rel.ok) return Err("relation not found"); // relation not found - const lastMessage = await prisma.message.findFirst({ - where: { - relationId: rel.value.id, - }, - orderBy: { - id: "desc", - }, - take: 1, - }); - if (!lastMessage) return Err("last message not found"); - return Ok(lastMessage); - } catch (e) { - return Err(e); - } +): Promise { + const rel = await getRelation(userId, friendId); + if (!rel) throw new Error("relation not found"); // relation not found + const lastMessage = await prisma.message.findFirst({ + where: { + relationId: rel.id, + }, + orderBy: { + id: "desc", + }, + take: 1, + }); + if (!lastMessage) throw new Error("last message not found"); + return lastMessage; } // only works on Relationship (= DM) for now. export async function unreadMessages(userId: UserID, roomId: RelationshipID) { - try { - // FIXME: this makes request twice to the database. it's not efficient. - const unreadMessages = await prisma.message.count({ - where: { - read: false, - relationId: roomId, - creator: { - not: { - equals: userId, - }, + // FIXME: this makes request twice to the database. it's not efficient. + const unreadMessages = await prisma.message.count({ + where: { + read: false, + relationId: roomId, + creator: { + not: { + equals: userId, }, }, - }); - return Ok(unreadMessages); - } catch (e) { - return Err(e); - } + }, + }); + return unreadMessages; } diff --git a/server/src/database/courses.ts b/server/src/database/courses.ts index 356337aa..bdb30612 100644 --- a/server/src/database/courses.ts +++ b/server/src/database/courses.ts @@ -1,17 +1,18 @@ +import { error } from "common/lib/panic"; import type { Course, Day, UserID } from "common/types"; import { prisma } from "./client"; -export async function getCourseByCourseId( - courseId: string, -): Promise { - return await prisma.course.findUnique({ - where: { - id: courseId, - }, - include: { - slots: true, - }, - }); +export async function getCourseByCourseId(courseId: string): Promise { + return ( + (await prisma.course.findUnique({ + where: { + id: courseId, + }, + include: { + slots: true, + }, + })) ?? error("course not found", 404) + ); } /** diff --git a/server/src/database/interest.ts b/server/src/database/interest.ts index e1e15b0d..ff56465d 100644 --- a/server/src/database/interest.ts +++ b/server/src/database/interest.ts @@ -1,3 +1,4 @@ +import { error } from "common/lib/panic"; import type { InterestSubject, UserID } from "common/types"; import { prisma } from "./client"; @@ -5,8 +6,11 @@ export async function all(): Promise { return await prisma.interestSubject.findMany(); } -export async function get(id: number): Promise { - return await prisma.interestSubject.findUnique({ where: { id } }); +export async function get(id: number): Promise { + return ( + (await prisma.interestSubject.findUnique({ where: { id } })) ?? + error("not found", 404) + ); } export async function create(name: string): Promise { @@ -14,7 +18,7 @@ export async function create(name: string): Promise { where: { name }, }); if (existingTag.length > 0) { - throw new Error("同名のタグがすでに存在します"); + error("同名のタグがすでに存在します"); } return await prisma.interestSubject.create({ data: { diff --git a/server/src/database/matches.ts b/server/src/database/matches.ts index 7e39c96f..3284e873 100644 --- a/server/src/database/matches.ts +++ b/server/src/database/matches.ts @@ -1,4 +1,4 @@ -import { Err, Ok, type Result } from "common/lib/result"; +import { error, panic } from "common/lib/panic"; import type { Relationship, UserID } from "common/types"; import asyncMap from "../lib/async/map"; import { prisma } from "./client"; @@ -6,135 +6,110 @@ import { prisma } from "./client"; export async function getRelation( u1: UserID, u2: UserID, -): Promise> { - try { - // FIXME: fix this findMany - const rel = await prisma.relationship.findMany({ - where: { - OR: [ - { sendingUserId: u1, receivingUserId: u2 }, - { sendingUserId: u2, receivingUserId: u1 }, - ], - }, - }); - return rel[0] ? Ok(rel[0]) : Err(404); - } catch (e) { - return Err(e); - } +): Promise { + // FIXME: fix this findMany + const rel = await prisma.relationship.findMany({ + where: { + OR: [ + { sendingUserId: u1, receivingUserId: u2 }, + { sendingUserId: u2, receivingUserId: u1 }, + ], + }, + }); + return rel[0] ?? panic("not found"); } -export async function getRelations( - user: UserID, -): Promise> { - try { - const relations: Relationship[] = await prisma.relationship.findMany({ - where: { - OR: [{ sendingUserId: user }, { receivingUserId: user }], - }, - }); - return Ok(relations); - } catch (e) { - return Err(e); - } +export async function getRelations(user: UserID): Promise { + const relations: Relationship[] = await prisma.relationship.findMany({ + where: { + OR: [{ sendingUserId: user }, { receivingUserId: user }], + }, + }); + return relations; } // returns false if u1 or u2 is not present. export async function areMatched(u1: UserID, u2: UserID): Promise { const match = await getRelation(u1, u2); - if (!match.ok) return false; + if (!match) return false; - return match.value.status === "MATCHED"; + return match.status === "MATCHED"; } export async function areAllMatched( user: UserID, friends: UserID[], -): Promise> { - try { - return Ok( - ( - await asyncMap(friends, (friend) => { - return areMatched(user, friend); - }) - ).reduce((a, b) => a && b), - ); - } catch (e) { - return Err(e); - } +): Promise { + return ( + await asyncMap(friends, (friend) => { + return areMatched(user, friend); + }) + ).reduce((a, b) => a && b); } // 特定のユーザIDを含むマッチの取得 export async function getMatchesByUserId( userId: UserID, -): Promise> { - try { - const m = await prisma.relationship.findMany({ - where: { - AND: [ - { status: "MATCHED" }, - { OR: [{ sendingUserId: userId }, { receivingUserId: userId }] }, - ], - }, - }); - return Ok(m); - } catch (e) { - return Err(e); - } +): Promise { + return await prisma.relationship.findMany({ + where: { + AND: [ + { status: "MATCHED" }, + { OR: [{ sendingUserId: userId }, { receivingUserId: userId }] }, + ], + }, + }); } // マッチの削除 export async function deleteMatch( senderId: UserID, receiverId: UserID, -): Promise> { - try { - // 最初の条件で削除を試みる - const recordToDelete = await prisma.relationship.findUnique({ +): Promise { + // 最初の条件で削除を試みる + const recordToDelete = await prisma.relationship.findUnique({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, + }, + }, + }); + + if (recordToDelete) { + await prisma.relationship.update({ where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, + id: recordToDelete.id, + }, + data: { + status: "REJECTED", }, }); + return; + } - if (recordToDelete) { - await prisma.relationship.update({ - where: { - id: recordToDelete.id, - }, - data: { - status: "REJECTED", - }, - }); - return Ok(undefined); - } + // 次の条件で削除を試みる + const altRecordToDelete = await prisma.relationship.findUnique({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: receiverId, + receivingUserId: senderId, + }, + }, + }); - // 次の条件で削除を試みる - const altRecordToDelete = await prisma.relationship.findUnique({ + if (altRecordToDelete) { + await prisma.relationship.update({ where: { - sendingUserId_receivingUserId: { - sendingUserId: receiverId, - receivingUserId: senderId, - }, + id: altRecordToDelete.id, + }, + data: { + status: "REJECTED", }, }); - - if (altRecordToDelete) { - await prisma.relationship.update({ - where: { - id: altRecordToDelete.id, - }, - data: { - status: "REJECTED", - }, - }); - return Ok(undefined); - } - - // `No matching records found for senderId=${senderId} and receiverId=${receiverId}`, - return Err(404); - } catch (e) { - return Err(e); + return; } + + // `No matching records found for senderId=${senderId} and receiverId=${receiverId}`, + error("not found", 404); } diff --git a/server/src/database/picture.ts b/server/src/database/picture.ts index 7c3f45bc..b29a7226 100644 --- a/server/src/database/picture.ts +++ b/server/src/database/picture.ts @@ -1,4 +1,4 @@ -import { Err, Ok, type Result } from "common/lib/result"; +import { panic } from "common/lib/panic"; import type { GUID } from "common/types"; import { prisma } from "./client"; @@ -35,10 +35,7 @@ export async function getPic(hash: string, passkey: string) { * is safe to await. * @returns URL of the file. **/ -export async function setProf( - guid: GUID, - buf: Buffer, -): Promise> { +export async function setProf(guid: GUID, buf: Buffer): Promise { return prisma.avatar .upsert({ where: { @@ -50,23 +47,15 @@ export async function setProf( .then(() => { // ?update=${date} is necessary to let the browsers properly cache the image. const pictureUrl = `/picture/profile/${guid}?update=${new Date().getTime()}`; - return Ok(pictureUrl); - }) - .catch((err) => { - console.error("Error uploading file:", err); - return Err(err); + return pictureUrl; }); } // is await-safe. -export async function getProf(guid: GUID): Promise> { +export async function getProf(guid: GUID): Promise { return prisma.avatar .findUnique({ where: { guid }, }) - .then((res) => (res ? Ok(res.data) : Err(404))) - .catch((err) => { - console.log("Error reading db: ", err); - return Err(err); - }); + .then((res) => res?.data ?? panic("not found")); } diff --git a/server/src/database/requests.ts b/server/src/database/requests.ts index cb4228cc..c73a3cd0 100644 --- a/server/src/database/requests.ts +++ b/server/src/database/requests.ts @@ -1,4 +1,4 @@ -import { Err, Ok, type Result } from "common/lib/result"; +import { panic } from "common/lib/panic"; import type { Relationship, UserID, @@ -13,305 +13,260 @@ export async function sendRequest({ }: { senderId: UserID; receiverId: UserID; -}): Promise> { +}): Promise { // 既存の関係をチェック - try { - // TODO: fix this findFirst to be findUnique - const existingRelationship = await prisma.relationship.findFirst({ - where: { - OR: [ - { sendingUserId: senderId, receivingUserId: receiverId }, - { sendingUserId: receiverId, receivingUserId: senderId }, // 逆の関係もチェック - ], - }, - }); - // 既存の関係がある場合はそのまま返す - if (existingRelationship) { - // 相手がすでにこちらに Request を送っている場合は approve (accept) したとみなす - if (existingRelationship.receivingUserId === senderId) - approveRequest( - existingRelationship.sendingUserId, - existingRelationship.receivingUserId, - ); - return Ok(existingRelationship); - } - const newRelationship = await prisma.relationship.create({ - data: { - sendingUser: { connect: { id: senderId } }, - receivingUser: { connect: { id: receiverId } }, - status: "PENDING", - }, - }); - return Ok(newRelationship); - } catch (e) { - return Err(e); + // TODO: fix this findFirst to be findUnique + const existingRelationship = await prisma.relationship.findFirst({ + where: { + OR: [ + { sendingUserId: senderId, receivingUserId: receiverId }, + { sendingUserId: receiverId, receivingUserId: senderId }, // 逆の関係もチェック + ], + }, + }); + // 既存の関係がある場合はそのまま返す + if (existingRelationship) { + // 相手がすでにこちらに Request を送っている場合は approve (accept) したとみなす + if (existingRelationship.receivingUserId === senderId) + approveRequest( + existingRelationship.sendingUserId, + existingRelationship.receivingUserId, + ); + return existingRelationship; } + const newRelationship = await prisma.relationship.create({ + data: { + sendingUser: { connect: { id: senderId } }, + receivingUser: { connect: { id: receiverId } }, + status: "PENDING", + }, + }); + return newRelationship; } // マッチリクエストの承認 export async function approveRequest( senderId: UserID, receiverId: UserID, -): Promise> { - try { - const updated = await prisma.relationship.update({ - where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, - }, - data: { - status: "MATCHED", +): Promise { + const updated = await prisma.relationship.update({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, }, - }); - return updated === null ? Err(404) : Ok(updated); - } catch (e) { - return Err(e); - } + }, + data: { + status: "MATCHED", + }, + }); + return updated === null ? panic("not found") : updated; } // マッチリクエストの拒否 export async function rejectRequest( senderId: UserID, receiverId: UserID, -): Promise> { - try { - const rel = await prisma.relationship.update({ - where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, - }, - data: { - status: "REJECTED", +): Promise { + const rel = await prisma.relationship.update({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, }, - }); - return rel === null ? Err(404) : Ok(rel); - } catch (e) { - return Err(e); - } + }, + data: { + status: "REJECTED", + }, + }); + return rel === null ? panic("not found") : rel; } export async function cancelRequest( senderId: UserID, receiverId: UserID, -): Promise> { - try { - return prisma.relationship - .delete({ - where: { - sendingUserId_receivingUserId: { - sendingUserId: senderId, - receivingUserId: receiverId, - }, +): Promise { + return prisma.relationship + .delete({ + where: { + sendingUserId_receivingUserId: { + sendingUserId: senderId, + receivingUserId: receiverId, }, - }) - .then(() => Ok(undefined)) - .catch((err) => Err(err)); - } catch (err) { - return Err(err); - } + }, + }) + .then(); } //ユーザーへのリクエストを探す 俺をリクエストしているのは誰だ export async function getPendingRequestsToUser( userId: UserID, -): Promise> { - try { - const found = await prisma.user.findMany({ - where: { - sendingUsers: { - some: { - receivingUserId: userId, - status: "PENDING", - }, +): Promise { + const found = await prisma.user.findMany({ + where: { + sendingUsers: { + some: { + receivingUserId: userId, + status: "PENDING", }, }, - include: { - enrollments: { - include: { - course: { - include: { - enrollments: true, - slots: true, - }, + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, }, }, }, - interests: { - include: { - subject: true, - }, + }, + interests: { + include: { + subject: true, }, }, - }); - return Ok( - found.map((user) => { - return { - ...user, - interestSubjects: user.interests.map((interest) => { - return interest.subject; - }), - courses: user.enrollments.map((enrollment) => { - return enrollment.course; - }), - }; + }, + }); + return found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; }), - ); - } catch (e) { - return Err(e); - } + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); } //ユーザーがリクエストしている人を探す 俺がリクエストしているのは誰だ export async function getPendingRequestsFromUser( userId: UserID, -): Promise> { - try { - const found = await prisma.user.findMany({ - where: { - receivingUsers: { - some: { - sendingUserId: userId, - status: "PENDING", - }, +): Promise { + const found = await prisma.user.findMany({ + where: { + receivingUsers: { + some: { + sendingUserId: userId, + status: "PENDING", }, }, - include: { - enrollments: { - include: { - course: { - include: { - enrollments: true, - slots: true, - }, + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, }, }, }, - interests: { - include: { - subject: true, - }, + }, + interests: { + include: { + subject: true, }, }, - }); - return Ok( - found.map((user) => { - return { - ...user, - interestSubjects: user.interests.map((interest) => { - return interest.subject; - }), - courses: user.enrollments.map((enrollment) => { - return enrollment.course; - }), - }; + }, + }); + return found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; }), - ); - } catch (e) { - return Err(e); - } + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); } //マッチした人の取得 export async function getMatchedUser( userId: UserID, -): Promise> { - try { - const found = await prisma.user.findMany({ - where: { - OR: [ - { - sendingUsers: { - some: { - receivingUserId: userId, - status: "MATCHED", - }, +): Promise { + const found = await prisma.user.findMany({ + where: { + OR: [ + { + sendingUsers: { + some: { + receivingUserId: userId, + status: "MATCHED", }, }, - { - receivingUsers: { - some: { - sendingUserId: userId, - status: "MATCHED", - }, + }, + { + receivingUsers: { + some: { + sendingUserId: userId, + status: "MATCHED", }, }, - ], - }, - include: { - enrollments: { - include: { - course: { - include: { - enrollments: true, - slots: true, - }, + }, + ], + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, }, }, }, - interests: { - include: { - subject: true, - }, + }, + interests: { + include: { + subject: true, }, }, - }); - return Ok( - found.map((user) => { - return { - ...user, - interestSubjects: user.interests.map((interest) => { - return interest.subject; - }), - courses: user.enrollments.map((enrollment) => { - return enrollment.course; - }), - }; + }, + }); + return found.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; }), - ); - } catch (e) { - return Err(e); - } + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; + }); } export async function getMatchedRelations( userId: UserID, -): Promise> { - try { - const found = await prisma.relationship.findMany({ - where: { - status: "MATCHED", - OR: [ - { - sendingUserId: userId, - }, - { - receivingUserId: userId, - }, - ], - }, - }); - return Ok(found); - } catch (e) { - return Err(e); - } +): Promise { + const found = await prisma.relationship.findMany({ + where: { + status: "MATCHED", + OR: [ + { + sendingUserId: userId, + }, + { + receivingUserId: userId, + }, + ], + }, + }); + return found; } export async function matchWithMemo(userId: UserID) { - try { - const result = await prisma.relationship.create({ - data: { - status: "MATCHED", - sendingUserId: userId, - receivingUserId: 0, //KeepメモのUserId - }, - }); - - return result; - } catch (error) { - return Err(error); - } + await prisma.relationship.create({ + data: { + status: "MATCHED", + sendingUserId: userId, + receivingUserId: 0, //KeepメモのUserId + }, + }); } diff --git a/server/src/database/users.ts b/server/src/database/users.ts index 3d008bd3..e009746c 100644 --- a/server/src/database/users.ts +++ b/server/src/database/users.ts @@ -1,4 +1,4 @@ -import { Err, Ok, type Result } from "common/lib/result"; +import { error, panic } from "common/lib/panic"; import type { Course, GUID, @@ -11,231 +11,195 @@ import type { import { prisma } from "./client"; // ユーザーの作成 -export async function createUser( - partialUser: Omit, -): Promise> { - try { - const newUser = await prisma.user.create({ - data: partialUser, - }); - return Ok(newUser); - } catch (e) { - return Err(e); - } +export async function createUser(partialUser: Omit): Promise { + const newUser = await prisma.user.create({ + data: partialUser, + }); + return newUser; } // ユーザーの取得 -export async function getUser( - guid: GUID, -): Promise> { - try { - const user = await prisma.user.findUnique({ - where: { - guid: guid, - }, - include: { - enrollments: { - include: { - course: { - include: { - enrollments: true, - slots: true, - }, +export async function getUser(guid: GUID): Promise { + const user = await prisma.user.findUnique({ + where: { + guid: guid, + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, }, }, }, - interests: { - include: { - subject: true, - }, + }, + interests: { + include: { + subject: true, }, }, - }); - if (!user) return Err(404); - return Ok({ - ...user, - interestSubjects: user.interests.map((interest) => { - return interest.subject; - }), - courses: user.enrollments.map((enrollment) => { - return enrollment.course; - }), - }); - } catch (e) { - return Err(e); - } + }, + }); + if (!user) error("not found", 404); + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; } -export async function getGUIDByUserID(id: UserID): Promise> { +export async function getGUIDByUserID(id: UserID): Promise { return prisma.user .findUnique({ where: { id }, select: { guid: true }, }) - .then((v) => (v ? Ok(v.guid) : Err(404))) - .catch((e) => Err(e)); + .then((v) => v?.guid ?? panic("not found")); } -export async function getUserIDByGUID(guid: GUID): Promise> { +export async function getUserIDByGUID(guid: GUID): Promise { return prisma.user .findUnique({ where: { guid }, select: { id: true }, }) - .then((res) => res?.id) - .then((id) => (id ? Ok(id) : Err(404))) - .catch((err) => Err(err)); + .then((res) => res?.id ?? panic("not found")); } export async function getUserByID( id: UserID, -): Promise> { - try { - const user = await prisma.user.findUnique({ - where: { - id, - }, - include: { - enrollments: { - include: { - course: { - include: { - enrollments: true, - slots: true, - }, +): Promise { + const user = await prisma.user.findUnique({ + where: { + id, + }, + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, }, }, }, - interests: { - include: { - subject: true, - }, + }, + interests: { + include: { + subject: true, }, }, - }); - if (!user) return Err(404); - return Ok({ - ...user, - interestSubjects: user.interests.map((interest) => { - return interest.subject; - }), - courses: user.enrollments.map((enrollment) => { - return enrollment.course; - }), - }); - } catch (e) { - return Err(e); - } + }, + }); + if (!user) throw new Error("not found"); + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; + }), + }; } // ユーザーの更新 export async function updateUser( userId: UserID, partialUser: Partial, -): Promise> { +): Promise { // undefined means do nothing to this field // https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined#use-case-null-and-undefined-in-a-graphql-resolver - try { - if (!partialUser.pictureUrl) partialUser.pictureUrl = undefined; // don't delete picture if not provided - const updateUser = { - id: undefined, - guid: undefined, - ...partialUser, - }; - const updatedUser = await prisma.user.update({ - where: { id: userId }, - data: updateUser, - }); - return updatedUser === null ? Err(404) : Ok(updatedUser); - } catch (e) { - return Err(e); - } + if (!partialUser.pictureUrl) partialUser.pictureUrl = undefined; // don't delete picture if not provided + const updateUser = { + id: undefined, + guid: undefined, + ...partialUser, + }; + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: updateUser, + }); + return updatedUser ?? panic("not found"); } // ユーザーの削除 -export async function deleteUser(userId: UserID): Promise> { - try { - const deletedUser = await prisma.user.delete({ - where: { id: userId }, - }); - return deletedUser === null ? Err(404) : Ok(deletedUser); - } catch (e) { - return Err(e); - } +export async function deleteUser(userId: UserID): Promise { + const deletedUser = await prisma.user.delete({ + where: { id: userId }, + }); + return deletedUser ?? panic("not found"); } // ユーザーの全取得 export async function getAllUsers(): Promise< - Result<(User & { courses: Course[]; interestSubjects: InterestSubject[] })[]> + (User & { courses: Course[]; interestSubjects: InterestSubject[] })[] > { - try { - const users = await prisma.user.findMany({ - include: { - enrollments: { - include: { - course: { - include: { - enrollments: true, - slots: true, - }, + const users = await prisma.user.findMany({ + include: { + enrollments: { + include: { + course: { + include: { + enrollments: true, + slots: true, }, }, }, - interests: { - include: { - subject: true, - }, + }, + interests: { + include: { + subject: true, }, }, - }); - return Ok( - users.map((user) => { - return { - ...user, - interestSubjects: user.interests.map((interest) => { - return interest.subject; - }), - courses: user.enrollments.map((enrollment) => { - return enrollment.course; - }), - }; + }, + }); + return users.map((user) => { + return { + ...user, + interestSubjects: user.interests.map((interest) => { + return interest.subject; + }), + courses: user.enrollments.map((enrollment) => { + return enrollment.course; }), - ); - } catch (e) { - return Err(e); - } + }; + }); } // TODO: FIXME: currently also showing users that the requester has already sent request to, to not change behavior. // but this is probably not ideal. consider only showing people with no relation. // (or just remove this function and use recommended() instead) -export async function unmatched(id: UserID): Promise> { - return prisma.user - .findMany({ - where: { - AND: [ - { - receivingUsers: { - none: { - sendingUserId: id, - status: "MATCHED", - }, +export async function unmatched(id: UserID): Promise { + return prisma.user.findMany({ + where: { + AND: [ + { + receivingUsers: { + none: { + sendingUserId: id, + status: "MATCHED", }, }, - { - sendingUsers: { - none: { - receivingUserId: id, - status: "MATCHED", - }, + }, + { + sendingUsers: { + none: { + receivingUserId: id, + status: "MATCHED", }, }, - { - NOT: { - id: id, - }, + }, + { + NOT: { + id: id, }, - ], - }, - }) - .then((res) => Ok(res)) - .catch((err) => Err(err)); + }, + ], + }, + }); } diff --git a/server/src/firebase/auth/db.ts b/server/src/firebase/auth/db.ts index 769e3c44..2344c5d8 100644 --- a/server/src/firebase/auth/db.ts +++ b/server/src/firebase/auth/db.ts @@ -1,9 +1,8 @@ -import { PrismaClient } from "@prisma/client"; -import { Err, Ok, type Result } from "common/lib/result"; import type { IDToken, UserID } from "common/types"; import type { Request } from "express"; import { getGUID, getGUIDFromToken } from "./lib"; +import { error } from "common/lib/panic"; import { prisma } from "../../database/client"; /** * REQUIRE: cookieParser middleware before this @@ -28,7 +27,7 @@ export async function getUserId(req: Request): Promise { id: true, }, }); - if (!user) throw new Error("User not found!"); + if (!user) error("auth error: unauthorized", 401); return user.id; } @@ -43,24 +42,6 @@ export async function getUserIdFromToken(token: IDToken): Promise { return user.id; } -/** - * never throws. - * Expected use case: - * ```js - * const result = await safeGetUserId(req); - * if (!result.ok) - * return res.status(401).send("auth error"); - * const userId = result.value; - * ``` - **/ -export async function safeGetUserId(req: Request): Promise> { - try { - return Ok(await getUserId(req)); - } catch (e) { - return Err(e); - } -} - /** returns true if userid is requester's id. * otherwise returns false. * never throws. @@ -75,9 +56,9 @@ export async function isRequester( req: Request, userid: UserID, ): Promise { - const result = await safeGetUserId(req); - if (!result.ok) return false; - if (result.value !== userid) return false; - - return true; + try { + return (await getUserId(req)) === userid; + } catch (_) { + return false; + } } diff --git a/server/src/firebase/auth/lib.ts b/server/src/firebase/auth/lib.ts index 20d066b7..a82a96b8 100644 --- a/server/src/firebase/auth/lib.ts +++ b/server/src/firebase/auth/lib.ts @@ -1,4 +1,3 @@ -import { Err, Ok, type Result } from "common/lib/result"; import type { GUID, IDToken } from "common/types"; import type { Request } from "express"; import * as admin from "firebase-admin/auth"; @@ -29,14 +28,6 @@ if (process.env.UNSAFE_SKIP_AUTH) { }; } -export async function safeGetGUID(req: Request): Promise> { - try { - return Ok(await getGUID(req)); - } catch (e) { - return Err(e); - } -} - export async function verifyIDToken(idToken: IDToken): Promise { if (!idToken) throw new Error( diff --git a/server/src/functions/chat.ts b/server/src/functions/chat.ts index 19c1659e..a86fe98f 100644 --- a/server/src/functions/chat.ts +++ b/server/src/functions/chat.ts @@ -1,4 +1,3 @@ -import type { Result } from "common/lib/result"; import type { InitRoom, SharedRoom, UserID } from "common/types"; import type { DMRoom, @@ -17,23 +16,21 @@ import * as http from "./share/http"; export async function getOverview( id: number, ): Promise> { - const overview: Result = await db.getOverview(id); - if (!overview.ok) { - console.error(overview.error); + try { + const overview: RoomOverview[] = await db.getOverview(id); + return { + ok: true, + code: 200, + body: overview, + }; + } catch (err) { + console.error(err); return { ok: false, code: 500, - body: overview.error as string, + body: (err as Error).message, }; } - - return { - ok: true, - code: 200, - body: overview.value, - }; - // SEND: RoomOverview[]. - // this is NOT ordered. you need to sort it on frontend. } export async function sendDM( @@ -42,7 +39,7 @@ export async function sendDM( send: SendMessage, ): Promise> { const rel = await getRelation(from, to); - if (!rel.ok || rel.value.status === "REJECTED") + if (rel.status === "REJECTED") return http.forbidden( "You cannot send a message because the friendship request was rejected.", ); @@ -55,9 +52,9 @@ export async function sendDM( ...send, }; - const result = await db.sendDM(rel.value.id, msg); - if (!result.ok) return http.internalError("Failed to send DM"); - return http.created(result.value); + const result = await db.sendDM(rel.id, msg); + if (!result) return http.internalError("Failed to send DM"); + return http.created(result); } export async function getDM( @@ -65,31 +62,26 @@ export async function getDM( friend: UserID, ): Promise> { const rel = await getRelation(user, friend); - if (!rel.ok || rel.value.status === "REJECTED") + if (rel.status === "REJECTED") return http.forbidden("cannot send to rejected-friend"); const room = await db.getDMbetween(user, friend); - if (!room.ok) return http.internalError(); const friendData = await getUserByID(friend); - if (!friendData.ok) return http.notFound("friend not found"); - const unreadCount = db.unreadMessages(user, rel.value.id).then((val) => { - if (val.ok) return val.value; - throw val.error; - }); + const unreadCount = db.unreadMessages(user, rel.id); const personalized: PersonalizedDMRoom & DMRoom = { unreadMessages: await unreadCount, - friendId: friendData.value.id, - name: friendData.value.name, - thumbnail: friendData.value.pictureUrl, + friendId: friendData.id, + name: friendData.name, + thumbnail: friendData.pictureUrl, matchingStatus: - rel.value.status === "MATCHED" + rel.status === "MATCHED" ? "matched" - : rel.value.sendingUserId === user //どっちが送ったリクエストなのかを判定 + : rel.sendingUserId === user //どっちが送ったリクエストなのかを判定 ? "myRequest" : "otherRequest", - ...room.value, + ...room, }; return http.ok(personalized); @@ -100,32 +92,25 @@ export async function createRoom( init: InitRoom, ): Promise> { const allMatched = await areAllMatched(creator, init.members); - if (!allMatched.ok) return http.unauthorized("db error"); + if (!allMatched) return http.unauthorized("db error"); - if (!allMatched.value) + if (!allMatched) return http.forbidden("some members are not matched with you"); const room = await db.createSharedRoom(init); - if (!room.ok) return http.internalError("failed to create"); + if (!room) return http.internalError("failed to create"); - return http.created(room.value); + return http.created(room); } export async function getRoom( user: UserID, roomId: ShareRoomID, ): Promise> { - const userInRoom = await db.isUserInRoom(roomId, user); - - if (!userInRoom.ok) return http.internalError("db error"); - if (!userInRoom.value) + if (!(await db.isUserInRoom(roomId, user))) return http.unauthorized("you don't belong to that room"); - const room = await db.getSharedRoom(roomId); - if (!room.ok) return http.internalError(); - if (!room.value) return http.notFound(); - - return http.ok(room.value); + return http.ok(room); } export async function patchRoom( @@ -134,11 +119,8 @@ export async function patchRoom( newRoom: SharedRoom, ): Promise>> { if (!(await db.isUserInRoom(roomId, user))) return http.forbidden(); - const room = await db.updateRoom(roomId, newRoom); - if (!room.ok) return http.internalError(); - - return http.created(room.value); + return http.created(room); } export async function inviteUserToRoom( @@ -148,12 +130,8 @@ export async function inviteUserToRoom( ): Promise>> { if (!(await areAllMatched(requester, invited))) return http.forbidden("some of the members are not friends with you"); - const room = await db.inviteUserToSharedRoom(roomId, invited); - - if (!room.ok) return http.internalError(); - - return http.ok(room.value); + return http.ok(room); } export async function updateMessage( @@ -162,12 +140,8 @@ export async function updateMessage( content: string, ): Promise> { const old = await db.getMessage(messageId as MessageID); - if (!old.ok) return http.notFound("couldn't find message"); - if (old.value.creator !== requester) + if (old.creator !== requester) return http.forbidden("cannot edit others' message"); - const msg = await db.updateMessage(messageId, content); - if (!msg.ok) return http.internalError(); - - return http.ok(msg.value); + return http.ok(msg); } diff --git a/server/src/functions/engines/recommendation.test.ts b/server/src/functions/engines/recommendation.test.ts index a92f409d..78444dd1 100644 --- a/server/src/functions/engines/recommendation.test.ts +++ b/server/src/functions/engines/recommendation.test.ts @@ -8,14 +8,11 @@ beforeAll(() => { test("recommendation engine", async () => { const usersFor101 = await recommendedTo(101, 5, 0); - if (!usersFor101.ok) throw console.error(usersFor101.error); - expect(usersFor101.value.map((entry) => entry.u.id)).toEqual([102, 103]); + expect(usersFor101.map((entry) => entry.u.id)).toEqual([102, 103]); const usersFor102 = await recommendedTo(102, 5, 0); - if (!usersFor102.ok) throw console.error(usersFor102.error); - expect(usersFor102.value.map((entry) => entry.u.id)).toEqual([103, 101]); + expect(usersFor102.map((entry) => entry.u.id)).toEqual([103, 101]); const usersFor103 = await recommendedTo(103, 5, 0); - if (!usersFor103.ok) throw console.error(usersFor103.error); - expect(usersFor103.value.map((entry) => entry.u.id)).toEqual([102, 101]); + expect(usersFor103.map((entry) => entry.u.id)).toEqual([102, 101]); }); diff --git a/server/src/functions/engines/recommendation.ts b/server/src/functions/engines/recommendation.ts index 8fd4e2dc..64222a0a 100644 --- a/server/src/functions/engines/recommendation.ts +++ b/server/src/functions/engines/recommendation.ts @@ -1,5 +1,4 @@ -import { recommend as sql } from "@prisma/client/sql"; -import { Err, Ok, type Result } from "common/lib/result"; +import { recommend } from "@prisma/client/sql"; import type { UserID, UserWithCoursesAndSubjects } from "common/types"; import { prisma } from "../../database/client"; import { getCoursesByUserId } from "../../database/courses"; @@ -11,39 +10,28 @@ export async function recommendedTo( limit: number, offset: number, ): Promise< - Result< - Array<{ - u: UserWithCoursesAndSubjects; - count: number; - }> - > + Array<{ + u: UserWithCoursesAndSubjects; + count: number; + }> > { - try { - const result = await prisma.$queryRawTyped(sql(user, limit, offset)); - return Promise.all( - result.map(async (res) => { - const { overlap: count, ...u } = res; - if (count === null) - throw new Error("count is null: something is wrong"); - // TODO: user の情報はここで再度 DB に問い合わせるのではなく、 recommend の sql で取得 - const user = await getUserByID(u.id); - if (!user.ok) throw new Error("user not found"); - const courses = getCoursesByUserId(u.id); - const subjects = interest.of(u.id); - return { - count: Number(count), - u: { - ...user.value, - courses: await courses, - interestSubjects: await subjects, - }, - }; - }), - ) - .then((val) => Ok(val)) - .catch((err) => Err(err)); - } catch (err) { - console.error("caught error: ", err); - return Err(500); - } + const result = await prisma.$queryRawTyped(recommend(user, limit, offset)); + return Promise.all( + result.map(async (res) => { + const { overlap: count, ...u } = res; + if (count === null) throw new Error("count is null: something is wrong"); + // TODO: user の情報はここで再度 DB に問い合わせるのではなく、 recommend の sql で取得 + const user = await getUserByID(u.id); + const courses = getCoursesByUserId(u.id); + const subjects = interest.of(u.id); + return { + count: Number(count), + u: { + ...user, + courses: await courses, + interestSubjects: await subjects, + }, + }; + }), + ); } diff --git a/server/src/functions/img/compress.ts b/server/src/functions/img/compress.ts index b07baccc..e4947158 100644 --- a/server/src/functions/img/compress.ts +++ b/server/src/functions/img/compress.ts @@ -1,17 +1,7 @@ -import { Err, Ok, type Result } from "common/lib/result"; import sharp from "sharp"; const IMAGE_SIZE_PX = 320; -export async function compressImage(buf: Buffer): Promise> { - try { - return sharp(buf) - .resize(IMAGE_SIZE_PX) - .webp() - .toBuffer() - .then((buf) => Ok(buf)) - .catch((e) => Err(e)); - } catch (e) { - return Err(e); - } +export async function compressImage(buf: Buffer): Promise { + return sharp(buf).resize(IMAGE_SIZE_PX).webp().toBuffer(); } diff --git a/server/src/functions/user.ts b/server/src/functions/user.ts index 228edc6e..fd641e30 100644 --- a/server/src/functions/user.ts +++ b/server/src/functions/user.ts @@ -10,41 +10,27 @@ import * as http from "./share/http"; export async function getAllUsers(): Promise> { const users = await db.getAllUsers(); - if (!users.ok) { - console.error(users.error); - return http.internalError(); - } - return http.ok(users.value); + return http.ok(users); } export async function getUser( guid: GUID, ): Promise> { const user = await db.getUser(guid); - if (!user.ok) { - if (user.error === 404) return http.notFound(); - console.error(user.error); - return http.internalError(); - } - return http.ok(user.value); + return http.ok(user); } export async function getUserByID( userId: UserID, ): Promise> { const user = await db.getUserByID(userId); - if (!user.ok) { - if (user.error === 404) return http.notFound(); - console.error(user.error); - return http.internalError(); - } - return http.ok(user.value); + return http.ok(user); } export async function userExists(guid: GUID): Promise> { const user = await db.getUser(guid); - if (user.ok) return http.ok(undefined); - if (user.error === 404) return http.notFound(undefined); + if (user) return http.ok(undefined); + if (user === 404) return http.notFound(undefined); return http.internalError("db error"); } @@ -52,7 +38,7 @@ export async function getMatched( user: UserID, ): Promise> { const matchedUsers = await getMatchedUser(user); - if (!matchedUsers.ok) return http.internalError(); + if (!matchedUsers) return http.internalError(); - return http.ok(matchedUsers.value); + return http.ok(matchedUsers); } diff --git a/server/src/index.ts b/server/src/index.ts index 0643cf3e..6739a6f1 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,5 +1,6 @@ +import { error } from "common/lib/panic"; import cookieParser from "cookie-parser"; -import express from "express"; +import express, { type Response } from "express"; import csrf from "./lib/cross-origin/block-unknown-origin"; import cors from "./lib/cross-origin/multi-origin-cors"; import { initializeSocket } from "./lib/socket/socket"; @@ -11,6 +12,7 @@ import pictureRoutes from "./router/picture"; import requestsRoutes from "./router/requests"; import subjectsRoutes from "./router/subjects"; import usersRoutes from "./router/users"; +import "express-async-errors"; const app = express(); @@ -18,6 +20,37 @@ const app = express(); // https://expressjs.com/ja/api.html#app.settings.table の query parser を参照。 app.set("query parser", "simple"); +// I don't understand any of those +// https://expressjs.com/en/guide/error-handling.html +// https://qiita.com/nyandora/items/cd4f12eb62295c10269c +// https://note.shiftinc.jp/n/n42b96d36f0cf +// エラーハンドラを Express に管理させる。 +// TEMPORARY: will be replaced by hono's `onError`. +app.use(async (err: unknown, _: unknown, res: unknown, next: unknown) => { + try { + if (typeof (err as Error)?.cause === "number") { + (res as Response) + .status((err as Error).cause as number) + .send((err as Error).message); + } else { + console.error(err); + (res as Response).status(500).send("Internal Error"); + } + await (next as () => Promise)(); + } catch (err) { + console.log("[ERR] failed to handle error:", err); + try { + (res as Response).status(500).send("Internal error"); + } catch (err) { + console.log("[ERR] failed to handle error twice:", err); + } + } finally { + try { + (res as Response).end("\n"); + } catch {} + } +}); + const port = process.env.PORT || 3000; const allowedOrigins = ( process.env.CORS_ALLOW_ORIGINS || panic("env CORS_ALLOW_ORIGINS is missing") @@ -26,7 +59,7 @@ const allowedOrigins = ( .filter((s) => s); // ignore empty string (trailing comma?) allUrlMustBeValid(allowedOrigins); -export const corsOptions = { +const corsOptions = { origins: allowedOrigins, methods: ["GET", "HEAD", "POST", "PUT", "DELETE"], credentials: true, diff --git a/server/src/lib/utils.ts b/server/src/lib/utils.ts index 8eddd99d..f4cf0e3a 100644 --- a/server/src/lib/utils.ts +++ b/server/src/lib/utils.ts @@ -1,6 +1,4 @@ -export function panic(reason: string): never { - throw new Error(`function panic() called for reason: "${reason}"`); -} +export { panic } from "common/lib/panic"; export function allUrlMustBeValid(urls: string[]) { for (const url of urls) { diff --git a/server/src/router/chat.ts b/server/src/router/chat.ts index cf4381bb..20c57d3e 100644 --- a/server/src/router/chat.ts +++ b/server/src/router/chat.ts @@ -1,4 +1,4 @@ -import { safeParseInt } from "common/lib/result/safeParseInt"; +import { panic } from "common/lib/panic"; import type { MessageID, UserID } from "common/types"; import { parseUserID } from "common/zod/methods"; import { @@ -8,52 +8,42 @@ import { SharedRoomSchema, } from "common/zod/schemas"; import express from "express"; +import { z } from "zod"; import * as db from "../database/chat"; import { getRelation } from "../database/matches"; -import { getUserId, safeGetUserId } from "../firebase/auth/db"; +import { getUserId } from "../firebase/auth/db"; import * as core from "../functions/chat"; import * as ws from "../lib/socket/socket"; const router = express.Router(); router.get("/overview", async (req, res) => { - const id = await safeGetUserId(req); - if (!id.ok) return res.status(401).send("auth error"); + const id = await getUserId(req); - const result = await core.getOverview(id.value); + const result = await core.getOverview(id); res.status(result.code).send(result.body); }); // send DM to userId. router.post("/dm/to/:userid", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const friend = safeParseInt(req.params.userid); - if (!friend.ok) return res.status(400).send("bad param encoding: `userId`"); - - const send = SendMessageSchema.safeParse(req.body); - if (!send.success) { - return res.status(400).send("invalid format"); - } - - const result = await core.sendDM(user.value, friend.value, send.data); + const user = await getUserId(req); + const friend = + Number.parseInt(req.params.userid) ?? panic("bad param encoding: `userId`"); + const send = SendMessageSchema.parse(req.body); + const result = await core.sendDM(user, friend, send); if (result.ok) { - ws.sendMessage(result.body, friend.value); + ws.sendMessage(result.body, friend); } res.status(result.code).send(result.body); }); // GET a DM Room with userId, CREATE one if not found. router.get("/dm/with/:userid", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - - const friend = safeParseInt(req.params.userid); - if (!friend.ok) - return res.status(400).send("invalid param `userId` formatting"); - - const result = await core.getDM(user.value, friend.value); - + const user = await getUserId(req); + const friend = + Number.parseInt(req.params.userid) ?? + panic("invalid param `userId` formatting"); + const result = await core.getDM(user, friend); return res.status(result.code).send(result.body); }); @@ -61,34 +51,25 @@ router.post("/mark-as-read/:rel/:messageId", async (req, res) => { const user = await getUserId(req); const message = Number.parseInt(req.params.messageId); const rel = Number.parseInt(req.params.rel); - try { - await db.markAsRead(rel, user, message); - return res.status(200).end("ok"); - } catch (err) { - return res.status(304).end("already marked"); - } + await db.markAsRead(rel, user, message); + return res.status(200).end("ok"); }); // create a shared chat room. router.post("/shared", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - - const init = InitRoomSchema.safeParse(req.body); - if (!init.success) return res.status(400).send("invalid format"); - - const result = await core.createRoom(user.value, init.data); - + const user = await getUserId(req); + const init = InitRoomSchema.parse(req.body); + const result = await core.createRoom(user, init); return res.status(result.code).send(result.body); }); router.get("/shared/:roomId", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const roomId = safeParseInt(req.params.roomId); - if (!roomId.ok) return res.status(400).send("invalid formatting of :roomId"); + const user = await getUserId(req); + const roomId = + Number.parseInt(req.params.roomId) ?? + panic("invalid formatting of :roomId"); - const result = await core.getRoom(user.value, roomId.value); + const result = await core.getRoom(user, roomId); return res.status(result.code).send(result.body); }); @@ -97,49 +78,35 @@ router.get("/shared/:roomId", async (req, res) => { * - body: UpdateRoom **/ router.patch("/shared/:room", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const roomId = safeParseInt(req.params.room); - if (!roomId.ok) return res.status(400).send("invalid :room"); - - const room = SharedRoomSchema.safeParse(req.body); - if (!room.success) return res.status(400).send("invalid format"); - - // todo: type check - const result = await core.patchRoom(user.value, roomId.value, room.data); + const user = await getUserId(req); + const roomId = + Number.parseInt(req.params.room) ?? panic("invalid param: room"); + const room = SharedRoomSchema.parse(req.body); + const result = await core.patchRoom(user, roomId, room); res.status(result.code).send(result.body); }); // POST: authorized body=UserID[] router.post("/shared/id/:room/invite", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const roomId = safeParseInt(req.params.room); - if (!roomId.ok) return res.status(400).send("invalid :room"); - - const invited: UserID[] = req.body; - try { - if (!Array.isArray(invited)) throw new TypeError(); - invited.map(parseUserID); - } catch (_) { - return res.status(400).send("bad formatting"); - } + const user = await getUserId(req); + const roomId = + Number.parseInt(req.params.room) ?? panic("invalid param: room"); + + const invited = z.array(z.number()).parse(req.body); + invited.map(parseUserID); - const result = await core.inviteUserToRoom(user.value, invited, roomId.value); + const result = await core.inviteUserToRoom(user, invited, roomId); return res.status(result.code).send(result.body); }); router.patch("/messages/id/:id", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const id = safeParseInt(req.params.id); - if (!id.ok) return res.status(400).send("invalid :id"); - const friend = req.body.friend; + const user = await getUserId(req); + const id = Number.parseInt(req.params.id) ?? panic("invalid param: id"); + const friend = z.number().parse(req.body.friend); - const content = ContentSchema.safeParse(req.body.newMessage.content); - if (!content.success) return res.status(400).send(); + const content = ContentSchema.parse(req.body.newMessage.content); - const result = await core.updateMessage(user.value, id.value, content.data); + const result = await core.updateMessage(user, id, content); res.status(result.code).send(result.body); if (result.ok) { ws.updateMessage(result.body, friend); @@ -147,14 +114,11 @@ router.patch("/messages/id/:id", async (req, res) => { }); router.delete("/messages/id/:id", async (req, res) => { - const user = await safeGetUserId(req); - if (!user.ok) return res.status(401).send("auth error"); - const id = safeParseInt(req.params.id); - if (!id.ok) return res.status(400).send("bad `id` format"); - const friend = req.body.friend; - - await db.deleteMessage(id.value as MessageID, user.value); - ws.deleteMessage(id.value, friend); + const user = await getUserId(req); + const id = Number.parseInt(req.params.id) ?? panic("invalid param: id"); + const friend = z.number().parse(req.body.friend); + await db.deleteMessage(id as MessageID, user); + ws.deleteMessage(id, friend); return res.status(204).send(); }); diff --git a/server/src/router/courses.ts b/server/src/router/courses.ts index 4706be1a..fcbee541 100644 --- a/server/src/router/courses.ts +++ b/server/src/router/courses.ts @@ -1,6 +1,6 @@ -import type { Day } from "common/types"; import { DaySchema, PeriodSchema } from "common/zod/schemas"; import express, { type Request, type Response } from "express"; +import { z } from "zod"; import { getCourseByCourseId, getCourseByDayPeriodAndUserId, @@ -8,147 +8,73 @@ import { getCoursesByUserId, } from "../database/courses"; import { createEnrollment, deleteEnrollment } from "../database/enrollments"; -import { safeGetUserId } from "../firebase/auth/db"; +import { getUserId } from "../firebase/auth/db"; const router = express.Router(); -function isDay(value: string): value is Day { - return DaySchema.safeParse(value).success; -} - // ある曜限に存在する全ての講義を取得 router.get("/day-period", async (req: Request, res: Response) => { - const day = DaySchema.safeParse(req.query.day); + const day = DaySchema.parse(req.query.day); // TODO: as の使用をやめ、Request 型を適切に拡張する https://stackoverflow.com/questions/63538665/how-to-type-request-query-in-express-using-typescript - const period = PeriodSchema.safeParse( + const period = PeriodSchema.parse( Number.parseInt(req.query.period as string), ); - if (!day.success || !period.success || !isDay(day.data)) { - return res.status(400).json({ error: "Invalid day" }); - } - - try { - const courses = await getCoursesByDayAndPeriod(day.data, period.data); - res.status(200).json(courses); - } catch (error) { - console.error("Error fetching courses by day and period:", error); - res - .status(500) - .json({ error: "Failed to fetch courses by day and period" }); - } + const courses = await getCoursesByDayAndPeriod(day, period); + res.status(200).json(courses); }); // 特定のユーザが履修している講義を取得 router.get("/userId/:userId", async (req: Request, res: Response) => { - const userId = Number.parseInt(req.params.userId); - if (Number.isNaN(userId)) { - return res.status(400).json({ error: "Invalid userId" }); - } + const userId = + Number.parseInt(req.params.userId) ?? + res.status(400).json({ error: "Invalid userId" }); - try { - const courses = await getCoursesByUserId(userId); - res.status(200).json(courses); - } catch (error) { - console.error("Error fetching courses by userId:", error); - res.status(500).json({ error: "Failed to fetch courses by userId" }); - } + const courses = await getCoursesByUserId(userId); + res.status(200).json(courses); }); // 自分が履修している講義を取得 router.get("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - - try { - const courses = await getCoursesByUserId(userId.value); - return res.status(200).json(courses); - } catch (error) { - console.error("Error fetching courses:", error); - res.status(500).json({ error: "Failed to fetch courses" }); - } + const userId = await getUserId(req); + const courses = await getCoursesByUserId(userId); + return res.status(200).json(courses); }); // ある講義と重複している自分の講義を取得 router.get("/mine/overlaps/:courseId", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - - try { - const targetCourse = await getCourseByCourseId(req.params.courseId); - if (!targetCourse) { - return res.status(404).json({ error: "Course not found" }); - } - const overlappingCourses = await Promise.all( - targetCourse.slots.map( - async (slot) => - await getCourseByDayPeriodAndUserId( - slot.day, - slot.period, - userId.value, - ), - ), - ); - const filteredOverlappingCourses = overlappingCourses.filter( - (course) => course !== null, - ); - const uniqueFilteredOverlappingCourses = filteredOverlappingCourses.filter( - (course, index, self) => - self.findIndex((c) => c?.id === course?.id) === index, - ); // id の重複を排除 - res.status(200).json(uniqueFilteredOverlappingCourses); - } catch (error) { - console.error("Error fetching overlapping courses:", error); - res.status(500).json({ error: "Failed to fetch overlapping courses" }); - } + const userId = await getUserId(req); + const targetCourse = await getCourseByCourseId(req.params.courseId); + const overlappingCourses = await Promise.all( + targetCourse.slots.map( + async (slot) => + await getCourseByDayPeriodAndUserId(slot.day, slot.period, userId), + ), + ); + const filteredOverlappingCourses = overlappingCourses.filter( + (course) => course !== null, + ); + const uniqueFilteredOverlappingCourses = filteredOverlappingCourses.filter( + (course, index, self) => + self.findIndex((c) => c?.id === course?.id) === index, + ); // id の重複を排除 + res.status(200).json(uniqueFilteredOverlappingCourses); }); // 自分の講義を編集 router.patch("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { courseId } = req.body; - // 指定された講義の存在確認 - try { - const newCourse = await getCourseByCourseId(courseId); - if (!newCourse) { - return res.status(404).json({ error: "Course not found" }); - } - } catch (err) { - console.error("Error fetching course:", err); - res.status(500).json({ error: "Failed to fetch course" }); - } - try { - const updatedCourses = await createEnrollment(courseId, userId.value); - res.status(200).json(updatedCourses); - } catch (error) { - console.error("Error updating courses:", error); - res.status(500).json({ error: "Failed to update courses" }); - } + const userId = await getUserId(req); + const { courseId } = z.object({ courseId: z.string() }).parse(req.body); + const updatedCourses = await createEnrollment(courseId, userId); + res.status(200).json(updatedCourses); }); // 自分の講義を削除 router.delete("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { courseId } = req.body; - // 指定された講義の存在確認 - try { - const newCourse = await getCourseByCourseId(courseId); - if (!newCourse) { - return res.status(404).json({ error: "Course not found" }); - } - } catch (err) { - console.error("Error fetching course:", err); - res.status(500).json({ error: "Failed to fetch course" }); - } - try { - const updatedCourses = await deleteEnrollment(userId.value, courseId); - res.status(200).json(updatedCourses); - } catch (error) { - console.error("Error deleting courses:", error); - res.status(500).json({ error: "Failed to delete courses" }); - } + const userId = await getUserId(req); + const { courseId } = z.object({ courseId: z.string() }).parse(req.body); + const updatedCourses = await deleteEnrollment(userId, courseId); + res.status(200).json(updatedCourses); }); export default router; diff --git a/server/src/router/matches.ts b/server/src/router/matches.ts index ef4d040a..81885c41 100644 --- a/server/src/router/matches.ts +++ b/server/src/router/matches.ts @@ -1,43 +1,28 @@ -import { safeParseInt } from "common/lib/result/safeParseInt"; +import { error } from "common/lib/panic"; import type { UserID } from "common/types"; import express, { type Request, type Response } from "express"; import { deleteMatch, getMatchesByUserId } from "../database/matches"; -import { safeGetUserId } from "../firebase/auth/db"; +import { getUserId } from "../firebase/auth/db"; const router = express.Router(); // SELECT * FROM "Relationship" WHERE user in (.sender, .recv) AND status = MATCHED router.get("/", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - - try { - const all = await getMatchesByUserId(userId.value); - if (!all.ok) return res.status(500).send(); - const matched = all.value.filter( - (relation) => relation.status === "MATCHED", - ); - res.status(200).json(matched); - } catch (error) { - console.error("Error fetching matches:", error); - res.status(500).json({ error: "Failed to fetch matches" }); - } + const userId = await getUserId(req); + const all = await getMatchesByUserId(userId); + const matched = all.filter((relation) => relation.status === "MATCHED"); + res.status(200).json(matched); }); // フレンドの削除 router.delete("/:opponentId", async (req: Request, res: Response) => { - const opponentId = safeParseInt(req.params.opponentId); - if (!opponentId.ok) return res.status(400).send("bad param encoding"); - + const opponentId = + Number.parseInt(req.params.opponentId) ?? error("bad param encoding", 400); // 削除操作を要求しているユーザ - const requesterId = await safeGetUserId(req); - if (!requesterId.ok) return res.status(401).send("auth error"); + const requesterId = await getUserId(req); - const result = await deleteMatch( - requesterId.value, - opponentId.value as UserID, - ); - res.status(result.ok ? 204 : 500).send(); + await deleteMatch(requesterId, opponentId as UserID); + res.status(204).send(); }); export default router; diff --git a/server/src/router/picture.ts b/server/src/router/picture.ts index 1b305ddd..4def6b17 100644 --- a/server/src/router/picture.ts +++ b/server/src/router/picture.ts @@ -1,11 +1,11 @@ import bodyParser from "body-parser"; -import { safeParseInt } from "common/lib/result/safeParseInt"; +import { panic } from "common/lib/panic"; import express from "express"; import * as chat from "../database/chat"; import * as relation from "../database/matches"; import * as storage from "../database/picture"; -import { safeGetUserId } from "../firebase/auth/db"; -import { safeGetGUID } from "../firebase/auth/lib"; +import { getUserId } from "../firebase/auth/db"; +import { getGUID } from "../firebase/auth/lib"; import { compressImage } from "../functions/img/compress"; import * as hashing from "../lib/hash"; @@ -21,14 +21,11 @@ router.post("/to/:userId", parseLargeBuffer, async (req, res) => { if (!Buffer.isBuffer(req.body)) return res.status(400).send("not buffer"); const buf = req.body; - const sender = await safeGetUserId(req); - if (!sender.ok) return res.status(401).end(); - const recv = safeParseInt(req.params.userId); - if (!recv.ok) return res.status(400).end(); + const sender = await getUserId(req); + const recv = Number.parseInt(req.params.userId) ?? panic("invalid params"); - const rel = await relation.getRelation(sender.value, recv.value); - if (!rel.ok) return res.status(401).send(); - if (rel.value.status !== "MATCHED") return res.status(401).send(); + const rel = await relation.getRelation(sender, recv); + if (rel.status !== "MATCHED") return res.status(401).send(); const hash = hashing.sha256(buf.toString("base64")); const passkey = hashing.sha256(crypto.randomUUID()); @@ -36,7 +33,7 @@ router.post("/to/:userId", parseLargeBuffer, async (req, res) => { return storage .uploadPic(hash, buf, passkey) .then(async (url) => { - await chat.createImageMessage(sender.value, rel.value.id, url); + await chat.createImageMessage(sender, rel.id, url); res.status(201).send(url).end(); }) .catch((err) => { @@ -70,27 +67,15 @@ router.get("/:id", async (req, res) => { router.get("/profile/:guid", async (req, res) => { const guid = req.params.guid; const result = await storage.getProf(guid); - switch (result.ok) { - case true: - return res.send(new Buffer(result.value)); - case false: - return res.status(404).send(); - } + return res.send(result); }); router.post("/profile", parseLargeBuffer, async (req, res) => { - const guid = await safeGetGUID(req); - if (!guid.ok) return res.status(401).send(); - + const guid = await getGUID(req); if (!Buffer.isBuffer(req.body)) return res.status(400).send("not buffer"); - const buf = await compressImage(req.body); - if (!buf.ok) return res.status(500).send("failed to compress image"); - - const url = await storage.setProf(guid.value, buf.value); - if (!url.ok) return res.status(500).send("failed to upload image"); - - return res.status(201).type("text/plain").send(url.value); + const url = await storage.setProf(guid, buf); + return res.status(201).type("text/plain").send(url); }); export default router; diff --git a/server/src/router/requests.ts b/server/src/router/requests.ts index 45218020..ce0aa73b 100644 --- a/server/src/router/requests.ts +++ b/server/src/router/requests.ts @@ -1,30 +1,26 @@ +import { error } from "common/lib/panic"; import type { UserID } from "common/types"; import express, { type Request, type Response } from "express"; - -import { safeParseInt } from "common/lib/result/safeParseInt"; import { approveRequest, cancelRequest, rejectRequest, sendRequest, } from "../database/requests"; -import { safeGetUserId } from "../firebase/auth/db"; -// import { Relationship } from "@prisma/client"; // ... not used? +import { getUserId } from "../firebase/auth/db"; const router = express.Router(); // リクエストの送信 router.put("/send/:receiverId", async (req: Request, res: Response) => { - const receiverId = safeParseInt(req.params.receiverId); - if (!receiverId.ok) return res.status(400).send("bad param encoding"); - - const senderId = await safeGetUserId(req); - if (!senderId.ok) return res.status(401).send("auth error"); - + const receiverId = + Number.parseInt(req.params.receiverId) ?? + error("bad encoding: receiverId", 400); + const senderId = await getUserId(req); try { const sentRequest = await sendRequest({ - senderId: senderId.value, - receiverId: receiverId.value as UserID, + senderId: senderId, + receiverId: receiverId as UserID, }); res.status(201).json(sentRequest); } catch (error) { @@ -35,53 +31,31 @@ router.put("/send/:receiverId", async (req: Request, res: Response) => { // リクエストの承認 router.put("/accept/:senderId", async (req: Request, res: Response) => { - const senderId = safeParseInt(req.params.senderId); - if (!senderId.ok) return res.status(400).send("bad param encoding"); + const senderId = + Number.parseInt(req.params.senderId) ?? + error("invalid param encoding: senderId", 400); - const receiverId = await safeGetUserId(req); - if (!receiverId.ok) return res.status(401).send("auth error"); + const receiverId = await getUserId(req); - try { - await approveRequest(senderId.value as UserID, receiverId.value); - res.status(201).send(); - } catch (error) { - console.error("Error approving match request:", error); - res.status(500).json({ error: "Failed to approve match request" }); - } + await approveRequest(senderId as UserID, receiverId); + res.status(201).send(); }); router.put("/cancel/:opponentId", async (req: Request, res: Response) => { - const opponentId = safeParseInt(req.params.opponentId); - if (!opponentId.ok) return res.status(400).send("bad param encoding"); - - const requesterId = await safeGetUserId(req); - if (!requesterId.ok) return res.status(401).send("auth error"); - - const result = await cancelRequest(requesterId.value, opponentId.value); - - switch (result.ok) { - case true: - return res.status(204).send(); - case false: - return res.status(500).send(); - } + const opponentId = + Number.parseInt(req.params.opponentId) ?? error("bad param encoding", 400); + const requesterId = await getUserId(req); + await cancelRequest(requesterId, opponentId); }); // リクエストの拒否 router.put("/reject/:opponentId", async (req: Request, res: Response) => { - const opponentId = safeParseInt(req.params.opponentId); - if (!opponentId.ok) return res.status(400).send("bad param encoding"); - - const requesterId = await safeGetUserId(req); - if (!requesterId.ok) return res.status(401).send("auth error"); + const opponentId = + Number.parseInt(req.params.opponentId) ?? error("bad param encoding", 400); + const requesterId = await getUserId(req); - try { - await rejectRequest(opponentId.value as UserID, requesterId.value); //TODO 名前を良いのに変える - res.status(204).send(); - } catch (error) { - console.error("Error rejecting match request:", error); - res.status(500).json({ error: "Failed to reject match request" }); - } + await rejectRequest(opponentId as UserID, requesterId); //TODO 名前を良いのに変える + res.status(204).send(); }); export default router; diff --git a/server/src/router/subjects.ts b/server/src/router/subjects.ts index 6a7eef0c..e5e83f7c 100644 --- a/server/src/router/subjects.ts +++ b/server/src/router/subjects.ts @@ -1,6 +1,8 @@ +import { error } from "common/lib/panic"; import express, { type Request, type Response } from "express"; +import { z } from "zod"; import * as interest from "../database/interest"; -import { safeGetUserId } from "../firebase/auth/db"; +import { getUserId } from "../firebase/auth/db"; const router = express.Router(); @@ -19,79 +21,38 @@ router.get("/userId/:userId", async (req: Request, res: Response) => { }); router.get("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - try { - const subjects = await interest.of(userId.value); - res.status(200).json(subjects); - } catch (error) { - console.error("Error fetching subjects:", error); - res.status(500).json({ error: "Failed to fetch subjects" }); - } + const userId = await getUserId(req); + const subjects = await interest.of(userId); + res.status(200).json(subjects); }); router.post("/", async (req: Request, res: Response) => { - const { name } = req.body; - if (typeof name !== "string") { - return res.status(400).json({ error: "name must be a string" }); - } - try { - const newSubject = await interest.create(name); - res.status(201).json(newSubject); - } catch (error) { - console.error("Error creating subject:", error); - res.status(500).json({ error: "Failed to create subject" }); - } + const { name } = z.object({ name: z.string() }).parse(req.body); + const newSubject = await interest.create(name); + res.status(201).json(newSubject); }); router.patch("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { subjectId } = req.body; - try { - const newSubject = await interest.get(subjectId); - if (!newSubject) { - return res.status(404).json({ error: "Subject not found" }); - } - } catch (err) { - console.error("Error fetching subject:", err); - res.status(500).json({ error: "Failed to fetch subject" }); - } - try { - const updatedSubjects = await interest.add(userId.value, subjectId); - res.status(200).json(updatedSubjects); - } catch (error) { - console.error("Error updating subjects:", error); - res.status(500).json({ error: "Failed to update subjects" }); - } + const userId = await getUserId(req); + const { subjectId } = z.object({ subjectId: z.number() }).parse(req.body); + const newSubject = await interest.get(subjectId); + if (!newSubject) error("subject not found", 404); + const updatedSubjects = await interest.add(userId, subjectId); + res.status(200).json(updatedSubjects); }); router.delete("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { subjectId } = req.body; - try { - const newSubject = await interest.get(subjectId); - if (!newSubject) { - return res.status(404).json({ error: "Subject not found" }); - } - } catch (err) { - console.error("Error fetching subject:", err); - res.status(500).json({ error: "Failed to fetch subject" }); - } - try { - const updatedSubjects = await interest.remove(userId.value, subjectId); - res.status(200).json(updatedSubjects); - } catch (error) { - console.error("Error deleting subjects:", error); - res.status(500).json({ error: "Failed to delete subjects" }); - } + const userId = await getUserId(req); + const { subjectId } = z.object({ subjectId: z.number() }).parse(req.body); + const updatedSubjects = await interest.remove(userId, subjectId); + res.status(200).json(updatedSubjects); }); router.put("/mine", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send("auth error"); - const { subjectIds } = req.body; + const userId = await getUserId(req); + const { subjectIds } = z + .object({ subjectIds: z.array(z.number()) }) + .parse(req.body); if (!Array.isArray(subjectIds)) { return res.status(400).json({ error: "subjectIds must be an array" }); } @@ -108,7 +69,7 @@ router.put("/mine", async (req: Request, res: Response) => { } try { const updatedSubjects = await interest.updateMultipleWithTransaction( - userId.value, + userId, subjectIds, ); res.status(200).json(updatedSubjects); diff --git a/server/src/router/users.ts b/server/src/router/users.ts index 87c6586e..a5f5d5fb 100644 --- a/server/src/router/users.ts +++ b/server/src/router/users.ts @@ -1,8 +1,4 @@ -import type { - GUID, - UpdateUser, - UserWithCoursesAndSubjects, -} from "common/types"; +import type { GUID } from "common/types"; import type { User } from "common/types"; import { GUIDSchema, @@ -20,11 +16,10 @@ import { deleteUser, getUser, getUserByID, - unmatched, updateUser, } from "../database/users"; -import { safeGetUserId } from "../firebase/auth/db"; -import { safeGetGUID } from "../firebase/auth/lib"; +import { getUserId } from "../firebase/auth/db"; +import { getGUID } from "../firebase/auth/lib"; import { recommendedTo } from "../functions/engines/recommendation"; import * as core from "../functions/user"; @@ -37,24 +32,15 @@ router.get("/", async (_: Request, res: Response) => { }); router.get("/recommended", async (req, res) => { - const u = await safeGetUserId(req); - if (!u.ok) return res.status(401).end(); - - const recommended = await recommendedTo(u.value, 20, 0); // とりあえず 20 人 - - if (recommended.ok) { - res.send(recommended.value.map((entry) => entry.u)); - } else { - res.status(500).send(recommended.error); - } + const u = await getUserId(req); + const recommended = await recommendedTo(u, 20, 0); // とりあえず 20 人 + res.send(recommended.map((entry) => entry.u)); }); // 自分の情報を確認するエンドポイント。 router.get("/me", async (req: Request, res: Response) => { - const guid = await safeGetGUID(req); - if (!guid.ok) return res.status(401).send("auth error"); - - const result = await core.getUser(guid.value); + const guid = await getGUID(req); + const result = await core.getUser(guid); res.status(result.code).send(result.body); }); @@ -67,37 +53,23 @@ router.get("/exists/:guid", async (req: Request, res: Response) => { // 特定のユーザーとマッチしたユーザーを取得 router.get("/matched", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - - const result = await core.getMatched(userId.value); + const userId = await getUserId(req); + const result = await core.getMatched(userId); res.status(result.code).json(result.body); }); // ユーザーにリクエストを送っているユーザーを取得 状態はPENDING router.get("/pending/to-me", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - - const sendingUsers = await getPendingRequestsToUser(userId.value); - if (!sendingUsers.ok) { - console.log(sendingUsers.error); - return res.status(500).send(); - } - res.status(200).json(sendingUsers.value); + const userId = await getUserId(req); + const sendingUsers = await getPendingRequestsToUser(userId); + res.status(200).json(sendingUsers); }); // ユーザーがリクエストを送っているユーザーを取得 状態はPENDING router.get("/pending/from-me", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - - const receivers = await getPendingRequestsFromUser(userId.value); - if (!receivers.ok) { - console.log(receivers.error); - return res.status(500).send(); - } - res.status(200).json(receivers.value); + const userId = await getUserId(req); + const receivers = await getPendingRequestsFromUser(userId); + res.status(200).json(receivers); }); // guidでユーザーを取得 @@ -107,65 +79,39 @@ router.get("/guid/:guid", async (req: Request, res: Response) => { const guid = guid_.data; const user = await getUser(guid as GUID); - if (!user.ok) { - return res.status(404).json({ error: "User not found" }); - } - const json: UserWithCoursesAndSubjects = user.value; - res.status(200).json(json); + res.status(200).json(user); }); // idでユーザーを取得 router.get("/id/:id", async (req: Request, res: Response) => { - const userId = await safeGetUserId(req); - if (!userId.ok) return res.status(401).send(`auth error: ${userId.error}`); - const user = await getUserByID(userId.value); - if (!user.ok) { - return res.status(404).json({ error: "User not found" }); - } - const json: User = user.value; + const userId = await getUserId(req); + const user = await getUserByID(userId); + const json: User = user; res.status(200).json(json); }); // INSERT INTO "User" VALUES (body...) router.post("/", async (req: Request, res: Response) => { - const partialUser = InitUserSchema.safeParse(req.body); - if (!partialUser.success) - return res.status(400).send({ - error: "Invalid input format", - details: partialUser.error.errors, - }); - - const user = await createUser(partialUser.data); - if (!user.ok) return res.status(500).send({ error: "Failed to create user" }); + const partialUser = InitUserSchema.parse(req.body); + const user = await createUser(partialUser); //ユーザー作成と同時にメモとマッチング - const result = await matchWithMemo(user.value.id); - if ("ok" in result && !result.ok) { - return res.status(500).send({ error: "Failed to match user with memo" }); - } - res.status(201).json(user.value); + await matchWithMemo(user.id); + res.status(201).json(user); }); // ユーザーの更新エンドポイント router.put("/me", async (req: Request, res: Response) => { - const id = await safeGetUserId(req); - if (!id.ok) return res.status(401).send("auth error"); - - const user = UpdateUserSchema.safeParse(req.body); - if (!user.success) return res.status(400).send("invalid format"); - - const updated = await updateUser(id.value, user.data); - if (!updated.ok) return res.status(500).send(); - res.status(200).json(updated.value); + const id = await getUserId(req); + const user = UpdateUserSchema.parse(req.body); + const updated = await updateUser(id, user); + res.status(200).json(updated); }); // ユーザーの削除エンドポイント router.delete("/me", async (req, res) => { - const id = await safeGetUserId(req); - if (!id.ok) return res.status(401).send("auth error"); - - const deleted = await deleteUser(id.value); - if (!deleted.ok) return res.status(500).send(); + const id = await getUserId(req); + const deleted = await deleteUser(id); res.status(204).send(); }); diff --git a/web/api/internal/fetch-func.ts b/web/api/internal/fetch-func.ts index 1c4486b8..99daf7e0 100644 --- a/web/api/internal/fetch-func.ts +++ b/web/api/internal/fetch-func.ts @@ -1,21 +1,5 @@ -import { Err, Ok, type Result } from "common/lib/result"; import { getIdToken } from "~/firebase/auth/lib"; -export async function safeFetch( - path: string, - method: string, -): Promise> { - try { - return Ok( - await fetch(path, { - method: method, - }), - ); - } catch (e) { - return Err(e); - } -} - type URL = string; export const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB diff --git a/web/hooks/useData.ts b/web/hooks/useData.ts index 5a120f2b..b023ff63 100644 --- a/web/hooks/useData.ts +++ b/web/hooks/useData.ts @@ -1,4 +1,3 @@ -import { Err, Ok, type Result } from "common/lib/result"; import { useCallback, useEffect, useState } from "react"; import { credFetch } from "~/firebase/auth/lib"; @@ -34,19 +33,15 @@ export default function useData(url: string) { return { data, isLoading, error, reload }; } -async function safeReadData( - url: string, - schema: Zod.Schema, -): Promise> { +async function readData(url: string, schema: Zod.Schema): Promise { try { const res = await credFetch("GET", url); const data = await res.json(); - const result = schema.parse(data); - return Ok(result); + return schema.parse(data); } catch (e) { console.error(` safeReadData: Schema Parse Error | in incoming data | Error: ${e}`); - return Err(e); + throw e; } } @@ -77,15 +72,16 @@ export function useAuthorizedData(url: string, schema: Zod.Schema) { setLoading(true); setError(null); - const result = await safeReadData(url, schema); - if (result.ok) { - setData(result.value); - setLoading(false); - return; - } - setError(result.error as Error); - setData(null); - setLoading(false); + await readData(url, schema) + .then((data) => { + setData(data); + setLoading(false); + }) + .catch((err) => { + setError(err as Error); + setData(null); + setLoading(false); + }); }, [url, schema]); useEffect(() => {