create-einja-app 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,17 @@
1
1
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2
- import { prefetchUsers } from "@/hooks/use-users";
2
+ import { prefetchUsers } from "@/hooks/api/prefetch-users";
3
+ import type { PaginatedUserList } from "@/shared/schemas/user";
3
4
  import { UserTableContainer } from "./_components/UserTableContainer";
4
5
 
5
6
  export default async function DataPage() {
6
- let initialData;
7
- let error;
7
+ let initialData: PaginatedUserList | undefined;
8
+ let error: string | undefined;
8
9
 
9
- try {
10
- initialData = await prefetchUsers({ page: 1, limit: 100 });
11
- } catch (e) {
12
- error = e instanceof Error ? e.message : "データの取得に失敗しました";
10
+ const result = await prefetchUsers({ page: 1, limit: 100 });
11
+ if (result === null) {
12
+ error = "データの取得に失敗しました";
13
+ } else {
14
+ initialData = result;
13
15
  }
14
16
 
15
17
  if (error) {
@@ -5,9 +5,9 @@
5
5
  * basePath: /api/rpc
6
6
  */
7
7
 
8
+ import { userRoutes } from "@web/server/presentation/routes/userRoutes";
8
9
  import { Hono } from "hono";
9
10
  import { handle } from "hono/vercel";
10
- import { userRoutes } from "@web/server/presentation/routes/userRoutes";
11
11
 
12
12
  const app = new Hono().basePath("/api/rpc");
13
13
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * prefetch-users.ts
3
+ *
4
+ * Server Component用のユーザーデータプリフェッチ関数
5
+ */
6
+
7
+ import { parseResponse } from "@/lib/api/parse-response";
8
+ import { type PaginatedUserList, paginatedUserListSchema } from "@/shared/schemas/user";
9
+ import { cookies, headers } from "next/headers";
10
+
11
+ /**
12
+ * User filters type
13
+ */
14
+ export interface UserFilters {
15
+ page?: number;
16
+ limit?: number;
17
+ search?: string;
18
+ status?: "active" | "inactive";
19
+ role?: "admin" | "user";
20
+ }
21
+
22
+ /**
23
+ * prefetchUsers
24
+ *
25
+ * Server Component用のデータ取得関数
26
+ */
27
+ export async function prefetchUsers(filters: UserFilters = {}): Promise<PaginatedUserList | null> {
28
+ try {
29
+ // リクエストヘッダーからホスト情報を取得
30
+ const headersList = await headers();
31
+ const host = headersList.get("host") || "localhost:3000";
32
+ const protocol = headersList.get("x-forwarded-proto") || "http";
33
+ const baseUrl = `${protocol}://${host}`;
34
+
35
+ // クッキーを取得して転送
36
+ const cookieStore = await cookies();
37
+ const cookieHeader = cookieStore
38
+ .getAll()
39
+ .map((c) => `${c.name}=${c.value}`)
40
+ .join("; ");
41
+
42
+ const params = new URLSearchParams();
43
+ if (filters.page) params.set("page", String(filters.page));
44
+ if (filters.limit) params.set("limit", String(filters.limit));
45
+ if (filters.search) params.set("search", filters.search);
46
+ if (filters.status) params.set("status", filters.status);
47
+ if (filters.role) params.set("role", filters.role);
48
+
49
+ const url = `${baseUrl}/api/rpc/users?${params.toString()}`;
50
+
51
+ const response = await fetch(url, {
52
+ headers: {
53
+ Cookie: cookieHeader,
54
+ },
55
+ cache: "no-store",
56
+ });
57
+
58
+ return parseResponse(response, paginatedUserListSchema);
59
+ } catch (error) {
60
+ console.error("Failed to prefetch users:", error);
61
+ return null;
62
+ }
63
+ }
@@ -6,9 +6,15 @@
6
6
 
7
7
  "use client";
8
8
 
9
+ import { apiClient } from "@/lib/api/client";
10
+ import { parseResponse } from "@/lib/api/parse-response";
11
+ import {
12
+ type PaginatedUserList,
13
+ type UserListItem,
14
+ paginatedUserListSchema,
15
+ userListItemSchema,
16
+ } from "@/shared/schemas/user";
9
17
  import { useQuery } from "@tanstack/react-query";
10
- import { apiClient } from "@/lib/api-client";
11
- import type { PaginatedUserList } from "@web/application/use-cases/UserUseCases";
12
18
 
13
19
  /**
14
20
  * QueryKey factory for users
@@ -37,10 +43,7 @@ export interface UserFilters {
37
43
  *
38
44
  * ユーザー一覧を取得するhook
39
45
  */
40
- export function useUsers(
41
- filters: UserFilters = {},
42
- initialData?: PaginatedUserList
43
- ) {
46
+ export function useUsers(filters: UserFilters = {}, initialData?: PaginatedUserList) {
44
47
  return useQuery({
45
48
  queryKey: userKeys.list(filters),
46
49
  queryFn: async () => {
@@ -54,11 +57,7 @@ export function useUsers(
54
57
  },
55
58
  });
56
59
 
57
- if (!response.ok) {
58
- throw new Error("Failed to fetch users");
59
- }
60
-
61
- return response.json() as Promise<PaginatedUserList>;
60
+ return parseResponse(response, paginatedUserListSchema);
62
61
  },
63
62
  initialData,
64
63
  });
@@ -77,42 +76,8 @@ export function useUser(id: string) {
77
76
  param: { id },
78
77
  });
79
78
 
80
- if (!response.ok) {
81
- throw new Error("Failed to fetch user");
82
- }
83
-
84
- return response.json();
79
+ return parseResponse(response, userListItemSchema);
85
80
  },
86
81
  enabled: !!id,
87
82
  });
