create-nexst-monorepo 0.1.0

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 (303) hide show
  1. package/dist/index.js +1727 -0
  2. package/package.json +37 -0
  3. package/template/.agents/plugins/marketplace.json +21 -0
  4. package/template/.claude/settings.json +6 -0
  5. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  6. package/template/.claude/skills/frontend-design/SKILL.md +154 -0
  7. package/template/.claude/skills/theme-factory/SKILL.md +95 -0
  8. package/template/.claude/skills/theme-factory/references/schema.md +104 -0
  9. package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
  10. package/template/.dockerignore +43 -0
  11. package/template/.editorconfig +10 -0
  12. package/template/.github/workflows/ci.yaml +289 -0
  13. package/template/.github/workflows/publish-app.yaml +66 -0
  14. package/template/.github/workflows/publish.yaml +148 -0
  15. package/template/.github/workflows/snyk.yaml +34 -0
  16. package/template/.vscode/settings.json +8 -0
  17. package/template/AGENTS.md +42 -0
  18. package/template/Dockerfile.base +9 -0
  19. package/template/README.md +54 -0
  20. package/template/_gitignore +10 -0
  21. package/template/apps/api/.env.example +32 -0
  22. package/template/apps/api/AGENTS.md +44 -0
  23. package/template/apps/api/Dockerfile +69 -0
  24. package/template/apps/api/_gitignore +8 -0
  25. package/template/apps/api/knip.config.ts +22 -0
  26. package/template/apps/api/kustomize/base/application.properties +16 -0
  27. package/template/apps/api/kustomize/base/deployment.yaml +94 -0
  28. package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
  29. package/template/apps/api/kustomize/base/service.yaml +10 -0
  30. package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
  31. package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
  32. package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
  33. package/template/apps/api/kustomize/secret.keys +14 -0
  34. package/template/apps/api/oxfmt.config.ts +8 -0
  35. package/template/apps/api/oxlint.config.ts +107 -0
  36. package/template/apps/api/package.json +79 -0
  37. package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +113 -0
  40. package/template/apps/api/prisma.config.ts +7 -0
  41. package/template/apps/api/scripts/auth.cli.ts +19 -0
  42. package/template/apps/api/scripts/seal-secret.sh +171 -0
  43. package/template/apps/api/sonar-project.properties +9 -0
  44. package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
  45. package/template/apps/api/src/app-info/app-info.module.ts +7 -0
  46. package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
  47. package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
  48. package/template/apps/api/src/app.module.spec.ts +93 -0
  49. package/template/apps/api/src/app.module.ts +77 -0
  50. package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
  51. package/template/apps/api/src/auth/auth.guard.ts +97 -0
  52. package/template/apps/api/src/auth/auth.ts +129 -0
  53. package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
  54. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
  55. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
  56. package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
  57. package/template/apps/api/src/auth/testing/make-session.ts +31 -0
  58. package/template/apps/api/src/auth/types/auth.types.ts +27 -0
  59. package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
  60. package/template/apps/api/src/bootstrap/security.ts +24 -0
  61. package/template/apps/api/src/bootstrap/swagger.ts +28 -0
  62. package/template/apps/api/src/config/app.config.spec.ts +22 -0
  63. package/template/apps/api/src/config/app.config.ts +122 -0
  64. package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
  65. package/template/apps/api/src/config/graphql.config.ts +77 -0
  66. package/template/apps/api/src/config/resources/app.yaml +29 -0
  67. package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
  68. package/template/apps/api/src/errors/app.exception.ts +47 -0
  69. package/template/apps/api/src/errors/error-code.enum.ts +54 -0
  70. package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
  71. package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
  72. package/template/apps/api/src/health/health.controller.spec.ts +82 -0
  73. package/template/apps/api/src/health/health.controller.ts +57 -0
  74. package/template/apps/api/src/health/health.module.ts +12 -0
  75. package/template/apps/api/src/logging/context-logger.ts +18 -0
  76. package/template/apps/api/src/logging/logger.config.ts +23 -0
  77. package/template/apps/api/src/logging/logging.module.ts +22 -0
  78. package/template/apps/api/src/mail/mail.module.ts +12 -0
  79. package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
  80. package/template/apps/api/src/mail/mail.service.ts +123 -0
  81. package/template/apps/api/src/main.ts +41 -0
  82. package/template/apps/api/src/prisma/prisma.module.ts +12 -0
  83. package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
  84. package/template/apps/api/src/prisma/prisma.service.ts +138 -0
  85. package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
  86. package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
  87. package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
  88. package/template/apps/api/src/queue/example-queue.service.ts +32 -0
  89. package/template/apps/api/src/queue/queue.module.ts +27 -0
  90. package/template/apps/api/src/r2/r2.module.ts +12 -0
  91. package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
  92. package/template/apps/api/src/r2/r2.service.ts +131 -0
  93. package/template/apps/api/src/user/dto/user.dto.ts +26 -0
  94. package/template/apps/api/src/user/user.module.ts +10 -0
  95. package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
  96. package/template/apps/api/src/user/user.resolver.ts +24 -0
  97. package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
  98. package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
  99. package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
  100. package/template/apps/api/src/utils/error-handler.util.ts +12 -0
  101. package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
  102. package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
  103. package/template/apps/api/src/utils/pagination.input.ts +20 -0
  104. package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
  105. package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
  106. package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
  107. package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
  108. package/template/apps/api/tests/app.e2e-spec.ts +35 -0
  109. package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
  110. package/template/apps/api/tests/helpers/api.ts +27 -0
  111. package/template/apps/api/tsconfig.json +11 -0
  112. package/template/apps/api/vitest.config.e2e.ts +16 -0
  113. package/template/apps/api/vitest.config.ts +40 -0
  114. package/template/apps/web/.env.example +7 -0
  115. package/template/apps/web/.storybook/decorators.tsx +38 -0
  116. package/template/apps/web/.storybook/main.ts +17 -0
  117. package/template/apps/web/.storybook/preview.ts +38 -0
  118. package/template/apps/web/.storybook/vitest.setup.ts +5 -0
  119. package/template/apps/web/AGENTS.md +29 -0
  120. package/template/apps/web/Dockerfile +49 -0
  121. package/template/apps/web/_gitignore +16 -0
  122. package/template/apps/web/knip.config.ts +15 -0
  123. package/template/apps/web/kustomize/base/application.properties +5 -0
  124. package/template/apps/web/kustomize/base/deployment.yaml +60 -0
  125. package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
  126. package/template/apps/web/kustomize/base/service.yaml +10 -0
  127. package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
  128. package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
  129. package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
  130. package/template/apps/web/messages/de.json +124 -0
  131. package/template/apps/web/messages/en.json +124 -0
  132. package/template/apps/web/next-env.d.ts +7 -0
  133. package/template/apps/web/next.config.ts +39 -0
  134. package/template/apps/web/oxfmt.config.ts +9 -0
  135. package/template/apps/web/oxlint.config.ts +36 -0
  136. package/template/apps/web/package.json +80 -0
  137. package/template/apps/web/playwright.config.ts +34 -0
  138. package/template/apps/web/postcss.config.mjs +7 -0
  139. package/template/apps/web/sonar-project.properties +9 -0
  140. package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
  141. package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
  142. package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
  143. package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
  144. package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
  145. package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
  146. package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
  147. package/template/apps/web/src/app/[locale]/page.tsx +23 -0
  148. package/template/apps/web/src/app/api/health/route.ts +3 -0
  149. package/template/apps/web/src/app/global-error.tsx +19 -0
  150. package/template/apps/web/src/app/globals.css +98 -0
  151. package/template/apps/web/src/app/icon.svg +1 -0
  152. package/template/apps/web/src/app/robots.ts +15 -0
  153. package/template/apps/web/src/app/sitemap.ts +24 -0
  154. package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
  155. package/template/apps/web/src/components/alert/alert.tsx +50 -0
  156. package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
  157. package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
  158. package/template/apps/web/src/components/button/button-variants.ts +28 -0
  159. package/template/apps/web/src/components/button/button.stories.tsx +33 -0
  160. package/template/apps/web/src/components/button/button.tsx +32 -0
  161. package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
  162. package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
  163. package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
  164. package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
  165. package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
  166. package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
  167. package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
  168. package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
  169. package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
  170. package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
  171. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
  172. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
  173. package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
  174. package/template/apps/web/src/components/empty-state.tsx +33 -0
  175. package/template/apps/web/src/components/field-group.stories.tsx +47 -0
  176. package/template/apps/web/src/components/field-group.tsx +20 -0
  177. package/template/apps/web/src/components/field-label.stories.tsx +34 -0
  178. package/template/apps/web/src/components/field-label.tsx +28 -0
  179. package/template/apps/web/src/components/icons.tsx +25 -0
  180. package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
  181. package/template/apps/web/src/components/input/input-otp.tsx +184 -0
  182. package/template/apps/web/src/components/input/input.stories.tsx +35 -0
  183. package/template/apps/web/src/components/input/input.tsx +180 -0
  184. package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
  185. package/template/apps/web/src/components/popover/popover.tsx +45 -0
  186. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
  187. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
  188. package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
  189. package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
  190. package/template/apps/web/src/components/select/select.stories.tsx +293 -0
  191. package/template/apps/web/src/components/select/select.tsx +445 -0
  192. package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
  193. package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
  194. package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
  195. package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
  196. package/template/apps/web/src/components/switch/switch.tsx +35 -0
  197. package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
  198. package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
  199. package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
  200. package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
  201. package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
  202. package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
  203. package/template/apps/web/src/components/toast/toaster.tsx +38 -0
  204. package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
  205. package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
  206. package/template/apps/web/src/config/csp.ts +59 -0
  207. package/template/apps/web/src/config/routes.ts +14 -0
  208. package/template/apps/web/src/config/site.ts +5 -0
  209. package/template/apps/web/src/env.ts +15 -0
  210. package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
  211. package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
  212. package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
  213. package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
  214. package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
  215. package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
  216. package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
  217. package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
  218. package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
  219. package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
  220. package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
  221. package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
  222. package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
  223. package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
  224. package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
  225. package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
  226. package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
  227. package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
  228. package/template/apps/web/src/features/shared/provider.tsx +16 -0
  229. package/template/apps/web/src/features/shared/site-header.tsx +48 -0
  230. package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
  231. package/template/apps/web/src/i18n/navigation.ts +5 -0
  232. package/template/apps/web/src/i18n/request.ts +21 -0
  233. package/template/apps/web/src/i18n/routing.ts +6 -0
  234. package/template/apps/web/src/lib/auth-client.ts +19 -0
  235. package/template/apps/web/src/lib/auth-server.ts +46 -0
  236. package/template/apps/web/src/lib/auth.ts +8 -0
  237. package/template/apps/web/src/lib/codegen.ts +30 -0
  238. package/template/apps/web/src/lib/graphql-client.ts +29 -0
  239. package/template/apps/web/src/lib/graphql-server.ts +28 -0
  240. package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
  241. package/template/apps/web/src/lib/graphql-utils.ts +76 -0
  242. package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
  243. package/template/apps/web/src/lib/metadata.ts +17 -0
  244. package/template/apps/web/src/lib/network-error.test.ts +30 -0
  245. package/template/apps/web/src/lib/network-error.ts +17 -0
  246. package/template/apps/web/src/lib/proxy-utils.ts +42 -0
  247. package/template/apps/web/src/lib/query-client.ts +29 -0
  248. package/template/apps/web/src/lib/utils.ts +5 -0
  249. package/template/apps/web/src/proxy.ts +57 -0
  250. package/template/apps/web/test-results/.last-run.json +4 -0
  251. package/template/apps/web/tests/auth.spec.ts +17 -0
  252. package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
  253. package/template/apps/web/tests/home.spec.ts +25 -0
  254. package/template/apps/web/tsconfig.build.json +4 -0
  255. package/template/apps/web/tsconfig.json +28 -0
  256. package/template/apps/web/vitest.config.ts +40 -0
  257. package/template/apps/web/vitest.shims.d.ts +1 -0
  258. package/template/apps/worker/.env.example +4 -0
  259. package/template/apps/worker/AGENTS.md +11 -0
  260. package/template/apps/worker/Dockerfile +35 -0
  261. package/template/apps/worker/knip.config.ts +3 -0
  262. package/template/apps/worker/kustomize/base/application.properties +4 -0
  263. package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
  264. package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
  265. package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
  266. package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
  267. package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
  268. package/template/apps/worker/oxfmt.config.ts +8 -0
  269. package/template/apps/worker/oxlint.config.ts +15 -0
  270. package/template/apps/worker/package.json +33 -0
  271. package/template/apps/worker/sonar-project.properties +8 -0
  272. package/template/apps/worker/src/env.ts +13 -0
  273. package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
  274. package/template/apps/worker/src/lib/logger.ts +20 -0
  275. package/template/apps/worker/src/main.ts +17 -0
  276. package/template/apps/worker/src/processors/example-processor.ts +15 -0
  277. package/template/apps/worker/src/worker/redis.ts +19 -0
  278. package/template/apps/worker/src/worker/worker.ts +37 -0
  279. package/template/apps/worker/tests/example-processor.test.ts +13 -0
  280. package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
  281. package/template/apps/worker/tsconfig.json +4 -0
  282. package/template/apps/worker/vitest.config.ts +13 -0
  283. package/template/bun.lock +3053 -0
  284. package/template/bunfig.toml +2 -0
  285. package/template/compose.yaml +53 -0
  286. package/template/docker-bake.hcl +86 -0
  287. package/template/package.json +47 -0
  288. package/template/packages/lint/knip.ts +40 -0
  289. package/template/packages/lint/oxfmt.ts +8 -0
  290. package/template/packages/lint/oxlint.ts +136 -0
  291. package/template/packages/lint/package.json +13 -0
  292. package/template/packages/queues/oxfmt.config.ts +8 -0
  293. package/template/packages/queues/oxlint.config.ts +7 -0
  294. package/template/packages/queues/package.json +15 -0
  295. package/template/packages/queues/src/example-job.ts +11 -0
  296. package/template/packages/queues/src/index.ts +3 -0
  297. package/template/packages/queues/src/queue-names.ts +2 -0
  298. package/template/packages/queues/src/redis-connection.ts +27 -0
  299. package/template/packages/queues/tsconfig.json +4 -0
  300. package/template/packages/tsconfig/base.json +23 -0
  301. package/template/packages/tsconfig/node.json +11 -0
  302. package/template/packages/tsconfig/package.json +4 -0
  303. package/template/turbo.json +50 -0
