Skip to content

Commit c82e424

Browse files
authored
SWR キャッシュによる UX の改善となんか挙動に文句言われたので SQL 書き直した (#340)
# PRの概要 closes #302 useSWR を Friend ページと Chat ページに適用 ## 具体的な変更内容 Friend + Chat。 Settings の Course は難しくて一旦パス。 Home は SWR と相性が悪い (すると Twitter みたいに出てきた人がすぐ流れるようになる) ので Skeleton UI などにする予定。 あとなんか Home 触ってたら #301#296 も直ったので、 close #301 and close #296 。 ## 影響範囲 Home 以外すべてのページとHome。 ## 動作要件 なし。 ## レビューリクエストを出す前にチェック! - [x] 改めてセルフレビューしたか - [x] 手動での動作検証を行ったか - [x] server の機能追加ならば、テストを書いたか - 理由: server の機能追加ではない - [x] 間違った使い方が存在するならば、それのドキュメントをコメントで書いたか - 理由: 間違った使い方は存在しない - [x] わかりやすいPRになっているか <!-- レビューリクエスト後は、Slackでもメンションしてお願いすることを推奨します。 -->
1 parent c3733b4 commit c82e424

File tree

26 files changed

+388
-399
lines changed

26 files changed

+388
-399
lines changed

server/prisma/sql/recommend.sql

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
1-
SELECT recv.id AS recv, COUNT(recv_enroll) AS overlap FROM "User" recv
2-
INNER JOIN "Enrollment" recv_enroll ON recv_enroll."userId" = recv.id
3-
INNER JOIN "Course" course ON recv_enroll."courseId" = course.id
4-
INNER JOIN "Enrollment" req_enroll ON req_enroll."courseId" = course.id
5-
WHERE req_enroll."userId" = $1 AND recv.id <> $1
6-
GROUP BY recv.id
7-
ORDER BY overlap DESC LIMIT $2 OFFSET $3;
1+
SELECT recv.id,
2+
(SELECT COUNT(*) FROM "Enrollment" recv_enroll
3+
INNER JOIN "Enrollment" req_enroll
4+
ON recv_enroll."courseId" = req_enroll."courseId"
5+
WHERE recv_enroll."userId" = recv.id
6+
AND req_enroll."userId" = $1)
7+
AS overlap FROM "User" recv
8+
WHERE recv.id <> $1
9+
10+
AND NOT EXISTS (
11+
SELECT * FROM "Relationship" rel
12+
WHERE rel."sendingUserId" IN ($1, recv.id) AND rel."receivingUserId" IN ($1, recv.id)
13+
AND status = 'MATCHED'
14+
)
15+
16+
AND NOT EXISTS (
17+
SELECT * FROM "Relationship" rel_pd
18+
WHERE rel_pd."sendingUserId" = $1 AND rel_pd."receivingUserId" = recv.id
19+
AND status = 'PENDING'
20+
)
21+
22+
ORDER BY overlap DESC
23+
LIMIT $2 OFFSET $3;
24+
25+
-- SELECT recv.id AS recv, COUNT(recv_enroll) AS overlap FROM "User" recv
26+
-- LEFT JOIN "Relationship" rel ON (rel."sendingUserId" = recv.id AND rel."receivingUserId" = $1) OR (rel."sendingUserId" = $1 AND rel."sendingUserId" = recv.id)
27+
-- LEFT JOIN "Enrollment" recv_enroll ON recv_enroll."userId" = recv.id
28+
-- INNER JOIN "Course" course ON recv_enroll."courseId" = course.id
29+
-- INNER JOIN "Enrollment" req_enroll ON req_enroll."courseId" = course.id
30+
-- WHERE req_enroll."userId" = $1 AND recv.id <> $1
31+
-- AND rel.status != 'MATCHED'
32+
-- GROUP BY recv.id
33+
-- ORDER BY overlap DESC LIMIT $2 OFFSET $3;

server/src/database/users.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,39 @@ export async function getAllUsers(): Promise<Result<User[]>> {
108108
return Err(e);
109109
}
110110
}
111+
112+
// TODO: FIXME: currently also showing users that the requester has already sent request to, to not change behavior.
113+
// but this is probably not ideal. consider only showing people with no relation.
114+
// (or just remove this function and use recommended() instead)
115+
export async function unmatched(id: UserID): Promise<Result<User[]>> {
116+
return prisma.user
117+
.findMany({
118+
where: {
119+
AND: [
120+
{
121+
receivingUsers: {
122+
none: {
123+
sendingUserId: id,
124+
status: "MATCHED",
125+
},
126+
},
127+
},
128+
{
129+
sendingUsers: {
130+
none: {
131+
receivingUserId: id,
132+
status: "MATCHED",
133+
},
134+
},
135+
},
136+
{
137+
NOT: {
138+
id: id,
139+
},
140+
},
141+
],
142+
},
143+
})
144+
.then((res) => Ok(res))
145+
.catch((err) => Err(err));
146+
}