88
83
  }
89
-
90
- /**
91
- * prefetchUsers
92
- *
93
- * Server Component用のデータ取得関数
94
- */
95
- export async function prefetchUsers(
96
- filters: UserFilters = {}
97
- ): Promise<PaginatedUserList> {
98
- const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
99
- const params = new URLSearchParams();
100
-
101
- if (filters.page) params.set("page", filters.page.toString());
102
- if (filters.limit) params.set("limit", filters.limit.toString());
103
- if (filters.search) params.set("search", filters.search);
104
- if (filters.status) params.set("status", filters.status);
105
- if (filters.role) params.set("role", filters.role);
106
-
107
- const url = `${baseUrl}/api/rpc/users?${params.toString()}`;
108
-
109
- const response = await fetch(url, {
110
- cache: "no-store",
111
- });
112
-
113
- if (!response.ok) {
114
- throw new Error("Failed to prefetch users");
115
- }
116
-
117
- return response.json();
118
- }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * parse-response.ts
3
+ *
4
+ * Zodスキーマによるレスポンス検証とエラーハンドリング
5
+ */
6
+
7
+ import type { z } from "zod";
8
+
9
+ /**
10
+ * APIエラークラス
11
+ *
12
+ * APIレスポンスのエラーを表現する
13
+ */
14
+ export class ApiError extends Error {
15
+ constructor(
16
+ public readonly code: string,
17
+ message: string,
18
+ public readonly statusCode: number,
19
+ public readonly details?: Record<string, unknown>
20
+ ) {
21
+ super(message);
22
+ this.name = "ApiError";
23
+ }
24
+ }
25
+
26
+ /**
27
+ * JSONを安全にパースする
28
+ *
29
+ * @param response - Fetchレスポンス
30
+ * @returns パース結果(失敗時は空オブジェクト)
31
+ */
32
+ async function safeJsonParse(response: Response): Promise<unknown> {
33
+ try {
34
+ return await response.json();
35
+ } catch {
36
+ // JSONパースに失敗した場合は空オブジェクトを返す
37
+ return {};
38
+ }
39
+ }
40
+
41
+ /**
42
+ * エラーレスポンスからエラー情報を抽出する
43
+ *
44
+ * バックエンドの形式: { error: string } または { error: { code, message, details } }
45
+ *
46
+ * @param json - パースされたJSON
47
+ * @param statusCode - HTTPステータスコード
48
+ * @returns エラー情報
49
+ */
50
+ function extractErrorInfo(
51
+ json: unknown,
52
+ statusCode: number
53
+ ): { code: string; message: string; details?: Record<string, unknown> } {
54
+ if (typeof json === "object" && json !== null && "error" in json) {
55
+ const errorField = (json as { error: unknown }).error;
56
+
57
+ // { error: string } 形式
58
+ if (typeof errorField === "string") {
59
+ return {
60
+ code: "API_ERROR",
61
+ message: errorField,
62
+ };
63
+ }
64
+
65
+ // { error: { code, message, details } } 形式
66
+ if (typeof errorField === "object" && errorField !== null) {
67
+ const errObj = errorField as {
68
+ code?: string;
69
+ message?: string;
70
+ details?: Record<string, unknown>;
71
+ };
72
+ return {
73
+ code: errObj.code ?? "API_ERROR",
74
+ message: errObj.message ?? "An unknown error occurred",
75
+ details: errObj.details,
76
+ };
77
+ }
78
+ }
79
+
80
+ return {
81
+ code: "UNKNOWN_ERROR",
82
+ message: "An unknown error occurred",
83
+ };
84
+ }
85
+
86
+ /**
87
+ * レスポンスをパースしてZodスキーマで検証
88
+ *
89
+ * @param response - Fetchレスポンス
90
+ * @param schema - Zodスキーマ
91
+ * @returns バリデーション済みのデータ
92
+ * @throws ApiError - レスポンスエラーまたはバリデーションエラー
93
+ */
94
+ export async function parseResponse<T>(response: Response, schema: z.ZodType<T>): Promise<T> {
95
+ // エラーレスポンスの場合
96
+ if (!response.ok) {
97
+ const json = await safeJsonParse(response);
98
+ const errorInfo = extractErrorInfo(json, response.status);
99
+ throw new ApiError(errorInfo.code, errorInfo.message, response.status, errorInfo.details);
100
+ }
101
+
102
+ // 成功レスポンスの場合
103
+ const json = await safeJsonParse(response);
104
+
105
+ // Zodスキーマでバリデーション
106
+ const result = schema.safeParse(json);
107
+ if (!result.success) {
108
+ throw new ApiError("VALIDATION_ERROR", "Response validation failed", response.status, {
109
+ zodErrors: result.error.flatten(),
110
+ });
111
+ }
112
+
113
+ return result.data;
114
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * user.ts
3
+ *
4
+ * ユーザー関連のZodスキーマと型定義
5
+ * APIレスポンスの型検証に使用
6
+ */
7
+
8
+ import { z } from "zod";
9
+
10
+ /**
11
+ * ユーザー一覧アイテムのレスポンススキーマ
12
+ * APIレスポンスの型検証に使用
13
+ */
14
+ export const userListItemSchema = z.object({
15
+ id: z.string(),
16
+ name: z.string(),
17
+ email: z.string(),
18
+ status: z.enum(["active", "inactive", "pending"]),
19
+ role: z.enum(["admin", "user", "moderator"]),
20
+ createdAt: z.string(),
21
+ lastLogin: z.string().nullable(),
22
+ });
23
+
24
+ /**
25
+ * ページネーション付きユーザー一覧のレスポンススキーマ
26
+ */
27
+ export const paginatedUserListSchema = z.object({
28
+ items: z.array(userListItemSchema),
29
+ total: z.number(),
30
+ page: z.number(),
31
+ limit: z.number(),
32
+ totalPages: z.number(),
33
+ });
34
+
35
+ export type UserListItem = z.infer<typeof userListItemSchema>;
36
+ export type PaginatedUserList = z.infer<typeof paginatedUserListSchema>;
@@ -1,32 +0,0 @@
1
- import { auth } from "@/lib/auth";
2
-
3
- export default auth((req) => {
4
- const { nextUrl } = req;
5
- const isLoggedIn = !!req.auth;
6
-
7
- // Protected routes that require authentication
8
- const protectedRoutes = ["/dashboard", "/profile"];
9
- const isProtectedRoute = protectedRoutes.some((route) =>
10
- nextUrl.pathname.startsWith(route),
11
- );
12
-
13
- // Public routes that authenticated users shouldn't access
14
- const publicRoutes = ["/signin", "/signup"];
15
- const isPublicRoute = publicRoutes.some((route) =>
16
- nextUrl.pathname.startsWith(route),
17
- );
18
-
19
- // Redirect authenticated users away from public routes to dashboard
20
- if (isLoggedIn && isPublicRoute) {
21
- return Response.redirect(new URL("/dashboard", nextUrl));
22
- }
23
-
24
- // Redirect unauthenticated users to signin
25
- if (!isLoggedIn && isProtectedRoute) {
26
- return Response.redirect(new URL("/signin", nextUrl));
27
- }
28
- });
29
-
30
- export const config = {
31
- matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
32
- };
@@ -5,8 +5,8 @@
5
5
  * 型安全なAPI呼び出しを提供
6
6
  */
7
7
 
8
- import { hc } from "hono/client";
9
8
  import type { AppType } from "@/app/api/rpc/[[...route]]/route";
9
+ import { hc } from "hono/client";
10
10
 
11
11
  /**
12
12
  * Hono Client インスタンス