qualia-framework 2.6.0 → 3.2.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 (321) hide show
  1. package/CLAUDE.md +64 -0
  2. package/README.md +103 -30
  3. package/agents/builder.md +110 -0
  4. package/agents/planner.md +134 -0
  5. package/agents/qa-browser.md +186 -0
  6. package/agents/verifier.md +221 -0
  7. package/bin/cli.js +336 -531
  8. package/bin/install.js +570 -0
  9. package/bin/qualia-ui.js +299 -0
  10. package/bin/state.js +630 -0
  11. package/bin/statusline.js +252 -0
  12. package/guide.md +63 -0
  13. package/hooks/auto-update.js +139 -0
  14. package/hooks/branch-guard.js +47 -0
  15. package/hooks/migration-guard.js +60 -0
  16. package/hooks/pre-compact.js +32 -0
  17. package/hooks/pre-deploy-gate.js +110 -0
  18. package/hooks/pre-push.js +33 -0
  19. package/hooks/session-start.js +170 -0
  20. package/package.json +29 -20
  21. package/rules/design-reference.md +179 -0
  22. package/rules/frontend.md +126 -0
  23. package/skills/qualia/SKILL.md +87 -0
  24. package/skills/qualia-build/SKILL.md +97 -0
  25. package/skills/qualia-debug/SKILL.md +87 -0
  26. package/skills/qualia-design/SKILL.md +93 -0
  27. package/skills/qualia-handoff/SKILL.md +66 -0
  28. package/skills/qualia-idk/SKILL.md +8 -0
  29. package/skills/qualia-learn/SKILL.md +88 -0
  30. package/skills/qualia-new/SKILL.md +323 -0
  31. package/{framework/skills → skills}/qualia-optimize/SKILL.md +1 -1
  32. package/skills/qualia-pause/SKILL.md +63 -0
  33. package/skills/qualia-plan/SKILL.md +101 -0
  34. package/skills/qualia-polish/SKILL.md +157 -0
  35. package/skills/qualia-quick/SKILL.md +37 -0
  36. package/skills/qualia-report/SKILL.md +105 -0
  37. package/skills/qualia-resume/SKILL.md +49 -0
  38. package/skills/qualia-review/SKILL.md +76 -0
  39. package/skills/qualia-ship/SKILL.md +90 -0
  40. package/skills/qualia-skill-new/SKILL.md +167 -0
  41. package/skills/qualia-task/SKILL.md +91 -0
  42. package/skills/qualia-verify/SKILL.md +113 -0
  43. package/templates/DESIGN.md +137 -0
  44. package/templates/plan.md +28 -0
  45. package/templates/project.md +22 -0
  46. package/templates/state.md +27 -0
  47. package/templates/tracking.json +20 -0
  48. package/tests/bin.test.sh +673 -0
  49. package/tests/hooks.test.sh +315 -0
  50. package/tests/state.test.sh +535 -0
  51. package/tests/statusline.test.sh +243 -0
  52. package/bin/collect-metrics.sh +0 -62
  53. package/framework/.claudeignore +0 -51
  54. package/framework/CLAUDE.md +0 -51
  55. package/framework/MCP_SETUP.md +0 -229
  56. package/framework/agents/architecture-strategist.md +0 -53
  57. package/framework/agents/backend-agent.md +0 -150
  58. package/framework/agents/code-simplicity-reviewer.md +0 -86
  59. package/framework/agents/frontend-agent.md +0 -111
  60. package/framework/agents/kieran-typescript-reviewer.md +0 -96
  61. package/framework/agents/performance-oracle.md +0 -111
  62. package/framework/agents/qualia-codebase-mapper.md +0 -761
  63. package/framework/agents/qualia-debugger.md +0 -1204
  64. package/framework/agents/qualia-executor.md +0 -882
  65. package/framework/agents/qualia-integration-checker.md +0 -424
  66. package/framework/agents/qualia-phase-researcher.md +0 -457
  67. package/framework/agents/qualia-plan-checker.md +0 -700
  68. package/framework/agents/qualia-planner.md +0 -1245
  69. package/framework/agents/qualia-project-researcher.md +0 -603
  70. package/framework/agents/qualia-research-synthesizer.md +0 -200
  71. package/framework/agents/qualia-roadmapper.md +0 -606
  72. package/framework/agents/qualia-verifier.md +0 -686
  73. package/framework/agents/red-team-qa.md +0 -130
  74. package/framework/agents/security-auditor.md +0 -72
  75. package/framework/agents/team-orchestrator.md +0 -229
  76. package/framework/agents/teams/framework-audit-team.md +0 -66
  77. package/framework/agents/teams/full-stack-team.md +0 -48
  78. package/framework/agents/teams/optimize-team.md +0 -53
  79. package/framework/agents/teams/review-team.md +0 -70
  80. package/framework/agents/teams/ship-team.md +0 -86
  81. package/framework/agents/test-agent.md +0 -182
  82. package/framework/hooks/auto-format.sh +0 -54
  83. package/framework/hooks/block-env-edit.sh +0 -42
  84. package/framework/hooks/branch-guard.sh +0 -43
  85. package/framework/hooks/confirm-delete.sh +0 -59
  86. package/framework/hooks/migration-validate.sh +0 -77
  87. package/framework/hooks/notification-speak.sh +0 -16
  88. package/framework/hooks/pre-commit.sh +0 -100
  89. package/framework/hooks/pre-compact.sh +0 -56
  90. package/framework/hooks/pre-deploy-gate.sh +0 -160
  91. package/framework/hooks/qualia-colors.sh +0 -32
  92. package/framework/hooks/retention-cleanup.sh +0 -62
  93. package/framework/hooks/save-session-state.sh +0 -185
  94. package/framework/hooks/session-context-loader.sh +0 -96
  95. package/framework/hooks/session-learn.sh +0 -32
  96. package/framework/hooks/skill-announce.sh +0 -123
  97. package/framework/hooks/tool-error-announce.sh +0 -27
  98. package/framework/install.ps1 +0 -323
  99. package/framework/install.sh +0 -313
  100. package/framework/qualia-framework/VERSION +0 -1
  101. package/framework/qualia-framework/assets/qualia-logo.png +0 -0
  102. package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
  103. package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
  104. package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
  105. package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
  106. package/framework/qualia-framework/references/checkpoints.md +0 -775
  107. package/framework/qualia-framework/references/completion-checklists.md +0 -359
  108. package/framework/qualia-framework/references/continuation-format.md +0 -249
  109. package/framework/qualia-framework/references/continuation-prompt.md +0 -97
  110. package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
  111. package/framework/qualia-framework/references/design-quality.md +0 -56
  112. package/framework/qualia-framework/references/employee-guide.md +0 -167
  113. package/framework/qualia-framework/references/git-integration.md +0 -254
  114. package/framework/qualia-framework/references/git-planning-commit.md +0 -50
  115. package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
  116. package/framework/qualia-framework/references/model-profiles.md +0 -73
  117. package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
  118. package/framework/qualia-framework/references/planning-config.md +0 -195
  119. package/framework/qualia-framework/references/questioning.md +0 -141
  120. package/framework/qualia-framework/references/tdd.md +0 -263
  121. package/framework/qualia-framework/references/ui-brand.md +0 -160
  122. package/framework/qualia-framework/references/verification-patterns.md +0 -612
  123. package/framework/qualia-framework/templates/DEBUG.md +0 -159
  124. package/framework/qualia-framework/templates/DESIGN.md +0 -81
  125. package/framework/qualia-framework/templates/UAT.md +0 -247
  126. package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
  127. package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
  128. package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
  129. package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
  130. package/framework/qualia-framework/templates/codebase/stack.md +0 -186
  131. package/framework/qualia-framework/templates/codebase/structure.md +0 -285
  132. package/framework/qualia-framework/templates/codebase/testing.md +0 -480
  133. package/framework/qualia-framework/templates/config.json +0 -35
  134. package/framework/qualia-framework/templates/context.md +0 -283
  135. package/framework/qualia-framework/templates/continue-here.md +0 -78
  136. package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
  137. package/framework/qualia-framework/templates/discovery.md +0 -146
  138. package/framework/qualia-framework/templates/lab-notes.md +0 -16
  139. package/framework/qualia-framework/templates/milestone-archive.md +0 -123
  140. package/framework/qualia-framework/templates/milestone.md +0 -115
  141. package/framework/qualia-framework/templates/phase-prompt.md +0 -567
  142. package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
  143. package/framework/qualia-framework/templates/project.md +0 -184
  144. package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
  145. package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
  146. package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
  147. package/framework/qualia-framework/templates/projects/website.md +0 -137
  148. package/framework/qualia-framework/templates/requirements.md +0 -231
  149. package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
  150. package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
  151. package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
  152. package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
  153. package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
  154. package/framework/qualia-framework/templates/research.md +0 -552
  155. package/framework/qualia-framework/templates/roadmap.md +0 -206
  156. package/framework/qualia-framework/templates/state.md +0 -179
  157. package/framework/qualia-framework/templates/summary-complex.md +0 -59
  158. package/framework/qualia-framework/templates/summary-minimal.md +0 -41
  159. package/framework/qualia-framework/templates/summary-standard.md +0 -48
  160. package/framework/qualia-framework/templates/summary.md +0 -246
  161. package/framework/qualia-framework/templates/user-setup.md +0 -311
  162. package/framework/qualia-framework/templates/verification-report.md +0 -322
  163. package/framework/qualia-framework/workflows/add-phase.md +0 -179
  164. package/framework/qualia-framework/workflows/add-todo.md +0 -157
  165. package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
  166. package/framework/qualia-framework/workflows/check-todos.md +0 -176
  167. package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
  168. package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
  169. package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
  170. package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
  171. package/framework/qualia-framework/workflows/execute-phase.md +0 -559
  172. package/framework/qualia-framework/workflows/execute-plan.md +0 -438
  173. package/framework/qualia-framework/workflows/help.md +0 -470
  174. package/framework/qualia-framework/workflows/insert-phase.md +0 -220
  175. package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
  176. package/framework/qualia-framework/workflows/map-codebase.md +0 -327
  177. package/framework/qualia-framework/workflows/new-milestone.md +0 -363
  178. package/framework/qualia-framework/workflows/new-project.md +0 -982
  179. package/framework/qualia-framework/workflows/pause-work.md +0 -122
  180. package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
  181. package/framework/qualia-framework/workflows/plan-phase.md +0 -422
  182. package/framework/qualia-framework/workflows/progress.md +0 -389
  183. package/framework/qualia-framework/workflows/quick.md +0 -252
  184. package/framework/qualia-framework/workflows/remove-phase.md +0 -326
  185. package/framework/qualia-framework/workflows/research-phase.md +0 -74
  186. package/framework/qualia-framework/workflows/resume-project.md +0 -306
  187. package/framework/qualia-framework/workflows/set-profile.md +0 -80
  188. package/framework/qualia-framework/workflows/settings.md +0 -145
  189. package/framework/qualia-framework/workflows/transition.md +0 -556
  190. package/framework/qualia-framework/workflows/update.md +0 -197
  191. package/framework/qualia-framework/workflows/verify-phase.md +0 -195
  192. package/framework/qualia-framework/workflows/verify-work.md +0 -625
  193. package/framework/rules/context7.md +0 -14
  194. package/framework/rules/frontend.md +0 -33
  195. package/framework/rules/speed.md +0 -23
  196. package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
  197. package/framework/scripts/apply-retention.sh +0 -120
  198. package/framework/scripts/bootstrap-pop-os.sh +0 -354
  199. package/framework/scripts/claude-voice +0 -13
  200. package/framework/scripts/cleanup.sh +0 -131
  201. package/framework/scripts/cowork-mode.sh +0 -141
  202. package/framework/scripts/generate-project-claude-md.sh +0 -153
  203. package/framework/scripts/load-test-webhook.js +0 -172
  204. package/framework/scripts/say.py +0 -236
  205. package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
  206. package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
  207. package/framework/scripts/speak.py +0 -55
  208. package/framework/scripts/speak.sh +0 -18
  209. package/framework/scripts/status.sh +0 -138
  210. package/framework/scripts/sync-to-framework.sh +0 -65
  211. package/framework/scripts/voice-hotkey.py +0 -227
  212. package/framework/scripts/voice-input.sh +0 -51
  213. package/framework/skills/animate/SKILL.md +0 -202
  214. package/framework/skills/bolder/SKILL.md +0 -144
  215. package/framework/skills/browser-qa/SKILL.md +0 -536
  216. package/framework/skills/clarify/SKILL.md +0 -179
  217. package/framework/skills/client-handoff/SKILL.md +0 -135
  218. package/framework/skills/collab-onboard/SKILL.md +0 -111
  219. package/framework/skills/colorize/SKILL.md +0 -170
  220. package/framework/skills/critique/SKILL.md +0 -126
  221. package/framework/skills/deep-research/SKILL.md +0 -240
  222. package/framework/skills/delight/SKILL.md +0 -329
  223. package/framework/skills/deploy/SKILL.md +0 -261
  224. package/framework/skills/deploy-verify/SKILL.md +0 -377
  225. package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
  226. package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
  227. package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
  228. package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
  229. package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
  230. package/framework/skills/design-quieter/SKILL.md +0 -130
  231. package/framework/skills/distill/SKILL.md +0 -149
  232. package/framework/skills/docs-lookup/SKILL.md +0 -79
  233. package/framework/skills/fcm-notifications/SKILL.md +0 -125
  234. package/framework/skills/financial-ledger/SKILL.md +0 -1039
  235. package/framework/skills/frontend-master/NOTICE.md +0 -4
  236. package/framework/skills/frontend-master/SKILL.md +0 -127
  237. package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
  238. package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
  239. package/framework/skills/frontend-master/reference/motion-design.md +0 -99
  240. package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
  241. package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
  242. package/framework/skills/frontend-master/reference/typography.md +0 -131
  243. package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
  244. package/framework/skills/harden/SKILL.md +0 -357
  245. package/framework/skills/i18n-rtl/SKILL.md +0 -752
  246. package/framework/skills/learn/SKILL.md +0 -95
  247. package/framework/skills/memory/SKILL.md +0 -50
  248. package/framework/skills/mobile-expo/SKILL.md +0 -977
  249. package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
  250. package/framework/skills/nestjs-backend/README.md +0 -73
  251. package/framework/skills/nestjs-backend/SKILL.md +0 -446
  252. package/framework/skills/nestjs-backend/references/templates.md +0 -1173
  253. package/framework/skills/normalize/SKILL.md +0 -79
  254. package/framework/skills/onboard/SKILL.md +0 -242
  255. package/framework/skills/openrouter-agent/SKILL.md +0 -922
  256. package/framework/skills/polish/SKILL.md +0 -209
  257. package/framework/skills/pr/SKILL.md +0 -66
  258. package/framework/skills/qualia/SKILL.md +0 -199
  259. package/framework/skills/qualia-add-todo/SKILL.md +0 -68
  260. package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
  261. package/framework/skills/qualia-check-todos/SKILL.md +0 -55
  262. package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
  263. package/framework/skills/qualia-debug/SKILL.md +0 -149
  264. package/framework/skills/qualia-design/SKILL.md +0 -203
  265. package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
  266. package/framework/skills/qualia-evolve/SKILL.md +0 -200
  267. package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
  268. package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
  269. package/framework/skills/qualia-guide/SKILL.md +0 -32
  270. package/framework/skills/qualia-help/SKILL.md +0 -114
  271. package/framework/skills/qualia-idk/SKILL.md +0 -352
  272. package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
  273. package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
  274. package/framework/skills/qualia-new-project/SKILL.md +0 -232
  275. package/framework/skills/qualia-pause-work/SKILL.md +0 -96
  276. package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
  277. package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
  278. package/framework/skills/qualia-production-check/SKILL.md +0 -0
  279. package/framework/skills/qualia-progress/SKILL.md +0 -53
  280. package/framework/skills/qualia-quick/SKILL.md +0 -89
  281. package/framework/skills/qualia-report/SKILL.md +0 -166
  282. package/framework/skills/qualia-research-phase/SKILL.md +0 -88
  283. package/framework/skills/qualia-resume-work/SKILL.md +0 -62
  284. package/framework/skills/qualia-review/SKILL.md +0 -263
  285. package/framework/skills/qualia-start/SKILL.md +0 -161
  286. package/framework/skills/qualia-verify-work/SKILL.md +0 -132
  287. package/framework/skills/rag/SKILL.md +0 -750
  288. package/framework/skills/responsive/SKILL.md +0 -231
  289. package/framework/skills/retro/SKILL.md +0 -284
  290. package/framework/skills/sakani-conventions/SKILL.md +0 -136
  291. package/framework/skills/sakani-conventions/evals/evals.json +0 -23
  292. package/framework/skills/sakani-conventions/references/entities.md +0 -365
  293. package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
  294. package/framework/skills/seo-master/SKILL.md +0 -490
  295. package/framework/skills/seo-master/references/checklist.md +0 -199
  296. package/framework/skills/seo-master/references/structured-data.md +0 -609
  297. package/framework/skills/ship/SKILL.md +0 -239
  298. package/framework/skills/stack-researcher/SKILL.md +0 -215
  299. package/framework/skills/status/SKILL.md +0 -154
  300. package/framework/skills/status/scripts/health-check.sh +0 -562
  301. package/framework/skills/subscription-payments/SKILL.md +0 -250
  302. package/framework/skills/supabase/SKILL.md +0 -973
  303. package/framework/skills/supabase/references/templates.md +0 -159
  304. package/framework/skills/team/SKILL.md +0 -67
  305. package/framework/skills/test-runner/SKILL.md +0 -202
  306. package/framework/skills/voice-agent/SKILL.md +0 -1312
  307. package/framework/skills/zoho-workflow/SKILL.md +0 -51
  308. package/framework/statusline-command.sh +0 -117
  309. package/framework/teams/default/inboxes/plan-04.json +0 -9
  310. package/framework/teams/review-team.md +0 -75
  311. package/framework/teams/ship-team.md +0 -86
  312. package/profiles/fawzi.json +0 -16
  313. package/profiles/hasan.json +0 -16
  314. package/profiles/moayad.json +0 -16
  315. package/templates/CLAUDE-owner.md +0 -52
  316. package/templates/CLAUDE.md.hbs +0 -58
  317. package/templates/env.claude.template +0 -12
  318. package/templates/settings.json +0 -172
  319. package/uninstall.sh +0 -90
  320. /package/{framework/rules → rules}/deployment.md +0 -0
  321. /package/{framework/rules → rules}/security.md +0 -0
