From a75229b36e89bbe1d2aac8c36295334feffd6806 Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 00:28:52 +0900 Subject: [PATCH 01/15] feat: refactoring backend --- products/backend/src/application/group.ts | 307 +++++++++++++ products/backend/src/application/info.ts | 229 +++++++++ products/backend/src/application/session.ts | 24 + .../backend/src/application/userProfile.ts | 45 ++ products/backend/src/application/utils/db.ts | 8 + .../backend/src/application/utils/group.ts | 42 ++ .../backend/src/application/utils/types.ts | 33 ++ .../backend/src/application/utils/user.ts | 58 +++ .../backend/src/presentation/api.protected.ts | 4 +- .../backend/src/presentation/routes/check.ts | 67 --- .../backend/src/presentation/routes/group.ts | 434 ++---------------- .../backend/src/presentation/routes/info.ts | 221 +-------- .../src/presentation/routes/session.ts | 44 ++ .../src/presentation/routes/userProfile.ts | 48 +- .../backend/src/presentation/utils/types.ts | 5 - 15 files changed, 858 insertions(+), 711 deletions(-) create mode 100644 products/backend/src/application/group.ts create mode 100644 products/backend/src/application/info.ts create mode 100644 products/backend/src/application/session.ts create mode 100644 products/backend/src/application/userProfile.ts create mode 100644 products/backend/src/application/utils/db.ts create mode 100644 products/backend/src/application/utils/group.ts create mode 100644 products/backend/src/application/utils/types.ts create mode 100644 products/backend/src/application/utils/user.ts delete mode 100644 products/backend/src/presentation/routes/check.ts create mode 100644 products/backend/src/presentation/routes/session.ts delete mode 100644 products/backend/src/presentation/utils/types.ts diff --git a/products/backend/src/application/group.ts b/products/backend/src/application/group.ts new file mode 100644 index 0000000..b542e67 --- /dev/null +++ b/products/backend/src/application/group.ts @@ -0,0 +1,307 @@ +// hono +import { HTTPException } from 'hono/http-exception'; +import { Bindings } from '../types'; +// validator +import { + type JoinGroupResponseSchemaType, + type CreateGroupResponseSchemaType, + type GetGroupInfoResponseMemberElementSchemaType, + type GetGroupInfoResponseSchemaType, + type GetGroupDebtHistoryResponseSchemaType, + type GetGroupDebtHistoryResponseElementSchemaType, +} from 'validator'; +// drizzle +import { createDbConnection } from './utils/db'; +import { eq, and, isNull, isNotNull } from 'drizzle-orm'; +import { debt, group, groupMembership, user } from '../db/schema'; +// utils +import { ensureGroupMembership, ensureNotGroupMembership } from './utils/group'; +import { getUserInfo, getUserNameMap } from './utils/user'; + +export const createGroupUseCase = async ( + env: Bindings, + loginUserId: string, + groupName: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + //* グループ情報を挿入 *// + const result = await db + .insert(group) + .values({ + id: crypto.randomUUID(), + name: groupName, + inviteId: `${crypto.randomUUID()}-${crypto.randomUUID()}`, + createdBy: loginUserId, + }) + .returning({ + id: group.id, + inviteId: group.inviteId, + }); + + //* グループ作成者をgroupMembershipに挿入 *// + await db.insert(groupMembership).values({ + id: crypto.randomUUID(), + groupId: result[0].id, + userId: loginUserId, + }); + + // レスポンス + return { + group_id: result[0].id, + invite_id: result[0].inviteId, + } satisfies CreateGroupResponseSchemaType; +}; + +export const joinGroupUseCase = async ( + env: Bindings, + loginUserId: string, + inviteId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // invite_id から group を取得 + const groupData = await db + .select({ + id: group.id, + }) + .from(group) + .where(eq(group.inviteId, inviteId)) + .limit(1); + + // invite_id が不正な場合はエラー + if (groupData.length === 0) { + throw new HTTPException(400, { message: 'Bad Request' }); + } + + // すでに参加している場合は即座に返す + const existingMembership = await db + .select({ + groupMembershipId: groupMembership.id, + }) + .from(groupMembership) + .where(and(eq(groupMembership.groupId, groupData[0].id), eq(groupMembership.userId, loginUserId))) + .limit(1); + + if (existingMembership.length > 0) { + // レスポンス + return { + group_id: groupData[0].id, + } satisfies JoinGroupResponseSchemaType; + } + + // loginUser を invite_id の group に参加させる + const result = await db + .insert(groupMembership) + .values({ + id: crypto.randomUUID(), + groupId: groupData[0].id, + userId: loginUserId, + }) + .returning({ + groupId: groupMembership.groupId, + }); + + // レスポンス + return { + group_id: result[0].groupId, + } satisfies JoinGroupResponseSchemaType; +}; + +export const getGroupInfoUseCase = async ( + env: Bindings, + loginUserId: string, + groupId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // loginUser が グループのメンバーであることを確認 + await ensureGroupMembership(db, groupId, loginUserId); + + //* body.group_id のグループ情報を取得 *// + const groupData = await db + .select({ + name: group.name, + inviteId: group.inviteId, + createdBy: group.createdBy, + }) + .from(group) + .where(eq(group.id, groupId)) + .limit(1); + + //* body.group_id のグループ作成者情報を取得 *// + // ユーザ情報を取得 + const createdByUserInfo = await getUserInfo(db, groupData[0].createdBy); + + // グループのメンバー情報を取得 + const members = (await ensureNotGroupMembership(db, groupId)).map((member) => ({ + user_id: member.id, + user_name: member.name, + })); + + // レスポンス + return { + group_name: groupData[0].name, + invite_id: groupData[0].inviteId, + created_by_id: groupData[0].createdBy, + created_by_name: createdByUserInfo.name, + members, + } satisfies GetGroupInfoResponseSchemaType; +}; + +export const getGroupDebtHistoryUseCase = async ( + env: Bindings, + loginUserId: string, + groupId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // loginUser がグループのメンバーであることを確認 + await ensureGroupMembership(db, groupId, loginUserId); + + //* グループの貸し借り履歴を取得 *// + // body.group_id の貸し借り履歴を取得 (debt table) + const rawDebtData = await db + .select({ + id: debt.id, + debtorId: debt.debtorId, + creditorId: debt.creditorId, + amount: debt.amount, + description: debt.description, + occurredAt: debt.occurredAt, + deletedAt: debt.deletedAt, + deletedBy: debt.deletedBy, + }) + .from(debt) + .where(eq(debt.groupId, groupId)); + + const userIds = rawDebtData.flatMap((debtEntry) => + [debtEntry.debtorId, debtEntry.creditorId, debtEntry.deletedBy].filter((id): id is string => id !== null) + ); + const uniqueUserIds = Array.from(new Set(userIds)); + const nameMap = await getUserNameMap(db, uniqueUserIds); + + // 貸し借り履歴データを配列に追加 + const debtData: GetGroupDebtHistoryResponseElementSchemaType[] = rawDebtData.map((debtEntry) => { + // debt_name, creditor_name を取得 + const debtorName = nameMap.get(debtEntry.debtorId); + const creditorName = nameMap.get(debtEntry.creditorId); + if (debtorName === undefined || creditorName === undefined) { + throw new HTTPException(500, { message: 'Internal Server Error' }); + } + + // deleted_by_name を取得 + let deletedByName = null; + if (debtEntry.deletedBy !== null) { + // deletedByが存在する場合のみ名前を取得 + const name = nameMap.get(debtEntry.deletedBy); + if (name === undefined) { + throw new HTTPException(500, { message: 'Internal Server Error' }); + } + deletedByName = name; + } + + return { + debt_id: debtEntry.id, + debtor_id: debtEntry.debtorId, + debtor_name: debtorName, + creditor_id: debtEntry.creditorId, + creditor_name: creditorName, + amount: debtEntry.amount, + description: debtEntry.description === null ? '' : debtEntry.description, + occurred_at: debtEntry.occurredAt, + deleted_at: + debtEntry.deletedAt + ?.toLocaleDateString('ja-JP', { + timeZone: 'Asia/Tokyo', + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + .replace(/\//g, '-') || null, + deleted_by_id: debtEntry.deletedBy, + deleted_by_name: deletedByName, + }; + }); + + // レスポンス + return { + debts: debtData, + } satisfies GetGroupDebtHistoryResponseSchemaType; +}; + +export const registerGroupDebtUseCase = async ( + env: Bindings, + loginUserId: string, + groupId: string, + creditorId: string, + debtorId: string, + amount: number, + occurredAt: string, + description: string | undefined +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // loginUser が グループのメンバーであることを確認 + await ensureGroupMembership(db, groupId, loginUserId); + + // body.group_id に貸し借り履歴を追加 (debt table) + await db.insert(debt).values({ + id: crypto.randomUUID(), + groupId, + creditorId, + debtorId, + amount, + description: typeof description === 'undefined' ? null : description, + occurredAt, + }); +}; + +export const deleteGroupDebtUseCase = async ( + env: Bindings, + loginUserId: string, + groupId: string, + debtId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // loginUser が グループのメンバーであることを確認 + await ensureGroupMembership(db, groupId, loginUserId); + + //* body.debt_id の貸し借りの履歴の削除 (論理削除) *// + await db + .update(debt) + .set({ + deletedBy: loginUserId, + deletedAt: new Date(), // 現在時刻の取得 (UTC) + }) + .where(and(eq(debt.id, debtId), eq(debt.groupId, groupId), isNull(debt.deletedAt))); +}; + +export const cancelGroupDebtUseCase = async ( + env: Bindings, + loginUserId: string, + groupId: string, + debtId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // loginUser が グループのメンバーであることを確認 + await ensureGroupMembership(db, groupId, loginUserId); + + //* body.debt_id の貸し借りの履歴の削除の取り消し (論理削除の取り消し) *// + await db + .update(debt) + .set({ + deletedBy: null, + deletedAt: null, + }) + .where(and(eq(debt.id, debtId), eq(debt.groupId, groupId), isNotNull(debt.deletedAt))); +}; diff --git a/products/backend/src/application/info.ts b/products/backend/src/application/info.ts new file mode 100644 index 0000000..aeba3e6 --- /dev/null +++ b/products/backend/src/application/info.ts @@ -0,0 +1,229 @@ +// hono +import { Bindings } from '../types'; +// validator +import { + type InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType, + type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, + type InfoAboutGroupsTheUserBelongsToResponseSchemaType, + type InfoAboutUserTransactionsResponseSchemaType, + type InfoAboutUserTransactionsResponseTransactionElementSchemaType, +} from 'validator'; +// drizzle +import { createDbConnection } from './utils/db'; +import { eq, and, isNull, inArray, or } from 'drizzle-orm'; +import { user } from '../db/auth-schema'; +import { debt, group, groupMembership } from '../db/schema'; +import { TransactionType } from './utils/types'; + +// TODO: 共通化 +export const infoAboutGroupsTheUserBelongsToUseCase = async ( + env: Bindings, + loginUserId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // グループ情報格納用配列 + let groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = []; + + //* ユーザが参加しているグループ情報を取得 (group table) *// + const groupsData = await db + .select({ + id: group.id, + name: group.name, + createdBy: group.createdBy, + }) + .from(group) + .where( + inArray( + group.id, + db + .select({ groupId: groupMembership.groupId }) + .from(groupMembership) + .where(eq(groupMembership.userId, loginUserId)) + ) + ); + + for (const groupData of groupsData) { + //* body.group_id のグループ作成者情報を取得 *// + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const createdByUserNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, groupData.createdBy)) + .limit(1); + + // NOTE: --- 共通化開始 --- + //* groupData.id のメンバー情報を取得 *// + // グループメンバーのユーザーIDを取得 (group_membership table) + const memberUserIds = await db + .select({ + userId: groupMembership.userId, + }) + .from(groupMembership) + .where(eq(groupMembership.groupId, groupData.id)); + + // メンバー情報を格納する配列 + const members: InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[] = []; + + for (const memberUserId of memberUserIds) { + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const userNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, memberUserId.userId)) + .limit(1); + + // メンバー情報を配列に追加 + members.push({ + user_id: memberUserId.userId, + user_name: + userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 + ? userNameInfo[0].displayName + : userNameInfo[0].name, + }); + } + // NOTE: --- 共通化終了 --- + + // グループ情報を配列に追加 + groupInfo.push({ + group_id: groupData.id, + group_name: groupData.name, + created_by_id: groupData.createdBy, + created_by_name: + createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 + ? createdByUserNameInfo[0].displayName + : createdByUserNameInfo[0].name, + members: members, + }); + } + + // レスポンス + return { + groups: groupInfo, + } satisfies InfoAboutGroupsTheUserBelongsToResponseSchemaType; +}; + +// TODO: 共通化 +export const infoAboutUserTransactionsUseCase = async ( + env: Bindings, + loginUserId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + //* loginUserが貸している取引履歴を取得 *// + // マイナス n 円 + const creditorTransactions = await db + .select({ + debtorId: debt.debtorId, + amount: debt.amount, + }) + .from(debt) + .where(and(eq(debt.creditorId, loginUserId), isNull(debt.deletedAt))); + + //* loginUserが借りている取引履歴を取得 *// + // プラス n 円 + const debtorTransactions = await db + .select({ + creditorId: debt.creditorId, + amount: debt.amount, + }) + .from(debt) + .where(and(eq(debt.debtorId, loginUserId), isNull(debt.deletedAt))); + + //* 取引相手ごとに集計 *// + // 集計結果を格納するMap + // Map + const transactions: Map = new Map(); + + // 貸している取引を集計 + for (const creditorTransaction of creditorTransactions) { + const userId = creditorTransaction.debtorId; + const amount = creditorTransaction.amount; + if (!transactions.has(userId)) { + transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 }); + } + const existing = transactions.get(userId)!; + existing.lent_amount += amount; + } + + // 借りている取引を集計 + for (const debtorTransaction of debtorTransactions) { + const userId = debtorTransaction.creditorId; + const amount = debtorTransaction.amount; + if (!transactions.has(userId)) { + transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 }); + } + const existing = transactions.get(userId)!; + existing.borrowed_amount += amount; + } + + // 集計結果を格納する配列 + const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = []; + + // 貸し借りの合算 + for (const transaction of transactions.values()) { + // 合算結果: netAmount = borrowed_amount - lent_amount + const netAmount = transaction.borrowed_amount - transaction.lent_amount; + + // NOTE: 共通化できそう + // ユーザ名を取得 (user table) + const userNameInfo = await db + .select({ + name: user.name, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, transaction.user_id)) + .limit(1); + + // netAmount が 0 でない場合のみ配列に追加 + if (netAmount !== 0) { + aggregatedTransactions.push({ + counterparty_id: transaction.user_id, + counterparty_name: + userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 + ? userNameInfo[0].displayName + : userNameInfo[0].name, + amount: netAmount, + }); + } + } + + // return response + return { + transactions: aggregatedTransactions, + } satisfies InfoAboutUserTransactionsResponseSchemaType; +}; + +export const infoUserRepaymentUseCase = async ( + env: Bindings, + loginUserId: string, + counterpartyId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // loginUser <-> counterparty_id 間の取引履歴を削除(deletedAt, deletedBy をセット) + await db + .update(debt) + .set({ + deletedBy: loginUserId, + deletedAt: new Date(), // 現在時刻の取得 (UTC) + }) + .where( + or( + and(eq(debt.creditorId, loginUserId), eq(debt.debtorId, counterpartyId), isNull(debt.deletedAt)), + and(eq(debt.debtorId, loginUserId), eq(debt.creditorId, counterpartyId), isNull(debt.deletedAt)) + ) + ); +}; diff --git a/products/backend/src/application/session.ts b/products/backend/src/application/session.ts new file mode 100644 index 0000000..15953f6 --- /dev/null +++ b/products/backend/src/application/session.ts @@ -0,0 +1,24 @@ +// hono +import { Bindings } from '../types'; +// validator +import { type SessionCheckResponseSchemaType } from 'validator'; +// drizzle +import { createDbConnection } from './utils/db'; +// utils +import { getUserInfo } from './utils/user'; + +export const getSessionCheckUseCase = async ( + env: Bindings, + loginUserId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + // ユーザ情報を取得 + const userInfo = await getUserInfo(db, loginUserId); + + return { + user_id: userInfo.id, + user_name: userInfo.name, + } satisfies SessionCheckResponseSchemaType; +}; diff --git a/products/backend/src/application/userProfile.ts b/products/backend/src/application/userProfile.ts new file mode 100644 index 0000000..0dd762b --- /dev/null +++ b/products/backend/src/application/userProfile.ts @@ -0,0 +1,45 @@ +// hono +import { Bindings } from '../types'; +// validator +import { type GetUserProfileResponseSchemaType } from 'validator'; +// drizzle +import { createDbConnection } from './utils/db'; +import { eq } from 'drizzle-orm'; +import { user } from '../db/auth-schema'; + +export const getUserProfileUseCase = async ( + env: Bindings, + loginUserId: string +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + //* ユーザ情報を取得 (user table) *// + const userData = await db + .select({ display_name: user.displayName }) + .from(user) + .where(eq(user.id, loginUserId)) + .limit(1); + + // レスポンス + return { + display_name: userData[0].display_name === null ? '' : userData[0].display_name, + } satisfies GetUserProfileResponseSchemaType; +}; + +export const updateUserProfileUseCase = async ( + env: Bindings, + loginUserId: string, + displayName: string | undefined +): Promise => { + // データベース接続 + const db = createDbConnection(env); + + //* ユーザ情報を更新 (user table) *// + await db + .update(user) + .set({ + displayName: typeof displayName === 'undefined' ? null : displayName, + }) + .where(eq(user.id, loginUserId)); +}; diff --git a/products/backend/src/application/utils/db.ts b/products/backend/src/application/utils/db.ts new file mode 100644 index 0000000..021b2cb --- /dev/null +++ b/products/backend/src/application/utils/db.ts @@ -0,0 +1,8 @@ +// drizzle +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Bindings } from '../../types'; +import { DatabaseType } from './types'; + +export const createDbConnection = (env: Bindings): DatabaseType => { + return drizzle({ connection: env.HYPERDRIVE }); +}; diff --git a/products/backend/src/application/utils/group.ts b/products/backend/src/application/utils/group.ts new file mode 100644 index 0000000..5e9fd84 --- /dev/null +++ b/products/backend/src/application/utils/group.ts @@ -0,0 +1,42 @@ +// hono +import { HTTPException } from 'hono/http-exception'; +// drizzle +import { eq, and } from 'drizzle-orm'; +import { groupMembership } from '../../db/schema'; +// types +import { DatabaseType, UserInfoType } from './types'; +// utils +import { getUserNameMap } from './user'; + +export const ensureGroupMembership = async (db: DatabaseType, groupId: string, loginUserId: string): Promise => { + // loginUser が body.group_id のグループのメンバーであることを確認 + const me = await db + .select({ + groupMembershipId: groupMembership.id, + }) + .from(groupMembership) + .where(and(eq(groupMembership.groupId, groupId), eq(groupMembership.userId, loginUserId))) + .limit(1); + + // body.group_id のグループのメンバーでない場合はエラー + if (me.length === 0) { + throw new HTTPException(400, { message: 'Bad Request' }); + } +}; + +export const ensureNotGroupMembership = async (db: DatabaseType, groupId: string): Promise => { + // グループに所属しているユーザIDを取得 + const memberUserIds = await db + .select({ userId: groupMembership.userId }) + .from(groupMembership) + .where(eq(groupMembership.groupId, groupId)); + + // グループに所属しているユーザ名を取得 + const userIds = memberUserIds.map((member) => member.userId); + const nameMap = await getUserNameMap(db, userIds); + + return memberUserIds.map((member) => ({ + id: member.userId, + name: nameMap.get(member.userId) ?? '', + })); +}; diff --git a/products/backend/src/application/utils/types.ts b/products/backend/src/application/utils/types.ts new file mode 100644 index 0000000..ef19f5a --- /dev/null +++ b/products/backend/src/application/utils/types.ts @@ -0,0 +1,33 @@ +// drizzle +import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; + +export type DatabaseType = NodePgDatabase> & { + $client: Pool; +}; + +export type TransactionType = { + user_id: string; // 取引相手のユーザID + lent_amount: number; // 貸した金額 + borrowed_amount: number; // 借りた金額 +}; + +export type UserNameType = { + name: string; + displayName: string | null; +}; + +export type UserInfoType = { + id: string; + name: string; +}; + +export type FormattedUserTableType = { + id: string; + name: string; + email: string; + emailVerified: boolean; + image: string | null; + createdAt: Date; + updatedAt: Date; +}; diff --git a/products/backend/src/application/utils/user.ts b/products/backend/src/application/utils/user.ts new file mode 100644 index 0000000..c77283b --- /dev/null +++ b/products/backend/src/application/utils/user.ts @@ -0,0 +1,58 @@ +// drizzle +import { eq, inArray } from 'drizzle-orm/sql/expressions/conditions'; +import { groupMembership, user } from '../../db/schema'; +// types +import { DatabaseType, FormattedUserTableType, UserInfoType, UserNameType } from './types'; + +export const formatUserName = (userName: UserNameType): string => { + return userName.displayName !== null && userName.displayName.length > 0 ? userName.displayName : userName.name; +}; + +export const getUserInfo = async (db: DatabaseType, userId: string): Promise => { + // user table からユーザ名を取得 + const userNameInfo = await db + .select({ + id: user.id, + name: user.name, + email: user.email, + emailVerified: user.emailVerified, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + displayName: user.displayName, + }) + .from(user) + .where(eq(user.id, userId)) + .limit(1); + + return { + id: userNameInfo[0].id, + // ユーザ名をフォーマット + name: formatUserName({ name: userNameInfo[0].name, displayName: userNameInfo[0].displayName }), + email: userNameInfo[0].email, + emailVerified: userNameInfo[0].emailVerified, + image: userNameInfo[0].image, + createdAt: userNameInfo[0].createdAt, + updatedAt: userNameInfo[0].updatedAt, + }; +}; + +export const getUserNameMap = async (db: DatabaseType, userIds: string[]): Promise> => { + const uniqueIds = Array.from(new Set(userIds)); + if (uniqueIds.length === 0) { + return new Map(); + } + + const rows = await db + .select({ id: user.id, name: user.name, displayName: user.displayName }) + .from(user) + .where(inArray(user.id, uniqueIds)); + + // Map + const nameMap = new Map(); + for (const row of rows) { + nameMap.set(row.id, formatUserName(row)); + } + + return nameMap; +}; diff --git a/products/backend/src/presentation/api.protected.ts b/products/backend/src/presentation/api.protected.ts index 09c02c6..1183f32 100644 --- a/products/backend/src/presentation/api.protected.ts +++ b/products/backend/src/presentation/api.protected.ts @@ -5,7 +5,7 @@ import honoFactory from './factory/hono'; // better-auth object factory import { AuthFactoryType } from './factory/auth'; // routes -import check from './routes/check'; +import session from './routes/session'; import group from './routes/group'; import info from './routes/info'; import userProfile from './routes/userProfile'; @@ -31,7 +31,7 @@ const apiProtected = (auth: AuthFactoryType) => { }); //* endpoint registration *// - app.route('/', check); + app.route('/', session); app.route('/', group); app.route('/', info); app.route('/', userProfile); diff --git a/products/backend/src/presentation/routes/check.ts b/products/backend/src/presentation/routes/check.ts deleted file mode 100644 index c9b5318..0000000 --- a/products/backend/src/presentation/routes/check.ts +++ /dev/null @@ -1,67 +0,0 @@ -// hono instance -import honoFactory from '../factory/hono'; -// validator -import { sessionCheckResponseSchema, type SessionCheckResponseSchemaType } from 'validator'; -// error schema -import { route } from '../share/error'; -// drizzle -import { drizzle } from 'drizzle-orm/node-postgres'; -import { eq } from 'drizzle-orm'; -import { user } from '../../db/auth-schema'; - -const hono = honoFactory(); - -//TODO: userProfileの取得 -const getUserProfile = route.createSchema( - { - path: '/api/session', - method: 'get', - description: 'セッションが有効か確認するエンドポイント', - security: [{ SessionCookie: [] }], - request: {}, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: sessionCheckResponseSchema, - }, - }, - }, - }, - }, - [401, 500] as const -); - -hono.openapi(getUserProfile, async (c) => { - const loginUser = c.get('user'); - - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const userName = await db - .select({ - id: user.id, - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, loginUser.id)) - .limit(1); - - // レスポンス - return c.json( - { - user_id: userName[0].id, - user_name: - userName[0].displayName !== null && userName[0].displayName.length > 0 - ? userName[0].displayName - : userName[0].name, - } satisfies SessionCheckResponseSchemaType, - 200 - ); -}); - -export default hono; diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts index 7e00079..1a19344 100644 --- a/products/backend/src/presentation/routes/group.ts +++ b/products/backend/src/presentation/routes/group.ts @@ -1,33 +1,31 @@ // hono instance -import { HTTPException } from 'hono/http-exception'; import honoFactory from '../factory/hono'; // validator import { createGroupRequestSchema, createGroupResponseSchema, - type CreateGroupResponseSchemaType, getGroupInfoRequestSchema, getGroupInfoResponseSchema, - type GetGroupInfoResponseSchemaType, joinGroupRequestSchema, joinGroupResponseSchema, - type JoinGroupResponseSchemaType, getGroupDebtHistoryRequestSchema, getGroupDebtHistoryResponseSchema, - type GetGroupDebtHistoryResponseElementSchemaType, - type GetGroupDebtHistoryResponseSchemaType, - GetGroupInfoResponseMemberElementSchemaType, registerGroupDebtRequestSchema, deleteGroupDebtRequestSchema, cancelGroupDebtRequestSchema, } from 'validator'; // error schema import { route } from '../share/error'; -// drizzle -import { drizzle } from 'drizzle-orm/node-postgres'; -import { and, eq, isNotNull, isNull } from 'drizzle-orm'; -import { group, debt, groupMembership } from '../../db/pay-crew2-schema'; -import { user } from '../../db/auth-schema'; +// application +import { + cancelGroupDebtUseCase, + createGroupUseCase, + deleteGroupDebtUseCase, + getGroupDebtHistoryUseCase, + getGroupInfoUseCase, + joinGroupUseCase, + registerGroupDebtUseCase, +} from '../../application/group'; const hono = honoFactory(); @@ -66,38 +64,11 @@ hono.openapi(createGroupSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - //* グループ情報を挿入 *// - const result = await db - .insert(group) - .values({ - id: crypto.randomUUID(), - name: body.group_name, - inviteId: `${crypto.randomUUID()}-${crypto.randomUUID()}`, - createdBy: loginUser.id, - }) - .returning({ - id: group.id, - inviteId: group.inviteId, - }); - - //* グループ作成者をgroupMembershipに挿入 *// - await db.insert(groupMembership).values({ - id: crypto.randomUUID(), - groupId: result[0].id, - userId: loginUser.id, - }); + // ビジネスロジック呼び出し + const response = await createGroupUseCase(c.env, loginUser.id, body.group_name); // レスポンス - return c.json( - { - group_id: result[0].id, - invite_id: result[0].inviteId, - } satisfies CreateGroupResponseSchemaType, - 201 - ); + return c.json(response, 201); }); //TODO: メンバー登録のエンドポイントの登録 @@ -132,64 +103,14 @@ const joinGroupSchema = route.createSchema( ); hono.openapi(joinGroupSchema, async (c) => { + const loginUser = c.get('user'); const body = c.req.valid('json'); - const user = c.get('user'); - - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - // invite_id から group を取得 - const groupData = await db - .select({ - id: group.id, - }) - .from(group) - .where(eq(group.inviteId, body.invite_id)) - .limit(1); - - // invite_id が不正な場合はエラー - if (groupData.length === 0) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - - // すでに参加している場合は即座に返スす - const existingMembership = await db - .select({ - groupMembershipId: groupMembership.id, - }) - .from(groupMembership) - .where(and(eq(groupMembership.groupId, groupData[0].id), eq(groupMembership.userId, user.id))) - .limit(1); - - if (existingMembership.length > 0) { - // レスポンス - return c.json( - { - group_id: groupData[0].id, - } satisfies JoinGroupResponseSchemaType, - 201 - ); - } - - // loginUser を invite_id の group に参加させる - const result = await db - .insert(groupMembership) - .values({ - id: crypto.randomUUID(), - groupId: groupData[0].id, - userId: user.id, - }) - .returning({ - groupId: groupMembership.groupId, - }); + // ビジネスロジック呼び出し + const response = await joinGroupUseCase(c.env, loginUser.id, body.invite_id); // レスポンス - return c.json( - { - group_id: result[0].groupId, - } satisfies JoinGroupResponseSchemaType, - 201 - ); + return c.json(response, 201); }); //TODO: 各グループ情報取得エンドポイントの登録 @@ -224,102 +145,14 @@ const getGroupInfoSchema = route.createSchema( ); hono.openapi(getGroupInfoSchema, async (c) => { - const body = c.req.valid('json'); const loginUser = c.get('user'); + const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // NOTE: --- 共通化開始 --- - // loginUser が body.group_id のグループのメンバーであることを確認 - const me = await db - .select({ - groupMembershipId: groupMembership.id, - }) - .from(groupMembership) - .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) - .limit(1); - - // body.group_id のグループのメンバーでない場合はエラー - if (me.length === 0) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - // NOTE: --- 共通化終了 --- - - //* body.group_id のグループ情報を取得 *// - const groupData = await db - .select({ - name: group.name, - inviteId: group.inviteId, - createdBy: group.createdBy, - }) - .from(group) - .where(eq(group.id, body.group_id)) - .limit(1); - - //* body.group_id のグループ作成者情報を取得 *// - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const createdByUserNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, groupData[0].createdBy)) - .limit(1); - - // NOTE: 共通化できそう (info.ts と group.ts で共通化) - // NOTE: --- 共通化開始 --- - //* body.group_id のメンバー情報を取得 *// - // グループメンバーのユーザーIDを取得 (group_membership table) - const memberUserIds = await db - .select({ - userId: groupMembership.userId, - }) - .from(groupMembership) - .where(eq(groupMembership.groupId, body.group_id)); - - // メンバー情報を格納する配列 - const members: GetGroupInfoResponseMemberElementSchemaType[] = []; - - for (const memberUserId of memberUserIds) { - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const memberUserNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, memberUserId.userId)) - .limit(1); - - // メンバー情報を配列に追加 - members.push({ - user_id: memberUserId.userId, - user_name: - memberUserNameInfo[0].displayName !== null && memberUserNameInfo[0].displayName.length > 0 - ? memberUserNameInfo[0].displayName - : memberUserNameInfo[0].name, - }); - } - // NOTE: --- 共通化終了 --- + // ビジネスロジック呼び出し + const response = await getGroupInfoUseCase(c.env, loginUser.id, body.group_id); - // return response - return c.json( - { - group_name: groupData[0].name, - invite_id: groupData[0].inviteId, - created_by_id: groupData[0].createdBy, - created_by_name: - createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 - ? createdByUserNameInfo[0].displayName - : createdByUserNameInfo[0].name, - members: members, - } satisfies GetGroupInfoResponseSchemaType, - 201 - ); + // レスポンス + return c.json(response, 201); }); // TODO: 各グループの貸し借り履歴取得エンドポイントの登録 @@ -357,126 +190,11 @@ hono.openapi(getGroupDebtHistorySchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // NOTE: --- 共通化開始 --- - // loginUser が body.group_id のグループのメンバーであることを確認 - const me = await db - .select({ - groupMembershipId: groupMembership.id, - }) - .from(groupMembership) - .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) - .limit(1); - - // body.group_id のグループのメンバーでない場合はエラー - if (me.length === 0) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - // NOTE: --- 共通化終了 --- - - //* グループの貸し借り履歴を取得 *// - // 貸し借り履歴を格納する配列 - let debtData: GetGroupDebtHistoryResponseElementSchemaType[] = []; - - // body.group_id の貸し借り履歴を取得 (debt table) - const rawDebtData = await db - .select({ - id: debt.id, - debtorId: debt.debtorId, - creditorId: debt.creditorId, - amount: debt.amount, - description: debt.description, - occurredAt: debt.occurredAt, - deletedAt: debt.deletedAt, - deletedBy: debt.deletedBy, - }) - .from(debt) - .where(eq(debt.groupId, body.group_id)); - - for (const debtEntry of rawDebtData) { - // debt_name を取得 - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const debtorNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, debtEntry.debtorId)) - .limit(1); - - // creditor_name を取得 - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const creditorNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, debtEntry.creditorId)) - .limit(1); - - // deleted_by_name を取得 - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - let deletedByNameInfo = null; - if (debtEntry.deletedBy !== null) { - deletedByNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, debtEntry.deletedBy)) - .limit(1); - } - - // 貸し借り履歴データを配列に追加 - debtData.push({ - debt_id: debtEntry.id, - debtor_id: debtEntry.debtorId, - debtor_name: - debtorNameInfo[0].displayName !== null && debtorNameInfo[0].displayName.length > 0 - ? debtorNameInfo[0].displayName - : debtorNameInfo[0].name, - creditor_id: debtEntry.creditorId, - creditor_name: - creditorNameInfo[0].displayName !== null && creditorNameInfo[0].displayName.length > 0 - ? creditorNameInfo[0].displayName - : creditorNameInfo[0].name, - amount: debtEntry.amount, - description: debtEntry.description === null ? '' : debtEntry.description, - occurred_at: debtEntry.occurredAt, - deleted_at: - debtEntry.deletedAt - ?.toLocaleDateString('ja-JP', { - timeZone: 'Asia/Tokyo', - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) - .replace(/\//g, '-') || null, - deleted_by_id: debtEntry.deletedBy, - deleted_by_name: - debtEntry.deletedBy && deletedByNameInfo - ? deletedByNameInfo[0].displayName !== null && deletedByNameInfo[0].displayName.length > 0 - ? deletedByNameInfo[0].displayName - : deletedByNameInfo[0].name - : null, - }); - } + // ビジネスロジック呼び出し + const response = await getGroupDebtHistoryUseCase(c.env, loginUser.id, body.group_id); // レスポンス - return c.json( - { - debts: debtData, - } satisfies GetGroupDebtHistoryResponseSchemaType, - 201 - ); + return c.json(response, 201); }); // TODO: 貸し借りの履歴の追加エンドポイントの登録 @@ -509,36 +227,19 @@ hono.openapi(registerGroupDebtSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // NOTE: --- 共通化開始 --- - // loginUser が body.group_id のグループのメンバーであることを確認 - const me = await db - .select({ - groupMembershipId: groupMembership.id, - }) - .from(groupMembership) - .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) - .limit(1); - - // body.group_id のグループのメンバーでない場合はエラー - if (me.length === 0) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - // NOTE: --- 共通化終了 --- - - // body.group_id に貸し借り履歴を追加 (debt table) - await db.insert(debt).values({ - id: crypto.randomUUID(), - groupId: body.group_id, - creditorId: body.creditor_id, - debtorId: body.debtor_id, - amount: body.amount, - description: typeof body.description === 'undefined' ? null : body.description, - occurredAt: body.occurred_at, - }); + // ビジネスロジック呼び出し + await registerGroupDebtUseCase( + c.env, + loginUser.id, + body.group_id, + body.creditor_id, + body.debtor_id, + body.amount, + body.occurred_at, + body.description + ); + // レスポンス return c.body(null, 204); }); @@ -572,36 +273,8 @@ hono.openapi(deleteGroupDebtSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // 現在時刻の取得 (UTC) - const now = new Date(); - - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // NOTE: --- 共通化開始 --- - // loginUser が body.group_id のグループのメンバーであることを確認 - const me = await db - .select({ - groupMembershipId: groupMembership.id, - }) - .from(groupMembership) - .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) - .limit(1); - - // body.group_id のグループのメンバーでない場合はエラー - if (me.length === 0) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - // NOTE: --- 共通化終了 --- - - //* body.debt_id の貸し借りの履歴の削除 (論理削除) *// - await db - .update(debt) - .set({ - deletedBy: loginUser.id, - deletedAt: now, - }) - .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, body.group_id), isNull(debt.deletedAt))); + // ビジネスロジック呼び出し + await deleteGroupDebtUseCase(c.env, loginUser.id, body.group_id, body.debt_id); // レスポンス return c.body(null, 204); @@ -637,33 +310,8 @@ hono.openapi(cancelGroupDebtSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // NOTE: --- 共通化開始 --- - // loginUser が body.group_id のグループのメンバーであることを確認 - const me = await db - .select({ - groupMembershipId: groupMembership.id, - }) - .from(groupMembership) - .where(and(eq(groupMembership.groupId, body.group_id), eq(groupMembership.userId, loginUser.id))) - .limit(1); - - // body.group_id のグループのメンバーでない場合はエラー - if (me.length === 0) { - throw new HTTPException(400, { message: 'Bad Request' }); - } - // NOTE: --- 共通化終了 --- - - //* body.debt_id の貸し借りの履歴の削除の取り消し (論理削除の取り消し) *// - await db - .update(debt) - .set({ - deletedBy: null, - deletedAt: null, - }) - .where(and(eq(debt.id, body.debt_id), eq(debt.groupId, body.group_id), isNotNull(debt.deletedAt))); + // ビジネスロジック呼び出し + await cancelGroupDebtUseCase(c.env, loginUser.id, body.group_id, body.debt_id); // レスポンス return c.body(null, 204); diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index 1d722ce..2dc71c5 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -3,23 +3,17 @@ import honoFactory from '../factory/hono'; // validator import { infoAboutUserTransactionsResponseSchema, - type InfoAboutUserTransactionsResponseTransactionElementSchemaType, - type InfoAboutUserTransactionsResponseSchemaType, infoAboutGroupsTheUserBelongsToResponseSchema, - type InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType, - type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, - type InfoAboutGroupsTheUserBelongsToResponseSchemaType, deleteInfoAboutUserRepaymentRequestSchema, } from 'validator'; // error schema import { route } from '../share/error'; -// drizzle -import { drizzle } from 'drizzle-orm/node-postgres'; -import { and, or, eq, inArray, isNull } from 'drizzle-orm'; -import { group, debt, groupMembership } from '../../db/pay-crew2-schema'; -import { user } from '../../db/auth-schema'; // types -import { TransactionType } from '../utils/types'; +import { + infoAboutGroupsTheUserBelongsToUseCase, + infoAboutUserTransactionsUseCase, + infoUserRepaymentUseCase, +} from '../../application/info'; const hono = honoFactory(); @@ -48,99 +42,11 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => { const loginUser = c.get('user'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // グループ情報格納用配列 - let groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = []; - - //* ユーザが参加しているグループ情報を取得 (group table) *// - const groupsData = await db - .select({ - id: group.id, - name: group.name, - createdBy: group.createdBy, - }) - .from(group) - .where( - inArray( - group.id, - db - .select({ groupId: groupMembership.groupId }) - .from(groupMembership) - .where(eq(groupMembership.userId, loginUser.id)) - ) - ); - - for (const groupData of groupsData) { - //* body.group_id のグループ作成者情報を取得 *// - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const createdByUserNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, groupData.createdBy)) - .limit(1); - - // NOTE: --- 共通化開始 --- - //* groupData.id のメンバー情報を取得 *// - // グループメンバーのユーザーIDを取得 (group_membership table) - const memberUserIds = await db - .select({ - userId: groupMembership.userId, - }) - .from(groupMembership) - .where(eq(groupMembership.groupId, groupData.id)); - - // メンバー情報を格納する配列 - const members: InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[] = []; - - for (const memberUserId of memberUserIds) { - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const userNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, memberUserId.userId)) - .limit(1); - - // メンバー情報を配列に追加 - members.push({ - user_id: memberUserId.userId, - user_name: - userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 - ? userNameInfo[0].displayName - : userNameInfo[0].name, - }); - } - // NOTE: --- 共通化終了 --- - - // グループ情報を配列に追加 - groupInfo.push({ - group_id: groupData.id, - group_name: groupData.name, - created_by_id: groupData.createdBy, - created_by_name: - createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 - ? createdByUserNameInfo[0].displayName - : createdByUserNameInfo[0].name, - members: members, - }); - } + // ビジネスロジック呼び出し + const response = await infoAboutGroupsTheUserBelongsToUseCase(c.env, loginUser.id); // レスポンス - return c.json( - { - groups: groupInfo, - } satisfies InfoAboutGroupsTheUserBelongsToResponseSchemaType, - 200 - ); + return c.json(response, 200); }); //TODO: ユーザの貸し借りの履歴を返す @@ -168,94 +74,11 @@ const infoAboutUserTransactionsSchema = route.createSchema( hono.openapi(infoAboutUserTransactionsSchema, async (c) => { const loginUser = c.get('user'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - //* loginUserが貸している取引履歴を取得 *// - // マイナス n 円 - const creditorTransactions = await db - .select({ - debtorId: debt.debtorId, - amount: debt.amount, - }) - .from(debt) - .where(and(eq(debt.creditorId, loginUser.id), isNull(debt.deletedAt))); + // ビジネスロジック呼び出し + const response = await infoAboutUserTransactionsUseCase(c.env, loginUser.id); - //* loginUserが借りている取引履歴を取得 *// - // プラス n 円 - const debtorTransactions = await db - .select({ - creditorId: debt.creditorId, - amount: debt.amount, - }) - .from(debt) - .where(and(eq(debt.debtorId, loginUser.id), isNull(debt.deletedAt))); - - //* 取引相手ごとに集計 *// - // 集計結果を格納するMap - const transactions: Map = new Map(); - - // 貸している取引を集計 - for (const creditorTransaction of creditorTransactions) { - const userId = creditorTransaction.debtorId; - const amount = creditorTransaction.amount; - if (!transactions.has(userId)) { - transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 }); - } - const existing = transactions.get(userId)!; - existing.lent_amount += amount; - } - - // 借りている取引を集計 - for (const debtorTransaction of debtorTransactions) { - const userId = debtorTransaction.creditorId; - const amount = debtorTransaction.amount; - if (!transactions.has(userId)) { - transactions.set(userId, { user_id: userId, lent_amount: 0, borrowed_amount: 0 }); - } - const existing = transactions.get(userId)!; - existing.borrowed_amount += amount; - } - - // 集計結果を格納する配列 - const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = []; - - // 貸し借りの合算 - for (const transaction of transactions.values()) { - // 合算結果: netAmount = borrowed_amount - lent_amount - const netAmount = transaction.borrowed_amount - transaction.lent_amount; - - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const userNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, transaction.user_id)) - .limit(1); - - // netAmount が 0 でない場合のみ配列に追加 - if (netAmount !== 0) { - aggregatedTransactions.push({ - counterparty_id: transaction.user_id, - counterparty_name: - userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 - ? userNameInfo[0].displayName - : userNameInfo[0].name, - amount: netAmount, - }); - } - } - - // return response - return c.json( - { - transactions: aggregatedTransactions, - } satisfies InfoAboutUserTransactionsResponseSchemaType, - 200 - ); + // レスポンス + return c.json(response, 200); }); //TODO: ユーザの返済処理を行う @@ -288,24 +111,8 @@ hono.openapi(infoUserRepaymentSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - // 現在時刻の取得 (UTC) - const now = new Date(); - // loginUser <-> counterparty_id 間の取引履歴を削除(deletedAt, deletedBy をセット) - await db - .update(debt) - .set({ - deletedBy: loginUser.id, - deletedAt: now, - }) - .where( - or( - and(eq(debt.creditorId, loginUser.id), eq(debt.debtorId, body.counterparty_id), isNull(debt.deletedAt)), - and(eq(debt.debtorId, loginUser.id), eq(debt.creditorId, body.counterparty_id), isNull(debt.deletedAt)) - ) - ); + // ビジネスロジック呼び出し + await infoUserRepaymentUseCase(c.env, loginUser.id, body.counterparty_id); // レスポンス return c.body(null, 204); diff --git a/products/backend/src/presentation/routes/session.ts b/products/backend/src/presentation/routes/session.ts new file mode 100644 index 0000000..815e48e --- /dev/null +++ b/products/backend/src/presentation/routes/session.ts @@ -0,0 +1,44 @@ +// hono instance +import honoFactory from '../factory/hono'; +// validator +import { sessionCheckResponseSchema, type SessionCheckResponseSchemaType } from 'validator'; +// error schema +import { route } from '../share/error'; +// application +import { getSessionCheckUseCase } from '../../application/session'; + +const hono = honoFactory(); + +//TODO: userProfileの取得 +const getSessionCheckSchema = route.createSchema( + { + path: '/api/session', + method: 'get', + description: 'セッションが有効か確認するエンドポイント', + security: [{ SessionCookie: [] }], + request: {}, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: sessionCheckResponseSchema, + }, + }, + }, + }, + }, + [401, 500] as const +); + +hono.openapi(getSessionCheckSchema, async (c) => { + const loginUser = c.get('user'); + + // ビジネスロジック呼び出し + const response = await getSessionCheckUseCase(c.env, loginUser.id); + + // レスポンス + return c.json(response, 200); +}); + +export default hono; diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts index dd0c6b8..96f924c 100644 --- a/products/backend/src/presentation/routes/userProfile.ts +++ b/products/backend/src/presentation/routes/userProfile.ts @@ -1,22 +1,16 @@ // hono instance import honoFactory from '../factory/hono'; // validator -import { - getUserProfileResponseSchema, - type GetUserProfileResponseSchemaType, - updateUserProfileRequestSchema, -} from 'validator'; +import { getUserProfileResponseSchema, updateUserProfileRequestSchema } from 'validator'; // error schema import { route } from '../share/error'; // drizzle -import { drizzle } from 'drizzle-orm/node-postgres'; -import { eq } from 'drizzle-orm'; -import { user } from '../../db/auth-schema'; +import { getUserProfileUseCase, updateUserProfileUseCase } from '../../application/userProfile'; const hono = honoFactory(); //TODO: userProfileの取得 -const getUserProfile = route.createSchema( +const getUserProfileSchema = route.createSchema( { path: '/api/profile', method: 'get', @@ -37,30 +31,18 @@ const getUserProfile = route.createSchema( [401, 500] as const ); -hono.openapi(getUserProfile, async (c) => { +hono.openapi(getUserProfileSchema, async (c) => { const loginUser = c.get('user'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - //* ユーザ情報を取得 (user table) *// - const userData = await db - .select({ display_name: user.displayName }) - .from(user) - .where(eq(user.id, loginUser.id)) - .limit(1); + // ビジネスロジック呼び出し + const response = await getUserProfileUseCase(c.env, loginUser.id); // レスポンス - return c.json( - { - display_name: userData[0].display_name === null ? '' : userData[0].display_name, - } satisfies GetUserProfileResponseSchemaType, - 200 - ); + return c.json(response, 200); }); //TODO: userProfileの更新 -const updateUserProfile = route.createSchema( +const updateUserProfileSchema = route.createSchema( { path: '/api/profile', method: 'patch', @@ -85,20 +67,12 @@ const updateUserProfile = route.createSchema( [401, 500] as const ); -hono.openapi(updateUserProfile, async (c) => { +hono.openapi(updateUserProfileSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); - // データベース接続 - const db = drizzle({ connection: c.env.HYPERDRIVE }); - - //* ユーザ情報を更新 (user table) *// - await db - .update(user) - .set({ - displayName: typeof body.display_name === 'undefined' ? null : body.display_name, - }) - .where(eq(user.id, loginUser.id)); + // ビジネスロジック呼び出し + await updateUserProfileUseCase(c.env, loginUser.id, body.display_name); // レスポンス return c.body(null, 204); diff --git a/products/backend/src/presentation/utils/types.ts b/products/backend/src/presentation/utils/types.ts deleted file mode 100644 index b853b82..0000000 --- a/products/backend/src/presentation/utils/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type TransactionType = { - user_id: string; // 取引相手のユーザID - lent_amount: number; // 貸した金額 - borrowed_amount: number; // 借りた金額 -}; From ddf82b5cd947cddc8135bb09d4050972e10a374d Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 00:45:40 +0900 Subject: [PATCH 02/15] chore: fix copilot suggestion --- products/backend/src/application/group.ts | 3 +-- products/backend/src/application/utils/types.ts | 2 +- products/backend/src/application/utils/user.ts | 4 ++-- products/backend/src/presentation/routes/group.ts | 14 +++++++------- products/backend/src/presentation/routes/info.ts | 6 +++--- .../backend/src/presentation/routes/session.ts | 4 ++-- .../backend/src/presentation/routes/userProfile.ts | 4 ++-- 7 files changed, 18 insertions(+), 19 deletions(-) diff --git a/products/backend/src/application/group.ts b/products/backend/src/application/group.ts index b542e67..492b4ad 100644 --- a/products/backend/src/application/group.ts +++ b/products/backend/src/application/group.ts @@ -5,7 +5,6 @@ import { Bindings } from '../types'; import { type JoinGroupResponseSchemaType, type CreateGroupResponseSchemaType, - type GetGroupInfoResponseMemberElementSchemaType, type GetGroupInfoResponseSchemaType, type GetGroupDebtHistoryResponseSchemaType, type GetGroupDebtHistoryResponseElementSchemaType, @@ -13,7 +12,7 @@ import { // drizzle import { createDbConnection } from './utils/db'; import { eq, and, isNull, isNotNull } from 'drizzle-orm'; -import { debt, group, groupMembership, user } from '../db/schema'; +import { debt, group, groupMembership } from '../db/schema'; // utils import { ensureGroupMembership, ensureNotGroupMembership } from './utils/group'; import { getUserInfo, getUserNameMap } from './utils/user'; diff --git a/products/backend/src/application/utils/types.ts b/products/backend/src/application/utils/types.ts index ef19f5a..891f6dd 100644 --- a/products/backend/src/application/utils/types.ts +++ b/products/backend/src/application/utils/types.ts @@ -1,5 +1,5 @@ // drizzle -import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; export type DatabaseType = NodePgDatabase> & { diff --git a/products/backend/src/application/utils/user.ts b/products/backend/src/application/utils/user.ts index c77283b..602c5a3 100644 --- a/products/backend/src/application/utils/user.ts +++ b/products/backend/src/application/utils/user.ts @@ -1,8 +1,8 @@ // drizzle import { eq, inArray } from 'drizzle-orm/sql/expressions/conditions'; -import { groupMembership, user } from '../../db/schema'; +import { user } from '../../db/schema'; // types -import { DatabaseType, FormattedUserTableType, UserInfoType, UserNameType } from './types'; +import { DatabaseType, FormattedUserTableType, UserNameType } from './types'; export const formatUserName = (userName: UserNameType): string => { return userName.displayName !== null && userName.displayName.length > 0 ? userName.displayName : userName.name; diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts index 1a19344..160b56a 100644 --- a/products/backend/src/presentation/routes/group.ts +++ b/products/backend/src/presentation/routes/group.ts @@ -29,7 +29,7 @@ import { const hono = honoFactory(); -//TODO: グループ作成エンドポイントの登録 +// NOTE: グループ作成エンドポイントの登録 const createGroupSchema = route.createSchema( { path: '/api/group/create', @@ -71,7 +71,7 @@ hono.openapi(createGroupSchema, async (c) => { return c.json(response, 201); }); -//TODO: メンバー登録のエンドポイントの登録 +// NOTE: メンバー登録のエンドポイントの登録 const joinGroupSchema = route.createSchema( { path: '/api/group/join', @@ -113,7 +113,7 @@ hono.openapi(joinGroupSchema, async (c) => { return c.json(response, 201); }); -//TODO: 各グループ情報取得エンドポイントの登録 +// NOTE: 各グループ情報取得エンドポイントの登録 const getGroupInfoSchema = route.createSchema( { path: '/api/group/info', @@ -155,7 +155,7 @@ hono.openapi(getGroupInfoSchema, async (c) => { return c.json(response, 201); }); -// TODO: 各グループの貸し借り履歴取得エンドポイントの登録 +// NOTE: 各グループの貸し借り履歴取得エンドポイントの登録 const getGroupDebtHistorySchema = route.createSchema( { path: '/api/group/debt/history', @@ -197,7 +197,7 @@ hono.openapi(getGroupDebtHistorySchema, async (c) => { return c.json(response, 201); }); -// TODO: 貸し借りの履歴の追加エンドポイントの登録 +// NOTE: 貸し借りの履歴の追加エンドポイントの登録 const registerGroupDebtSchema = route.createSchema( { path: '/api/group/debt/register', @@ -243,7 +243,7 @@ hono.openapi(registerGroupDebtSchema, async (c) => { return c.body(null, 204); }); -// TODO: 貸し借りの履歴の削除エンドポイントの登録 +// NOTE: 貸し借りの履歴の削除エンドポイントの登録 const deleteGroupDebtSchema = route.createSchema( { path: '/api/group/debt/delete', @@ -280,7 +280,7 @@ hono.openapi(deleteGroupDebtSchema, async (c) => { return c.body(null, 204); }); -// TODO: 貸し借り履歴の削除の取り消しエンドポイントの登録 +// NOTE: 貸し借り履歴の削除の取り消しエンドポイントの登録 const cancelGroupDebtSchema = route.createSchema( { path: '/api/group/debt/cancel', diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index 2dc71c5..e628c27 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -17,7 +17,7 @@ import { const hono = honoFactory(); -//TODO: ユーザが参加しているグループ一覧を返す +// NOTE: ユーザが参加しているグループ一覧を返す const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( { path: '/api/info/group', @@ -49,7 +49,7 @@ hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => { return c.json(response, 200); }); -//TODO: ユーザの貸し借りの履歴を返す +// NOTE: ユーザの貸し借りの履歴を返す const infoAboutUserTransactionsSchema = route.createSchema( { path: '/api/info/transaction', @@ -81,7 +81,7 @@ hono.openapi(infoAboutUserTransactionsSchema, async (c) => { return c.json(response, 200); }); -//TODO: ユーザの返済処理を行う +// NOTE: ユーザの返済処理を行う const infoUserRepaymentSchema = route.createSchema( { path: '/api/info/transaction', diff --git a/products/backend/src/presentation/routes/session.ts b/products/backend/src/presentation/routes/session.ts index 815e48e..0d8f38a 100644 --- a/products/backend/src/presentation/routes/session.ts +++ b/products/backend/src/presentation/routes/session.ts @@ -1,7 +1,7 @@ // hono instance import honoFactory from '../factory/hono'; // validator -import { sessionCheckResponseSchema, type SessionCheckResponseSchemaType } from 'validator'; +import { sessionCheckResponseSchema } from 'validator'; // error schema import { route } from '../share/error'; // application @@ -9,7 +9,7 @@ import { getSessionCheckUseCase } from '../../application/session'; const hono = honoFactory(); -//TODO: userProfileの取得 +// NOTE: セッションが有効か確認するエンドポイントのスキーマ定義 const getSessionCheckSchema = route.createSchema( { path: '/api/session', diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts index 96f924c..0fb403f 100644 --- a/products/backend/src/presentation/routes/userProfile.ts +++ b/products/backend/src/presentation/routes/userProfile.ts @@ -9,7 +9,7 @@ import { getUserProfileUseCase, updateUserProfileUseCase } from '../../applicati const hono = honoFactory(); -//TODO: userProfileの取得 +// NOTE: userProfileの取得 const getUserProfileSchema = route.createSchema( { path: '/api/profile', @@ -41,7 +41,7 @@ hono.openapi(getUserProfileSchema, async (c) => { return c.json(response, 200); }); -//TODO: userProfileの更新 +// NOTE: userProfileの更新 const updateUserProfileSchema = route.createSchema( { path: '/api/profile', From 9ca20f02c1c65f9699c343cc86816299a2a12de5 Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 00:51:29 +0900 Subject: [PATCH 03/15] wip --- products/backend/src/application/group.ts | 12 ++++++------ products/backend/src/application/utils/group.ts | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/products/backend/src/application/group.ts b/products/backend/src/application/group.ts index 492b4ad..4b55265 100644 --- a/products/backend/src/application/group.ts +++ b/products/backend/src/application/group.ts @@ -14,7 +14,7 @@ import { createDbConnection } from './utils/db'; import { eq, and, isNull, isNotNull } from 'drizzle-orm'; import { debt, group, groupMembership } from '../db/schema'; // utils -import { ensureGroupMembership, ensureNotGroupMembership } from './utils/group'; +import { getGroupMembers, ensureNotGroupMembership } from './utils/group'; import { getUserInfo, getUserNameMap } from './utils/user'; export const createGroupUseCase = async ( @@ -118,7 +118,7 @@ export const getGroupInfoUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await ensureGroupMembership(db, groupId, loginUserId); + await getGroupMembers(db, groupId, loginUserId); //* body.group_id のグループ情報を取得 *// const groupData = await db @@ -160,7 +160,7 @@ export const getGroupDebtHistoryUseCase = async ( const db = createDbConnection(env); // loginUser がグループのメンバーであることを確認 - await ensureGroupMembership(db, groupId, loginUserId); + await getGroupMembers(db, groupId, loginUserId); //* グループの貸し借り履歴を取得 *// // body.group_id の貸し借り履歴を取得 (debt table) @@ -247,7 +247,7 @@ export const registerGroupDebtUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await ensureGroupMembership(db, groupId, loginUserId); + await getGroupMembers(db, groupId, loginUserId); // body.group_id に貸し借り履歴を追加 (debt table) await db.insert(debt).values({ @@ -271,7 +271,7 @@ export const deleteGroupDebtUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await ensureGroupMembership(db, groupId, loginUserId); + await getGroupMembers(db, groupId, loginUserId); //* body.debt_id の貸し借りの履歴の削除 (論理削除) *// await db @@ -293,7 +293,7 @@ export const cancelGroupDebtUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await ensureGroupMembership(db, groupId, loginUserId); + await getGroupMembers(db, groupId, loginUserId); //* body.debt_id の貸し借りの履歴の削除の取り消し (論理削除の取り消し) *// await db diff --git a/products/backend/src/application/utils/group.ts b/products/backend/src/application/utils/group.ts index 5e9fd84..68c30d8 100644 --- a/products/backend/src/application/utils/group.ts +++ b/products/backend/src/application/utils/group.ts @@ -8,7 +8,7 @@ import { DatabaseType, UserInfoType } from './types'; // utils import { getUserNameMap } from './user'; -export const ensureGroupMembership = async (db: DatabaseType, groupId: string, loginUserId: string): Promise => { +export const getGroupMembers = async (db: DatabaseType, groupId: string, loginUserId: string): Promise => { // loginUser が body.group_id のグループのメンバーであることを確認 const me = await db .select({ @@ -35,8 +35,15 @@ export const ensureNotGroupMembership = async (db: DatabaseType, groupId: string const userIds = memberUserIds.map((member) => member.userId); const nameMap = await getUserNameMap(db, userIds); - return memberUserIds.map((member) => ({ - id: member.userId, - name: nameMap.get(member.userId) ?? '', - })); + return memberUserIds.map((member) => { + const name = nameMap.get(member.userId); + if (typeof name === 'undefined') { + // データ整合性の問題として扱い、サーバエラーを返す + throw new HTTPException(500, { message: 'Internal Server Error' }); + } + return { + id: member.userId, + name: name, + }; + }); }; From bbc90ed4ea698c8171ab75f8f8767545ab5c1ae1 Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 01:51:28 +0900 Subject: [PATCH 04/15] chore: refactoring info usecase layer logic --- products/backend/src/application/group.ts | 24 +-- products/backend/src/application/info.ts | 155 +++++++----------- .../backend/src/application/utils/group.ts | 4 +- 3 files changed, 75 insertions(+), 108 deletions(-) diff --git a/products/backend/src/application/group.ts b/products/backend/src/application/group.ts index 4b55265..6428d51 100644 --- a/products/backend/src/application/group.ts +++ b/products/backend/src/application/group.ts @@ -14,7 +14,7 @@ import { createDbConnection } from './utils/db'; import { eq, and, isNull, isNotNull } from 'drizzle-orm'; import { debt, group, groupMembership } from '../db/schema'; // utils -import { getGroupMembers, ensureNotGroupMembership } from './utils/group'; +import { validateIsGroupMember, getGroupMembers } from './utils/group'; import { getUserInfo, getUserNameMap } from './utils/user'; export const createGroupUseCase = async ( @@ -118,7 +118,7 @@ export const getGroupInfoUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await getGroupMembers(db, groupId, loginUserId); + await validateIsGroupMember(db, groupId, loginUserId); //* body.group_id のグループ情報を取得 *// const groupData = await db @@ -136,7 +136,7 @@ export const getGroupInfoUseCase = async ( const createdByUserInfo = await getUserInfo(db, groupData[0].createdBy); // グループのメンバー情報を取得 - const members = (await ensureNotGroupMembership(db, groupId)).map((member) => ({ + const members = (await getGroupMembers(db, groupId)).map((member) => ({ user_id: member.id, user_name: member.name, })); @@ -160,7 +160,7 @@ export const getGroupDebtHistoryUseCase = async ( const db = createDbConnection(env); // loginUser がグループのメンバーであることを確認 - await getGroupMembers(db, groupId, loginUserId); + await validateIsGroupMember(db, groupId, loginUserId); //* グループの貸し借り履歴を取得 *// // body.group_id の貸し借り履歴を取得 (debt table) @@ -181,14 +181,16 @@ export const getGroupDebtHistoryUseCase = async ( const userIds = rawDebtData.flatMap((debtEntry) => [debtEntry.debtorId, debtEntry.creditorId, debtEntry.deletedBy].filter((id): id is string => id !== null) ); + + // ユーザ名の取得 const uniqueUserIds = Array.from(new Set(userIds)); - const nameMap = await getUserNameMap(db, uniqueUserIds); + const userNameMap = await getUserNameMap(db, uniqueUserIds); // 貸し借り履歴データを配列に追加 const debtData: GetGroupDebtHistoryResponseElementSchemaType[] = rawDebtData.map((debtEntry) => { // debt_name, creditor_name を取得 - const debtorName = nameMap.get(debtEntry.debtorId); - const creditorName = nameMap.get(debtEntry.creditorId); + const debtorName = userNameMap.get(debtEntry.debtorId); + const creditorName = userNameMap.get(debtEntry.creditorId); if (debtorName === undefined || creditorName === undefined) { throw new HTTPException(500, { message: 'Internal Server Error' }); } @@ -197,7 +199,7 @@ export const getGroupDebtHistoryUseCase = async ( let deletedByName = null; if (debtEntry.deletedBy !== null) { // deletedByが存在する場合のみ名前を取得 - const name = nameMap.get(debtEntry.deletedBy); + const name = userNameMap.get(debtEntry.deletedBy); if (name === undefined) { throw new HTTPException(500, { message: 'Internal Server Error' }); } @@ -247,7 +249,7 @@ export const registerGroupDebtUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await getGroupMembers(db, groupId, loginUserId); + await validateIsGroupMember(db, groupId, loginUserId); // body.group_id に貸し借り履歴を追加 (debt table) await db.insert(debt).values({ @@ -271,7 +273,7 @@ export const deleteGroupDebtUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await getGroupMembers(db, groupId, loginUserId); + await validateIsGroupMember(db, groupId, loginUserId); //* body.debt_id の貸し借りの履歴の削除 (論理削除) *// await db @@ -293,7 +295,7 @@ export const cancelGroupDebtUseCase = async ( const db = createDbConnection(env); // loginUser が グループのメンバーであることを確認 - await getGroupMembers(db, groupId, loginUserId); + await validateIsGroupMember(db, groupId, loginUserId); //* body.debt_id の貸し借りの履歴の削除の取り消し (論理削除の取り消し) *// await db diff --git a/products/backend/src/application/info.ts b/products/backend/src/application/info.ts index aeba3e6..d3d49b0 100644 --- a/products/backend/src/application/info.ts +++ b/products/backend/src/application/info.ts @@ -1,4 +1,5 @@ // hono +import { HTTPException } from 'hono/http-exception'; import { Bindings } from '../types'; // validator import { @@ -11,11 +12,13 @@ import { // drizzle import { createDbConnection } from './utils/db'; import { eq, and, isNull, inArray, or } from 'drizzle-orm'; -import { user } from '../db/auth-schema'; import { debt, group, groupMembership } from '../db/schema'; +// types import { TransactionType } from './utils/types'; +// utils +import { getUserNameMap } from './utils/user'; +import { getGroupMembers } from './utils/group'; -// TODO: 共通化 export const infoAboutGroupsTheUserBelongsToUseCase = async ( env: Bindings, loginUserId: string @@ -23,11 +26,8 @@ export const infoAboutGroupsTheUserBelongsToUseCase = async ( // データベース接続 const db = createDbConnection(env); - // グループ情報格納用配列 - let groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = []; - //* ユーザが参加しているグループ情報を取得 (group table) *// - const groupsData = await db + const groupData = await db .select({ id: group.id, name: group.name, @@ -44,67 +44,36 @@ export const infoAboutGroupsTheUserBelongsToUseCase = async ( ) ); - for (const groupData of groupsData) { - //* body.group_id のグループ作成者情報を取得 *// - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const createdByUserNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, groupData.createdBy)) - .limit(1); - - // NOTE: --- 共通化開始 --- - //* groupData.id のメンバー情報を取得 *// - // グループメンバーのユーザーIDを取得 (group_membership table) - const memberUserIds = await db - .select({ - userId: groupMembership.userId, - }) - .from(groupMembership) - .where(eq(groupMembership.groupId, groupData.id)); - - // メンバー情報を格納する配列 - const members: InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType[] = []; - - for (const memberUserId of memberUserIds) { - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const userNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, memberUserId.userId)) - .limit(1); - - // メンバー情報を配列に追加 - members.push({ - user_id: memberUserId.userId, - user_name: - userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 - ? userNameInfo[0].displayName - : userNameInfo[0].name, + // createdByのユーザ名の取得 + const uniqueCreatedByIds = Array.from(new Set(groupData.map((group) => group.createdBy))); + const createdByNameMap = await getUserNameMap(db, uniqueCreatedByIds); + + // グループ情報の整形 + const groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( + groupData.map(async (groupData) => { + // createdByのユーザ名取得 + const createdByName = createdByNameMap.get(groupData.createdBy); + if (createdByName === undefined) { + throw new HTTPException(500, { message: 'Internal Server Error' }); + } + + // グループメンバーの取得 + const members = (await getGroupMembers(db, groupData.id)).map((member) => { + return { + user_id: member.id, + user_name: member.name, + } as InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; }); - } - // NOTE: --- 共通化終了 --- - - // グループ情報を配列に追加 - groupInfo.push({ - group_id: groupData.id, - group_name: groupData.name, - created_by_id: groupData.createdBy, - created_by_name: - createdByUserNameInfo[0].displayName !== null && createdByUserNameInfo[0].displayName.length > 0 - ? createdByUserNameInfo[0].displayName - : createdByUserNameInfo[0].name, - members: members, - }); - } + + return { + group_id: groupData.id, + group_name: groupData.name, + created_by_id: groupData.createdBy, + created_by_name: createdByName, + members: members, + }; + }) + ); // レスポンス return { @@ -112,7 +81,6 @@ export const infoAboutGroupsTheUserBelongsToUseCase = async ( } satisfies InfoAboutGroupsTheUserBelongsToResponseSchemaType; }; -// TODO: 共通化 export const infoAboutUserTransactionsUseCase = async ( env: Bindings, loginUserId: string @@ -167,39 +135,36 @@ export const infoAboutUserTransactionsUseCase = async ( existing.borrowed_amount += amount; } - // 集計結果を格納する配列 - const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = []; + // ユーザ名の取得 + const userIds = Array.from(transactions.keys()); + const userNameMap = await getUserNameMap(db, userIds); // 貸し借りの合算 - for (const transaction of transactions.values()) { - // 合算結果: netAmount = borrowed_amount - lent_amount - const netAmount = transaction.borrowed_amount - transaction.lent_amount; - - // NOTE: 共通化できそう - // ユーザ名を取得 (user table) - const userNameInfo = await db - .select({ - name: user.name, - displayName: user.displayName, - }) - .from(user) - .where(eq(user.id, transaction.user_id)) - .limit(1); - - // netAmount が 0 でない場合のみ配列に追加 - if (netAmount !== 0) { - aggregatedTransactions.push({ + const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = Array.from( + transactions.values().map((transaction) => { + // 合算結果: netAmount = borrowed_amount - lent_amount + const netAmount = transaction.borrowed_amount - transaction.lent_amount; + + // netAmount が 0 の場合はスキップ + if (netAmount === 0) { + return null; + } + + // ユーザ名の取得 + const counterpartyName = userNameMap.get(transaction.user_id); + if (counterpartyName === undefined) { + throw new HTTPException(500, { message: 'Internal Server Error' }); + } + + return { counterparty_id: transaction.user_id, - counterparty_name: - userNameInfo[0].displayName !== null && userNameInfo[0].displayName.length > 0 - ? userNameInfo[0].displayName - : userNameInfo[0].name, + counterparty_name: counterpartyName, amount: netAmount, - }); - } - } + } as InfoAboutUserTransactionsResponseTransactionElementSchemaType; + }) + ).filter((item): item is InfoAboutUserTransactionsResponseTransactionElementSchemaType => item !== null); - // return response + // レスポンス return { transactions: aggregatedTransactions, } satisfies InfoAboutUserTransactionsResponseSchemaType; diff --git a/products/backend/src/application/utils/group.ts b/products/backend/src/application/utils/group.ts index 68c30d8..8d85ee6 100644 --- a/products/backend/src/application/utils/group.ts +++ b/products/backend/src/application/utils/group.ts @@ -8,7 +8,7 @@ import { DatabaseType, UserInfoType } from './types'; // utils import { getUserNameMap } from './user'; -export const getGroupMembers = async (db: DatabaseType, groupId: string, loginUserId: string): Promise => { +export const validateIsGroupMember = async (db: DatabaseType, groupId: string, loginUserId: string): Promise => { // loginUser が body.group_id のグループのメンバーであることを確認 const me = await db .select({ @@ -24,7 +24,7 @@ export const getGroupMembers = async (db: DatabaseType, groupId: string, loginUs } }; -export const ensureNotGroupMembership = async (db: DatabaseType, groupId: string): Promise => { +export const getGroupMembers = async (db: DatabaseType, groupId: string): Promise => { // グループに所属しているユーザIDを取得 const memberUserIds = await db .select({ userId: groupMembership.userId }) From 7b535a09fdd24578d826758b3bab7da9542bcade Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 02:00:19 +0900 Subject: [PATCH 05/15] chore: rename info section --- products/backend/src/application/info.ts | 34 +++++++++---------- .../backend/src/presentation/routes/info.ts | 32 ++++++++--------- products/validator/src/index.ts | 20 +++++------ products/validator/src/response/info.ts | 34 +++++++++---------- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/products/backend/src/application/info.ts b/products/backend/src/application/info.ts index d3d49b0..ca32727 100644 --- a/products/backend/src/application/info.ts +++ b/products/backend/src/application/info.ts @@ -3,11 +3,11 @@ import { HTTPException } from 'hono/http-exception'; import { Bindings } from '../types'; // validator import { - type InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType, - type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, - type InfoAboutGroupsTheUserBelongsToResponseSchemaType, - type InfoAboutUserTransactionsResponseSchemaType, - type InfoAboutUserTransactionsResponseTransactionElementSchemaType, + type GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType, + type GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, + type GetInfoAboutGroupsTheUserBelongsToResponseSchemaType, + type GetInfoAboutUserTransactionsResponseSchemaType, + type GetInfoAboutUserTransactionsResponseTransactionElementSchemaType, } from 'validator'; // drizzle import { createDbConnection } from './utils/db'; @@ -19,10 +19,10 @@ import { TransactionType } from './utils/types'; import { getUserNameMap } from './utils/user'; import { getGroupMembers } from './utils/group'; -export const infoAboutGroupsTheUserBelongsToUseCase = async ( +export const getInfoAboutGroupsTheUserBelongsToUseCase = async ( env: Bindings, loginUserId: string -): Promise => { +): Promise => { // データベース接続 const db = createDbConnection(env); @@ -49,7 +49,7 @@ export const infoAboutGroupsTheUserBelongsToUseCase = async ( const createdByNameMap = await getUserNameMap(db, uniqueCreatedByIds); // グループ情報の整形 - const groupInfo: InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( + const groupInfo: GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( groupData.map(async (groupData) => { // createdByのユーザ名取得 const createdByName = createdByNameMap.get(groupData.createdBy); @@ -62,7 +62,7 @@ export const infoAboutGroupsTheUserBelongsToUseCase = async ( return { user_id: member.id, user_name: member.name, - } as InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; + } as GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; }); return { @@ -78,13 +78,13 @@ export const infoAboutGroupsTheUserBelongsToUseCase = async ( // レスポンス return { groups: groupInfo, - } satisfies InfoAboutGroupsTheUserBelongsToResponseSchemaType; + } satisfies GetInfoAboutGroupsTheUserBelongsToResponseSchemaType; }; -export const infoAboutUserTransactionsUseCase = async ( +export const getInfoAboutUserTransactionsUseCase = async ( env: Bindings, loginUserId: string -): Promise => { +): Promise => { // データベース接続 const db = createDbConnection(env); @@ -140,7 +140,7 @@ export const infoAboutUserTransactionsUseCase = async ( const userNameMap = await getUserNameMap(db, userIds); // 貸し借りの合算 - const aggregatedTransactions: InfoAboutUserTransactionsResponseTransactionElementSchemaType[] = Array.from( + const aggregatedTransactions: GetInfoAboutUserTransactionsResponseTransactionElementSchemaType[] = Array.from( transactions.values().map((transaction) => { // 合算結果: netAmount = borrowed_amount - lent_amount const netAmount = transaction.borrowed_amount - transaction.lent_amount; @@ -160,17 +160,17 @@ export const infoAboutUserTransactionsUseCase = async ( counterparty_id: transaction.user_id, counterparty_name: counterpartyName, amount: netAmount, - } as InfoAboutUserTransactionsResponseTransactionElementSchemaType; + } as GetInfoAboutUserTransactionsResponseTransactionElementSchemaType; }) - ).filter((item): item is InfoAboutUserTransactionsResponseTransactionElementSchemaType => item !== null); + ).filter((item): item is GetInfoAboutUserTransactionsResponseTransactionElementSchemaType => item !== null); // レスポンス return { transactions: aggregatedTransactions, - } satisfies InfoAboutUserTransactionsResponseSchemaType; + } satisfies GetInfoAboutUserTransactionsResponseSchemaType; }; -export const infoUserRepaymentUseCase = async ( +export const deleteInfoUserRepaymentUseCase = async ( env: Bindings, loginUserId: string, counterpartyId: string diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index e628c27..18b355e 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -2,23 +2,23 @@ import honoFactory from '../factory/hono'; // validator import { - infoAboutUserTransactionsResponseSchema, - infoAboutGroupsTheUserBelongsToResponseSchema, + getInfoAboutUserTransactionsResponseSchema, + getInfoAboutGroupsTheUserBelongsToResponseSchema, deleteInfoAboutUserRepaymentRequestSchema, } from 'validator'; // error schema import { route } from '../share/error'; // types import { - infoAboutGroupsTheUserBelongsToUseCase, - infoAboutUserTransactionsUseCase, - infoUserRepaymentUseCase, + getInfoAboutGroupsTheUserBelongsToUseCase, + getInfoAboutUserTransactionsUseCase, + deleteInfoUserRepaymentUseCase, } from '../../application/info'; const hono = honoFactory(); // NOTE: ユーザが参加しているグループ一覧を返す -const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( +const getInfoAboutGroupsTheUserBelongsToSchema = route.createSchema( { path: '/api/info/group', method: 'get', @@ -30,7 +30,7 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( description: 'OK', content: { 'application/json': { - schema: infoAboutGroupsTheUserBelongsToResponseSchema, + schema: getInfoAboutGroupsTheUserBelongsToResponseSchema, }, }, }, @@ -39,18 +39,18 @@ const infoAboutGroupsTheUserBelongsToSchema = route.createSchema( [401, 500] as const ); -hono.openapi(infoAboutGroupsTheUserBelongsToSchema, async (c) => { +hono.openapi(getInfoAboutGroupsTheUserBelongsToSchema, async (c) => { const loginUser = c.get('user'); // ビジネスロジック呼び出し - const response = await infoAboutGroupsTheUserBelongsToUseCase(c.env, loginUser.id); + const response = await getInfoAboutGroupsTheUserBelongsToUseCase(c.env, loginUser.id); // レスポンス return c.json(response, 200); }); // NOTE: ユーザの貸し借りの履歴を返す -const infoAboutUserTransactionsSchema = route.createSchema( +const getInfoAboutUserTransactionsSchema = route.createSchema( { path: '/api/info/transaction', method: 'get', @@ -62,7 +62,7 @@ const infoAboutUserTransactionsSchema = route.createSchema( description: 'OK', content: { 'application/json': { - schema: infoAboutUserTransactionsResponseSchema, + schema: getInfoAboutUserTransactionsResponseSchema, }, }, }, @@ -71,18 +71,18 @@ const infoAboutUserTransactionsSchema = route.createSchema( [401, 500] as const ); -hono.openapi(infoAboutUserTransactionsSchema, async (c) => { +hono.openapi(getInfoAboutUserTransactionsSchema, async (c) => { const loginUser = c.get('user'); // ビジネスロジック呼び出し - const response = await infoAboutUserTransactionsUseCase(c.env, loginUser.id); + const response = await getInfoAboutUserTransactionsUseCase(c.env, loginUser.id); // レスポンス return c.json(response, 200); }); // NOTE: ユーザの返済処理を行う -const infoUserRepaymentSchema = route.createSchema( +const deleteInfoUserRepaymentSchema = route.createSchema( { path: '/api/info/transaction', method: 'delete', @@ -107,12 +107,12 @@ const infoUserRepaymentSchema = route.createSchema( [401, 500] as const ); -hono.openapi(infoUserRepaymentSchema, async (c) => { +hono.openapi(deleteInfoUserRepaymentSchema, async (c) => { const loginUser = c.get('user'); const body = c.req.valid('json'); // ビジネスロジック呼び出し - await infoUserRepaymentUseCase(c.env, loginUser.id, body.counterparty_id); + await deleteInfoUserRepaymentUseCase(c.env, loginUser.id, body.counterparty_id); // レスポンス return c.body(null, 204); diff --git a/products/validator/src/index.ts b/products/validator/src/index.ts index 5ffeb18..c86acf5 100644 --- a/products/validator/src/index.ts +++ b/products/validator/src/index.ts @@ -8,24 +8,24 @@ export { } from './request/info'; // info response schema export { - infoAboutGroupsTheUserBelongsToResponseMemberElementSchema, - type InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType, + getInfoAboutGroupsTheUserBelongsToResponseMemberElementSchema, + type GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType, } from './response/info'; export { - infoAboutGroupsTheUserBelongsToResponseGroupElementSchema, - type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, + getInfoAboutGroupsTheUserBelongsToResponseGroupElementSchema, + type GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType, } from './response/info'; export { - infoAboutGroupsTheUserBelongsToResponseSchema, - type InfoAboutGroupsTheUserBelongsToResponseSchemaType, + getInfoAboutGroupsTheUserBelongsToResponseSchema, + type GetInfoAboutGroupsTheUserBelongsToResponseSchemaType, } from './response/info'; export { - infoAboutUserTransactionsResponseTransactionElementSchema, - type InfoAboutUserTransactionsResponseTransactionElementSchemaType, + getInfoAboutUserTransactionsResponseTransactionElementSchema, + type GetInfoAboutUserTransactionsResponseTransactionElementSchemaType, } from './response/info'; export { - infoAboutUserTransactionsResponseSchema, - type InfoAboutUserTransactionsResponseSchemaType, + getInfoAboutUserTransactionsResponseSchema, + type GetInfoAboutUserTransactionsResponseSchemaType, } from './response/info'; // userProfile request schema diff --git a/products/validator/src/response/info.ts b/products/validator/src/response/info.ts index 2dc9077..a7d5034 100644 --- a/products/validator/src/response/info.ts +++ b/products/validator/src/response/info.ts @@ -1,46 +1,46 @@ import { z } from '@hono/zod-openapi'; -export const infoAboutGroupsTheUserBelongsToResponseMemberElementSchema = z.object({ +export const getInfoAboutGroupsTheUserBelongsToResponseMemberElementSchema = z.object({ user_id: z.string().min(1), user_name: z.string().min(1), }); -export const infoAboutGroupsTheUserBelongsToResponseGroupElementSchema = z.object({ +export const getInfoAboutGroupsTheUserBelongsToResponseGroupElementSchema = z.object({ group_id: z.uuid(), group_name: z.string().min(1), created_by_id: z.string().min(1), created_by_name: z.string().min(1), - members: z.array(infoAboutGroupsTheUserBelongsToResponseMemberElementSchema), + members: z.array(getInfoAboutGroupsTheUserBelongsToResponseMemberElementSchema), }); -export const infoAboutGroupsTheUserBelongsToResponseSchema = z.object({ - groups: z.array(infoAboutGroupsTheUserBelongsToResponseGroupElementSchema), +export const getInfoAboutGroupsTheUserBelongsToResponseSchema = z.object({ + groups: z.array(getInfoAboutGroupsTheUserBelongsToResponseGroupElementSchema), }); -export type InfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType = z.infer< - typeof infoAboutGroupsTheUserBelongsToResponseMemberElementSchema +export type GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType = z.infer< + typeof getInfoAboutGroupsTheUserBelongsToResponseMemberElementSchema >; -export type InfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType = z.infer< - typeof infoAboutGroupsTheUserBelongsToResponseGroupElementSchema +export type GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType = z.infer< + typeof getInfoAboutGroupsTheUserBelongsToResponseGroupElementSchema >; -export type InfoAboutGroupsTheUserBelongsToResponseSchemaType = z.infer< - typeof infoAboutGroupsTheUserBelongsToResponseSchema +export type GetInfoAboutGroupsTheUserBelongsToResponseSchemaType = z.infer< + typeof getInfoAboutGroupsTheUserBelongsToResponseSchema >; -export const infoAboutUserTransactionsResponseTransactionElementSchema = z.object({ +export const getInfoAboutUserTransactionsResponseTransactionElementSchema = z.object({ counterparty_id: z.string().min(1), counterparty_name: z.string().min(1), amount: z.number().min(0), }); -export const infoAboutUserTransactionsResponseSchema = z.object({ - transactions: z.array(infoAboutUserTransactionsResponseTransactionElementSchema), +export const getInfoAboutUserTransactionsResponseSchema = z.object({ + transactions: z.array(getInfoAboutUserTransactionsResponseTransactionElementSchema), }); -export type InfoAboutUserTransactionsResponseTransactionElementSchemaType = z.infer< - typeof infoAboutUserTransactionsResponseTransactionElementSchema +export type GetInfoAboutUserTransactionsResponseTransactionElementSchemaType = z.infer< + typeof getInfoAboutUserTransactionsResponseTransactionElementSchema >; -export type InfoAboutUserTransactionsResponseSchemaType = z.infer; +export type GetInfoAboutUserTransactionsResponseSchemaType = z.infer; From 6533062179d25aef0e63e25bd74c72ccd13bca1a Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 02:17:32 +0900 Subject: [PATCH 06/15] chore: fix info usecase layer --- products/backend/src/application/info.ts | 30 ++++++++++++++----- .../backend/src/application/utils/types.ts | 10 ------- .../backend/src/application/utils/user.ts | 22 ++++++-------- .../backend/src/presentation/api.protected.ts | 4 +-- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/products/backend/src/application/info.ts b/products/backend/src/application/info.ts index ca32727..f86c520 100644 --- a/products/backend/src/application/info.ts +++ b/products/backend/src/application/info.ts @@ -48,6 +48,20 @@ export const getInfoAboutGroupsTheUserBelongsToUseCase = async ( 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 groupMembersMap: Map = new Map(); + for (const groupId of uniqueGroupIds) { + const members = await getGroupMembers(db, groupId); + const formattedMembers = members.map((member) => { + return { + user_id: member.id, + user_name: member.name, + } as GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; + }); + groupMembersMap.set(groupId, formattedMembers); + } + // グループ情報の整形 const groupInfo: GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( groupData.map(async (groupData) => { @@ -58,12 +72,10 @@ export const getInfoAboutGroupsTheUserBelongsToUseCase = async ( } // グループメンバーの取得 - const members = (await getGroupMembers(db, groupData.id)).map((member) => { - return { - user_id: member.id, - user_name: member.name, - } as GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; - }); + const members = groupMembersMap.get(groupData.id); + if (members === undefined) { + throw new HTTPException(500, { message: 'Internal Server Error' }); + } return { group_id: groupData.id, @@ -141,7 +153,9 @@ export const getInfoAboutUserTransactionsUseCase = async ( // 貸し借りの合算 const aggregatedTransactions: GetInfoAboutUserTransactionsResponseTransactionElementSchemaType[] = Array.from( - transactions.values().map((transaction) => { + transactions.values() + ) + .map((transaction) => { // 合算結果: netAmount = borrowed_amount - lent_amount const netAmount = transaction.borrowed_amount - transaction.lent_amount; @@ -162,7 +176,7 @@ export const getInfoAboutUserTransactionsUseCase = async ( amount: netAmount, } as GetInfoAboutUserTransactionsResponseTransactionElementSchemaType; }) - ).filter((item): item is GetInfoAboutUserTransactionsResponseTransactionElementSchemaType => item !== null); + .filter((item): item is GetInfoAboutUserTransactionsResponseTransactionElementSchemaType => item !== null); // レスポンス return { diff --git a/products/backend/src/application/utils/types.ts b/products/backend/src/application/utils/types.ts index 891f6dd..6f7a996 100644 --- a/products/backend/src/application/utils/types.ts +++ b/products/backend/src/application/utils/types.ts @@ -21,13 +21,3 @@ export type UserInfoType = { id: string; name: string; }; - -export type FormattedUserTableType = { - id: string; - name: string; - email: string; - emailVerified: boolean; - image: string | null; - createdAt: Date; - updatedAt: Date; -}; diff --git a/products/backend/src/application/utils/user.ts b/products/backend/src/application/utils/user.ts index 602c5a3..281c57f 100644 --- a/products/backend/src/application/utils/user.ts +++ b/products/backend/src/application/utils/user.ts @@ -1,39 +1,35 @@ // drizzle -import { eq, inArray } from 'drizzle-orm/sql/expressions/conditions'; +import { eq, inArray } from 'drizzle-orm'; import { user } from '../../db/schema'; // types -import { DatabaseType, FormattedUserTableType, UserNameType } from './types'; +import { DatabaseType, UserInfoType, UserNameType } from './types'; +import { HTTPException } from 'hono/http-exception'; export const formatUserName = (userName: UserNameType): string => { return userName.displayName !== null && userName.displayName.length > 0 ? userName.displayName : userName.name; }; -export const getUserInfo = async (db: DatabaseType, userId: string): Promise => { +export const getUserInfo = async (db: DatabaseType, userId: string): Promise => { // user table からユーザ名を取得 const userNameInfo = await db .select({ id: user.id, name: user.name, - email: user.email, - emailVerified: user.emailVerified, - image: user.image, - createdAt: user.createdAt, - updatedAt: user.updatedAt, displayName: user.displayName, }) .from(user) .where(eq(user.id, userId)) .limit(1); + // ユーザが存在しない場合はエラー + if (userNameInfo.length === 0) { + throw new HTTPException(500, { message: 'Internal Server Error' }); + } + return { id: userNameInfo[0].id, // ユーザ名をフォーマット name: formatUserName({ name: userNameInfo[0].name, displayName: userNameInfo[0].displayName }), - email: userNameInfo[0].email, - emailVerified: userNameInfo[0].emailVerified, - image: userNameInfo[0].image, - createdAt: userNameInfo[0].createdAt, - updatedAt: userNameInfo[0].updatedAt, }; }; diff --git a/products/backend/src/presentation/api.protected.ts b/products/backend/src/presentation/api.protected.ts index 1183f32..65d27eb 100644 --- a/products/backend/src/presentation/api.protected.ts +++ b/products/backend/src/presentation/api.protected.ts @@ -5,7 +5,7 @@ import honoFactory from './factory/hono'; // better-auth object factory import { AuthFactoryType } from './factory/auth'; // routes -import session from './routes/session'; +import sessionRoute from './routes/session'; import group from './routes/group'; import info from './routes/info'; import userProfile from './routes/userProfile'; @@ -31,7 +31,7 @@ const apiProtected = (auth: AuthFactoryType) => { }); //* endpoint registration *// - app.route('/', session); + app.route('/', sessionRoute); app.route('/', group); app.route('/', info); app.route('/', userProfile); From baf513d0fb2048b14fb3f643fafe9abc8cf98b10 Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 02:35:01 +0900 Subject: [PATCH 07/15] wip --- products/backend/src/application/group.ts | 5 +++++ products/backend/src/application/userProfile.ts | 6 ++++++ products/validator/src/response/info.ts | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/products/backend/src/application/group.ts b/products/backend/src/application/group.ts index 6428d51..ef29152 100644 --- a/products/backend/src/application/group.ts +++ b/products/backend/src/application/group.ts @@ -131,6 +131,11 @@ export const getGroupInfoUseCase = async ( .where(eq(group.id, groupId)) .limit(1); + // group が存在しない場合は例外をスロー + if (groupData.length === 0) { + throw new HTTPException(404, { message: 'Group not found' }); + } + //* body.group_id のグループ作成者情報を取得 *// // ユーザ情報を取得 const createdByUserInfo = await getUserInfo(db, groupData[0].createdBy); diff --git a/products/backend/src/application/userProfile.ts b/products/backend/src/application/userProfile.ts index 0dd762b..f6f67f1 100644 --- a/products/backend/src/application/userProfile.ts +++ b/products/backend/src/application/userProfile.ts @@ -6,6 +6,7 @@ import { type GetUserProfileResponseSchemaType } from 'validator'; import { createDbConnection } from './utils/db'; import { eq } from 'drizzle-orm'; import { user } from '../db/auth-schema'; +import { HTTPException } from 'hono/http-exception'; export const getUserProfileUseCase = async ( env: Bindings, @@ -21,6 +22,11 @@ export const getUserProfileUseCase = async ( .where(eq(user.id, loginUserId)) .limit(1); + // ユーザが存在しない場合は例外をスロー + if (userData.length === 0) { + throw new HTTPException(404, { message: 'User not found' }); + } + // レスポンス return { display_name: userData[0].display_name === null ? '' : userData[0].display_name, diff --git a/products/validator/src/response/info.ts b/products/validator/src/response/info.ts index a7d5034..8408562 100644 --- a/products/validator/src/response/info.ts +++ b/products/validator/src/response/info.ts @@ -32,7 +32,7 @@ export type GetInfoAboutGroupsTheUserBelongsToResponseSchemaType = z.infer< export const getInfoAboutUserTransactionsResponseTransactionElementSchema = z.object({ counterparty_id: z.string().min(1), counterparty_name: z.string().min(1), - amount: z.number().min(0), + amount: z.number(), }); export const getInfoAboutUserTransactionsResponseSchema = z.object({ From eda66bfc0c362a308f6ca6f09fdf9aaa25c0c771 Mon Sep 17 00:00:00 2001 From: Yuki Osada <78744619+Myxogastria0808@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:45:53 +0900 Subject: [PATCH 08/15] Update products/backend/src/application/info.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- products/backend/src/application/info.ts | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/products/backend/src/application/info.ts b/products/backend/src/application/info.ts index f86c520..924c00f 100644 --- a/products/backend/src/application/info.ts +++ b/products/backend/src/application/info.ts @@ -50,17 +50,21 @@ export const getInfoAboutGroupsTheUserBelongsToUseCase = async ( // グループメンバーのユーザ名の取得 const uniqueGroupIds = Array.from(new Set(groupData.map((group) => group.id))); - const groupMembersMap: Map = new Map(); - for (const groupId of uniqueGroupIds) { - const members = await getGroupMembers(db, groupId); - const formattedMembers = members.map((member) => { - return { - user_id: member.id, - user_name: member.name, - } as GetInfoAboutGroupsTheUserBelongsToResponseMemberElementSchemaType; - }); - groupMembersMap.set(groupId, formattedMembers); - } + 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 = new Map( + groupMembersEntries + ); // グループ情報の整形 const groupInfo: GetInfoAboutGroupsTheUserBelongsToResponseGroupElementSchemaType[] = await Promise.all( From 546abd2c55b4cde1ea112fff9a27d8803734d562 Mon Sep 17 00:00:00 2001 From: Yuki Osada <78744619+Myxogastria0808@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:55:15 +0900 Subject: [PATCH 09/15] Update products/backend/src/presentation/routes/userProfile.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- products/backend/src/presentation/routes/userProfile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts index 0fb403f..a9479b2 100644 --- a/products/backend/src/presentation/routes/userProfile.ts +++ b/products/backend/src/presentation/routes/userProfile.ts @@ -28,7 +28,7 @@ const getUserProfileSchema = route.createSchema( }, }, }, - [401, 500] as const + [401, 404, 500] as const ); hono.openapi(getUserProfileSchema, async (c) => { From 5f761849da554fd687fc92bd87d88da6d7e9fe9f Mon Sep 17 00:00:00 2001 From: Yuki Osada <78744619+Myxogastria0808@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:55:33 +0900 Subject: [PATCH 10/15] Update products/backend/src/presentation/routes/userProfile.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- products/backend/src/presentation/routes/userProfile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts index a9479b2..9e277b5 100644 --- a/products/backend/src/presentation/routes/userProfile.ts +++ b/products/backend/src/presentation/routes/userProfile.ts @@ -4,7 +4,7 @@ import honoFactory from '../factory/hono'; import { getUserProfileResponseSchema, updateUserProfileRequestSchema } from 'validator'; // error schema import { route } from '../share/error'; -// drizzle +// application layer use cases import { getUserProfileUseCase, updateUserProfileUseCase } from '../../application/userProfile'; const hono = honoFactory(); From 941506ff9b146c01d1bd290ddc8f6f8f1caf86b6 Mon Sep 17 00:00:00 2001 From: Yuki Osada <78744619+Myxogastria0808@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:56:54 +0900 Subject: [PATCH 11/15] Update products/backend/src/presentation/routes/info.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- products/backend/src/presentation/routes/info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index 18b355e..3a87d60 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -8,7 +8,7 @@ import { } from 'validator'; // error schema import { route } from '../share/error'; -// types +// application import { getInfoAboutGroupsTheUserBelongsToUseCase, getInfoAboutUserTransactionsUseCase, From b1f665d8d002d4a957f5dd28886c53ec82b3236f Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 03:01:42 +0900 Subject: [PATCH 12/15] wip --- products/backend/src/presentation/routes/group.ts | 2 +- products/backend/src/presentation/share/error.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts index 160b56a..8c12831 100644 --- a/products/backend/src/presentation/routes/group.ts +++ b/products/backend/src/presentation/routes/group.ts @@ -141,7 +141,7 @@ const getGroupInfoSchema = route.createSchema( }, }, }, - [400, 401, 500] as const + [400, 401, 404, 500] as const ); hono.openapi(getGroupInfoSchema, async (c) => { diff --git a/products/backend/src/presentation/share/error.ts b/products/backend/src/presentation/share/error.ts index 68e393c..bf67605 100644 --- a/products/backend/src/presentation/share/error.ts +++ b/products/backend/src/presentation/share/error.ts @@ -10,6 +10,10 @@ export const route = new ZodOpenAPISchema({ description: 'Unauthorized', content: { 'application/json': { schema: errorResponseSchema } }, }, + 404: { + description: 'Not Found', + content: { 'application/json': { schema: errorResponseSchema } }, + }, 500: { description: 'Internal Server Error', content: { 'application/json': { schema: errorResponseSchema } }, From d08ab533b3be9ca587054e781a6cd32c5d6878ae Mon Sep 17 00:00:00 2001 From: Myxogastria0808 Date: Tue, 10 Feb 2026 03:10:48 +0900 Subject: [PATCH 13/15] wip --- products/backend/src/application/group.ts | 5 ----- products/backend/src/application/utils/types.ts | 2 +- products/backend/src/presentation/routes/group.ts | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/products/backend/src/application/group.ts b/products/backend/src/application/group.ts index ef29152..6428d51 100644 --- a/products/backend/src/application/group.ts +++ b/products/backend/src/application/group.ts @@ -131,11 +131,6 @@ export const getGroupInfoUseCase = async ( .where(eq(group.id, groupId)) .limit(1); - // group が存在しない場合は例外をスロー - if (groupData.length === 0) { - throw new HTTPException(404, { message: 'Group not found' }); - } - //* body.group_id のグループ作成者情報を取得 *// // ユーザ情報を取得 const createdByUserInfo = await getUserInfo(db, groupData[0].createdBy); diff --git a/products/backend/src/application/utils/types.ts b/products/backend/src/application/utils/types.ts index 6f7a996..bfd98e7 100644 --- a/products/backend/src/application/utils/types.ts +++ b/products/backend/src/application/utils/types.ts @@ -1,6 +1,6 @@ // drizzle import { NodePgDatabase } from 'drizzle-orm/node-postgres'; -import { Pool } from 'pg'; +import { type Pool } from 'pg'; export type DatabaseType = NodePgDatabase> & { $client: Pool; diff --git a/products/backend/src/presentation/routes/group.ts b/products/backend/src/presentation/routes/group.ts index 8c12831..160b56a 100644 --- a/products/backend/src/presentation/routes/group.ts +++ b/products/backend/src/presentation/routes/group.ts @@ -141,7 +141,7 @@ const getGroupInfoSchema = route.createSchema( }, }, }, - [400, 401, 404, 500] as const + [400, 401, 500] as const ); hono.openapi(getGroupInfoSchema, async (c) => { From 2417dde126a269082e41ded7732fd778069d8932 Mon Sep 17 00:00:00 2001 From: Yuki Osada <78744619+Myxogastria0808@users.noreply.github.com> Date: Tue, 10 Feb 2026 03:17:20 +0900 Subject: [PATCH 14/15] Update products/backend/src/presentation/routes/userProfile.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- products/backend/src/presentation/routes/userProfile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/backend/src/presentation/routes/userProfile.ts b/products/backend/src/presentation/routes/userProfile.ts index 9e277b5..28e4a92 100644 --- a/products/backend/src/presentation/routes/userProfile.ts +++ b/products/backend/src/presentation/routes/userProfile.ts @@ -64,7 +64,7 @@ const updateUserProfileSchema = route.createSchema( }, }, }, - [401, 500] as const + [400, 401, 500] as const ); hono.openapi(updateUserProfileSchema, async (c) => { From 327bb2f43798aa919db4e6d2a7f8ec618fd4b950 Mon Sep 17 00:00:00 2001 From: Yuki Osada <78744619+Myxogastria0808@users.noreply.github.com> Date: Tue, 10 Feb 2026 03:17:47 +0900 Subject: [PATCH 15/15] Update products/backend/src/presentation/routes/info.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- products/backend/src/presentation/routes/info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/backend/src/presentation/routes/info.ts b/products/backend/src/presentation/routes/info.ts index 3a87d60..7b421ae 100644 --- a/products/backend/src/presentation/routes/info.ts +++ b/products/backend/src/presentation/routes/info.ts @@ -104,7 +104,7 @@ const deleteInfoUserRepaymentSchema = route.createSchema( }, }, }, - [401, 500] as const + [400, 401, 500] as const ); hono.openapi(deleteInfoUserRepaymentSchema, async (c) => {