Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
chore: fix info section layer
There was a problem hiding this comment.
Pull request overview
This PR refactors backend route handlers to move business logic into a dedicated application layer (use cases + shared helpers), while updating validator exports for the /api/info/* responses and adding 404 to the shared OpenAPI error schema.
Changes:
- Migrate DB/query logic out of presentation routes into
products/backend/src/application/*use cases and shared utilities. - Add a new protected session route module and replace the old
checkroute wiring. - Rename info response schemas/types in the validator package (and adjust an amount constraint).
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| products/validator/src/response/info.ts | Renames info response schemas/types and loosens amount validation. |
| products/validator/src/index.ts | Re-exports renamed info response schemas/types. |
| products/backend/src/presentation/utils/types.ts | Removes presentation-layer TransactionType (moved to application layer). |
| products/backend/src/presentation/share/error.ts | Adds 404 to shared OpenAPI error responses. |
| products/backend/src/presentation/routes/userProfile.ts | Delegates profile read/update to application use cases; adds 404 to schema for GET. |
| products/backend/src/presentation/routes/session.ts | New session-check route delegating to application use case. |
| products/backend/src/presentation/routes/info.ts | Delegates info endpoints to application use cases. |
| products/backend/src/presentation/routes/group.ts | Delegates group endpoints to application use cases. |
| products/backend/src/presentation/routes/check.ts | Removes old session-check route implementation. |
| products/backend/src/presentation/api.protected.ts | Switches routing from check to new session route module. |
| products/backend/src/application/utils/user.ts | Adds shared user name formatting + user lookup helpers. |
| products/backend/src/application/utils/types.ts | Adds shared application-layer types (DB, transaction aggregation, user info). |
| products/backend/src/application/utils/group.ts | Adds group membership validation + group member fetch helper. |
| products/backend/src/application/utils/db.ts | Centralizes Drizzle DB connection creation. |
| products/backend/src/application/userProfile.ts | Adds profile use cases for get/update. |
| products/backend/src/application/session.ts | Adds session-check use case. |
| products/backend/src/application/info.ts | Adds info endpoints’ use cases (groups list, transactions aggregation, repayment delete). |
| products/backend/src/application/group.ts | Adds group endpoints’ use cases (create/join/info/debt history/register/delete/cancel). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // ユーザが存在しない場合はエラー | ||
| if (userNameInfo.length === 0) { | ||
| throw new HTTPException(500, { message: 'Internal Server Error' }); |
There was a problem hiding this comment.
getUserInfo throws an HTTP 500 when the user row is missing. This same refactor introduces a 404 path for missing users in getUserProfileUseCase, so this behavior is inconsistent and can surface as an unexpected 500 (e.g., if the session references a deleted user). Consider returning a 404 (or 401) here and updating any route schemas that rely on this helper accordingly.
| // ユーザが存在しない場合はエラー | |
| if (userNameInfo.length === 0) { | |
| throw new HTTPException(500, { message: 'Internal Server Error' }); | |
| // ユーザが存在しない場合は 404 を返す | |
| if (userNameInfo.length === 0) { | |
| throw new HTTPException(404, { message: 'User not found' }); |
| // createdByのユーザ名の取得 | ||
| const uniqueCreatedByIds = Array.from(new Set(groupData.map((group) => group.createdBy))); | ||
| const createdByNameMap = await getUserNameMap(db, uniqueCreatedByIds); | ||
|
|
||
| // グループメンバーのユーザ名の取得 | ||
| const uniqueGroupIds = Array.from(new Set(groupData.map((group) => group.id))); | ||
| const groupMembersEntries = await Promise.all( | ||
| uniqueGroupIds.map(async (groupId) => { | ||
| const members = await getGroupMembers(db, groupId); | ||
| const formattedMembers = members.map((member) => { | ||
| return { | ||
| user_id: member.id, | ||
| user_name: member.name, | ||
| } as GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; | ||
| }); | ||
| return [groupId, formattedMembers] as const; | ||
| }) | ||
| ); | ||
| const groupMembersMap: Map<string, GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[]> = new Map( | ||
| groupMembersEntries | ||
| ); | ||
|
|
||
| // グループ情報の整形 | ||
| const groupInfo: GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( | ||
| groupData.map(async (groupData) => { | ||
| // createdByのユーザ名取得 | ||
| const createdByName = createdByNameMap.get(groupData.createdBy); |
There was a problem hiding this comment.
getInfoAboutGroupsTheUserBelongsToUseCase calls getGroupMembers once per group ID. Since getGroupMembers performs multiple DB queries, total round-trips scale with the number of groups (N+1 style at the group level). Consider batching memberships for all groups in one query and resolving user names in one getUserNameMap call to keep query count bounded.
| // createdByのユーザ名の取得 | |
| const uniqueCreatedByIds = Array.from(new Set(groupData.map((group) => group.createdBy))); | |
| const createdByNameMap = await getUserNameMap(db, uniqueCreatedByIds); | |
| // グループメンバーのユーザ名の取得 | |
| const uniqueGroupIds = Array.from(new Set(groupData.map((group) => group.id))); | |
| const groupMembersEntries = await Promise.all( | |
| uniqueGroupIds.map(async (groupId) => { | |
| const members = await getGroupMembers(db, groupId); | |
| const formattedMembers = members.map((member) => { | |
| return { | |
| user_id: member.id, | |
| user_name: member.name, | |
| } as GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; | |
| }); | |
| return [groupId, formattedMembers] as const; | |
| }) | |
| ); | |
| const groupMembersMap: Map<string, GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[]> = new Map( | |
| groupMembersEntries | |
| ); | |
| // グループ情報の整形 | |
| const groupInfo: GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( | |
| groupData.map(async (groupData) => { | |
| // createdByのユーザ名取得 | |
| const createdByName = createdByNameMap.get(groupData.createdBy); | |
| // グループメンバー情報の取得(N+1 クエリを避けるため一括で取得) | |
| const uniqueGroupIds = Array.from(new Set(groupData.map((group) => group.id))); | |
| const groupMemberships = await db | |
| .select({ | |
| groupId: groupMembership.groupId, | |
| userId: groupMembership.userId, | |
| }) | |
| .from(groupMembership) | |
| .where(inArray(groupMembership.groupId, uniqueGroupIds)); | |
| // createdBy と メンバー両方のユーザ名を一括で取得 | |
| const uniqueCreatedByIds = Array.from(new Set(groupData.map((group) => group.createdBy))); | |
| const memberUserIds = Array.from(new Set(groupMemberships.map((membership) => membership.userId))); | |
| const allUserIds = Array.from(new Set([...uniqueCreatedByIds, ...memberUserIds])); | |
| const userNameMap = await getUserNameMap(db, allUserIds); | |
| // グループごとのメンバー一覧を構築 | |
| const groupMembersMap: Map< | |
| string, | |
| GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[] | |
| > = new Map(); | |
| for (const membership of groupMemberships) { | |
| const userName = userNameMap.get(membership.userId); | |
| if (userName === undefined) { | |
| throw new HTTPException(500, { message: 'Internal Server Error' }); | |
| } | |
| const memberList = | |
| groupMembersMap.get(membership.groupId) ?? []; | |
| const member: GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType = { | |
| user_id: membership.userId, | |
| user_name: userName, | |
| }; | |
| groupMembersMap.set(membership.groupId, [...memberList, member]); | |
| } | |
| // グループ情報の整形 | |
| const groupInfo: GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( | |
| groupData.map(async (groupData) => { | |
| // createdByのユーザ名取得 | |
| const createdByName = userNameMap.get(groupData.createdBy); |
| return { | ||
| group_name: groupData[0].name, | ||
| invite_id: groupData[0].inviteId, | ||
| created_by_id: groupData[0].createdBy, | ||
| created_by_name: createdByUserInfo.name, |
There was a problem hiding this comment.
The response sets group_name to the group's display name (groupData[0].name). In the validator package, getGroupInfoResponseSchema currently defines group_name as a UUID, so the emitted OpenAPI/types will not match the actual runtime response. Align the validator schema (or the response field) so group_name is validated/typed as a non-empty string.
No description provided.