create-einja-app 0.1.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.
Files changed (235) hide show
  1. package/README.md +307 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +1041 -0
  4. package/dist/cli.js.map +1 -0
  5. package/package.json +62 -0
  6. package/templates/turborepo-pandacss/.biomeignore +15 -0
  7. package/templates/turborepo-pandacss/.claude/hooks/einja/biome-format.sh +49 -0
  8. package/templates/turborepo-pandacss/.claude/hooks/einja/design-doc-check.sh +61 -0
  9. package/templates/turborepo-pandacss/.claude/hooks/einja/detect-secrets.sh +62 -0
  10. package/templates/turborepo-pandacss/.claude/hooks/einja/large-file-warning.sh +42 -0
  11. package/templates/turborepo-pandacss/.claude/hooks/einja/playwright-resize.sh +36 -0
  12. package/templates/turborepo-pandacss/.claude/hooks/einja/typecheck.sh +37 -0
  13. package/templates/turborepo-pandacss/.claude/hooks/einja/unset-volta-recursion.sh +32 -0
  14. package/templates/turborepo-pandacss/.claude/hooks/einja/validate-git-commit.sh +239 -0
  15. package/templates/turborepo-pandacss/.claude/hooks/einja/warn-index-ts.sh +34 -0
  16. package/templates/turborepo-pandacss/.claude/hooks/einja/warn-relative-import.sh +48 -0
  17. package/templates/turborepo-pandacss/.claude/settings.json +174 -0
  18. package/templates/turborepo-pandacss/.claude/skills/create-einja-app-release/SKILL.md +186 -0
  19. package/templates/turborepo-pandacss/.claude/skills/dev-cli-release/SKILL.md +173 -0
  20. package/templates/turborepo-pandacss/.cursor/commands/spec-create.md +227 -0
  21. package/templates/turborepo-pandacss/.cursor/commands/start-dev.md +98 -0
  22. package/templates/turborepo-pandacss/.cursor/commands/task-exec.md +287 -0
  23. package/templates/turborepo-pandacss/.cursor/commands/task-vibe-kanban-loop.md +532 -0
  24. package/templates/turborepo-pandacss/.cursor/commands/update-docs-by-task-specs.md +448 -0
  25. package/templates/turborepo-pandacss/.cursor/mcp.json +45 -0
  26. package/templates/turborepo-pandacss/.cursor/rules/api-rules.mdc +171 -0
  27. package/templates/turborepo-pandacss/.cursor/rules/api-test-rules.mdc +181 -0
  28. package/templates/turborepo-pandacss/.cursor/rules/base-code.mdc +70 -0
  29. package/templates/turborepo-pandacss/.cursor/rules/base-commit-rules.mdc +174 -0
  30. package/templates/turborepo-pandacss/.cursor/rules/base-design.mdc +12 -0
  31. package/templates/turborepo-pandacss/.cursor/rules/base-rules.mdc +231 -0
  32. package/templates/turborepo-pandacss/.cursor/rules/error-handling-rules.mdc +188 -0
  33. package/templates/turborepo-pandacss/.cursor/rules/refactor-rules.mdc +93 -0
  34. package/templates/turborepo-pandacss/.dockerignore +126 -0
  35. package/templates/turborepo-pandacss/.einja-sync.json +35 -0
  36. package/templates/turborepo-pandacss/.env.ci +25 -0
  37. package/templates/turborepo-pandacss/.env.example +35 -0
  38. package/templates/turborepo-pandacss/.env.personal.example +27 -0
  39. package/templates/turborepo-pandacss/.envrc +4 -0
  40. package/templates/turborepo-pandacss/.gitattributes +5 -0
  41. package/templates/turborepo-pandacss/.husky/pre-commit +1 -0
  42. package/templates/turborepo-pandacss/.lintstagedrc.js +24 -0
  43. package/templates/turborepo-pandacss/.mcp.json +45 -0
  44. package/templates/turborepo-pandacss/.node-version +1 -0
  45. package/templates/turborepo-pandacss/.templateignore +60 -0
  46. package/templates/turborepo-pandacss/.vscode/extensions.json +3 -0
  47. package/templates/turborepo-pandacss/CLAUDE.md +415 -0
  48. package/templates/turborepo-pandacss/README.md +322 -0
  49. package/templates/turborepo-pandacss/apps/web/middleware.ts +28 -0
  50. package/templates/turborepo-pandacss/apps/web/next.config.ts +10 -0
  51. package/templates/turborepo-pandacss/apps/web/package.json +80 -0
  52. package/templates/turborepo-pandacss/apps/web/panda.config.ts +114 -0
  53. package/templates/turborepo-pandacss/apps/web/postcss.config.cjs +6 -0
  54. package/templates/turborepo-pandacss/apps/web/public/file.svg +1 -0
  55. package/templates/turborepo-pandacss/apps/web/public/globe.svg +1 -0
  56. package/templates/turborepo-pandacss/apps/web/public/next.svg +1 -0
  57. package/templates/turborepo-pandacss/apps/web/public/vercel.svg +1 -0
  58. package/templates/turborepo-pandacss/apps/web/public/window.svg +1 -0
  59. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/dashboard/page.tsx +79 -0
  60. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/data/_components/UserTable.tsx +203 -0
  61. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/data/page.tsx +57 -0
  62. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/layout-client.tsx +31 -0
  63. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/layout.tsx +17 -0
  64. package/templates/turborepo-pandacss/apps/web/src/app/(authenticated)/profile/page.tsx +59 -0
  65. package/templates/turborepo-pandacss/apps/web/src/app/api/auth/[...nextauth]/route.ts +3 -0
  66. package/templates/turborepo-pandacss/apps/web/src/app/api/auth/signup/route.ts +70 -0
  67. package/templates/turborepo-pandacss/apps/web/src/app/error.tsx +106 -0
  68. package/templates/turborepo-pandacss/apps/web/src/app/favicon.ico +0 -0
  69. package/templates/turborepo-pandacss/apps/web/src/app/global-error.tsx +110 -0
  70. package/templates/turborepo-pandacss/apps/web/src/app/globals.css +121 -0
  71. package/templates/turborepo-pandacss/apps/web/src/app/layout.tsx +28 -0
  72. package/templates/turborepo-pandacss/apps/web/src/app/not-found.tsx +54 -0
  73. package/templates/turborepo-pandacss/apps/web/src/app/page.module.css +165 -0
  74. package/templates/turborepo-pandacss/apps/web/src/app/page.test.tsx +52 -0
  75. package/templates/turborepo-pandacss/apps/web/src/app/page.tsx +284 -0
  76. package/templates/turborepo-pandacss/apps/web/src/app/signin/page.tsx +296 -0
  77. package/templates/turborepo-pandacss/apps/web/src/app/signup/page.tsx +395 -0
  78. package/templates/turborepo-pandacss/apps/web/src/application/use-cases/UserUseCases.test.ts +229 -0
  79. package/templates/turborepo-pandacss/apps/web/src/application/use-cases/UserUseCases.ts +115 -0
  80. package/templates/turborepo-pandacss/apps/web/src/components/auth/login-button.tsx +35 -0
  81. package/templates/turborepo-pandacss/apps/web/src/components/auth/logout-button.tsx +24 -0
  82. package/templates/turborepo-pandacss/apps/web/src/components/auth/user-avatar.test.tsx +68 -0
  83. package/templates/turborepo-pandacss/apps/web/src/components/auth/user-avatar.tsx +43 -0
  84. package/templates/turborepo-pandacss/apps/web/src/components/dashboard/dashboard-stats.tsx +128 -0
  85. package/templates/turborepo-pandacss/apps/web/src/components/providers/query-provider.tsx +30 -0
  86. package/templates/turborepo-pandacss/apps/web/src/components/providers/session-provider.tsx +12 -0
  87. package/templates/turborepo-pandacss/apps/web/src/components/shared/Sidebar.tsx +175 -0
  88. package/templates/turborepo-pandacss/apps/web/src/components/shared/header.tsx +166 -0
  89. package/templates/turborepo-pandacss/apps/web/src/components/ui/accordion.tsx +64 -0
  90. package/templates/turborepo-pandacss/apps/web/src/components/ui/alert-dialog.tsx +135 -0
  91. package/templates/turborepo-pandacss/apps/web/src/components/ui/alert.tsx +60 -0
  92. package/templates/turborepo-pandacss/apps/web/src/components/ui/aspect-ratio.tsx +9 -0
  93. package/templates/turborepo-pandacss/apps/web/src/components/ui/avatar.tsx +41 -0
  94. package/templates/turborepo-pandacss/apps/web/src/components/ui/badge.tsx +39 -0
  95. package/templates/turborepo-pandacss/apps/web/src/components/ui/breadcrumb.tsx +101 -0
  96. package/templates/turborepo-pandacss/apps/web/src/components/ui/button.tsx +56 -0
  97. package/templates/turborepo-pandacss/apps/web/src/components/ui/card.tsx +75 -0
  98. package/templates/turborepo-pandacss/apps/web/src/components/ui/checkbox.tsx +29 -0
  99. package/templates/turborepo-pandacss/apps/web/src/components/ui/data-table.tsx +189 -0
  100. package/templates/turborepo-pandacss/apps/web/src/components/ui/dialog-hook.tsx +210 -0
  101. package/templates/turborepo-pandacss/apps/web/src/components/ui/dialog.tsx +129 -0
  102. package/templates/turborepo-pandacss/apps/web/src/components/ui/drawer.tsx +124 -0
  103. package/templates/turborepo-pandacss/apps/web/src/components/ui/dropdown-menu.tsx +228 -0
  104. package/templates/turborepo-pandacss/apps/web/src/components/ui/form.tsx +152 -0
  105. package/templates/turborepo-pandacss/apps/web/src/components/ui/hover-card.tsx +38 -0
  106. package/templates/turborepo-pandacss/apps/web/src/components/ui/input.tsx +21 -0
  107. package/templates/turborepo-pandacss/apps/web/src/components/ui/label.tsx +21 -0
  108. package/templates/turborepo-pandacss/apps/web/src/components/ui/pagination.tsx +105 -0
  109. package/templates/turborepo-pandacss/apps/web/src/components/ui/popover.tsx +42 -0
  110. package/templates/turborepo-pandacss/apps/web/src/components/ui/progress.tsx +28 -0
  111. package/templates/turborepo-pandacss/apps/web/src/components/ui/select.tsx +170 -0
  112. package/templates/turborepo-pandacss/apps/web/src/components/ui/separator.tsx +28 -0
  113. package/templates/turborepo-pandacss/apps/web/src/components/ui/skeleton.tsx +13 -0
  114. package/templates/turborepo-pandacss/apps/web/src/components/ui/sonner.tsx +25 -0
  115. package/templates/turborepo-pandacss/apps/web/src/components/ui/table.tsx +92 -0
  116. package/templates/turborepo-pandacss/apps/web/src/components/ui/tabs.tsx +54 -0
  117. package/templates/turborepo-pandacss/apps/web/src/components/ui/textarea.tsx +18 -0
  118. package/templates/turborepo-pandacss/apps/web/src/components/ui/tooltip.tsx +57 -0
  119. package/templates/turborepo-pandacss/apps/web/src/components/ui/typography.tsx +158 -0
  120. package/templates/turborepo-pandacss/apps/web/src/lib/auth/guard.ts +36 -0
  121. package/templates/turborepo-pandacss/apps/web/src/lib/auth/index.ts +22 -0
  122. package/templates/turborepo-pandacss/apps/web/src/lib/prisma.ts +3 -0
  123. package/templates/turborepo-pandacss/apps/web/src/lib/utils.ts +6 -0
  124. package/templates/turborepo-pandacss/apps/web/test/globals.d.ts +1 -0
  125. package/templates/turborepo-pandacss/apps/web/test/matchers.d.ts +1 -0
  126. package/templates/turborepo-pandacss/apps/web/test/setup.ts +22 -0
  127. package/templates/turborepo-pandacss/apps/web/tsconfig.json +37 -0
  128. package/templates/turborepo-pandacss/apps/web/vitest.config.ts +20 -0
  129. package/templates/turborepo-pandacss/apps/web/vitest.d.ts +2 -0
  130. package/templates/turborepo-pandacss/biome.json +60 -0
  131. package/templates/turborepo-pandacss/components.json +21 -0
  132. package/templates/turborepo-pandacss/docker-compose.yml +27 -0
  133. package/templates/turborepo-pandacss/middleware.ts +32 -0
  134. package/templates/turborepo-pandacss/next.config.ts +10 -0
  135. package/templates/turborepo-pandacss/package-lock.json +9346 -0
  136. package/templates/turborepo-pandacss/package.json +64 -0
  137. package/templates/turborepo-pandacss/packages/config/package.json +41 -0
  138. package/templates/turborepo-pandacss/packages/config/panda.config.ts +114 -0
  139. package/templates/turborepo-pandacss/packages/config/src/index.ts +24 -0
  140. package/templates/turborepo-pandacss/packages/config/src/worktree-config-loader.ts +129 -0
  141. package/templates/turborepo-pandacss/packages/config/src/worktree-config.ts +75 -0
  142. package/templates/turborepo-pandacss/packages/config/tsconfig.build.json +19 -0
  143. package/templates/turborepo-pandacss/packages/config/tsconfig.json +24 -0
  144. package/templates/turborepo-pandacss/packages/config/typescript/base.json +19 -0
  145. package/templates/turborepo-pandacss/packages/front-core/package.json +24 -0
  146. package/templates/turborepo-pandacss/packages/front-core/src/auth/config.ts +84 -0
  147. package/templates/turborepo-pandacss/packages/front-core/src/auth/index.ts +5 -0
  148. package/templates/turborepo-pandacss/packages/front-core/src/auth/types/next-auth.d.ts +20 -0
  149. package/templates/turborepo-pandacss/packages/front-core/src/auth/utils.ts +29 -0
  150. package/templates/turborepo-pandacss/packages/front-core/src/context/index.ts +2 -0
  151. package/templates/turborepo-pandacss/packages/front-core/src/hooks/index.ts +2 -0
  152. package/templates/turborepo-pandacss/packages/front-core/src/index.ts +4 -0
  153. package/templates/turborepo-pandacss/packages/front-core/src/utils/index.ts +2 -0
  154. package/templates/turborepo-pandacss/packages/front-core/tsconfig.json +14 -0
  155. package/templates/turborepo-pandacss/packages/server-core/package.json +32 -0
  156. package/templates/turborepo-pandacss/packages/server-core/prisma/schema.prisma +102 -0
  157. package/templates/turborepo-pandacss/packages/server-core/prisma/seed.ts +67 -0
  158. package/templates/turborepo-pandacss/packages/server-core/prisma.config.ts +8 -0
  159. package/templates/turborepo-pandacss/packages/server-core/src/__generated__/fabbrica/index.d.ts +270 -0
  160. package/templates/turborepo-pandacss/packages/server-core/src/__generated__/fabbrica/index.js +484 -0
  161. package/templates/turborepo-pandacss/packages/server-core/src/core/result.test.ts +78 -0
  162. package/templates/turborepo-pandacss/packages/server-core/src/core/result.ts +53 -0
  163. package/templates/turborepo-pandacss/packages/server-core/src/domain/.gitkeep +0 -0
  164. package/templates/turborepo-pandacss/packages/server-core/src/domain/entities/User.test.ts +232 -0
  165. package/templates/turborepo-pandacss/packages/server-core/src/domain/entities/User.ts +105 -0
  166. package/templates/turborepo-pandacss/packages/server-core/src/domain/repository-interfaces/IUserRepository.ts +101 -0
  167. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/client.ts +15 -0
  168. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/mappers/UserMapper.test.ts +278 -0
  169. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/mappers/UserMapper.ts +103 -0
  170. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/repositories/UserRepository.test.ts +317 -0
  171. package/templates/turborepo-pandacss/packages/server-core/src/infrastructure/database/repositories/UserRepository.ts +169 -0
  172. package/templates/turborepo-pandacss/packages/server-core/src/testing/factories/index.ts +22 -0
  173. package/templates/turborepo-pandacss/packages/server-core/src/testing/factories/user.factory.ts +123 -0
  174. package/templates/turborepo-pandacss/packages/server-core/src/testing/fixtures/users.ts +92 -0
  175. package/templates/turborepo-pandacss/packages/server-core/src/testing/helpers/date.ts +49 -0
  176. package/templates/turborepo-pandacss/packages/server-core/src/testing/helpers/password.ts +50 -0
  177. package/templates/turborepo-pandacss/packages/server-core/src/testing/index.ts +26 -0
  178. package/templates/turborepo-pandacss/packages/server-core/tsconfig.json +14 -0
  179. package/templates/turborepo-pandacss/packages/server-core/vitest.config.ts +15 -0
  180. package/templates/turborepo-pandacss/packages/ui/package.json +37 -0
  181. package/templates/turborepo-pandacss/packages/ui/src/accordion.tsx +66 -0
  182. package/templates/turborepo-pandacss/packages/ui/src/alert-dialog.tsx +157 -0
  183. package/templates/turborepo-pandacss/packages/ui/src/alert.tsx +66 -0
  184. package/templates/turborepo-pandacss/packages/ui/src/aspect-ratio.tsx +11 -0
  185. package/templates/turborepo-pandacss/packages/ui/src/avatar.tsx +53 -0
  186. package/templates/turborepo-pandacss/packages/ui/src/badge.tsx +46 -0
  187. package/templates/turborepo-pandacss/packages/ui/src/breadcrumb.tsx +108 -0
  188. package/templates/turborepo-pandacss/packages/ui/src/button.tsx +59 -0
  189. package/templates/turborepo-pandacss/packages/ui/src/card.tsx +92 -0
  190. package/templates/turborepo-pandacss/packages/ui/src/checkbox.tsx +32 -0
  191. package/templates/turborepo-pandacss/packages/ui/src/data-table.tsx +216 -0
  192. package/templates/turborepo-pandacss/packages/ui/src/dialog-hook.tsx +226 -0
  193. package/templates/turborepo-pandacss/packages/ui/src/dialog.tsx +143 -0
  194. package/templates/turborepo-pandacss/packages/ui/src/drawer.tsx +135 -0
  195. package/templates/turborepo-pandacss/packages/ui/src/dropdown-menu.tsx +257 -0
  196. package/templates/turborepo-pandacss/packages/ui/src/form.tsx +168 -0
  197. package/templates/turborepo-pandacss/packages/ui/src/hover-card.tsx +44 -0
  198. package/templates/turborepo-pandacss/packages/ui/src/input.tsx +21 -0
  199. package/templates/turborepo-pandacss/packages/ui/src/label.tsx +24 -0
  200. package/templates/turborepo-pandacss/packages/ui/src/lib/utils.ts +6 -0
  201. package/templates/turborepo-pandacss/packages/ui/src/pagination.tsx +126 -0
  202. package/templates/turborepo-pandacss/packages/ui/src/popover.tsx +48 -0
  203. package/templates/turborepo-pandacss/packages/ui/src/progress.tsx +31 -0
  204. package/templates/turborepo-pandacss/packages/ui/src/select.tsx +185 -0
  205. package/templates/turborepo-pandacss/packages/ui/src/separator.tsx +28 -0
  206. package/templates/turborepo-pandacss/packages/ui/src/skeleton.tsx +13 -0
  207. package/templates/turborepo-pandacss/packages/ui/src/sonner.tsx +25 -0
  208. package/templates/turborepo-pandacss/packages/ui/src/table.tsx +116 -0
  209. package/templates/turborepo-pandacss/packages/ui/src/tabs.tsx +66 -0
  210. package/templates/turborepo-pandacss/packages/ui/src/textarea.tsx +18 -0
  211. package/templates/turborepo-pandacss/packages/ui/src/tooltip.tsx +61 -0
  212. package/templates/turborepo-pandacss/packages/ui/src/typography.tsx +187 -0
  213. package/templates/turborepo-pandacss/packages/ui/tsconfig.json +20 -0
  214. package/templates/turborepo-pandacss/panda.config.ts +114 -0
  215. package/templates/turborepo-pandacss/pnpm-lock.yaml +9032 -0
  216. package/templates/turborepo-pandacss/pnpm-workspace.yaml +11 -0
  217. package/templates/turborepo-pandacss/postcss.config.cjs +6 -0
  218. package/templates/turborepo-pandacss/prisma/schema.prisma +82 -0
  219. package/templates/turborepo-pandacss/public/file.svg +1 -0
  220. package/templates/turborepo-pandacss/public/globe.svg +1 -0
  221. package/templates/turborepo-pandacss/public/next.svg +1 -0
  222. package/templates/turborepo-pandacss/public/vercel.svg +1 -0
  223. package/templates/turborepo-pandacss/public/window.svg +1 -0
  224. package/templates/turborepo-pandacss/scripts/cli-template-update.ts +387 -0
  225. package/templates/turborepo-pandacss/scripts/env-show.ts +129 -0
  226. package/templates/turborepo-pandacss/scripts/env.ts +555 -0
  227. package/templates/turborepo-pandacss/scripts/init.sh +92 -0
  228. package/templates/turborepo-pandacss/scripts/setup-dev.ts +640 -0
  229. package/templates/turborepo-pandacss/scripts/template-update.ts +277 -0
  230. package/templates/turborepo-pandacss/scripts/worktree/dev.ts +872 -0
  231. package/templates/turborepo-pandacss/test/globals.d.ts +1 -0
  232. package/templates/turborepo-pandacss/test/setup.ts +22 -0
  233. package/templates/turborepo-pandacss/tsconfig.json +46 -0
  234. package/templates/turborepo-pandacss/turbo.json +57 -0
  235. package/templates/turborepo-pandacss/vitest.config.ts +20 -0