@@ -0,0 +1,82 @@
1
+ import { HealthCheckService, MemoryHealthIndicator } from "@nestjs/terminus";
2
+ import type { TestingModule } from "@nestjs/testing";
3
+ import { Test } from "@nestjs/testing";
4
+ import { vi } from "vitest";
5
+ import { HealthController } from "#src/health/health.controller.js";
6
+ import { PrismaService } from "#src/prisma/prisma.service.js";
7
+
8
+ describe("healthController", () => {
9
+ let controller: HealthController;
10
+
11
+ const prismaMock = {
12
+ isHealthy: vi.fn<() => Promise<boolean>>(),
13
+ };
14
+
15
+ const healthCheckServiceMock = {
16
+ check: vi.fn(async (indicators: Array<() => Promise<Record<string, unknown>>>) => {
17
+ const results = await Promise.all(indicators.map((function_) => function_()));
18
+ const combined = {};
19
+ for (const result of results) {
20
+ Object.assign(combined, result);
21
+ }
22
+ return combined;
23
+ }),
24
+ };
25
+
26
+ const memoryIndicatorMock = {};
27
+
28
+ beforeEach(async () => {
29
+ vi.clearAllMocks();
30
+
31
+ const module: TestingModule = await Test.createTestingModule({
32
+ controllers: [HealthController],
33
+ providers: [
34
+ { provide: HealthCheckService, useValue: healthCheckServiceMock },
35
+ { provide: MemoryHealthIndicator, useValue: memoryIndicatorMock },
36
+ { provide: PrismaService, useValue: prismaMock },
37
+ ],
38
+ }).compile();
39
+
40
+ controller = module.get<HealthController>(HealthController);
41
+ });
42
+
43
+ it("should be defined", () => {
44
+ expect(controller).toBeDefined();
45
+ });
46
+
47
+ describe("liveness", () => {
48
+ it("returns ok with timestamp and uptime", () => {
49
+ const uptimeSpy = vi.spyOn(process, "uptime").mockReturnValue(123.456);
50
+ const res = controller.liveness();
51
+
52
+ expect(res.status).toBe("ok");
53
+ expect(typeof res.timestamp).toBe("string");
54
+ expect(() => new Date(res.timestamp)).not.toThrow();
55
+ expect(res.uptime).toBe(123);
56
+
57
+ uptimeSpy.mockRestore();
58
+ });
59
+ });
60
+
61
+ describe("check", () => {
62
+ it("returns database: up when prisma is healthy", async () => {
63
+ prismaMock.isHealthy.mockResolvedValueOnce(true);
64
+ const res = await controller.check();
65
+
66
+ expect(healthCheckServiceMock.check).toHaveBeenCalledTimes(1);
67
+ expect(res).toEqual({ database: { status: "up" } });
68
+ expect(prismaMock.isHealthy).toHaveBeenCalledTimes(1);
69
+ });
70
+
71
+ it("returns database: down when prisma is NOT healthy", async () => {
72
+ prismaMock.isHealthy.mockResolvedValueOnce(false);
73
+ const res = await controller.check();
74
+
75
+ expect(healthCheckServiceMock.check).toHaveBeenCalledTimes(1);
76
+ expect(res).toEqual({
77
+ database: { status: "down" },
78
+ });
79
+ expect(prismaMock.isHealthy).toHaveBeenCalledTimes(1);
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,57 @@
1
+ import { Controller, Get } from "@nestjs/common";
2
+ import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
3
+ import { HealthCheck, HealthCheckService } from "@nestjs/terminus";
4
+ import { AllowAnonymous } from "#src/auth/decorator/allow-anonymous.decorator.js"; // @if auth
5
+ import { PrismaService } from "#src/prisma/prisma.service.js";
6
+
7
+ /**
8
+ * Controller for health check endpoints.
9
+ *
10
+ * - Provides readiness probe with database connectivity check
11
+ * - Provides liveness probe for basic service status
12
+ * - Used by Kubernetes or Docker health checks
13
+ */
14
+ @AllowAnonymous() // @if auth
15
+ @ApiTags("Health")
16
+ @Controller("health")
17
+ export class HealthController {
18
+ constructor(
19
+ private readonly health: HealthCheckService,
20
+ private readonly prisma: PrismaService,
21
+ ) {}
22
+
23
+ /**
24
+ * Performs readiness check including database connectivity.
25
+ *
26
+ * @returns Health check result with database status
27
+ */
28
+ @ApiOperation({ summary: "Health check with database and memory" })
29
+ @ApiResponse({ description: "Service is healthy", status: 200 })
30
+ @ApiResponse({ description: "Service is unhealthy", status: 503 })
31
+ @Get()
32
+ @HealthCheck()
33
+ check() {
34
+ return this.health.check([
35
+ async () => {
36
+ const isHealthy = await this.prisma.isHealthy();
37
+ return { database: { status: isHealthy ? "up" : "down" } };
38
+ },
39
+ ]);
40
+ }
41
+
42
+ /**
43
+ * Performs basic liveness check.
44
+ *
45
+ * @returns Object with status, timestamp, and uptime
46
+ */
47
+ @ApiOperation({ summary: "Liveness probe" })
48
+ @ApiResponse({ description: "Service is alive", status: 200 })
49
+ @Get("live")
50
+ liveness() {
51
+ return {
52
+ status: "ok",
53
+ timestamp: new Date().toISOString(),
54
+ uptime: Math.floor(process.uptime()),
55
+ };
56
+ }
57
+ }
@@ -0,0 +1,12 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { TerminusModule } from "@nestjs/terminus";
3
+ import { HealthController } from "#src/health/health.controller.js";
4
+
5
+ /**
6
+ * Module exposing standard health and readiness endpoints.
7
+ */
8
+ @Module({
9
+ controllers: [HealthController],
10
+ imports: [TerminusModule],
11
+ })
12
+ export class HealthModule {}
@@ -0,0 +1,18 @@
1
+ import { Inject, Injectable, Scope } from "@nestjs/common";
2
+ import { INQUIRER } from "@nestjs/core";
3
+ import { type Params, PARAMS_PROVIDER_TOKEN, PinoLogger } from "nestjs-pino";
4
+
5
+ /**
6
+ * A `PinoLogger` that sets its own context from the injecting class via
7
+ * `INQUIRER`, so consumers no longer need to call `setContext` themselves.
8
+ */
9
+ @Injectable({ scope: Scope.TRANSIENT })
10
+ export class ContextLogger extends PinoLogger {
11
+ constructor(
12
+ @Inject(INQUIRER) parentClass: object,
13
+ @Inject(PARAMS_PROVIDER_TOKEN) params: Params,
14
+ ) {
15
+ super(params);
16
+ this.setContext(parentClass?.constructor?.name ?? "App");
17
+ }
18
+ }
@@ -0,0 +1,23 @@
1
+ import type { Params } from "nestjs-pino";
2
+
3
+ export const loggerConfig: Params = {
4
+ pinoHttp: {
5
+ autoLogging: false,
6
+ formatters: {
7
+ level: (label: string) => ({ level: label.toUpperCase() }),
8
+ },
9
+ level: process.env.LOG_LEVEL ?? "debug",
10
+ ...(process.env.NODE_ENV === "development" && {
11
+ transport: {
12
+ options: {
13
+ colorize: true,
14
+ ignore: "pid,hostname,req,res,context",
15
+ messageFormat: "[{context}] {msg}",
16
+ singleLine: true,
17
+ translateTime: "dd.mm.yyyy, HH:MM:ss",
18
+ },
19
+ target: "pino-pretty",
20
+ },
21
+ }),
22
+ },
23
+ };
@@ -0,0 +1,22 @@
1
+ import { Global, Module } from "@nestjs/common";
2
+ import { LoggerModule } from "nestjs-pino";
3
+ import { ContextLogger } from "#src/logging/context-logger.js";
4
+ import { loggerConfig } from "#src/logging/logger.config.js";
5
+
6
+ /**
7
+ * The application's logging module.
8
+ *
9
+ * It wires `nestjs-pino` (pino transport + the request-scoped
10
+ * `Logger`/`PinoLogger` providers, configured via `loggerConfig`) and layers
11
+ * `ContextLogger` on top, which derives its log context from the injecting
12
+ * class. Feature modules inject `ContextLogger`; `main.ts` pulls `Logger` for
13
+ * `app.useLogger`. Nothing should import `nestjs-pino`'s `LoggerModule`
14
+ * directly — import this module instead.
15
+ */
16
+ @Global()
17
+ @Module({
18
+ exports: [ContextLogger, LoggerModule],
19
+ imports: [LoggerModule.forRoot(loggerConfig)],
20
+ providers: [ContextLogger],
21
+ })
22
+ export class LoggingModule {}
@@ -0,0 +1,12 @@
1
+ import { Global, Module } from "@nestjs/common";
2
+ import { MailService } from "#src/mail/mail.service.js";
3
+
4
+ /**
5
+ * Global module sharing the mail-sending service.
6
+ */
7
+ @Global()
8
+ @Module({
9
+ exports: [MailService],
10
+ providers: [MailService],
11
+ })
12
+ export class MailModule {}
@@ -0,0 +1,47 @@
1
+ import type { ConfigService } from "@nestjs/config";
2
+ import { vi } from "vitest";
3
+ import type { AppConfig } from "#src/config/app.config.js";
4
+ import type { ContextLogger } from "#src/logging/context-logger.js";
5
+ import { MailService } from "#src/mail/mail.service.js";
6
+
7
+ const { sendMail } = vi.hoisted(() => ({ sendMail: vi.fn() }));
8
+
9
+ vi.mock("nodemailer", () => ({
10
+ createTransport: vi.fn(() => ({ sendMail })),
11
+ }));
12
+
13
+ describe("mailService (SMTP outside production)", () => {
14
+ const cfg = {
15
+ get: vi.fn(() => ({
16
+ from: "App <no-reply@example.com>",
17
+ smtp: { host: "localhost", port: 1025 },
18
+ })),
19
+ } as unknown as ConfigService<AppConfig, true>;
20
+ const logger = { debug: vi.fn() } as unknown as ContextLogger;
21
+
22
+ beforeEach(() => {
23
+ sendMail.mockReset();
24
+ });
25
+
26
+ it("sends the verification link in both the text and the HTML part", async () => {
27
+ const service = new MailService(cfg, logger);
28
+
29
+ await service.sendVerificationEmail("user@example.com", "https://example.com/verify?t=1");
30
+
31
+ expect(sendMail).toHaveBeenCalledTimes(1);
32
+ const [message] = sendMail.mock.calls[0] as [{ html: string; text: string; to: string }];
33
+ expect(message.to).toBe("user@example.com");
34
+ expect(message.text).toContain("https://example.com/verify?t=1");
35
+ expect(message.html).toContain("https://example.com/verify?t=1");
36
+ });
37
+
38
+ it("sends the password reset link", async () => {
39
+ const service = new MailService(cfg, logger);
40
+
41
+ await service.sendPasswordResetEmail("user@example.com", "https://example.com/reset?t=2");
42
+
43
+ const [message] = sendMail.mock.calls[0] as [{ subject: string; text: string }];
44
+ expect(message.subject).toMatch(/reset/i);
45
+ expect(message.text).toContain("https://example.com/reset?t=2");
46
+ });
47
+ });
@@ -0,0 +1,123 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { ConfigService } from "@nestjs/config";
3
+ import { createTransport } from "nodemailer";
4
+ import { Resend } from "resend";
5
+ import type { AppConfig } from "#src/config/app.config.js";
6
+ import { ContextLogger } from "#src/logging/context-logger.js";
7
+
8
+ const APP_NAME = "Nexst";
9
+
10
+ export type MailInput = {
11
+ html: string;
12
+ subject: string;
13
+ text: string;
14
+ to: string;
15
+ };
16
+
17
+ /** Sends a single email. Lets callers depend on the capability, not the service. */
18
+ export type MailSender = (input: MailInput) => Promise<void>;
19
+
20
+ /**
21
+ * Service for sending authentication emails.
22
+ *
23
+ * The provider is derived from `NODE_ENV`: Resend in production and SMTP
24
+ * (MailHog in local development) otherwise. The sender is built once at
25
+ * construction and reused for every message.
26
+ */
27
+ @Injectable()
28
+ export class MailService {
29
+ private readonly sender: MailSender;
30
+
31
+ constructor(
32
+ private readonly cfg: ConfigService<AppConfig, true>,
33
+ private readonly logger: ContextLogger,
34
+ ) {
35
+ this.sender = this.createSender();
36
+ }
37
+
38
+ /** Sends an email through the configured mail provider. */
39
+ async send(input: MailInput): Promise<void> {
40
+ await this.sender(input);
41
+ this.logger.debug({ subject: input.subject, to: input.to }, "Email sent");
42
+ }
43
+
44
+ /** Sends the password-reset message used by Better Auth. */
45
+ async sendPasswordResetEmail(to: string, url: string): Promise<void> {
46
+ await this.send({
47
+ html: this.layout(
48
+ "Reset your password",
49
+ `We received a request to reset your ${APP_NAME} password. This link expires soon. If you did not request it, you can ignore this email.`,
50
+ url,
51
+ "Reset password",
52
+ ),
53
+ subject: `Reset your ${APP_NAME} password`,
54
+ text: `Reset your ${APP_NAME} password by opening this link:\n\n${url}\n\nIf you did not request this, ignore this email.`,
55
+ to,
56
+ });
57
+ }
58
+
59
+ /** Sends the email-verification message used by Better Auth. */
60
+ async sendVerificationEmail(to: string, url: string): Promise<void> {
61
+ await this.send({
62
+ html: this.layout(
63
+ "Confirm your email",
64
+ `Welcome to ${APP_NAME}! Please confirm your email address to activate your account.`,
65
+ url,
66
+ "Verify email",
67
+ ),
68
+ subject: `Confirm your ${APP_NAME} email`,
69
+ text: `Welcome to ${APP_NAME}! Confirm your email address by opening this link:\n\n${url}`,
70
+ to,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Builds the mail sender for the configured provider: Resend in production and
76
+ * SMTP (MailHog in local development) otherwise.
77
+ */
78
+ private createSender(): MailSender {
79
+ const mail = this.cfg.get("mail", { infer: true });
80
+ const { from } = mail;
81
+
82
+ if (process.env.NODE_ENV === "production") {
83
+ const resend = new Resend(mail.resendApiKey);
84
+
85
+ return async (input) => {
86
+ const { error } = await resend.emails.send({ from, ...input });
87
+ if (error) {
88
+ throw new Error(`Resend failed to send email: ${error.message}`);
89
+ }
90
+ };
91
+ }
92
+
93
+ const transporter = createTransport(mail.smtp);
94
+
95
+ return async (input) => {
96
+ await transporter.sendMail({ from, ...input });
97
+ };
98
+ }
99
+
100
+ private layout(heading: string, body: string, actionUrl: string, actionLabel: string): string {
101
+ return `<!doctype html>
102
+ <html>
103
+ <body style="margin:0;background:#f4f5f7;font-family:Arial,Helvetica,sans-serif;color:#111">
104
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="padding:32px 0">
105
+ <tr>
106
+ <td align="center">
107
+ <table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;padding:32px">
108
+ <tr><td style="font-size:20px;font-weight:700;padding-bottom:12px">${heading}</td></tr>
109
+ <tr><td style="font-size:15px;line-height:1.6;color:#374151;padding-bottom:24px">${body}</td></tr>
110
+ <tr>
111
+ <td>
112
+ <a href="${actionUrl}" style="display:inline-block;background:#2563eb;color:#fff;text-decoration:none;font-weight:600;padding:12px 24px;border-radius:8px">${actionLabel}</a>
113
+ </td>
114
+ </tr>
115
+ <tr><td style="font-size:12px;color:#9ca3af;padding-top:24px">If the button does not work, copy this link into your browser:<br>${actionUrl}</td></tr>
116
+ </table>
117
+ </td>
118
+ </tr>
119
+ </table>
120
+ </body>
121
+ </html>`;
122
+ }
123
+ }
@@ -0,0 +1,41 @@
1
+ import { ValidationPipe } from "@nestjs/common";
2
+ import { NestFactory } from "@nestjs/core";
3
+ import type { NestFastifyApplication } from "@nestjs/platform-fastify";
4
+ import { FastifyAdapter } from "@nestjs/platform-fastify";
5
+ import { Logger } from "nestjs-pino";
6
+ import { AppModule } from "#src/app.module.js";
7
+ import { registerGraphqlEnums } from "#src/bootstrap/register-graphql-enums.js"; // @if auth
8
+ import { addSecurity } from "#src/bootstrap/security.js";
9
+ import { addSwagger } from "#src/bootstrap/swagger.js";
10
+ import { validationExceptionFactory } from "#src/errors/validation-exception.factory.js";
11
+
12
+ async function bootstrap() {
13
+ const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
14
+ // @if auth
15
+ // Better Auth parses its own request bodies.
16
+ bodyParser: false,
17
+ // @endif
18
+ });
19
+
20
+ await addSecurity(app);
21
+ registerGraphqlEnums(); // @if auth
22
+ app.useLogger(app.get(Logger));
23
+
24
+ app.useGlobalPipes(
25
+ new ValidationPipe({
26
+ exceptionFactory: validationExceptionFactory,
27
+ transform: true,
28
+ transformOptions: { enableImplicitConversion: true },
29
+ whitelist: true,
30
+ }),
31
+ );
32
+
33
+ if (process.env.NODE_ENV === "development") {
34
+ addSwagger(app);
35
+ }
36
+
37
+ app.enableShutdownHooks();
38
+ await app.listen(process.env.PORT ?? 3001, "0.0.0.0");
39
+ }
40
+
41
+ void bootstrap();
@@ -0,0 +1,12 @@
1
+ import { Global, Module } from "@nestjs/common";
2
+ import { PrismaService } from "#src/prisma/prisma.service.js";
3
+
4
+ /**
5
+ * Global module exposing PrismaService so repositories can access the database.
6
+ */
7
+ @Global()
8
+ @Module({
9
+ exports: [PrismaService],
10
+ providers: [PrismaService],
11
+ })
12
+ export class PrismaModule {}
@@ -0,0 +1,136 @@
1
+ import type { ConfigService } from "@nestjs/config";
2
+ import { vi } from "vitest";
3
+ import type { Mocked } from "vitest";
4
+ import type { ContextLogger } from "#src/logging/context-logger.js";
5
+ import { PrismaService } from "#src/prisma/prisma.service.js";
6
+
7
+ vi.mock("#src/generated/prisma/client.js", () => {
8
+ class PrismaClient {
9
+ $connect = vi.fn(async () => {});
10
+
11
+ $disconnect = vi.fn(async () => {});
12
+ $on = vi.fn();
13
+ $queryRaw = vi.fn();
14
+ }
15
+ return { PrismaClient };
16
+ });
17
+
18
+ vi.mock("@prisma/adapter-pg", () => ({
19
+ PrismaPg: vi.fn(function () {
20
+ return {};
21
+ }),
22
+ }));
23
+
24
+ describe("prismaService (unit)", () => {
25
+ let service: PrismaService;
26
+ let configMock: Mocked<ConfigService>;
27
+ let loggerMock: Mocked<ContextLogger>;
28
+
29
+ const originalAutoMigrate = process.env.PRISMA_AUTO_MIGRATE;
30
+
31
+ beforeAll(() => {
32
+ process.env.PRISMA_AUTO_MIGRATE = "false";
33
+ });
34
+
35
+ afterAll(() => {
36
+ if (originalAutoMigrate === undefined) {
37
+ delete process.env.PRISMA_AUTO_MIGRATE;
38
+ } else {
39
+ process.env.PRISMA_AUTO_MIGRATE = originalAutoMigrate;
40
+ }
41
+ });
42
+
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+
46
+ configMock = {
47
+ get: vi.fn().mockImplementation((key: string) => {
48
+ if (key === "db.url") {
49
+ return "postgresql://user:pass@localhost:5432/db";
50
+ }
51
+ }),
52
+ } as unknown as Mocked<ConfigService>;
53
+
54
+ loggerMock = {
55
+ debug: vi.fn(),
56
+ error: vi.fn(),
57
+ info: vi.fn(),
58
+ setContext: vi.fn(),
59
+ warn: vi.fn(),
60
+ } as unknown as Mocked<ContextLogger>;
61
+
62
+ service = new PrismaService(configMock, loggerMock);
63
+ });
64
+
65
+ describe("isHealthy", () => {
66
+ it("returns true when $queryRaw resolves", async () => {
67
+ vi.spyOn(service, "$queryRaw").mockResolvedValue([{ "?column?": 1 }]);
68
+
69
+ const result = await service.isHealthy();
70
+
71
+ expect(result).toBe(true);
72
+ expect(service.$queryRaw).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ it("returns false and logs error when $queryRaw rejects", async () => {
76
+ const error = new Error("boom");
77
+ vi.spyOn(service, "$queryRaw").mockRejectedValue(error);
78
+
79
+ const result = await service.isHealthy();
80
+
81
+ expect(result).toBe(false);
82
+ expect(service.$queryRaw).toHaveBeenCalledTimes(1);
83
+ expect(loggerMock.error).toHaveBeenCalledWith(
84
+ { error: "boom" },
85
+ "Database health check failed",
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("onModuleDestroy", () => {
91
+ it('calls $disconnect and logs "closed"', async () => {
92
+ vi.spyOn(service, "$disconnect").mockResolvedValue();
93
+
94
+ await service.onModuleDestroy();
95
+
96
+ expect(service.$disconnect).toHaveBeenCalledTimes(1);
97
+ expect(loggerMock.info).toHaveBeenCalledWith("Database connection closed");
98
+ });
99
+
100
+ it("logs error if $disconnect throws", async () => {
101
+ const error = new Error("cannot close");
102
+ vi.spyOn(service, "$disconnect").mockRejectedValue(error);
103
+
104
+ await service.onModuleDestroy();
105
+
106
+ expect(service.$disconnect).toHaveBeenCalled();
107
+ expect(loggerMock.error).toHaveBeenCalledWith(
108
+ { error: "cannot close" },
109
+ "Error during database disconnect",
110
+ );
111
+ });
112
+ });
113
+
114
+ describe("onModuleInit", () => {
115
+ it("calls $connect and logs success", async () => {
116
+ vi.spyOn(service, "$connect").mockResolvedValue();
117
+
118
+ await service.onModuleInit();
119
+
120
+ expect(service.$connect).toHaveBeenCalledTimes(1);
121
+ expect(loggerMock.info).toHaveBeenCalledWith("Successfully connected to database");
122
+ });
123
+
124
+ it("logs error and rethrows if $connect throws", async () => {
125
+ const error = new Error("cannot connect");
126
+ vi.spyOn(service, "$connect").mockRejectedValue(error);
127
+
128
+ await expect(service.onModuleInit()).rejects.toThrow("cannot connect");
129
+ expect(service.$connect).toHaveBeenCalled();
130
+ expect(loggerMock.error).toHaveBeenCalledWith(
131
+ { error: "cannot connect" },
132
+ "Failed to connect to database",
133
+ );
134
+ });
135
+ });
136
+ });