create-rigel 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.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/cli.js +122 -0
- package/package.json +47 -0
- package/templates/express/.claude/CLAUDE.md +114 -0
- package/templates/express/.claude/agents/arch-validator.md +85 -0
- package/templates/express/.claude/agents/db-optimizer.md +72 -0
- package/templates/express/.claude/agents/doc-gardener.md +67 -0
- package/templates/express/.claude/agents/garbage-collector.md +94 -0
- package/templates/express/.claude/agents/gate-checker.md +176 -0
- package/templates/express/.claude/agents/reviewer.md +84 -0
- package/templates/express/.claude/agents/security-auditor.md +82 -0
- package/templates/express/.claude/hooks/post-write.sh +93 -0
- package/templates/express/.claude/rules/api.md +149 -0
- package/templates/express/.claude/rules/architecture.md +78 -0
- package/templates/express/.claude/rules/database.md +104 -0
- package/templates/express/.claude/rules/jobs.md +81 -0
- package/templates/express/.claude/rules/security.md +92 -0
- package/templates/express/.claude/rules/testing.md +97 -0
- package/templates/express/.claude/settings.json +35 -0
- package/templates/express/.claude/skills/00-infra-setup/SKILL.md +479 -0
- package/templates/express/.claude/skills/01-write-roadmap/SKILL.md +72 -0
- package/templates/express/.claude/skills/02-write-spec/SKILL.md +119 -0
- package/templates/express/.claude/skills/03-write-plan/SKILL.md +124 -0
- package/templates/express/.claude/skills/04-build-layer/SKILL.md +216 -0
- package/templates/express/.claude/skills/05-validate-layer/SKILL.md +43 -0
- package/templates/express/.claude/skills/06-push-layer/SKILL.md +59 -0
- package/templates/express/.claude/skills/07-layer-check/SKILL.md +62 -0
- package/templates/express/.claude/skills/08-garbage-collect/SKILL.md +86 -0
- package/templates/express/.claude/skills/09-doc-garden/SKILL.md +64 -0
- package/templates/express/.claude/skills/10-db-optimize/SKILL.md +96 -0
- package/templates/express/.claude/skills/11-load-test/SKILL.md +77 -0
- package/templates/express/.dockerignore +43 -0
- package/templates/express/.env.example +42 -0
- package/templates/express/.github/CODEOWNERS +22 -0
- package/templates/express/.github/dependabot.yml +48 -0
- package/templates/express/.github/pull_request_template.md +58 -0
- package/templates/express/.github/workflows/ci.yml +278 -0
- package/templates/express/.github/workflows/load-test.yml +41 -0
- package/templates/express/.gitleaks.toml +18 -0
- package/templates/express/.lintstagedrc.json +3 -0
- package/templates/express/.nvmrc +1 -0
- package/templates/express/.prettierrc +8 -0
- package/templates/express/.sequelizerc +8 -0
- package/templates/express/AGENTS.md +94 -0
- package/templates/express/ARCHITECTURE.md +161 -0
- package/templates/express/Dockerfile +40 -0
- package/templates/express/Makefile +79 -0
- package/templates/express/QUICKSTART.md +131 -0
- package/templates/express/docker-compose.yml +114 -0
- package/templates/express/docs/PLANS.md +111 -0
- package/templates/express/docs/QUALITY_SCORE.md +66 -0
- package/templates/express/docs/design-docs/core-beliefs.md +89 -0
- package/templates/express/docs/design-docs/decisions/ADR-000-infrastructure.md +70 -0
- package/templates/express/docs/design-docs/decisions/ADR-001-observability.md +82 -0
- package/templates/express/docs/design-docs/decisions/index.md +23 -0
- package/templates/express/docs/design-docs/index.md +11 -0
- package/templates/express/docs/design-docs/observability.md +194 -0
- package/templates/express/docs/design-docs/team-workflow.md +64 -0
- package/templates/express/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/express/docs/exec-plans/tech-debt-tracker.md +31 -0
- package/templates/express/docs/generated/.gitkeep +0 -0
- package/templates/express/docs/product-specs/ROADMAP.md +49 -0
- package/templates/express/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/express/docs/product-specs/index.md +21 -0
- package/templates/express/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/express/eslint.config.mjs +94 -0
- package/templates/express/gitignore +36 -0
- package/templates/express/infra/monitoring/alloy/config.alloy +59 -0
- package/templates/express/jest.config.ts +61 -0
- package/templates/express/package.json +45 -0
- package/templates/express/scripts/openapi.export.ts +77 -0
- package/templates/express/tests/architecture/isolation.test.ts +51 -0
- package/templates/express/tests/architecture/layers.test.ts +83 -0
- package/templates/express/tests/integration/health.test.ts +40 -0
- package/templates/express/tests/integration/isolation.test.template.ts +88 -0
- package/templates/express/tests/load/smoke.js +46 -0
- package/templates/express/tests/load/soak.js +45 -0
- package/templates/express/tests/load/stress.js +44 -0
- package/templates/express/tests/unit/utils/response.util.test.ts +53 -0
- package/templates/express/tsconfig.build.json +8 -0
- package/templates/express/tsconfig.json +20 -0
- package/templates/fastapi/.claude/CLAUDE.md +122 -0
- package/templates/fastapi/.claude/agents/arch-validator.md +64 -0
- package/templates/fastapi/.claude/agents/db-optimizer.md +59 -0
- package/templates/fastapi/.claude/agents/doc-gardener.md +55 -0
- package/templates/fastapi/.claude/agents/garbage-collector.md +74 -0
- package/templates/fastapi/.claude/agents/gate-checker.md +133 -0
- package/templates/fastapi/.claude/agents/reviewer.md +74 -0
- package/templates/fastapi/.claude/agents/security-auditor.md +69 -0
- package/templates/fastapi/.claude/hooks/post-write.sh +65 -0
- package/templates/fastapi/.claude/rules/api.md +119 -0
- package/templates/fastapi/.claude/rules/architecture.md +84 -0
- package/templates/fastapi/.claude/rules/database.md +153 -0
- package/templates/fastapi/.claude/rules/jobs.md +85 -0
- package/templates/fastapi/.claude/rules/security.md +102 -0
- package/templates/fastapi/.claude/rules/testing.md +207 -0
- package/templates/fastapi/.claude/settings.json +50 -0
- package/templates/fastapi/.claude/skills/00-infra-setup/SKILL.md +311 -0
- package/templates/fastapi/.claude/skills/01-write-roadmap/SKILL.md +61 -0
- package/templates/fastapi/.claude/skills/02-write-spec/SKILL.md +82 -0
- package/templates/fastapi/.claude/skills/03-write-plan/SKILL.md +83 -0
- package/templates/fastapi/.claude/skills/04-build-layer/SKILL.md +170 -0
- package/templates/fastapi/.claude/skills/05-validate-layer/SKILL.md +20 -0
- package/templates/fastapi/.claude/skills/06-push-layer/SKILL.md +31 -0
- package/templates/fastapi/.claude/skills/07-layer-check/SKILL.md +51 -0
- package/templates/fastapi/.claude/skills/08-garbage-collect/SKILL.md +47 -0
- package/templates/fastapi/.claude/skills/09-doc-garden/SKILL.md +9 -0
- package/templates/fastapi/.claude/skills/10-db-optimize/SKILL.md +53 -0
- package/templates/fastapi/.claude/skills/11-load-test/SKILL.md +81 -0
- package/templates/fastapi/.dockerignore +30 -0
- package/templates/fastapi/.github/dependabot.yml +24 -0
- package/templates/fastapi/AGENTS.md +87 -0
- package/templates/fastapi/ARCHITECTURE.md +141 -0
- package/templates/fastapi/Makefile +42 -0
- package/templates/fastapi/README.md +179 -0
- package/templates/fastapi/docs/PLANS.md +26 -0
- package/templates/fastapi/docs/QUALITY_SCORE.md +26 -0
- package/templates/fastapi/docs/design-docs/core-beliefs.md +46 -0
- package/templates/fastapi/docs/design-docs/decisions/ADR-000-infrastructure.md +79 -0
- package/templates/fastapi/docs/design-docs/decisions/index.md +7 -0
- package/templates/fastapi/docs/design-docs/index.md +9 -0
- package/templates/fastapi/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/fastapi/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/fastapi/docs/generated/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/ROADMAP.md +49 -0
- package/templates/fastapi/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/fastapi/docs/product-specs/index.md +18 -0
- package/templates/fastapi/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/fastapi/gitignore +46 -0
- package/templates/fastapi/scripts/gate.sh +68 -0
- package/templates/nestjs/.claude/CLAUDE.md +121 -0
- package/templates/nestjs/.claude/agents/db-optimizer.md +51 -0
- package/templates/nestjs/.claude/agents/doc-gardener.md +45 -0
- package/templates/nestjs/.claude/agents/garbage-collector.md +50 -0
- package/templates/nestjs/.claude/agents/gate-checker.md +127 -0
- package/templates/nestjs/.claude/agents/reviewer.md +68 -0
- package/templates/nestjs/.claude/agents/security-auditor.md +72 -0
- package/templates/nestjs/.claude/hooks/post-write.sh +91 -0
- package/templates/nestjs/.claude/rules/api.md +87 -0
- package/templates/nestjs/.claude/rules/architecture.md +104 -0
- package/templates/nestjs/.claude/rules/database.md +111 -0
- package/templates/nestjs/.claude/rules/security.md +110 -0
- package/templates/nestjs/.claude/rules/testing.md +145 -0
- package/templates/nestjs/.claude/settings.json +33 -0
- package/templates/nestjs/.claude/skills/00-infra-setup/SKILL.md +222 -0
- package/templates/nestjs/.claude/skills/01-write-spec/SKILL.md +25 -0
- package/templates/nestjs/.claude/skills/02-write-plan/SKILL.md +18 -0
- package/templates/nestjs/.claude/skills/03-build-layer/SKILL.md +242 -0
- package/templates/nestjs/.claude/skills/04-validate-layer/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/05-push-layer/SKILL.md +6 -0
- package/templates/nestjs/.claude/skills/06-layer-check/SKILL.md +12 -0
- package/templates/nestjs/.claude/skills/07-garbage-collect/SKILL.md +2 -0
- package/templates/nestjs/.claude/skills/08-doc-garden/SKILL.md +4 -0
- package/templates/nestjs/.claude/skills/09-api-sync/SKILL.md +36 -0
- package/templates/nestjs/.claude/skills/10-load-test/SKILL.md +29 -0
- package/templates/nestjs/.env.example +11 -0
- package/templates/nestjs/AGENTS.md +88 -0
- package/templates/nestjs/ARCHITECTURE.md +192 -0
- package/templates/nestjs/docs/PLANS.md +22 -0
- package/templates/nestjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nestjs/docs/design-docs/core-beliefs.md +18 -0
- package/templates/nestjs/docs/design-docs/decisions/ADR-000-infrastructure.md +58 -0
- package/templates/nestjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nestjs/docs/design-docs/index.md +7 -0
- package/templates/nestjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nestjs/docs/exec-plans/tech-debt-tracker.md +16 -0
- package/templates/nestjs/docs/generated/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nestjs/docs/product-specs/index.md +7 -0
- package/templates/nestjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nestjs/gitignore +8 -0
- package/templates/nextjs/.claude/CLAUDE.md +139 -0
- package/templates/nextjs/.claude/agents/arch-validator.md +66 -0
- package/templates/nextjs/.claude/agents/contract-checker.md +78 -0
- package/templates/nextjs/.claude/agents/doc-gardener.md +52 -0
- package/templates/nextjs/.claude/agents/garbage-collector.md +61 -0
- package/templates/nextjs/.claude/agents/gate-checker.md +129 -0
- package/templates/nextjs/.claude/agents/perf-auditor.md +109 -0
- package/templates/nextjs/.claude/agents/reviewer.md +79 -0
- package/templates/nextjs/.claude/agents/security-auditor.md +50 -0
- package/templates/nextjs/.claude/hooks/post-write.sh +121 -0
- package/templates/nextjs/.claude/rules/api-contract.md +166 -0
- package/templates/nextjs/.claude/rules/architecture.md +99 -0
- package/templates/nextjs/.claude/rules/components.md +122 -0
- package/templates/nextjs/.claude/rules/security.md +122 -0
- package/templates/nextjs/.claude/rules/testing.md +187 -0
- package/templates/nextjs/.claude/scripts/infra-setup.sh +550 -0
- package/templates/nextjs/.claude/settings.json +33 -0
- package/templates/nextjs/.claude/skills/00-infra-setup/SKILL.md +471 -0
- package/templates/nextjs/.claude/skills/01-write-roadmap/SKILL.md +77 -0
- package/templates/nextjs/.claude/skills/02-write-spec/SKILL.md +78 -0
- package/templates/nextjs/.claude/skills/03-write-plan/SKILL.md +91 -0
- package/templates/nextjs/.claude/skills/04-build-layer/SKILL.md +141 -0
- package/templates/nextjs/.claude/skills/05-validate-layer/SKILL.md +13 -0
- package/templates/nextjs/.claude/skills/06-push-layer/SKILL.md +20 -0
- package/templates/nextjs/.claude/skills/07-layer-check/SKILL.md +28 -0
- package/templates/nextjs/.claude/skills/08-garbage-collect/SKILL.md +14 -0
- package/templates/nextjs/.claude/skills/09-doc-garden/SKILL.md +12 -0
- package/templates/nextjs/.claude/skills/10-api-sync/SKILL.md +50 -0
- package/templates/nextjs/.claude/skills/11-perf-budget/SKILL.md +51 -0
- package/templates/nextjs/.dockerignore +25 -0
- package/templates/nextjs/.gitattributes +10 -0
- package/templates/nextjs/.github/CODEOWNERS +24 -0
- package/templates/nextjs/.github/dependabot.yml +31 -0
- package/templates/nextjs/.github/pull_request_template.md +59 -0
- package/templates/nextjs/.github/workflows/ci.yml +143 -0
- package/templates/nextjs/.github/workflows/lighthouse.yml +54 -0
- package/templates/nextjs/.github/workflows/load-test.yml +54 -0
- package/templates/nextjs/.husky/pre-commit +1 -0
- package/templates/nextjs/.lintstagedrc.json +4 -0
- package/templates/nextjs/.mcp.json +8 -0
- package/templates/nextjs/.prettierignore +13 -0
- package/templates/nextjs/.prettierrc +9 -0
- package/templates/nextjs/AGENTS.md +88 -0
- package/templates/nextjs/ARCHITECTURE.md +165 -0
- package/templates/nextjs/Dockerfile +38 -0
- package/templates/nextjs/Makefile +50 -0
- package/templates/nextjs/QUICKSTART.md +128 -0
- package/templates/nextjs/docs/PLANS.md +23 -0
- package/templates/nextjs/docs/QUALITY_SCORE.md +27 -0
- package/templates/nextjs/docs/design-docs/core-beliefs.md +43 -0
- package/templates/nextjs/docs/design-docs/decisions/ADR-000-infrastructure.md +72 -0
- package/templates/nextjs/docs/design-docs/decisions/index.md +7 -0
- package/templates/nextjs/docs/design-docs/index.md +7 -0
- package/templates/nextjs/docs/design-docs/team-workflow.md +66 -0
- package/templates/nextjs/docs/exec-plans/active/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/completed/.gitkeep +0 -0
- package/templates/nextjs/docs/exec-plans/tech-debt-tracker.md +19 -0
- package/templates/nextjs/docs/generated/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/ROADMAP.md +59 -0
- package/templates/nextjs/docs/product-specs/draft/.gitkeep +0 -0
- package/templates/nextjs/docs/product-specs/index.md +18 -0
- package/templates/nextjs/docs/product-specs/ready/.gitkeep +0 -0
- package/templates/nextjs/eslint.config.mjs +150 -0
- package/templates/nextjs/gitignore +35 -0
- package/templates/nextjs/lighthouserc.js +37 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.spec.ts"
|
|
4
|
+
- "test/**/*.spec.ts"
|
|
5
|
+
- "test/**/*.e2e-spec.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Testing Rules — Auto-injected on test file edits
|
|
9
|
+
|
|
10
|
+
## Coverage Thresholds (enforced in CI)
|
|
11
|
+
|
|
12
|
+
| Layer | Threshold |
|
|
13
|
+
|---|---|
|
|
14
|
+
| Services | **90%** — happy path + all error paths |
|
|
15
|
+
| Repositories | **80%** — queries, Zod parse, cursor pagination |
|
|
16
|
+
| Controllers | **75%** — via E2E: 201, 401, 422, 404 |
|
|
17
|
+
| Utils/Common | **100%** — every branch |
|
|
18
|
+
|
|
19
|
+
## Service Unit Tests — Mock the Repository
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/applications/application.service.spec.ts
|
|
23
|
+
import { Test } from '@nestjs/testing'
|
|
24
|
+
import { ApplicationService } from './application.service'
|
|
25
|
+
import { ApplicationRepository } from './application.repository'
|
|
26
|
+
import { NotFoundException, ConflictException } from '@nestjs/common'
|
|
27
|
+
|
|
28
|
+
describe('ApplicationService', () => {
|
|
29
|
+
let service: ApplicationService
|
|
30
|
+
let repo: jest.Mocked<ApplicationRepository>
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
const module = await Test.createTestingModule({
|
|
34
|
+
providers: [
|
|
35
|
+
ApplicationService,
|
|
36
|
+
{
|
|
37
|
+
provide: ApplicationRepository,
|
|
38
|
+
useValue: {
|
|
39
|
+
findById: jest.fn(),
|
|
40
|
+
create: jest.fn(),
|
|
41
|
+
list: jest.fn(),
|
|
42
|
+
updateStage: jest.fn(),
|
|
43
|
+
softDelete: jest.fn(),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
}).compile()
|
|
48
|
+
|
|
49
|
+
service = module.get(ApplicationService)
|
|
50
|
+
repo = module.get(ApplicationRepository)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('findOne', () => {
|
|
54
|
+
it('returns application when found', async () => {
|
|
55
|
+
repo.findById.mockResolvedValue(mockApplication)
|
|
56
|
+
const result = await service.findOne('user-1', 'app-1')
|
|
57
|
+
expect(result).toEqual(mockApplication)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('propagates NotFoundException from repository', async () => {
|
|
61
|
+
repo.findById.mockRejectedValue(new NotFoundException())
|
|
62
|
+
await expect(service.findOne('user-1', 'bad-id')).rejects.toThrow(NotFoundException)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('transitionStage', () => {
|
|
67
|
+
it('throws ConflictException on invalid transition', async () => {
|
|
68
|
+
repo.findById.mockResolvedValue({ ...mockApplication, stage: 'ACCEPTED' })
|
|
69
|
+
await expect(service.transitionStage('user-1', 'app-1', 'APPLIED'))
|
|
70
|
+
.rejects.toThrow(ConflictException)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## E2E Tests — Full HTTP Stack
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// test/applications.e2e-spec.ts
|
|
80
|
+
import { Test } from '@nestjs/testing'
|
|
81
|
+
import * as request from 'supertest'
|
|
82
|
+
import { AppModule } from '../src/app.module'
|
|
83
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common'
|
|
84
|
+
|
|
85
|
+
describe('ApplicationController (e2e)', () => {
|
|
86
|
+
let app: INestApplication
|
|
87
|
+
let token: string
|
|
88
|
+
|
|
89
|
+
beforeAll(async () => {
|
|
90
|
+
const module = await Test.createTestingModule({
|
|
91
|
+
imports: [AppModule],
|
|
92
|
+
}).compile()
|
|
93
|
+
|
|
94
|
+
app = module.createNestApplication()
|
|
95
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }))
|
|
96
|
+
await app.init()
|
|
97
|
+
|
|
98
|
+
// Register + login to get token
|
|
99
|
+
const res = await request(app.getHttpServer())
|
|
100
|
+
.post('/v1/auth/login')
|
|
101
|
+
.send({ email: 'test@test.com', password: 'password123' })
|
|
102
|
+
token = res.body.data.tokens.accessToken
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
afterAll(() => app.close())
|
|
106
|
+
|
|
107
|
+
it('POST /v1/applications — 201 with valid body', async () => {
|
|
108
|
+
const res = await request(app.getHttpServer())
|
|
109
|
+
.post('/v1/applications')
|
|
110
|
+
.set('Authorization', `Bearer ${token}`)
|
|
111
|
+
.send({ company: 'Acme', role: 'Engineer' })
|
|
112
|
+
|
|
113
|
+
expect(res.status).toBe(201)
|
|
114
|
+
expect(res.body.data).toHaveProperty('id')
|
|
115
|
+
expect(res.body.data.stage).toBe('SAVED')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('POST /v1/applications — 401 without token', async () => {
|
|
119
|
+
const res = await request(app.getHttpServer())
|
|
120
|
+
.post('/v1/applications')
|
|
121
|
+
.send({ company: 'Acme', role: 'Engineer' })
|
|
122
|
+
expect(res.status).toBe(401)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('POST /v1/applications — 422 with invalid body', async () => {
|
|
126
|
+
const res = await request(app.getHttpServer())
|
|
127
|
+
.post('/v1/applications')
|
|
128
|
+
.set('Authorization', `Bearer ${token}`)
|
|
129
|
+
.send({ role: 'Engineer' }) // missing company
|
|
130
|
+
expect(res.status).toBe(422)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## NestJS TestingModule Pattern
|
|
136
|
+
```typescript
|
|
137
|
+
// Always use Test.createTestingModule — never instantiate classes directly
|
|
138
|
+
const module = await Test.createTestingModule({
|
|
139
|
+
providers: [
|
|
140
|
+
MyService,
|
|
141
|
+
{ provide: MyRepository, useValue: mockRepo },
|
|
142
|
+
{ provide: ConfigService, useValue: { get: jest.fn(), getOrThrow: jest.fn() } },
|
|
143
|
+
],
|
|
144
|
+
}).compile()
|
|
145
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"model": "claude-sonnet-4-5",
|
|
3
|
+
"permissions": {
|
|
4
|
+
"allow": [
|
|
5
|
+
"Bash(npm *)",
|
|
6
|
+
"Bash(npx *)",
|
|
7
|
+
"Bash(node *)",
|
|
8
|
+
"Bash(git *)",
|
|
9
|
+
"Bash(bash *)",
|
|
10
|
+
"Bash(sh *)",
|
|
11
|
+
"Bash(cat *)",
|
|
12
|
+
"Bash(ls *)",
|
|
13
|
+
"Bash(grep *)",
|
|
14
|
+
"Bash(find *)",
|
|
15
|
+
"Bash(wc *)",
|
|
16
|
+
"Bash(k6 *)"
|
|
17
|
+
],
|
|
18
|
+
"deny": []
|
|
19
|
+
},
|
|
20
|
+
"hooks": {
|
|
21
|
+
"PostToolUse": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "bash .claude/hooks/post-write.sh"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# /infra-setup — Phase 0: Full NestJS Scaffold
|
|
2
|
+
|
|
3
|
+
Triggered by: `/infra-setup`
|
|
4
|
+
Run ONCE. If `package.json` exists → abort.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Step 1 — Create NestJS App
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @nestjs/cli
|
|
11
|
+
nest new . --package-manager npm --strict
|
|
12
|
+
```
|
|
13
|
+
Select: `npm` as package manager. This creates the NestJS scaffold.
|
|
14
|
+
|
|
15
|
+
## Step 2 — Install All Dependencies
|
|
16
|
+
```bash
|
|
17
|
+
# Core
|
|
18
|
+
npm install @nestjs/config joi
|
|
19
|
+
npm install @nestjs/sequelize sequelize sequelize-typescript pg pg-hstore
|
|
20
|
+
npm install @nestjs/jwt @nestjs/passport passport passport-jwt
|
|
21
|
+
npm install argon2
|
|
22
|
+
npm install nestjs-pino pino-http pino
|
|
23
|
+
npm install @nestjs/throttler
|
|
24
|
+
npm install @nestjs/swagger
|
|
25
|
+
npm install @nestjs/terminus @nestjs/schedule
|
|
26
|
+
npm install @nestjs/bullmq bullmq ioredis
|
|
27
|
+
npm install class-validator class-transformer
|
|
28
|
+
npm install uuid zod
|
|
29
|
+
npm install helmet
|
|
30
|
+
|
|
31
|
+
# Dev
|
|
32
|
+
npm install -D @types/passport-jwt @types/sequelize @types/pg
|
|
33
|
+
npm install -D @types/node @types/uuid
|
|
34
|
+
npm install -D sequelize-cli
|
|
35
|
+
npm install -D pino-pretty
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Step 3 — Directory Structure
|
|
39
|
+
```bash
|
|
40
|
+
mkdir -p \
|
|
41
|
+
src/common/{decorators,filters,guards,interceptors,pipes,dto} \
|
|
42
|
+
src/config \
|
|
43
|
+
src/database \
|
|
44
|
+
src/auth/{dto,strategies,guards} \
|
|
45
|
+
src/users/{dto,models} \
|
|
46
|
+
src/health \
|
|
47
|
+
src/prisma \
|
|
48
|
+
db/{migrations,seeders} \
|
|
49
|
+
docs/{product-specs/{draft,ready},exec-plans/{active,completed},design-docs/decisions,generated} \
|
|
50
|
+
test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Step 4 — Configuration Files to Write
|
|
54
|
+
|
|
55
|
+
### src/config/configuration.ts
|
|
56
|
+
Typed config factory returning all env vars. Validated by Joi schema.
|
|
57
|
+
|
|
58
|
+
### src/config/config.module.ts
|
|
59
|
+
```typescript
|
|
60
|
+
ConfigModule.forRoot({
|
|
61
|
+
isGlobal: true,
|
|
62
|
+
load: [configuration],
|
|
63
|
+
validationSchema: Joi.object({
|
|
64
|
+
NODE_ENV: Joi.string().valid('development','test','production').default('development'),
|
|
65
|
+
PORT: Joi.number().default(3000),
|
|
66
|
+
DATABASE_URL: Joi.string().uri().required(),
|
|
67
|
+
DATABASE_POOL_MAX: Joi.number().default(10),
|
|
68
|
+
DATABASE_POOL_MIN: Joi.number().default(2),
|
|
69
|
+
REDIS_URL: Joi.string().uri().required(),
|
|
70
|
+
JWT_SECRET: Joi.string().min(32).required(),
|
|
71
|
+
JWT_ACCESS_EXPIRY: Joi.string().default('15m'),
|
|
72
|
+
JWT_REFRESH_EXPIRY: Joi.string().default('7d'),
|
|
73
|
+
CORS_ORIGINS: Joi.string().default('http://localhost:3001'),
|
|
74
|
+
LOG_LEVEL: Joi.string().default('info'),
|
|
75
|
+
}),
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### src/database/database.module.ts
|
|
80
|
+
```typescript
|
|
81
|
+
SequelizeModule.forRootAsync({
|
|
82
|
+
imports: [ConfigModule],
|
|
83
|
+
inject: [ConfigService],
|
|
84
|
+
useFactory: (config: ConfigService) => ({
|
|
85
|
+
dialect: 'postgres',
|
|
86
|
+
uri: config.getOrThrow('DATABASE_URL'),
|
|
87
|
+
pool: {
|
|
88
|
+
max: config.get('DATABASE_POOL_MAX', 10),
|
|
89
|
+
min: config.get('DATABASE_POOL_MIN', 2),
|
|
90
|
+
acquire: 30_000,
|
|
91
|
+
idle: 10_000,
|
|
92
|
+
},
|
|
93
|
+
dialectOptions: {
|
|
94
|
+
statement_timeout: 5000,
|
|
95
|
+
},
|
|
96
|
+
autoLoadModels: true,
|
|
97
|
+
synchronize: false,
|
|
98
|
+
logging: false,
|
|
99
|
+
benchmark: true,
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### src/common/decorators/current-user.decorator.ts
|
|
105
|
+
`@CurrentUser()` — extracts user from JWT payload on request object.
|
|
106
|
+
|
|
107
|
+
### src/common/decorators/public.decorator.ts
|
|
108
|
+
`@Public()` — metadata marker to skip global JWT guard.
|
|
109
|
+
|
|
110
|
+
### src/common/decorators/roles.decorator.ts
|
|
111
|
+
`@Roles('ADMIN')` — metadata for RolesGuard.
|
|
112
|
+
|
|
113
|
+
### src/common/guards/jwt-auth.guard.ts
|
|
114
|
+
Checks `@Public()` metadata — if present, skip. Otherwise validate JWT.
|
|
115
|
+
|
|
116
|
+
### src/common/guards/roles.guard.ts
|
|
117
|
+
Reads `@Roles()` metadata from reflector, checks user.role.
|
|
118
|
+
|
|
119
|
+
### src/common/filters/all-exceptions.filter.ts
|
|
120
|
+
Global exception filter — maps all exceptions to standard envelope:
|
|
121
|
+
`{ statusCode, message, timestamp, path, requestId }`. No stack traces in production.
|
|
122
|
+
|
|
123
|
+
### src/common/interceptors/logging.interceptor.ts
|
|
124
|
+
Global interceptor — logs every request/response via nestjs-pino Logger with duration.
|
|
125
|
+
|
|
126
|
+
### src/common/dto/pagination.dto.ts
|
|
127
|
+
```typescript
|
|
128
|
+
export class PaginationDto {
|
|
129
|
+
@IsOptional() @IsString() cursor?: string
|
|
130
|
+
@IsOptional() @IsInt() @Min(1) @Max(100) @Transform(({ value }) => parseInt(value))
|
|
131
|
+
limit?: number = 20
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### src/auth/* — Full JWT auth module
|
|
136
|
+
- `JwtStrategy` validates token, checks revocation in Redis, attaches user to request
|
|
137
|
+
- `AuthService` — register (argon2.hash), login (argon2.verify), refresh (rotate), logout (revoke)
|
|
138
|
+
- `AuthController` — `@Public()` on all auth routes, strict rate limit
|
|
139
|
+
|
|
140
|
+
### src/health/health.module.ts
|
|
141
|
+
```typescript
|
|
142
|
+
TerminusModule — /health (liveness) + /ready (DB + Redis checks)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### src/utils/uuid.util.ts
|
|
146
|
+
```typescript
|
|
147
|
+
import { v7 as uuidv7 } from 'uuid'
|
|
148
|
+
export const newId = (): string => uuidv7()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### main.ts — Global setup
|
|
152
|
+
```typescript
|
|
153
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true }))
|
|
154
|
+
app.useGlobalFilters(new AllExceptionsFilter())
|
|
155
|
+
app.useGlobalInterceptors(new LoggingInterceptor())
|
|
156
|
+
// JwtAuthGuard registered as APP_GUARD in AppModule providers
|
|
157
|
+
// Helmet
|
|
158
|
+
app.use(helmet())
|
|
159
|
+
// CORS
|
|
160
|
+
app.enableCors({ origin: config.get('CORS_ORIGINS').split(','), credentials: true })
|
|
161
|
+
// Swagger
|
|
162
|
+
const swaggerConfig = new DocumentBuilder()
|
|
163
|
+
.setTitle('API').setVersion('1.0').addBearerAuth().build()
|
|
164
|
+
SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, swaggerConfig))
|
|
165
|
+
// Listen
|
|
166
|
+
await app.listen(config.get('PORT', 3000))
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### .sequelizerc
|
|
170
|
+
```javascript
|
|
171
|
+
module.exports = {
|
|
172
|
+
'config': 'src/database/sequelize-cli.cjs',
|
|
173
|
+
'migrations-path': 'db/migrations',
|
|
174
|
+
'seeders-path': 'db/seeders',
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### src/database/sequelize-cli.cjs
|
|
179
|
+
Loads DATABASE_URL from .env for sequelize-cli.
|
|
180
|
+
|
|
181
|
+
### Docker
|
|
182
|
+
`Dockerfile` — multi-stage: deps → builder → runner, non-root user, HEALTHCHECK.
|
|
183
|
+
`docker-compose.yml` — app + postgres:16 + redis:7 with healthchecks.
|
|
184
|
+
|
|
185
|
+
### GitHub Actions
|
|
186
|
+
`.github/workflows/ci.yml` — lint → typecheck → test → npm audit → secret-scan.
|
|
187
|
+
|
|
188
|
+
### Scaffold Docs
|
|
189
|
+
`AGENTS.md`, `ARCHITECTURE.md`, `ADR-000-infrastructure.md`, `docs/product-specs/index.md`,
|
|
190
|
+
`docs/exec-plans/tech-debt-tracker.md`, `docs/QUALITY_SCORE.md`, `.env.example`, `.gitignore`.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Gate Check
|
|
195
|
+
```bash
|
|
196
|
+
npx tsc --noEmit
|
|
197
|
+
npx eslint src/ --max-warnings=0
|
|
198
|
+
npx jest --no-coverage 2>&1 | tail -5
|
|
199
|
+
curl http://localhost:3000/health
|
|
200
|
+
curl http://localhost:3000/ready
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Commit
|
|
204
|
+
```bash
|
|
205
|
+
git add -A
|
|
206
|
+
git commit -m "chore(infra): phase 0 NestJS infrastructure setup
|
|
207
|
+
|
|
208
|
+
- NestJS 11 + TypeScript 5 strict
|
|
209
|
+
- @nestjs/sequelize + sequelize-typescript + sequelize-cli
|
|
210
|
+
- Global JWT guard (opt-out with @Public())
|
|
211
|
+
- Global ValidationPipe, AllExceptionsFilter, LoggingInterceptor
|
|
212
|
+
- nestjs-pino structured logging
|
|
213
|
+
- @nestjs/swagger Swagger UI at /api/docs
|
|
214
|
+
- @nestjs/terminus health checks (/health + /ready)
|
|
215
|
+
- @nestjs/throttler rate limiting
|
|
216
|
+
- @nestjs/bullmq queue infrastructure
|
|
217
|
+
- Helmet security headers
|
|
218
|
+
- Docker multi-stage + docker-compose
|
|
219
|
+
- GitHub Actions CI
|
|
220
|
+
- ADR-000: infrastructure decisions"
|
|
221
|
+
git push origin main
|
|
222
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# /write-spec
|
|
2
|
+
1. Get next number: ls docs/product-specs/{draft,ready}/ 2>/dev/null | grep SPEC | sort | tail -1
|
|
3
|
+
2. Write to docs/product-specs/draft/SPEC-XXX-{slug}.md
|
|
4
|
+
|
|
5
|
+
Template:
|
|
6
|
+
```markdown
|
|
7
|
+
# SPEC-XXX — {Name}
|
|
8
|
+
**Status:** DRAFT
|
|
9
|
+
**Created:** YYYY-MM-DD
|
|
10
|
+
**Plan:** —
|
|
11
|
+
|
|
12
|
+
## Problem Statement
|
|
13
|
+
## What We're Building
|
|
14
|
+
## Core Entities
|
|
15
|
+
## API Endpoints (will map to NestJS controllers)
|
|
16
|
+
## Business Rules
|
|
17
|
+
## State Machines (if any)
|
|
18
|
+
## Non-Functional Requirements
|
|
19
|
+
## Out of Scope (v1)
|
|
20
|
+
## Acceptance Criteria
|
|
21
|
+
- [ ] criterion
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. Update docs/product-specs/index.md
|
|
25
|
+
4. Tell human: move to ready/ and change Status to READY, then run /write-plan.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# /write-plan
|
|
2
|
+
1. Check docs/product-specs/ready/ — stop if empty
|
|
3
|
+
2. Get next plan number
|
|
4
|
+
3. Write to docs/exec-plans/active/PLAN-XXX-{slug}.md
|
|
5
|
+
|
|
6
|
+
Layer order for each NestJS feature:
|
|
7
|
+
| # | Layer | Gate |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| 1 | Model | paranoid, UUIDv7, indexes |
|
|
10
|
+
| 2 | Migration | runs clean, has down() |
|
|
11
|
+
| 3 | DTOs | class-validator + @ApiProperty on every field |
|
|
12
|
+
| 4 | Repository | Zod parse, cursor pagination, ownership, no N+1 |
|
|
13
|
+
| 5 | Service | no HTTP, NestJS exceptions, ≥90% coverage |
|
|
14
|
+
| 6 | Controller | @ApiTags, @ApiOperation, @ApiResponse, one-liner handlers |
|
|
15
|
+
| 7 | Module | SequelizeModule.forFeature, registered in AppModule |
|
|
16
|
+
| 8 | Tests | service unit (mock repo) + e2e (201, 401, 422) |
|
|
17
|
+
|
|
18
|
+
Tell human: Run /build-layer to start Layer 1.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# /build-layer — Build Next Layer from Active Plan
|
|
2
|
+
|
|
3
|
+
Triggered by: `/build-layer` (no argument)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Find Active Plan and Next Layer
|
|
8
|
+
```bash
|
|
9
|
+
PLAN=$(ls docs/exec-plans/active/*.md 2>/dev/null | head -1)
|
|
10
|
+
[[ -z "$PLAN" ]] && echo "No active plan. Run /write-plan first." && exit 1
|
|
11
|
+
cat "$PLAN"
|
|
12
|
+
```
|
|
13
|
+
Find first `[ ]` row. If all `[x]` → run `/garbage-collect`.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 2 — Load Context
|
|
18
|
+
Read: active plan, spec, `ARCHITECTURE.md`, path-scoped rule:
|
|
19
|
+
- Model → `.claude/rules/database.md` + `.claude/rules/architecture.md`
|
|
20
|
+
- DTOs → `.claude/rules/api.md` + `.claude/rules/architecture.md`
|
|
21
|
+
- Repository → `.claude/rules/database.md` + `.claude/rules/security.md`
|
|
22
|
+
- Service → `.claude/rules/architecture.md` + `.claude/rules/security.md`
|
|
23
|
+
- Controller → `.claude/rules/api.md` + `.claude/rules/security.md`
|
|
24
|
+
- Module → `.claude/rules/architecture.md`
|
|
25
|
+
- Tests → `.claude/rules/testing.md`
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Step 3 — Build the Layer
|
|
30
|
+
|
|
31
|
+
### Model (`src/[feature]/models/[feature].model.ts`)
|
|
32
|
+
```typescript
|
|
33
|
+
import { Table, Column, Model, DataType, Default, DeletedAt, CreatedAt, UpdatedAt, BelongsTo, ForeignKey, HasMany, Index } from 'sequelize-typescript'
|
|
34
|
+
import { newId } from '../../utils/uuid.util'
|
|
35
|
+
|
|
36
|
+
@Table({
|
|
37
|
+
tableName: 'applications',
|
|
38
|
+
paranoid: true, // ← always
|
|
39
|
+
indexes: [
|
|
40
|
+
{ fields: ['user_id'] },
|
|
41
|
+
{ fields: ['user_id', 'stage'] },
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
export class Application extends Model {
|
|
45
|
+
@Default(() => newId()) // ← UUIDv7 always
|
|
46
|
+
@Column({ type: DataType.UUID, primaryKey: true })
|
|
47
|
+
id!: string
|
|
48
|
+
|
|
49
|
+
@ForeignKey(() => User)
|
|
50
|
+
@Column(DataType.UUID)
|
|
51
|
+
userId!: string
|
|
52
|
+
|
|
53
|
+
@Column(DataType.STRING(200)) company!: string
|
|
54
|
+
@Column(DataType.STRING(200)) role!: string
|
|
55
|
+
@Column({ type: DataType.ENUM(...STAGES), defaultValue: 'SAVED' }) stage!: string
|
|
56
|
+
|
|
57
|
+
@CreatedAt createdAt!: Date
|
|
58
|
+
@UpdatedAt updatedAt!: Date
|
|
59
|
+
@DeletedAt deletedAt?: Date
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Migration (`db/migrations/YYYYMMDDHHMMSS-create-[feature].js`)
|
|
64
|
+
```javascript
|
|
65
|
+
module.exports = {
|
|
66
|
+
up: async (queryInterface, Sequelize) => {
|
|
67
|
+
await queryInterface.createTable('applications', {
|
|
68
|
+
id: { type: Sequelize.UUID, primaryKey: true },
|
|
69
|
+
user_id: { type: Sequelize.UUID, allowNull: false, references: { model: 'users', key: 'id' } },
|
|
70
|
+
company: { type: Sequelize.STRING(200), allowNull: false },
|
|
71
|
+
stage: { type: Sequelize.ENUM(...stages), defaultValue: 'SAVED' },
|
|
72
|
+
created_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
|
|
73
|
+
updated_at: { type: Sequelize.DATE, defaultValue: Sequelize.NOW },
|
|
74
|
+
deleted_at: { type: Sequelize.DATE, allowNull: true },
|
|
75
|
+
})
|
|
76
|
+
await queryInterface.sequelize.query(
|
|
77
|
+
'CREATE INDEX CONCURRENTLY idx_applications_user_id ON applications(user_id)'
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
down: async (queryInterface) => queryInterface.dropTable('applications'),
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
Run: `npm run db:migrate`
|
|
84
|
+
|
|
85
|
+
### DTOs (`src/[feature]/dto/`)
|
|
86
|
+
```typescript
|
|
87
|
+
// create-application.dto.ts
|
|
88
|
+
import { IsString, IsOptional, MinLength, MaxLength, IsUrl } from 'class-validator'
|
|
89
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
|
90
|
+
|
|
91
|
+
export class CreateApplicationDto {
|
|
92
|
+
@IsString() @MinLength(1) @MaxLength(200)
|
|
93
|
+
@ApiProperty({ example: 'Acme Corp', description: 'Company name' })
|
|
94
|
+
company: string
|
|
95
|
+
|
|
96
|
+
@IsString() @MinLength(1) @MaxLength(200)
|
|
97
|
+
@ApiProperty({ example: 'Software Engineer' })
|
|
98
|
+
role: string
|
|
99
|
+
|
|
100
|
+
@IsOptional() @IsUrl()
|
|
101
|
+
@ApiPropertyOptional({ example: 'https://jobs.acme.com/123' })
|
|
102
|
+
postingUrl?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// application-response.dto.ts — what we return (controls serialisation)
|
|
106
|
+
export class ApplicationResponseDto {
|
|
107
|
+
@ApiProperty() id: string
|
|
108
|
+
@ApiProperty() company: string
|
|
109
|
+
@ApiProperty({ enum: ApplicationStage }) stage: ApplicationStage
|
|
110
|
+
@ApiProperty() createdAt: string
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Repository (`src/[feature]/[feature].repository.ts`)
|
|
115
|
+
```typescript
|
|
116
|
+
@Injectable()
|
|
117
|
+
export class ApplicationRepository {
|
|
118
|
+
constructor(
|
|
119
|
+
@InjectModel(Application) private appModel: typeof Application,
|
|
120
|
+
@InjectConnection() private sequelize: Sequelize,
|
|
121
|
+
) {}
|
|
122
|
+
|
|
123
|
+
async findById(id: string, userId: string): Promise<ApplicationRecord> {
|
|
124
|
+
const raw = await this.appModel.findOne({
|
|
125
|
+
where: { id, userId }, // ownership always
|
|
126
|
+
include: [{ model: Note }, { model: Contact }], // eager load
|
|
127
|
+
})
|
|
128
|
+
if (!raw) throw new NotFoundException(`Application ${id} not found`)
|
|
129
|
+
return ApplicationSchema.parse(raw.toJSON()) // Zod ALWAYS
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async list(userId: string, cursor?: PageCursor, limit = 20): Promise<PageResult<ApplicationRecord>> {
|
|
133
|
+
const where: WhereOptions = { userId }
|
|
134
|
+
if (cursor) {
|
|
135
|
+
where[Op.or as symbol] = [
|
|
136
|
+
{ createdAt: { [Op.lt]: cursor.createdAt } },
|
|
137
|
+
{ createdAt: cursor.createdAt, id: { [Op.lt]: cursor.id } },
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
const rows = await this.appModel.findAll({
|
|
141
|
+
where, order: [['createdAt', 'DESC'], ['id', 'DESC']], limit: limit + 1,
|
|
142
|
+
})
|
|
143
|
+
const hasMore = rows.length > limit
|
|
144
|
+
const items = hasMore ? rows.slice(0, limit) : rows
|
|
145
|
+
return {
|
|
146
|
+
items: items.map(r => ApplicationSchema.parse(r.toJSON())),
|
|
147
|
+
nextCursor: hasMore ? { id: items.at(-1)!.id, createdAt: items.at(-1)!.createdAt } : null,
|
|
148
|
+
hasMore,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Service (`src/[feature]/[feature].service.ts`)
|
|
155
|
+
```typescript
|
|
156
|
+
@Injectable()
|
|
157
|
+
export class ApplicationService {
|
|
158
|
+
private readonly logger = new Logger(ApplicationService.name)
|
|
159
|
+
|
|
160
|
+
constructor(private readonly applicationRepository: ApplicationRepository) {}
|
|
161
|
+
|
|
162
|
+
async findOne(userId: string, id: string): Promise<ApplicationRecord> {
|
|
163
|
+
return this.applicationRepository.findById(id, userId)
|
|
164
|
+
// NotFoundException thrown by repo if not found — no try/catch needed
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async transitionStage(userId: string, id: string, to: ApplicationStage): Promise<ApplicationRecord> {
|
|
168
|
+
const app = await this.applicationRepository.findById(id, userId)
|
|
169
|
+
const validNext = VALID_TRANSITIONS[app.stage]
|
|
170
|
+
if (!validNext.includes(to)) {
|
|
171
|
+
throw new ConflictException(`Cannot transition from ${app.stage} to ${to}`)
|
|
172
|
+
}
|
|
173
|
+
const result = await this.applicationRepository.updateStage(id, to)
|
|
174
|
+
this.logger.log({ event: 'application.stage_changed', id, from: app.stage, to })
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Controller (`src/[feature]/[feature].controller.ts`)
|
|
181
|
+
```typescript
|
|
182
|
+
@ApiTags('Applications')
|
|
183
|
+
@ApiBearerAuth()
|
|
184
|
+
@Controller('v1/applications')
|
|
185
|
+
export class ApplicationController {
|
|
186
|
+
constructor(private readonly appService: ApplicationService) {}
|
|
187
|
+
|
|
188
|
+
@Get(':id')
|
|
189
|
+
@ApiOperation({ summary: 'Get application by ID' })
|
|
190
|
+
@ApiOkResponse({ type: ApplicationResponseDto })
|
|
191
|
+
@ApiNotFoundResponse()
|
|
192
|
+
findOne(
|
|
193
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
194
|
+
@CurrentUser() user: JwtPayload,
|
|
195
|
+
) {
|
|
196
|
+
return this.appService.findOne(user.sub, id)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Post()
|
|
200
|
+
@HttpCode(HttpStatus.CREATED)
|
|
201
|
+
@ApiOperation({ summary: 'Create application' })
|
|
202
|
+
@ApiCreatedResponse({ type: ApplicationResponseDto })
|
|
203
|
+
create(@Body() dto: CreateApplicationDto, @CurrentUser() user: JwtPayload) {
|
|
204
|
+
return this.appService.createApplication(user.sub, dto)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Module (`src/[feature]/[feature].module.ts`)
|
|
210
|
+
```typescript
|
|
211
|
+
@Module({
|
|
212
|
+
imports: [SequelizeModule.forFeature([Application, Note, Contact])],
|
|
213
|
+
controllers: [ApplicationController],
|
|
214
|
+
providers: [ApplicationRepository, ApplicationService],
|
|
215
|
+
exports: [ApplicationService], // only if other modules need it
|
|
216
|
+
})
|
|
217
|
+
export class ApplicationModule {}
|
|
218
|
+
```
|
|
219
|
+
Then add to `AppModule.imports`.
|
|
220
|
+
|
|
221
|
+
### Tests
|
|
222
|
+
- `src/[feature]/[feature].service.spec.ts` — mock repository, test all error paths
|
|
223
|
+
- `test/[feature].e2e-spec.ts` — supertest full HTTP cycle
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Step 4 — Gate
|
|
228
|
+
Call `gate-checker` agent. Auto-fix failures. Re-run up to 3 times.
|
|
229
|
+
|
|
230
|
+
## Step 5 — ADR if needed
|
|
231
|
+
`docs/design-docs/decisions/ADR-XXX-{slug}.md`
|
|
232
|
+
|
|
233
|
+
## Step 6 — Commit
|
|
234
|
+
```bash
|
|
235
|
+
git add -A
|
|
236
|
+
git commit -m "{feat|chore|test}({feature}): {description}
|
|
237
|
+
PLAN-XXX Layer N/Total"
|
|
238
|
+
git push origin main
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Step 7 — Report + Wait
|
|
242
|
+
Show progress, auto-fixed items, ADR. Wait for human `yes` before next layer.
|