@@ -0,0 +1,278 @@
1
+ import type { User as PrismaUser, UserRole as PrismaUserRole, UserStatus as PrismaUserStatus } from "@prisma/client";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+ import { User } from "../../../domain/entities/User";
4
+ import { UserFactory, initialize } from "../../../testing";
5
+ import { UserMapper } from "./UserMapper";
6
+
7
+ describe("UserMapper", () => {
8
+ beforeAll(() => {
9
+ // マッパーテストではPrismaクライアントは使用しないため、空のオブジェクトを渡す
10
+ // biome-ignore lint/suspicious/noExplicitAny: test fixture initialization
11
+ initialize({ prisma: {} as any });
12
+ });
13
+
14
+ describe("toDomain", () => {
15
+ it("PrismaUserをDomain Userに変換できる", async () => {
16
+ // Given
17
+ const prismaUser = await UserFactory.build();
18
+
19
+ // When
20
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
21
+ const domainUser = UserMapper.toDomain(prismaUser as any);
22
+
23
+ // Then
24
+ expect(domainUser).toBeInstanceOf(User);
25
+ expect(domainUser.id).toBe(prismaUser.id);
26
+ expect(domainUser.email).toBe(prismaUser.email);
27
+ expect(domainUser.name).toBe(prismaUser.name);
28
+ expect(domainUser.status).toBe(prismaUser.status);
29
+ expect(domainUser.role).toBe(prismaUser.role);
30
+ expect(domainUser.createdAt).toEqual(prismaUser.createdAt);
31
+ expect(domainUser.lastLogin).toEqual(prismaUser.lastLogin);
32
+ });
33
+
34
+ it("nameがnullでも変換できる", async () => {
35
+ // Given
36
+ const prismaUser = await UserFactory.build({ name: null });
37
+
38
+ // When
39
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
40
+ const domainUser = UserMapper.toDomain(prismaUser as any);
41
+
42
+ // Then
43
+ expect(domainUser.name).toBeNull();
44
+ });
45
+
46
+ it("lastLoginがnullでも変換できる", async () => {
47
+ // Given
48
+ const prismaUser = await UserFactory.build({ lastLogin: null });
49
+
50
+ // When
51
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
52
+ const domainUser = UserMapper.toDomain(prismaUser as any);
53
+
54
+ // Then
55
+ expect(domainUser.lastLogin).toBeNull();
56
+ });
57
+
58
+ describe("status変換", () => {
59
+ it("activeを変換できる", async () => {
60
+ // Given
61
+ const prismaUser = await UserFactory.build({ status: "active" as PrismaUserStatus });
62
+
63
+ // When
64
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
65
+ const domainUser = UserMapper.toDomain(prismaUser as any);
66
+
67
+ // Then
68
+ expect(domainUser.status).toBe("active");
69
+ });
70
+
71
+ it("inactiveを変換できる", async () => {
72
+ // Given
73
+ const prismaUser = await UserFactory.build({ status: "inactive" as PrismaUserStatus });
74
+
75
+ // When
76
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
77
+ const domainUser = UserMapper.toDomain(prismaUser as any);
78
+
79
+ // Then
80
+ expect(domainUser.status).toBe("inactive");
81
+ });
82
+
83
+ it("pendingを変換できる", async () => {
84
+ // Given
85
+ const prismaUser = await UserFactory.build({ status: "pending" as PrismaUserStatus });
86
+
87
+ // When
88
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
89
+ const domainUser = UserMapper.toDomain(prismaUser as any);
90
+
91
+ // Then
92
+ expect(domainUser.status).toBe("pending");
93
+ });
94
+ });
95
+
96
+ describe("role変換", () => {
97
+ it("adminを変換できる", async () => {
98
+ // Given
99
+ const prismaUser = await UserFactory.build({ role: "admin" as PrismaUserRole });
100
+
101
+ // When
102
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
103
+ const domainUser = UserMapper.toDomain(prismaUser as any);
104
+
105
+ // Then
106
+ expect(domainUser.role).toBe("admin");
107
+ });
108
+
109
+ it("userを変換できる", async () => {
110
+ // Given
111
+ const prismaUser = await UserFactory.build({ role: "user" as PrismaUserRole });
112
+
113
+ // When
114
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
115
+ const domainUser = UserMapper.toDomain(prismaUser as any);
116
+
117
+ // Then
118
+ expect(domainUser.role).toBe("user");
119
+ });
120
+
121
+ it("moderatorを変換できる", async () => {
122
+ // Given
123
+ const prismaUser = await UserFactory.build({ role: "moderator" as PrismaUserRole });
124
+
125
+ // When
126
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
127
+ const domainUser = UserMapper.toDomain(prismaUser as any);
128
+
129
+ // Then
130
+ expect(domainUser.role).toBe("moderator");
131
+ });
132
+ });
133
+ });
134
+
135
+ describe("toPrismaUpdate", () => {
136
+ it("Domain UserをPrisma更新データに変換できる", async () => {
137
+ // Given
138
+ const props = await UserFactory.build({
139
+ name: "Updated Name",
140
+ status: "active" as PrismaUserStatus,
141
+ role: "admin" as PrismaUserRole,
142
+ lastLogin: new Date("2025-01-03T00:00:00Z"),
143
+ });
144
+ const domainUser = new User(props);
145
+
146
+ // When
147
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
148
+
149
+ // Then
150
+ expect(prismaData.name).toBe("Updated Name");
151
+ expect(prismaData.status).toBe("active");
152
+ expect(prismaData.role).toBe("admin");
153
+ expect(prismaData.lastLogin).toEqual(new Date("2025-01-03T00:00:00Z"));
154
+ });
155
+
156
+ it("nameがnullでも変換できる", async () => {
157
+ // Given
158
+ const props = await UserFactory.build({
159
+ name: null,
160
+ status: "active" as PrismaUserStatus,
161
+ role: "user" as PrismaUserRole,
162
+ lastLogin: null,
163
+ });
164
+ const domainUser = new User(props);
165
+
166
+ // When
167
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
168
+
169
+ // Then
170
+ expect(prismaData.name).toBeNull();
171
+ });
172
+
173
+ it("lastLoginがnullでも変換できる", async () => {
174
+ // Given
175
+ const props = await UserFactory.build({
176
+ name: "Test",
177
+ status: "pending" as PrismaUserStatus,
178
+ role: "user" as PrismaUserRole,
179
+ lastLogin: null,
180
+ });
181
+ const domainUser = new User(props);
182
+
183
+ // When
184
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
185
+
186
+ // Then
187
+ expect(prismaData.lastLogin).toBeNull();
188
+ });
189
+
190
+ describe("status逆変換", () => {
191
+ it("activeをPrisma形式に変換できる", async () => {
192
+ // Given
193
+ const props = await UserFactory.build({
194
+ status: "active" as PrismaUserStatus,
195
+ });
196
+ const domainUser = new User(props);
197
+
198
+ // When
199
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
200
+
201
+ // Then
202
+ expect(prismaData.status).toBe("active");
203
+ });
204
+
205
+ it("inactiveをPrisma形式に変換できる", async () => {
206
+ // Given
207
+ const props = await UserFactory.build({
208
+ status: "inactive" as PrismaUserStatus,
209
+ });
210
+ const domainUser = new User(props);
211
+
212
+ // When
213
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
214
+
215
+ // Then
216
+ expect(prismaData.status).toBe("inactive");
217
+ });
218
+
219
+ it("pendingをPrisma形式に変換できる", async () => {
220
+ // Given
221
+ const props = await UserFactory.build({
222
+ status: "pending" as PrismaUserStatus,
223
+ });
224
+ const domainUser = new User(props);
225
+
226
+ // When
227
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
228
+
229
+ // Then
230
+ expect(prismaData.status).toBe("pending");
231
+ });
232
+ });
233
+
234
+ describe("role逆変換", () => {
235
+ it("adminをPrisma形式に変換できる", async () => {
236
+ // Given
237
+ const props = await UserFactory.build({
238
+ role: "admin" as PrismaUserRole,
239
+ });
240
+ const domainUser = new User(props);
241
+
242
+ // When
243
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
244
+
245
+ // Then
246
+ expect(prismaData.role).toBe("admin");
247
+ });
248
+
249
+ it("userをPrisma形式に変換できる", async () => {
250
+ // Given
251
+ const props = await UserFactory.build({
252
+ role: "user" as PrismaUserRole,
253
+ });
254
+ const domainUser = new User(props);
255
+
256
+ // When
257
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
258
+
259
+ // Then
260
+ expect(prismaData.role).toBe("user");
261
+ });
262
+
263
+ it("moderatorをPrisma形式に変換できる", async () => {
264
+ // Given
265
+ const props = await UserFactory.build({
266
+ role: "moderator" as PrismaUserRole,
267
+ });
268
+ const domainUser = new User(props);
269
+
270
+ // When
271
+ const prismaData = UserMapper.toPrismaUpdate(domainUser);
272
+
273
+ // Then
274
+ expect(prismaData.role).toBe("moderator");
275
+ });
276
+ });
277
+ });
278
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * UserMapper
3
+ *
4
+ * Prismaモデル ⇔ ドメインエンティティの変換を担当。
5
+ */
6
+
7
+ import type { User as PrismaUser, UserRole as PrismaUserRole, UserStatus as PrismaUserStatus } from "@prisma/client";
8
+ import { User, type UserRole, type UserStatus } from "../../../domain/entities/User";
9
+
10
+ /**
11
+ * Prismaのステータスをドメインのステータスに変換
12
+ */
13
+ function mapPrismaStatusToDomain(status: PrismaUserStatus): UserStatus {
14
+ switch (status) {
15
+ case "active":
16
+ return "active";
17
+ case "inactive":
18
+ return "inactive";
19
+ case "pending":
20
+ return "pending";
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Prismaのロールをドメインのロールに変換
26
+ */
27
+ function mapPrismaRoleToDomain(role: PrismaUserRole): UserRole {
28
+ switch (role) {
29
+ case "admin":
30
+ return "admin";
31
+ case "user":
32
+ return "user";
33
+ case "moderator":
34
+ return "moderator";
35
+ }
36
+ }
37
+
38
+ /**
39
+ * ドメインのステータスをPrismaのステータスに変換
40
+ */
41
+ function mapDomainStatusToPrisma(status: UserStatus): PrismaUserStatus {
42
+ switch (status) {
43
+ case "active":
44
+ return "active";
45
+ case "inactive":
46
+ return "inactive";
47
+ case "pending":
48
+ return "pending";
49
+ }
50
+ }
51
+
52
+ /**
53
+ * ドメインのロールをPrismaのロールに変換
54
+ */
55
+ function mapDomainRoleToPrisma(role: UserRole): PrismaUserRole {
56
+ switch (role) {
57
+ case "admin":
58
+ return "admin";
59
+ case "user":
60
+ return "user";
61
+ case "moderator":
62
+ return "moderator";
63
+ }
64
+ }
65
+
66
+ /**
67
+ * UserMapper
68
+ *
69
+ * Prisma ⇔ Domain の変換を行うマッパークラス
70
+ */
71
+ export const UserMapper = {
72
+ /**
73
+ * PrismaのUserをドメインのUserに変換
74
+ */
75
+ toDomain(prismaUser: PrismaUser): User {
76
+ return new User({
77
+ id: prismaUser.id,
78
+ email: prismaUser.email,
79
+ name: prismaUser.name,
80
+ status: mapPrismaStatusToDomain(prismaUser.status),
81
+ role: mapPrismaRoleToDomain(prismaUser.role),
82
+ createdAt: prismaUser.createdAt,
83
+ lastLogin: prismaUser.lastLogin,
84
+ });
85
+ },
86
+
87
+ /**
88
+ * ドメインのUserをPrismaの更新用データに変換
89
+ */
90
+ toPrismaUpdate(user: User): {
91
+ name: string | null;
92
+ status: PrismaUserStatus;
93
+ role: PrismaUserRole;
94
+ lastLogin: Date | null;
95
+ } {
96
+ return {
97
+ name: user.name,
98
+ status: mapDomainStatusToPrisma(user.status),
99
+ role: mapDomainRoleToPrisma(user.role),
100
+ lastLogin: user.lastLogin,
101
+ };
102
+ },
103
+ };
@@ -0,0 +1,317 @@
1
+ import type { User as PrismaUser, UserRole as PrismaUserRole, UserStatus as PrismaUserStatus } from "@prisma/client";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { isFailure, isSuccess } from "../../../core/result";
4
+ import { UserFactory, initialize } from "../../../testing";
5
+
6
+ // Prismaクライアントをモック
7
+ vi.mock("../client", () => ({
8
+ prisma: {
9
+ user: {
10
+ findMany: vi.fn(),
11
+ findFirst: vi.fn(),
12
+ findUnique: vi.fn(),
13
+ count: vi.fn(),
14
+ update: vi.fn(),
15
+ },
16
+ },
17
+ }));
18
+
19
+ // モックしたprismaをインポート
20
+ import { prisma } from "../client";
21
+ import { userRepository } from "./UserRepository";
22
+
23
+ describe("UserRepository", () => {
24
+ beforeEach(() => {
25
+ // リポジトリテストではモックprismaを使用するため、空のオブジェクトを渡す
26
+ // biome-ignore lint/suspicious/noExplicitAny: test fixture initialization
27
+ initialize({ prisma: {} as any });
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ describe("search", () => {
32
+ it("ページネーション付きでユーザー一覧を取得できる", async () => {
33
+ // Given
34
+ const mockUsers = [
35
+ await UserFactory.build({ id: "user-1", email: "user1@example.com" }),
36
+ await UserFactory.build({ id: "user-2", email: "user2@example.com" }),
37
+ ];
38
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
39
+ vi.mocked(prisma.user.findMany).mockResolvedValue(mockUsers as any);
40
+ vi.mocked(prisma.user.count).mockResolvedValue(25);
41
+
42
+ // When
43
+ const result = await userRepository.search({}, { page: 2, limit: 10 });
44
+
45
+ // Then
46
+ expect(isSuccess(result)).toBe(true);
47
+ if (isSuccess(result)) {
48
+ expect(result.value.items).toHaveLength(2);
49
+ expect(result.value.total).toBe(25);
50
+ expect(result.value.page).toBe(2);
51
+ expect(result.value.limit).toBe(10);
52
+ expect(result.value.totalPages).toBe(3);
53
+ }
54
+ });
55
+
56
+ it("検索条件なしで全件取得できる", async () => {
57
+ // Given
58
+ const mockUsers = [await UserFactory.build()];
59
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
60
+ vi.mocked(prisma.user.findMany).mockResolvedValue(mockUsers as any);
61
+ vi.mocked(prisma.user.count).mockResolvedValue(1);
62
+
63
+ // When
64
+ const result = await userRepository.search({});
65
+
66
+ // Then
67
+ expect(isSuccess(result)).toBe(true);
68
+ if (isSuccess(result)) {
69
+ expect(result.value.items).toHaveLength(1);
70
+ }
71
+ });
72
+
73
+ it("statusで絞り込みできる", async () => {
74
+ // Given
75
+ vi.mocked(prisma.user.findMany).mockResolvedValue([]);
76
+ vi.mocked(prisma.user.count).mockResolvedValue(0);
77
+
78
+ // When
79
+ await userRepository.search({ status: "active" });
80
+
81
+ // Then
82
+ expect(prisma.user.findMany).toHaveBeenCalledWith(
83
+ expect.objectContaining({
84
+ where: expect.objectContaining({ status: "active" }),
85
+ }),
86
+ );
87
+ });
88
+
89
+ it("roleで絞り込みできる", async () => {
90
+ // Given
91
+ vi.mocked(prisma.user.findMany).mockResolvedValue([]);
92
+ vi.mocked(prisma.user.count).mockResolvedValue(0);
93
+
94
+ // When
95
+ await userRepository.search({ role: "admin" });
96
+
97
+ // Then
98
+ expect(prisma.user.findMany).toHaveBeenCalledWith(
99
+ expect.objectContaining({
100
+ where: expect.objectContaining({ role: "admin" }),
101
+ }),
102
+ );
103
+ });
104
+
105
+ it("検索テキストで名前・メールを絞り込みできる", async () => {
106
+ // Given
107
+ vi.mocked(prisma.user.findMany).mockResolvedValue([]);
108
+ vi.mocked(prisma.user.count).mockResolvedValue(0);
109
+
110
+ // When
111
+ await userRepository.search({ search: "john" });
112
+
113
+ // Then
114
+ expect(prisma.user.findMany).toHaveBeenCalledWith(
115
+ expect.objectContaining({
116
+ where: expect.objectContaining({
117
+ OR: [
118
+ { name: { contains: "john", mode: "insensitive" } },
119
+ { email: { contains: "john", mode: "insensitive" } },
120
+ ],
121
+ }),
122
+ }),
123
+ );
124
+ });
125
+
126
+ it("空の検索結果でも成功を返す", async () => {
127
+ // Given
128
+ vi.mocked(prisma.user.findMany).mockResolvedValue([]);
129
+ vi.mocked(prisma.user.count).mockResolvedValue(0);
130
+
131
+ // When
132
+ const result = await userRepository.search({});
133
+
134
+ // Then
135
+ expect(isSuccess(result)).toBe(true);
136
+ if (isSuccess(result)) {
137
+ expect(result.value.items).toHaveLength(0);
138
+ expect(result.value.total).toBe(0);
139
+ expect(result.value.totalPages).toBe(0);
140
+ }
141
+ });
142
+
143
+ it("ページネーションのデフォルト値が適用される", async () => {
144
+ // Given
145
+ vi.mocked(prisma.user.findMany).mockResolvedValue([]);
146
+ vi.mocked(prisma.user.count).mockResolvedValue(0);
147
+
148
+ // When
149
+ const result = await userRepository.search({});
150
+
151
+ // Then
152
+ expect(prisma.user.findMany).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ skip: 0,
155
+ take: 10,
156
+ }),
157
+ );
158
+ if (isSuccess(result)) {
159
+ expect(result.value.page).toBe(1);
160
+ expect(result.value.limit).toBe(10);
161
+ }
162
+ });
163
+
164
+ it("DBエラー時にfailure Resultを返す", async () => {
165
+ // Given
166
+ vi.mocked(prisma.user.findMany).mockRejectedValue(new Error("DB connection failed"));
167
+
168
+ // When
169
+ const result = await userRepository.search({});
170
+
171
+ // Then
172
+ expect(isFailure(result)).toBe(true);
173
+ if (isFailure(result)) {
174
+ expect(result.error.message).toBe("DB connection failed");
175
+ }
176
+ });
177
+ });
178
+
179
+ describe("find", () => {
180
+ it("条件に一致するユーザーを取得できる", async () => {
181
+ // Given
182
+ const mockUser = await UserFactory.build();
183
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
184
+ vi.mocked(prisma.user.findFirst).mockResolvedValue(mockUser as any);
185
+
186
+ // When
187
+ const result = await userRepository.find({ email: "test@example.com" });
188
+
189
+ // Then
190
+ expect(isSuccess(result)).toBe(true);
191
+ if (isSuccess(result)) {
192
+ expect(result.value).not.toBeNull();
193
+ expect(result.value?.email).toBeDefined();
194
+ }
195
+ });
196
+
197
+ it("見つからない場合はnullを返す", async () => {
198
+ // Given
199
+ vi.mocked(prisma.user.findFirst).mockResolvedValue(null);
200
+
201
+ // When
202
+ const result = await userRepository.find({ email: "notfound@example.com" });
203
+
204
+ // Then
205
+ expect(isSuccess(result)).toBe(true);
206
+ if (isSuccess(result)) {
207
+ expect(result.value).toBeNull();
208
+ }
209
+ });
210
+
211
+ it("DBエラー時にfailure Resultを返す", async () => {
212
+ // Given
213
+ vi.mocked(prisma.user.findFirst).mockRejectedValue(new Error("Query failed"));
214
+
215
+ // When
216
+ const result = await userRepository.find({ email: "test@example.com" });
217
+
218
+ // Then
219
+ expect(isFailure(result)).toBe(true);
220
+ });
221
+ });
222
+
223
+ describe("findById", () => {
224
+ it("IDでユーザーを取得できる", async () => {
225
+ // Given
226
+ const mockUser = await UserFactory.build({ id: "user-456" });
227
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
228
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any);
229
+
230
+ // When
231
+ const result = await userRepository.findById("user-456");
232
+
233
+ // Then
234
+ expect(isSuccess(result)).toBe(true);
235
+ if (isSuccess(result)) {
236
+ expect(result.value?.id).toBe("user-456");
237
+ }
238
+ });
239
+
240
+ it("見つからない場合はnullを返す", async () => {
241
+ // Given
242
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
243
+
244
+ // When
245
+ const result = await userRepository.findById("nonexistent");
246
+
247
+ // Then
248
+ expect(isSuccess(result)).toBe(true);
249
+ if (isSuccess(result)) {
250
+ expect(result.value).toBeNull();
251
+ }
252
+ });
253
+ });
254
+
255
+ describe("findByEmail", () => {
256
+ it("メールアドレスでユーザーを取得できる", async () => {
257
+ // Given
258
+ const mockUser = await UserFactory.build({ email: "specific@example.com" });
259
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
260
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser as any);
261
+
262
+ // When
263
+ const result = await userRepository.findByEmail("specific@example.com");
264
+
265
+ // Then
266
+ expect(isSuccess(result)).toBe(true);
267
+ if (isSuccess(result)) {
268
+ expect(result.value?.email).toBe("specific@example.com");
269
+ }
270
+ });
271
+
272
+ it("見つからない場合はnullを返す", async () => {
273
+ // Given
274
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
275
+
276
+ // When
277
+ const result = await userRepository.findByEmail("notfound@example.com");
278
+
279
+ // Then
280
+ expect(isSuccess(result)).toBe(true);
281
+ if (isSuccess(result)) {
282
+ expect(result.value).toBeNull();
283
+ }
284
+ });
285
+ });
286
+
287
+ describe("updateLastLogin", () => {
288
+ it("最終ログイン日時を更新できる", async () => {
289
+ // Given
290
+ const loginTime = new Date("2025-01-03T12:00:00Z");
291
+ const updatedUser = await UserFactory.build({ lastLogin: loginTime });
292
+ // biome-ignore lint/suspicious/noExplicitAny: test with factory-generated data
293
+ vi.mocked(prisma.user.update).mockResolvedValue(updatedUser as any);
294
+
295
+ // When
296
+ const result = await userRepository.updateLastLogin("user-123", loginTime);
297
+
298
+ // Then
299
+ expect(isSuccess(result)).toBe(true);
300
+ expect(prisma.user.update).toHaveBeenCalledWith({
301
+ where: { id: "user-123" },
302
+ data: { lastLogin: loginTime },
303
+ });
304
+ });
305
+
306
+ it("存在しないユーザーの更新でfailure Resultを返す", async () => {
307
+ // Given
308
+ vi.mocked(prisma.user.update).mockRejectedValue(new Error("Record not found"));
309
+
310
+ // When
311
+ const result = await userRepository.updateLastLogin("nonexistent", new Date());
312
+
313
+ // Then
314
+ expect(isFailure(result)).toBe(true);
315
+ });
316
+ });
317
+ });