@@ -1,1173 +0,0 @@
1
- # NestJS Backend Templates
2
-
3
- Production-quality starter templates for common NestJS patterns. All templates use Supabase as the database and Zod for validation.
4
-
5
- ---
6
-
7
- ## 1. Complete Feature Module
8
-
9
- Full working example of a feature module with all layers (module, controller, service, DTO, spec).
10
-
11
- ### building.module.ts
12
-
13
- ```typescript
14
- import { Module } from '@nestjs/common';
15
- import { BuildingController } from './controllers/building.controller';
16
- import { BuildingService } from './services/building.service';
17
- import { SupabaseService } from '../database/supabase.service';
18
- import { LoggerService } from '../logger/logger.service';
19
-
20
- @Module({
21
- controllers: [BuildingController],
22
- providers: [BuildingService, SupabaseService, LoggerService],
23
- exports: [BuildingService],
24
- })
25
- export class BuildingModule {}
26
- ```
27
-
28
- ### controllers/building.controller.ts
29
-
30
- ```typescript
31
- import {
32
- Controller,
33
- Get,
34
- Post,
35
- Body,
36
- Param,
37
- UseGuards,
38
- BadRequestException,
39
- } from '@nestjs/common';
40
- import { BuildingService } from '../services/building.service';
41
- import { AuthGuard } from '../../common/guards/auth.guard';
42
- import { CurrentUser } from '../../common/decorators/current-user.decorator';
43
- import { ZodValidationPipe } from '../../common/pipes/zod-validation.pipe';
44
- import { JwtPayload } from '../../common/types/jwt.types';
45
- import {
46
- CreateBuildingDtoSchema,
47
- CreateBuildingDto,
48
- } from '../dtos/create-building.dto';
49
- import {
50
- UpdateBuildingDtoSchema,
51
- UpdateBuildingDto,
52
- } from '../dtos/update-building.dto';
53
-
54
- @Controller('buildings')
55
- @UseGuards(AuthGuard)
56
- export class BuildingController {
57
- constructor(private readonly buildingService: BuildingService) {}
58
-
59
- @Post()
60
- async create(
61
- @Body(new ZodValidationPipe(CreateBuildingDtoSchema))
62
- dto: CreateBuildingDto,
63
- @CurrentUser() user: JwtPayload,
64
- ) {
65
- const building = await this.buildingService.create(dto, user.id);
66
- return { data: building };
67
- }
68
-
69
- @Get()
70
- async findAll(@CurrentUser() user: JwtPayload) {
71
- const buildings = await this.buildingService.findByOwner(user.id);
72
- return { data: buildings };
73
- }
74
-
75
- @Get(':id')
76
- async findOne(
77
- @Param('id') id: string,
78
- @CurrentUser() user: JwtPayload,
79
- ) {
80
- const building = await this.buildingService.findById(id, user.id);
81
- return { data: building };
82
- }
83
-
84
- @Post(':id')
85
- async update(
86
- @Param('id') id: string,
87
- @Body(new ZodValidationPipe(UpdateBuildingDtoSchema))
88
- dto: UpdateBuildingDto,
89
- @CurrentUser() user: JwtPayload,
90
- ) {
91
- const building = await this.buildingService.update(id, dto, user.id);
92
- return { data: building };
93
- }
94
- }
95
- ```
96
-
97
- ### services/building.service.ts
98
-
99
- ```typescript
100
- import { Injectable } from '@nestjs/common';
101
- import { SupabaseService } from '../../database/supabase.service';
102
- import { LoggerService } from '../../logger/logger.service';
103
- import { DomainException } from '../../common/exceptions/domain.exception';
104
- import {
105
- CreateBuildingDto,
106
- } from '../dtos/create-building.dto';
107
- import {
108
- UpdateBuildingDto,
109
- } from '../dtos/update-building.dto';
110
- import { Building } from '../entities/building.entity';
111
-
112
- @Injectable()
113
- export class BuildingService {
114
- constructor(
115
- private readonly supabase: SupabaseService,
116
- private readonly logger: LoggerService,
117
- ) {}
118
-
119
- async create(dto: CreateBuildingDto, userId: string): Promise<Building> {
120
- // Validate business rules
121
- const { count, error: countError } = await this.supabase
122
- .from('buildings')
123
- .select('id', { count: 'exact', head: true })
124
- .eq('owner_id', userId);
125
-
126
- if (countError) {
127
- this.logger.error('Failed to count buildings', { error: countError });
128
- throw new DomainException('INTERNAL_ERROR');
129
- }
130
-
131
- if (count >= 100) {
132
- throw new DomainException('MAX_BUILDINGS_EXCEEDED', {
133
- limit: 100,
134
- current: count,
135
- });
136
- }
137
-
138
- // Create record
139
- const { data, error } = await this.supabase
140
- .from('buildings')
141
- .insert([
142
- {
143
- ...dto,
144
- owner_id: userId,
145
- },
146
- ])
147
- .select()
148
- .single();
149
-
150
- if (error) {
151
- this.logger.error('Failed to create building', { error, dto, userId });
152
- throw new DomainException('BUILDING_CREATE_FAILED', {
153
- reason: error.message,
154
- });
155
- }
156
-
157
- this.logger.info('Building created', { id: data.id, owner_id: userId });
158
- return data;
159
- }
160
-
161
- async findByOwner(userId: string): Promise<Building[]> {
162
- const { data, error } = await this.supabase
163
- .from('buildings')
164
- .select('*')
165
- .eq('owner_id', userId)
166
- .order('created_at', { ascending: false });
167
-
168
- if (error) {
169
- this.logger.error('Failed to fetch buildings', { error, userId });
170
- throw new DomainException('INTERNAL_ERROR');
171
- }
172
-
173
- return data ?? [];
174
- }
175
-
176
- async findById(id: string, userId?: string): Promise<Building> {
177
- const query = this.supabase
178
- .from('buildings')
179
- .select('*')
180
- .eq('id', id);
181
-
182
- if (userId) {
183
- query.eq('owner_id', userId);
184
- }
185
-
186
- const { data, error } = await query.single();
187
-
188
- if (error || !data) {
189
- throw new DomainException('BUILDING_NOT_FOUND', { id });
190
- }
191
-
192
- return data;
193
- }
194
-
195
- async update(
196
- id: string,
197
- dto: UpdateBuildingDto,
198
- userId: string,
199
- ): Promise<Building> {
200
- // Verify ownership
201
- await this.findById(id, userId);
202
-
203
- const { data, error } = await this.supabase
204
- .from('buildings')
205
- .update(dto)
206
- .eq('id', id)
207
- .eq('owner_id', userId)
208
- .select()
209
- .single();
210
-
211
- if (error) {
212
- this.logger.error('Failed to update building', { error, id, userId });
213
- throw new DomainException('BUILDING_UPDATE_FAILED');
214
- }
215
-
216
- this.logger.info('Building updated', { id, owner_id: userId });
217
- return data;
218
- }
219
- }
220
- ```
221
-
222
- ### dtos/create-building.dto.ts
223
-
224
- ```typescript
225
- import { z } from 'zod';
226
-
227
- export const CreateBuildingDtoSchema = z.object({
228
- nameAr: z.string().min(1).max(200),
229
- nameEn: z.string().min(1).max(200),
230
- buildingType: z.enum(['RESIDENTIAL', 'COMMERCIAL', 'MIXED']),
231
- unitCount: z.number().int().positive().max(10000),
232
- address: z.string().min(1).max(500).optional(),
233
- latitude: z.number().min(-90).max(90).optional(),
234
- longitude: z.number().min(-180).max(180).optional(),
235
- });
236
-
237
- export type CreateBuildingDto = z.infer<typeof CreateBuildingDtoSchema>;
238
- ```
239
-
240
- ### dtos/update-building.dto.ts
241
-
242
- ```typescript
243
- import { z } from 'zod';
244
-
245
- export const UpdateBuildingDtoSchema = z.object({
246
- nameAr: z.string().min(1).max(200).optional(),
247
- nameEn: z.string().min(1).max(200).optional(),
248
- buildingType: z.enum(['RESIDENTIAL', 'COMMERCIAL', 'MIXED']).optional(),
249
- unitCount: z.number().int().positive().max(10000).optional(),
250
- address: z.string().min(1).max(500).optional(),
251
- latitude: z.number().min(-90).max(90).optional(),
252
- longitude: z.number().min(-180).max(180).optional(),
253
- });
254
-
255
- export type UpdateBuildingDto = z.infer<typeof UpdateBuildingDtoSchema>;
256
- ```
257
-
258
- ### entities/building.entity.ts
259
-
260
- ```typescript
261
- export interface Building {
262
- id: string;
263
- owner_id: string;
264
- nameAr: string;
265
- nameEn: string;
266
- buildingType: 'RESIDENTIAL' | 'COMMERCIAL' | 'MIXED';
267
- unitCount: number;
268
- address?: string;
269
- latitude?: number;
270
- longitude?: number;
271
- created_at: string;
272
- updated_at: string;
273
- }
274
- ```
275
-
276
- ### __tests__/building.service.spec.ts
277
-
278
- ```typescript
279
- import { Test, TestingModule } from '@nestjs/testing';
280
- import { BuildingService } from '../services/building.service';
281
- import { SupabaseService } from '../../database/supabase.service';
282
- import { LoggerService } from '../../logger/logger.service';
283
- import { DomainException } from '../../common/exceptions/domain.exception';
284
- import { CreateBuildingDto } from '../dtos/create-building.dto';
285
-
286
- describe('BuildingService', () => {
287
- let service: BuildingService;
288
- let mockSupabase: jest.Mocked<SupabaseService>;
289
- let mockLogger: jest.Mocked<LoggerService>;
290
-
291
- beforeEach(async () => {
292
- mockSupabase = {
293
- from: jest.fn(),
294
- } as any;
295
-
296
- mockLogger = {
297
- error: jest.fn(),
298
- info: jest.fn(),
299
- warn: jest.fn(),
300
- debug: jest.fn(),
301
- } as any;
302
-
303
- const module: TestingModule = await Test.createTestingModule({
304
- providers: [
305
- BuildingService,
306
- { provide: SupabaseService, useValue: mockSupabase },
307
- { provide: LoggerService, useValue: mockLogger },
308
- ],
309
- }).compile();
310
-
311
- service = module.get<BuildingService>(BuildingService);
312
- });
313
-
314
- describe('create', () => {
315
- it('should create a building successfully', async () => {
316
- const dto: CreateBuildingDto = {
317
- nameAr: 'مبنى تجريبي',
318
- nameEn: 'Test Building',
319
- buildingType: 'RESIDENTIAL',
320
- unitCount: 20,
321
- address: '123 Main St',
322
- };
323
-
324
- const mockResult = {
325
- id: '1',
326
- owner_id: 'user-1',
327
- ...dto,
328
- created_at: new Date().toISOString(),
329
- updated_at: new Date().toISOString(),
330
- };
331
-
332
- mockSupabase.from.mockReturnValue({
333
- select: jest.fn().mockReturnThis(),
334
- eq: jest.fn().mockReturnThis(),
335
- count: jest.fn().mockResolvedValue({ count: 0, error: null }),
336
- insert: jest.fn().mockReturnThis(),
337
- single: jest.fn().mockResolvedValue({ data: mockResult, error: null }),
338
- } as any);
339
-
340
- const result = await service.create(dto, 'user-1');
341
- expect(result.id).toBe('1');
342
- expect(result.nameEn).toBe('Test Building');
343
- });
344
-
345
- it('should throw DomainException if max buildings exceeded', async () => {
346
- const dto: CreateBuildingDto = {
347
- nameAr: 'مبنى',
348
- nameEn: 'Building',
349
- buildingType: 'RESIDENTIAL',
350
- unitCount: 10,
351
- };
352
-
353
- mockSupabase.from.mockReturnValue({
354
- select: jest.fn().mockReturnThis(),
355
- eq: jest.fn().mockReturnThis(),
356
- count: jest.fn().mockResolvedValue({ count: 100, error: null }),
357
- } as any);
358
-
359
- await expect(service.create(dto, 'user-1')).rejects.toThrow(
360
- DomainException,
361
- );
362
- });
363
- });
364
-
365
- describe('findById', () => {
366
- it('should find a building by id', async () => {
367
- const mockBuilding = { id: '1', nameEn: 'Test', owner_id: 'user-1' };
368
-
369
- mockSupabase.from.mockReturnValue({
370
- select: jest.fn().mockReturnThis(),
371
- eq: jest.fn().mockReturnThis(),
372
- single: jest.fn().mockResolvedValue({ data: mockBuilding, error: null }),
373
- } as any);
374
-
375
- const result = await service.findById('1', 'user-1');
376
- expect(result.id).toBe('1');
377
- });
378
-
379
- it('should throw DomainException if building not found', async () => {
380
- mockSupabase.from.mockReturnValue({
381
- select: jest.fn().mockReturnThis(),
382
- eq: jest.fn().mockReturnThis(),
383
- single: jest
384
- .fn()
385
- .mockResolvedValue({ data: null, error: { message: 'Not found' } }),
386
- } as any);
387
-
388
- await expect(service.findById('999', 'user-1')).rejects.toThrow(
389
- DomainException,
390
- );
391
- });
392
- });
393
- });
394
- ```
395
-
396
- ---
397
-
398
- ## 2. Global Exception Filter
399
-
400
- Centralized error handling that converts domain exceptions to HTTP responses.
401
-
402
- ### common/filters/exception.filter.ts
403
-
404
- ```typescript
405
- import {
406
- ExceptionFilter,
407
- Catch,
408
- ArgumentsHost,
409
- HttpStatus,
410
- BadRequestException,
411
- } from '@nestjs/common';
412
- import { Response } from 'express';
413
- import { DomainException } from '../exceptions/domain.exception';
414
- import { LoggerService } from '../../logger/logger.service';
415
-
416
- interface ErrorResponse {
417
- http_status: number;
418
- error_code: string;
419
- message_key: string;
420
- request_id: string;
421
- details?: Record<string, any>;
422
- timestamp: string;
423
- }
424
-
425
- @Catch()
426
- export class GlobalExceptionFilter implements ExceptionFilter {
427
- constructor(private readonly logger: LoggerService) {}
428
-
429
- catch(exception: unknown, host: ArgumentsHost) {
430
- const ctx = host.switchToHttp();
431
- const response = ctx.getResponse<Response>();
432
- const request = ctx.getRequest();
433
- const requestId = request.id || 'unknown';
434
-
435
- let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
436
- let errorCode = 'INTERNAL_ERROR';
437
- let messageKey = 'error.internal_error';
438
- let details: Record<string, any> | undefined;
439
-
440
- // Handle domain exceptions
441
- if (exception instanceof DomainException) {
442
- ({ statusCode, errorCode, messageKey, details } =
443
- this.handleDomainException(exception));
444
- this.logger.warn('Domain exception', {
445
- errorCode,
446
- details,
447
- requestId,
448
- path: request.path,
449
- });
450
- }
451
- // Handle validation errors (Zod)
452
- else if (exception instanceof BadRequestException) {
453
- statusCode = HttpStatus.BAD_REQUEST;
454
- errorCode = 'VALIDATION_FAILED';
455
- messageKey = 'error.validation_failed';
456
- const errorResponse = exception.getResponse();
457
- if (typeof errorResponse === 'object' && 'message' in errorResponse) {
458
- details = (errorResponse as any).message;
459
- }
460
- this.logger.warn('Validation error', {
461
- details,
462
- requestId,
463
- path: request.path,
464
- });
465
- }
466
- // Handle generic errors
467
- else {
468
- this.logger.error('Unhandled exception', {
469
- error: exception instanceof Error ? exception.message : String(exception),
470
- stack: exception instanceof Error ? exception.stack : undefined,
471
- requestId,
472
- path: request.path,
473
- });
474
- }
475
-
476
- const errorResponse: ErrorResponse = {
477
- http_status: statusCode,
478
- error_code: errorCode,
479
- message_key: messageKey,
480
- request_id: requestId,
481
- ...(details && { details }),
482
- timestamp: new Date().toISOString(),
483
- };
484
-
485
- response.status(statusCode).json(errorResponse);
486
- }
487
-
488
- private handleDomainException(
489
- exception: DomainException,
490
- ): {
491
- statusCode: number;
492
- errorCode: string;
493
- messageKey: string;
494
- details?: Record<string, any>;
495
- } {
496
- const statusMap: Record<string, number> = {
497
- NOT_FOUND: HttpStatus.NOT_FOUND,
498
- UNAUTHORIZED: HttpStatus.UNAUTHORIZED,
499
- FORBIDDEN: HttpStatus.FORBIDDEN,
500
- VALIDATION_FAILED: HttpStatus.BAD_REQUEST,
501
- CONFLICT: HttpStatus.CONFLICT,
502
- INTERNAL_ERROR: HttpStatus.INTERNAL_SERVER_ERROR,
503
- MAX_BUILDINGS_EXCEEDED: HttpStatus.BAD_REQUEST,
504
- BUILDING_CREATE_FAILED: HttpStatus.BAD_REQUEST,
505
- BUILDING_UPDATE_FAILED: HttpStatus.BAD_REQUEST,
506
- BUILDING_NOT_FOUND: HttpStatus.NOT_FOUND,
507
- };
508
-
509
- const statusCode = statusMap[exception.code] || HttpStatus.INTERNAL_SERVER_ERROR;
510
- const messageKey = `error.${exception.code.toLowerCase()}`;
511
-
512
- return {
513
- statusCode,
514
- errorCode: exception.code,
515
- messageKey,
516
- details: exception.details,
517
- };
518
- }
519
- }
520
- ```
521
-
522
- ### common/exceptions/domain.exception.ts
523
-
524
- ```typescript
525
- export class DomainException extends Error {
526
- constructor(
527
- public readonly code: string,
528
- public readonly details?: Record<string, any>,
529
- ) {
530
- super(`Domain error: ${code}`);
531
- this.name = 'DomainException';
532
- }
533
- }
534
- ```
535
-
536
- ### app.module.ts (register filter)
537
-
538
- ```typescript
539
- import { APP_FILTER } from '@nestjs/core';
540
- import { GlobalExceptionFilter } from './common/filters/exception.filter';
541
- import { LoggerService } from './logger/logger.service';
542
-
543
- @Module({
544
- imports: [/* ... */],
545
- controllers: [AppController],
546
- providers: [
547
- {
548
- provide: APP_FILTER,
549
- useClass: GlobalExceptionFilter,
550
- },
551
- LoggerService,
552
- ],
553
- })
554
- export class AppModule {}
555
- ```
556
-
557
- ---
558
-
559
- ## 3. Auth Guard with JWT Validation
560
-
561
- Validates JWT token and attaches user to request.
562
-
563
- ### common/guards/auth.guard.ts
564
-
565
- ```typescript
566
- import {
567
- Injectable,
568
- CanActivate,
569
- ExecutionContext,
570
- UnauthorizedException,
571
- } from '@nestjs/common';
572
- import { ConfigService } from '@nestjs/config';
573
- import { Request } from 'express';
574
- import * as jwt from 'jsonwebtoken';
575
- import { JwtPayload } from '../types/jwt.types';
576
-
577
- @Injectable()
578
- export class AuthGuard implements CanActivate {
579
- constructor(private readonly configService: ConfigService) {}
580
-
581
- canActivate(context: ExecutionContext): boolean {
582
- const request = context.switchToHttp().getRequest<Request>();
583
- const authHeader = request.headers.authorization;
584
-
585
- if (!authHeader) {
586
- throw new UnauthorizedException('Missing authorization header');
587
- }
588
-
589
- const [scheme, token] = authHeader.split(' ');
590
-
591
- if (scheme !== 'Bearer') {
592
- throw new UnauthorizedException('Invalid authorization scheme');
593
- }
594
-
595
- if (!token) {
596
- throw new UnauthorizedException('Missing bearer token');
597
- }
598
-
599
- try {
600
- const secret = this.configService.get<string>('JWT_SECRET');
601
- const payload = jwt.verify(token, secret) as JwtPayload;
602
-
603
- request.user = payload;
604
- request.id = request.headers['x-request-id'] as string;
605
- return true;
606
- } catch (error) {
607
- if (error instanceof jwt.TokenExpiredError) {
608
- throw new UnauthorizedException('Token expired');
609
- }
610
- if (error instanceof jwt.JsonWebTokenError) {
611
- throw new UnauthorizedException('Invalid token');
612
- }
613
- throw new UnauthorizedException('Unauthorized');
614
- }
615
- }
616
- }
617
- ```
618
-
619
- ### common/types/jwt.types.ts
620
-
621
- ```typescript
622
- export interface JwtPayload {
623
- sub: string; // user ID
624
- id: string; // user ID (duplicate for compatibility)
625
- email: string;
626
- role: 'USER' | 'ADMIN';
627
- iat: number;
628
- exp: number;
629
- }
630
-
631
- declare global {
632
- namespace Express {
633
- interface Request {
634
- user?: JwtPayload;
635
- id?: string; // request ID
636
- }
637
- }
638
- }
639
- ```
640
-
641
- ---
642
-
643
- ## 4. Roles Guard with Decorator
644
-
645
- Checks user role against @Roles() decorator on method.
646
-
647
- ### common/guards/roles.guard.ts
648
-
649
- ```typescript
650
- import {
651
- Injectable,
652
- CanActivate,
653
- ExecutionContext,
654
- ForbiddenException,
655
- } from '@nestjs/common';
656
- import { Reflector } from '@nestjs/core';
657
- import { Request } from 'express';
658
-
659
- @Injectable()
660
- export class RolesGuard implements CanActivate {
661
- constructor(private readonly reflector: Reflector) {}
662
-
663
- canActivate(context: ExecutionContext): boolean {
664
- const requiredRoles = this.reflector.get<string[]>(
665
- 'roles',
666
- context.getHandler(),
667
- );
668
-
669
- if (!requiredRoles || requiredRoles.length === 0) {
670
- return true;
671
- }
672
-
673
- const request = context.switchToHttp().getRequest<Request>();
674
- const user = request.user;
675
-
676
- if (!user) {
677
- throw new ForbiddenException('User not authenticated');
678
- }
679
-
680
- if (!requiredRoles.includes(user.role)) {
681
- throw new ForbiddenException(
682
- `Insufficient permissions. Required roles: ${requiredRoles.join(', ')}`,
683
- );
684
- }
685
-
686
- return true;
687
- }
688
- }
689
- ```
690
-
691
- ### common/decorators/roles.decorator.ts
692
-
693
- ```typescript
694
- import { SetMetadata } from '@nestjs/common';
695
-
696
- export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
697
- ```
698
-
699
- ### Usage in controller
700
-
701
- ```typescript
702
- @Post()
703
- @UseGuards(AuthGuard, RolesGuard)
704
- @Roles('ADMIN', 'MANAGER')
705
- async create(@Body() dto: CreateDto) {
706
- // Only authenticated admins/managers reach here
707
- }
708
- ```
709
-
710
- ---
711
-
712
- ## 5. Request ID Interceptor
713
-
714
- Generates or propagates a unique request_id UUID for tracing.
715
-
716
- ### common/interceptors/request-id.interceptor.ts
717
-
718
- ```typescript
719
- import {
720
- Injectable,
721
- NestInterceptor,
722
- ExecutionContext,
723
- CallHandler,
724
- } from '@nestjs/common';
725
- import { Observable } from 'rxjs';
726
- import { Request, Response } from 'express';
727
- import { v4 as uuidv4 } from 'uuid';
728
-
729
- @Injectable()
730
- export class RequestIdInterceptor implements NestInterceptor {
731
- intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
732
- const request = context.switchToHttp().getRequest<Request>();
733
- const response = context.switchToHttp().getResponse<Response>();
734
-
735
- // Use existing request ID or generate new one
736
- const requestId =
737
- (request.headers['x-request-id'] as string) || uuidv4();
738
-
739
- request.id = requestId;
740
- response.setHeader('x-request-id', requestId);
741
-
742
- return next.handle();
743
- }
744
- }
745
- ```
746
-
747
- ### app.module.ts (register interceptor)
748
-
749
- ```typescript
750
- import { APP_INTERCEPTOR } from '@nestjs/core';
751
- import { RequestIdInterceptor } from './common/interceptors/request-id.interceptor';
752
-
753
- @Module({
754
- providers: [
755
- {
756
- provide: APP_INTERCEPTOR,
757
- useClass: RequestIdInterceptor,
758
- },
759
- ],
760
- })
761
- export class AppModule {}
762
- ```
763
-
764
- ---
765
-
766
- ## 6. Zod Validation Pipe
767
-
768
- Validates request body against Zod schema.
769
-
770
- ### common/pipes/zod-validation.pipe.ts
771
-
772
- ```typescript
773
- import {
774
- PipeTransform,
775
- Injectable,
776
- BadRequestException,
777
- ArgumentMetadata,
778
- } from '@nestjs/common';
779
- import { ZodSchema, ZodError } from 'zod';
780
-
781
- @Injectable()
782
- export class ZodValidationPipe implements PipeTransform {
783
- constructor(private schema: ZodSchema) {}
784
-
785
- transform(value: any, metadata: ArgumentMetadata) {
786
- try {
787
- const parsed = this.schema.parse(value);
788
- return parsed;
789
- } catch (error) {
790
- if (error instanceof ZodError) {
791
- const formatted = error.errors.map((err) => ({
792
- field: err.path.join('.'),
793
- message: err.message,
794
- code: err.code,
795
- }));
796
-
797
- throw new BadRequestException({
798
- message: 'Validation failed',
799
- errors: formatted,
800
- });
801
- }
802
- throw error;
803
- }
804
- }
805
- }
806
- ```
807
-
808
- ### Usage in controller
809
-
810
- ```typescript
811
- @Post()
812
- async create(
813
- @Body(new ZodValidationPipe(CreateBuildingDtoSchema))
814
- dto: CreateBuildingDto,
815
- ) {
816
- return { data: await this.buildingService.create(dto) };
817
- }
818
- ```
819
-
820
- ---
821
-
822
- ## 7. Supabase Database Service
823
-
824
- Wrapper around Supabase client with RLS and transaction support.
825
-
826
- ### database/supabase.service.ts
827
-
828
- ```typescript
829
- import { Injectable } from '@nestjs/common';
830
- import { ConfigService } from '@nestjs/config';
831
- import { createClient, SupabaseClient } from '@supabase/supabase-js';
832
- import { LoggerService } from '../logger/logger.service';
833
-
834
- @Injectable()
835
- export class SupabaseService {
836
- private client: SupabaseClient;
837
-
838
- constructor(
839
- private readonly configService: ConfigService,
840
- private readonly logger: LoggerService,
841
- ) {
842
- const url = this.configService.get<string>('SUPABASE_URL');
843
- const key = this.configService.get<string>('SUPABASE_ANON_KEY');
844
-
845
- if (!url || !key) {
846
- throw new Error('Missing Supabase configuration');
847
- }
848
-
849
- this.client = createClient(url, key);
850
- }
851
-
852
- /**
853
- * Get typed access to a table
854
- * Usage: this.supabase.from('buildings').select('*')
855
- */
856
- from<T = any>(table: string) {
857
- return this.client.from<T>(table);
858
- }
859
-
860
- /**
861
- * Execute a stored procedure (for transactions)
862
- * Usage: this.supabase.rpc('my_function', { arg1: 'value' })
863
- */
864
- rpc(name: string, params?: Record<string, any>) {
865
- return this.client.rpc(name, params);
866
- }
867
-
868
- /**
869
- * Direct access to client for advanced operations
870
- */
871
- getClient() {
872
- return this.client;
873
- }
874
-
875
- /**
876
- * Health check
877
- */
878
- async healthCheck(): Promise<boolean> {
879
- try {
880
- const { error } = await this.client.from('_supabase_migrations').select('id').limit(1);
881
- if (error) {
882
- this.logger.warn('Supabase health check failed', { error });
883
- return false;
884
- }
885
- return true;
886
- } catch (error) {
887
- this.logger.error('Supabase health check exception', { error });
888
- return false;
889
- }
890
- }
891
- }
892
- ```
893
-
894
- ### Usage in services
895
-
896
- ```typescript
897
- // Select query
898
- const { data, error } = await this.supabase
899
- .from('buildings')
900
- .select('*')
901
- .eq('owner_id', userId);
902
-
903
- // Insert
904
- const { data, error } = await this.supabase
905
- .from('buildings')
906
- .insert([{ nameEn: 'Test', owner_id: userId }])
907
- .select()
908
- .single();
909
-
910
- // Update
911
- const { data, error } = await this.supabase
912
- .from('buildings')
913
- .update({ nameEn: 'Updated' })
914
- .eq('id', buildingId)
915
- .select()
916
- .single();
917
-
918
- // Transaction via stored procedure
919
- const { data, error } = await this.supabase.rpc('create_building_with_units', {
920
- p_owner_id: userId,
921
- p_name_en: 'Test',
922
- p_unit_count: 10,
923
- });
924
- ```
925
-
926
- ---
927
-
928
- ## 8. Config Module with Zod Validation
929
-
930
- Centralized configuration with startup validation.
931
-
932
- ### config/configuration.ts
933
-
934
- ```typescript
935
- import { z } from 'zod';
936
-
937
- const envSchema = z.object({
938
- // Server
939
- NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
940
- PORT: z.string().pipe(z.coerce.number()).default('3000'),
941
-
942
- // Database
943
- SUPABASE_URL: z.string().url(),
944
- SUPABASE_ANON_KEY: z.string(),
945
- SUPABASE_SERVICE_ROLE_KEY: z.string().optional(),
946
-
947
- // JWT
948
- JWT_SECRET: z.string().min(32),
949
- JWT_EXPIRY: z.string().default('24h'),
950
-
951
- // Logging
952
- LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
953
-
954
- // CORS
955
- CORS_ORIGIN: z.string().default('*'),
956
-
957
- // Features
958
- FEATURE_BUILDING_VERIFICATION: z
959
- .string()
960
- .transform((v) => v === 'true')
961
- .default('true'),
962
- });
963
-
964
- export type Environment = z.infer<typeof envSchema>;
965
-
966
- export const validateConfig = (config: Record<string, unknown>): Environment => {
967
- try {
968
- return envSchema.parse(config);
969
- } catch (error) {
970
- if (error instanceof z.ZodError) {
971
- const formatted = error.errors
972
- .map((err) => `${err.path.join('.')}: ${err.message}`)
973
- .join(', ');
974
- throw new Error(`Configuration validation failed: ${formatted}`);
975
- }
976
- throw error;
977
- }
978
- };
979
-
980
- export default () => ({
981
- nodeEnv: process.env.NODE_ENV,
982
- port: parseInt(process.env.PORT || '3000', 10),
983
- supabase: {
984
- url: process.env.SUPABASE_URL,
985
- anonKey: process.env.SUPABASE_ANON_KEY,
986
- serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
987
- },
988
- jwt: {
989
- secret: process.env.JWT_SECRET,
990
- expiry: process.env.JWT_EXPIRY,
991
- },
992
- logging: {
993
- level: process.env.LOG_LEVEL,
994
- },
995
- cors: {
996
- origin: process.env.CORS_ORIGIN,
997
- },
998
- features: {
999
- buildingVerification: process.env.FEATURE_BUILDING_VERIFICATION === 'true',
1000
- },
1001
- });
1002
- ```
1003
-
1004
- ### config/config.module.ts
1005
-
1006
- ```typescript
1007
- import { Module } from '@nestjs/common';
1008
- import { ConfigModule as NestConfigModule } from '@nestjs/config';
1009
- import configuration, { validateConfig } from './configuration';
1010
-
1011
- @Module({
1012
- imports: [
1013
- NestConfigModule.forRoot({
1014
- load: [configuration],
1015
- validate: validateConfig,
1016
- isGlobal: true,
1017
- expandVariables: true,
1018
- }),
1019
- ],
1020
- })
1021
- export class ConfigModule {}
1022
- ```
1023
-
1024
- ### app.module.ts (import config)
1025
-
1026
- ```typescript
1027
- import { Module } from '@nestjs/common';
1028
- import { ConfigModule } from './config/config.module';
1029
- import { BuildingModule } from './modules/building/building.module';
1030
-
1031
- @Module({
1032
- imports: [ConfigModule, BuildingModule],
1033
- })
1034
- export class AppModule {}
1035
- ```
1036
-
1037
- ### Usage in services
1038
-
1039
- ```typescript
1040
- import { ConfigService } from '@nestjs/config';
1041
- import { Environment } from '../config/configuration';
1042
-
1043
- constructor(private readonly configService: ConfigService<Environment>) {}
1044
-
1045
- someMethod() {
1046
- const secret = this.configService.get('JWT_SECRET');
1047
- const supabaseUrl = this.configService.get('SUPABASE_URL');
1048
- const isFeatureEnabled = this.configService.get('FEATURE_BUILDING_VERIFICATION');
1049
- }
1050
- ```
1051
-
1052
- ---
1053
-
1054
- ## 9. Current User Decorator
1055
-
1056
- Extracts authenticated user from request.
1057
-
1058
- ### common/decorators/current-user.decorator.ts
1059
-
1060
- ```typescript
1061
- import { createParamDecorator, ExecutionContext } from '@nestjs/common';
1062
- import { Request } from 'express';
1063
- import { JwtPayload } from '../types/jwt.types';
1064
-
1065
- export const CurrentUser = createParamDecorator(
1066
- (data: unknown, ctx: ExecutionContext): JwtPayload | undefined => {
1067
- const request = ctx.switchToHttp().getRequest<Request>();
1068
- return request.user;
1069
- },
1070
- );
1071
- ```
1072
-
1073
- ### Usage in controller
1074
-
1075
- ```typescript
1076
- @Get()
1077
- async getProfile(@CurrentUser() user: JwtPayload) {
1078
- return { data: { id: user.id, email: user.email } };
1079
- }
1080
- ```
1081
-
1082
- ---
1083
-
1084
- ## 10. Logging Service
1085
-
1086
- Structured logging with request tracing.
1087
-
1088
- ### logger/logger.service.ts
1089
-
1090
- ```typescript
1091
- import { Injectable } from '@nestjs/common';
1092
- import { ConfigService } from '@nestjs/config';
1093
-
1094
- export enum LogLevel {
1095
- DEBUG = 'debug',
1096
- INFO = 'info',
1097
- WARN = 'warn',
1098
- ERROR = 'error',
1099
- }
1100
-
1101
- @Injectable()
1102
- export class LoggerService {
1103
- private logLevel: LogLevel = LogLevel.INFO;
1104
-
1105
- constructor(private readonly configService: ConfigService) {
1106
- const level = this.configService.get<string>('LOG_LEVEL');
1107
- if (level) {
1108
- this.logLevel = level as LogLevel;
1109
- }
1110
- }
1111
-
1112
- private shouldLog(level: LogLevel): boolean {
1113
- const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];
1114
- return levels.indexOf(level) >= levels.indexOf(this.logLevel);
1115
- }
1116
-
1117
- debug(message: string, context?: Record<string, any>) {
1118
- if (this.shouldLog(LogLevel.DEBUG)) {
1119
- console.debug(
1120
- JSON.stringify({
1121
- level: 'debug',
1122
- timestamp: new Date().toISOString(),
1123
- message,
1124
- ...context,
1125
- }),
1126
- );
1127
- }
1128
- }
1129
-
1130
- info(message: string, context?: Record<string, any>) {
1131
- if (this.shouldLog(LogLevel.INFO)) {
1132
- console.log(
1133
- JSON.stringify({
1134
- level: 'info',
1135
- timestamp: new Date().toISOString(),
1136
- message,
1137
- ...context,
1138
- }),
1139
- );
1140
- }
1141
- }
1142
-
1143
- warn(message: string, context?: Record<string, any>) {
1144
- if (this.shouldLog(LogLevel.WARN)) {
1145
- console.warn(
1146
- JSON.stringify({
1147
- level: 'warn',
1148
- timestamp: new Date().toISOString(),
1149
- message,
1150
- ...context,
1151
- }),
1152
- );
1153
- }
1154
- }
1155
-
1156
- error(message: string, context?: Record<string, any>) {
1157
- if (this.shouldLog(LogLevel.ERROR)) {
1158
- console.error(
1159
- JSON.stringify({
1160
- level: 'error',
1161
- timestamp: new Date().toISOString(),
1162
- message,
1163
- ...context,
1164
- }),
1165
- );
1166
- }
1167
- }
1168
- }
1169
- ```
1170
-
1171
- ---
1172
-
1173
- All templates are production-ready and follow NestJS best practices. Adapt to your specific needs while maintaining the architectural patterns.