server/src/functions/engines/recommendation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export async function recommendedTo(
1313
const result = await prisma.$queryRawTyped(sql(user, limit, offset));
1414
return Promise.all(
1515
result.map(async (res) => {
16-
const user = await getUserByID(res.recv);
16+
const user = await getUserByID(res.id);
1717
if (!user.ok) throw new Error("not found"); // this shouldn't happen
1818
return {
1919
count: Number.parseInt(res.overlap?.toString() ?? "0"),

server/src/router/users.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
deleteUser,
1616
getUser,
1717
getUserByID,
18+
unmatched,
1819
updateUser,
1920
} from "../database/users";
2021
import { safeGetUserId } from "../firebase/auth/db";
@@ -37,7 +38,7 @@ router.get("/recommended", async (req, res) => {
3738
const recommended = await recommendedTo(u.value, 20, 0); // とりあえず 20 人
3839

3940
if (recommended.ok) {
40-
res.send(recommended.value);
41+
res.send(recommended.value.map((entry) => entry.u));
4142
} else {
4243
res.status(500).send(recommended.error);
4344
}

web/src/api/chat/chat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function updateMessage(
4949
);
5050
return res.json();
5151
}
52+
5253
// 自身の参加しているすべての Room (DM グループチャットともに) の概要 (Overview) の取得 (メッセージの履歴を除く)
5354
export async function overview(): Promise<RoomOverview[]> {
5455
const res = await credFetch("GET", endpoints.roomOverview);
@@ -73,7 +74,7 @@ export async function sendDM(
7374
return res.json();
7475
}
7576

76-
// 相手のIDを指定して、
77+
// WARNING: don't use this outside of api/
7778
export async function getDM(friendId: UserID): Promise<DMRoom> {
7879
const res = await credFetch("GET", endpoints.dmWith(friendId));
7980
if (res.status === 401) throw new ErrUnauthorized();

web/src/api/chat/hooks.ts

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,25 @@
1+
import { useCallback } from "react";
12
import { z } from "zod";
23
// import { useCallback, useEffect, useState } from "react";
3-
import type { RoomOverview } from "../../common/types";
4-
import { RoomOverviewSchema } from "../../common/zod/schemas";
4+
import type { Message, RoomOverview } from "../../common/types";
5+
import { MessageSchema, RoomOverviewSchema } from "../../common/zod/schemas";
56
import { type Hook, useSWR } from "../../hooks/useSWR";
7+
import type { UserID } from "../internal/endpoints";
68
// import type { Hook } from "../share/types";
79
import * as chat from "./chat";
810

911
const OverviewListSchema = z.array(RoomOverviewSchema);
1012
export function useRoomsOverview(): Hook<RoomOverview[]> {
1113
return useSWR("useRoomsOverview", chat.overview, OverviewListSchema);
1214
}
13-
// export function useRoomsOverview(): Hook<RoomOverview[]> {
14-
// const [data, setData] = useState<RoomOverview[] | null>(null);
15-
// const [error, setError] = useState<Error | null>(null);
16-
// const [loading, setLoading] = useState<boolean>(true);
1715

18-
// const reload = useCallback(async () => {
19-
// setLoading(true);
20-
// try {
21-
// const data = await chat.overview();
22-
// setData(data);
23-
// setError(null);
24-
// setLoading(false);
25-
// } catch (e) {
26-
// setData(null);
27-
// setError(e as Error);
28-
// setLoading(false);
29-
// }
30-
// }, []);
31-
32-
// useEffect(() => {
33-
// reload();
34-
// }, [reload]);
35-
36-
// return {
37-
// data,
38-
// error,
39-
// loading,
40-
// reload,
41-
// };
42-
// }
16+
const MessageListSchema = z.array(MessageSchema);
17+
// 相手のIDを指定して、
18+
export function useMessages(friendId: UserID): Hook<Message[]> {
19+
const key = `chat::dm::${friendId}`;
20+
const fetcher = useCallback(
21+
async () => (await chat.getDM(friendId)).messages,
22+
[friendId],
23+
);
24+
return useSWR(key, fetcher, MessageListSchema);
25+
}

web/src/api/hooks.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

web/src/api/internal/endpoints.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ export const user = (userId: UserID) => {
3838
**/
3939
export const users = `${origin}/users`;
4040

41+
/**
42+
* [v] 実装済み
43+
* GET -> get top N users recommended to me.
44+
* - statuses:
45+
* - 200: good.
46+
* - body: User[]
47+
* - 401: auth error.
48+
* - 500: internal error
49+
**/
50+
export const recommendedUsers = `${origin}/users/recommended`;
51+
4152
/**
4253
* [v] 実装済み
4354
* GET -> get info of me.
@@ -358,8 +369,7 @@ export const picture = `${origin}/picture`;
358369
export default {
359370
user,
360371
me,
361-
coursesMine,
362-
coursesMineOverlaps,
372+
recommendedUsers,
363373
userByGUID,
364374
userExists,
365375
users,
@@ -381,6 +391,8 @@ export default {
381391
sharedRooms,
382392
roomInvite,
383393
message,
394+
coursesMine,
395+
coursesMineOverlaps,
384396
pictureOf,
385397
picture,
386398
};

web/src/api/user.ts

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,57 @@
1+
import { z } from "zod";
12
import type { GUID, UpdateUser, User, UserID } from "../common/types";
23
import { parseUser } from "../common/zod/methods.ts";
4+
import { UserIDSchema, UserSchema } from "../common/zod/schemas.ts";
35
import { credFetch } from "../firebase/auth/lib.ts";
6+
import { useAuthorizedData } from "../hooks/useData.ts";
7+
import { type Hook, useSWR } from "../hooks/useSWR.ts";
48
import endpoints from "./internal/endpoints.ts";
9+
import type { Hook as UseHook } from "./share/types.ts";
510

6-
// TODO: migrate to safe functions
11+
const UserListSchema = z.array(UserSchema);
712

8-
//全てのユーザ情報を取得する
9-
export async function all(): Promise<User[]> {
10-
const res = await credFetch("GET", endpoints.users);
11-
const users = await res.json();
12-
const safeUsers: User[] = users.map((user: User) => parseUser(user));
13-
return safeUsers;
13+
export function useRecommended(): UseHook<User[]> {
14+
const url = endpoints.recommendedUsers;
15+
return useAuthorizedData<User[]>(url);
16+
}
17+
export function useMatched(): Hook<User[]> {
18+
return useSWR("users::matched", matched, UserListSchema);
19+
}
20+
export function usePendingToMe(): Hook<User[]> {
21+
return useSWR("users::pending::to-me", pendingToMe, UserListSchema);
22+
}
23+
export function usePendingFromMe(): Hook<User[]> {
24+
return useSWR("users::pending::from-me", pendingFromMe, UserListSchema);
1425
}
1526

16-
export async function matched(): Promise<User[]> {
27+
async function matched(): Promise<User[]> {
1728
const res = await credFetch("GET", endpoints.matchedUsers);
1829
return res.json();
1930
}
31+
async function pendingToMe(): Promise<User[]> {
32+
const res = await credFetch("GET", endpoints.pendingRequestsToMe);
33+
return await res.json();
34+
}
35+
async function pendingFromMe(): Promise<User[]> {
36+
const res = await credFetch("GET", endpoints.pendingRequestsFromMe);
37+
return await res.json();
38+
}
2039

2140
// 自身のユーザー情報を取得する
22-
export async function aboutMe(): Promise<User> {
41+
export function useAboutMe(): Hook<User> {
42+
return useSWR("users::aboutMe", aboutMe, UserSchema);
43+
}
44+
45+
async function aboutMe(): Promise<User> {
2346
const res = await credFetch("GET", endpoints.me);
2447
return res.json();
2548
}
2649

2750
// 自身のユーザーIDを取得する
28-
export async function getMyId(): Promise<UserID> {
51+
export function useMyID(): Hook<UserID> {
52+
return useSWR("users::myId", getMyId, UserIDSchema);
53+
}
54+
async function getMyId(): Promise<UserID> {
2955
const me = await aboutMe();
3056
return me.id;
3157
}
@@ -41,17 +67,6 @@ export async function remove(): Promise<void> {
4167
await credFetch("DELETE", endpoints.me);
4268
}
4369

44-
//指定した id のユーザ情報を除いた全てのユーザ情報を取得する
45-
export async function except(id: UserID): Promise<User[]> {
46-
try {
47-
const data = await all();
48-
return data.filter((user: User) => user.id !== id);
49-
} catch (err) {
50-
console.error("Error fetching data:", err);
51-
throw err;
52-
}
53-
}
54-
5570
/**
5671
* Google アカウントの uid を用いて CourseMate ユーザの情報とステータスコードを取得する。
5772
* @param guid Google アカウントの uid
@@ -88,13 +103,12 @@ export async function getByGUID(
88103

89104
//指定した guid のユーザが存在するかどうかを取得する
90105
export async function exists(guid: GUID): Promise<boolean> {
91-
try {
92-
const res = await credFetch("GET", endpoints.userExists(guid));
93-
if (res.status === 404) return false;
94-
return true;
95-
} catch {
96-
return false;
97-
}
106+
const res = await credFetch("GET", endpoints.userExists(guid));
107+
if (res.status === 404) return false;
108+
if (res.status === 200) return true;
109+
throw new Error(
110+
`Unexpected status code: expected 200 or 404, got ${res.status}`,
111+
);
98112
}
99113

100114
// 指定した id のユーザ情報を取得する
@@ -108,16 +122,3 @@ export async function create(userdata: Omit<User, "id">): Promise<User> {
108122
const res = await credFetch("POST", endpoints.users, userdata);
109123
return await res.json();
110124
}
111-
112-
export default {
113-
get,
114-
aboutMe,
115-
getByGUID,
116-
all,
117-
matched,
118-
except,
119-
exists,
120-
create,
121-
update,
122-
remove,
123-
};

0 commit comments

Comments
 (0)