@quanticjs/create-app 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deps.d.ts +4 -0
- package/dist/deps.js +97 -0
- package/dist/deps.js.map +1 -0
- package/dist/generators/backend.d.ts +2 -0
- package/dist/generators/backend.js +20 -0
- package/dist/generators/backend.js.map +1 -0
- package/dist/generators/bff.d.ts +2 -0
- package/dist/generators/bff.js +9 -0
- package/dist/generators/bff.js.map +1 -0
- package/dist/generators/claude.d.ts +2 -0
- package/dist/generators/claude.js +45 -0
- package/dist/generators/claude.js.map +1 -0
- package/dist/generators/docker.d.ts +2 -0
- package/dist/generators/docker.js +10 -0
- package/dist/generators/docker.js.map +1 -0
- package/dist/generators/e2e.d.ts +2 -0
- package/dist/generators/e2e.js +8 -0
- package/dist/generators/e2e.js.map +1 -0
- package/dist/generators/frontend.d.ts +2 -0
- package/dist/generators/frontend.js +28 -0
- package/dist/generators/frontend.js.map +1 -0
- package/dist/generators/module.d.ts +2 -0
- package/dist/generators/module.js +35 -0
- package/dist/generators/module.js.map +1 -0
- package/dist/generators/root.d.ts +2 -0
- package/dist/generators/root.js +7 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/scripts.d.ts +2 -0
- package/dist/generators/scripts.js +10 -0
- package/dist/generators/scripts.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +8 -0
- package/dist/prompts.js +53 -0
- package/dist/prompts.js.map +1 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +79 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/utils/exec.d.ts +2 -0
- package/dist/utils/exec.js +14 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/template.d.ts +10 -0
- package/dist/utils/template.js +20 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validate.d.ts +4 -0
- package/dist/utils/validate.js +27 -0
- package/dist/utils/validate.js.map +1 -0
- package/package.json +50 -0
- package/templates/backend/app.module.ts.ejs +61 -0
- package/templates/backend/bff.controller.ts.ejs +60 -0
- package/templates/backend/bff.module.ts.ejs +10 -0
- package/templates/backend/bff.service.ts.ejs +6 -0
- package/templates/backend/create-schema.migration.ts.ejs +15 -0
- package/templates/backend/data-source.ts.ejs +13 -0
- package/templates/backend/env.example.ejs +30 -0
- package/templates/backend/main.ts.ejs +43 -0
- package/templates/backend/module.ts.ejs +14 -0
- package/templates/backend/nest-cli.json.ejs +8 -0
- package/templates/backend/package.json.ejs +23 -0
- package/templates/backend/tsconfig.build.json.ejs +4 -0
- package/templates/backend/tsconfig.json.ejs +24 -0
- package/templates/claude/CLAUDE.md.ejs +86 -0
- package/templates/claude/hooks/auto-format.sh +22 -0
- package/templates/claude/hooks/check-secrets.sh +49 -0
- package/templates/claude/hooks/guard-destructive.sh +42 -0
- package/templates/claude/hooks/on-compaction.sh +29 -0
- package/templates/claude/mcp.json +10 -0
- package/templates/claude/rules/api-patterns.md +86 -0
- package/templates/claude/rules/auth-patterns.md +109 -0
- package/templates/claude/rules/backend-patterns.md +421 -0
- package/templates/claude/rules/database-patterns.md +96 -0
- package/templates/claude/rules/docker-patterns.md +86 -0
- package/templates/claude/rules/frontend-patterns.md +262 -0
- package/templates/claude/rules/observability-backend.md +132 -0
- package/templates/claude/rules/observability-frontend.md +49 -0
- package/templates/claude/rules/playwright-mcp.md +80 -0
- package/templates/claude/rules/resilience-ops.md +103 -0
- package/templates/claude/rules/testing-e2e-ui.md +190 -0
- package/templates/claude/rules/testing-patterns.md +94 -0
- package/templates/claude/rules/workflow-backend.md +64 -0
- package/templates/claude/rules/workflow-frontend.md +60 -0
- package/templates/claude/settings.json +68 -0
- package/templates/claude/skills/add-api-endpoint/SKILL.md +59 -0
- package/templates/claude/skills/add-auth-endpoint/SKILL.md +68 -0
- package/templates/claude/skills/add-entity/SKILL.md +56 -0
- package/templates/claude/skills/add-event/SKILL.md +127 -0
- package/templates/claude/skills/add-feature/SKILL.md +20 -0
- package/templates/claude/skills/add-frontend-page/SKILL.md +75 -0
- package/templates/claude/skills/add-handler/SKILL.md +105 -0
- package/templates/claude/skills/add-integration/SKILL.md +176 -0
- package/templates/claude/skills/add-migration/SKILL.md +20 -0
- package/templates/claude/skills/add-module/SKILL.md +89 -0
- package/templates/claude/skills/add-realtime/SKILL.md +119 -0
- package/templates/claude/skills/audit-rules/SKILL.md +120 -0
- package/templates/claude/skills/debugging/SKILL.md +105 -0
- package/templates/claude/skills/docker-dev/SKILL.md +86 -0
- package/templates/claude/skills/e2e-audit/SKILL.md +85 -0
- package/templates/claude/skills/e2e-full/SKILL.md +132 -0
- package/templates/claude/skills/e2e-scan/SKILL.md +171 -0
- package/templates/claude/skills/e2e-verify/SKILL.md +145 -0
- package/templates/claude/skills/fix-bug/SKILL.md +33 -0
- package/templates/claude/skills/implement-spec/SKILL.md +98 -0
- package/templates/claude/skills/review-code/SKILL.md +109 -0
- package/templates/claude/skills/review-spec/SKILL.md +216 -0
- package/templates/claude/skills/run-tests/SKILL.md +37 -0
- package/templates/claude/skills/specify/SKILL.md +87 -0
- package/templates/claude/skills/write-backend-tests/SKILL.md +182 -0
- package/templates/claude/skills/write-ui-tests/SKILL.md +118 -0
- package/templates/docker/Dockerfile.client.ejs +14 -0
- package/templates/docker/Dockerfile.ejs +28 -0
- package/templates/docker/docker-compose.test.yml.ejs +54 -0
- package/templates/docker/docker-compose.yml.ejs +76 -0
- package/templates/docker/nginx.conf.ejs +21 -0
- package/templates/frontend/App.tsx.ejs +64 -0
- package/templates/frontend/DashboardPage.tsx.ejs +37 -0
- package/templates/frontend/LoginPage.tsx.ejs +20 -0
- package/templates/frontend/NotFoundPage.tsx.ejs +15 -0
- package/templates/frontend/api-client.ts.ejs +15 -0
- package/templates/frontend/index.css.ejs +57 -0
- package/templates/frontend/index.html.ejs +13 -0
- package/templates/frontend/main.tsx.ejs +10 -0
- package/templates/frontend/package.json.ejs +16 -0
- package/templates/frontend/playwright.config.ts.ejs +20 -0
- package/templates/frontend/postcss.config.js.ejs +3 -0
- package/templates/frontend/smoke.spec.ts.ejs +37 -0
- package/templates/frontend/tailwind.config.ts.ejs +56 -0
- package/templates/frontend/tsconfig.json.ejs +25 -0
- package/templates/frontend/tsconfig.node.json.ejs +15 -0
- package/templates/frontend/utils.ts.ejs +6 -0
- package/templates/frontend/vite-env.d.ts.ejs +1 -0
- package/templates/frontend/vite.config.ts.ejs +20 -0
- package/templates/root/gitignore.ejs +9 -0
- package/templates/root/prettierrc.ejs +7 -0
- package/templates/scripts/init-db.sh.ejs +8 -0
- package/templates/scripts/save-auth-state.ts.ejs +24 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Add External Integration
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
When the project integrates with an external system (third-party API, AI provider, partner service).
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. **Create adapter service** in `src/<module>/services/<SystemName>Adapter.ts`:
|
|
8
|
+
```typescript
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class AcmeAdapter {
|
|
11
|
+
private readonly logger = new Logger(AcmeAdapter.name);
|
|
12
|
+
private readonly baseUrl: string;
|
|
13
|
+
private readonly apiKey: string;
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
this.baseUrl = process.env.ACME_API_URL!;
|
|
17
|
+
this.apiKey = process.env.ACME_API_KEY!;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async createResource(payload: CreatePayload): Promise<AcmeResponse> {
|
|
21
|
+
return this.request('POST', '/api/resources', payload);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
25
|
+
const url = `${this.baseUrl}${path}`;
|
|
26
|
+
const response = await fetch(url, {
|
|
27
|
+
method,
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
31
|
+
'X-Idempotency-Key': crypto.randomUUID(),
|
|
32
|
+
},
|
|
33
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (response.status === 429) {
|
|
37
|
+
const retryAfter = parseInt(response.headers.get('Retry-After') || '5');
|
|
38
|
+
await new Promise(r => setTimeout(r, retryAfter * 1000));
|
|
39
|
+
return this.request(method, path, body);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorBody = await response.text();
|
|
44
|
+
this.logger.error({ method, path, status: response.status }, 'External API failed');
|
|
45
|
+
throw new Error(`${method} ${path}: ${response.status}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return response.json() as T;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
2. **Define types** for request/response payloads
|
|
53
|
+
3. **Create command with @Validate** — every integration command needs validation:
|
|
54
|
+
```typescript
|
|
55
|
+
@Validate(SyncResourceValidator)
|
|
56
|
+
@DistributedLock('sync-resource:{resourceId}')
|
|
57
|
+
export class SyncResourceCommand {
|
|
58
|
+
constructor(
|
|
59
|
+
public readonly resourceId: string,
|
|
60
|
+
public readonly data: Record<string, unknown>,
|
|
61
|
+
) {}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
4. **Create .validator.ts** — co-located Zod schema
|
|
65
|
+
5. **Use in command handler** — inject adapter, use `getTransactionalRepo()`:
|
|
66
|
+
```typescript
|
|
67
|
+
@CommandHandler(SyncResourceCommand)
|
|
68
|
+
export class SyncResourceHandler implements ICommandHandler<SyncResourceCommand> {
|
|
69
|
+
constructor(
|
|
70
|
+
private readonly acme: AcmeAdapter,
|
|
71
|
+
@InjectRepository(Resource) private readonly resourceRepo: Repository<Resource>,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
async execute(command: SyncResourceCommand): Promise<Result<ResourceDto>> {
|
|
75
|
+
const resourceRepo = getTransactionalRepo(this.resourceRepo);
|
|
76
|
+
|
|
77
|
+
const externalResult = await this.acme.createResource({
|
|
78
|
+
reference: command.resourceId,
|
|
79
|
+
...command.data,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const resource = await resourceRepo.save(
|
|
83
|
+
resourceRepo.create({
|
|
84
|
+
...command,
|
|
85
|
+
externalId: externalResult.id,
|
|
86
|
+
syncStatus: 'synced',
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return Result.success(toDto(resource));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
6. **Add webhook controller** (if the external system sends callbacks):
|
|
95
|
+
```typescript
|
|
96
|
+
@Public()
|
|
97
|
+
@Controller('webhooks/<system>')
|
|
98
|
+
export class AcmeWebhookController {
|
|
99
|
+
constructor(private readonly commandBus: CommandBus) {}
|
|
100
|
+
|
|
101
|
+
@Post()
|
|
102
|
+
async handleWebhook(
|
|
103
|
+
@Req() req: RawBodyRequest<Request>,
|
|
104
|
+
@Headers('x-signature') signature: string,
|
|
105
|
+
) {
|
|
106
|
+
const expectedSig = crypto
|
|
107
|
+
.createHmac('sha256', process.env.ACME_WEBHOOK_SECRET!)
|
|
108
|
+
.update(req.rawBody!)
|
|
109
|
+
.digest('hex');
|
|
110
|
+
|
|
111
|
+
if (signature !== expectedSig) {
|
|
112
|
+
throw new UnauthorizedException('Invalid webhook signature');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const event = JSON.parse(req.rawBody!.toString());
|
|
116
|
+
return this.commandBus.execute(new HandleWebhookEventCommand(event));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
7. **Add environment variables** to `.env` and `docker-compose.yml`
|
|
121
|
+
8. **Add circuit breaker** (MANDATORY for all external integrations):
|
|
122
|
+
```typescript
|
|
123
|
+
import { createCircuitBreaker } from '@quanticjs/core';
|
|
124
|
+
|
|
125
|
+
private readonly breaker = createCircuitBreaker({
|
|
126
|
+
maxRetries: 2, // 3 total attempts, exponential backoff
|
|
127
|
+
consecutiveFailures: 5, // open circuit after 5 consecutive failures
|
|
128
|
+
halfOpenAfterMs: 30_000, // test one request after 30s
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
async createResource(payload: CreatePayload): Promise<AcmeResponse> {
|
|
132
|
+
return this.breaker.execute(() => this.request('POST', '/api/resources', payload));
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
- **States:** Closed (normal) → Open (fast-fail) → Half-open (test one) → Closed on success
|
|
136
|
+
- 4xx responses are **never retried** and do not count toward circuit-breaker failures
|
|
137
|
+
- Each integration gets its **own** circuit breaker instance — never share across integrations
|
|
138
|
+
9. **Add tests** — run `/write-backend-tests` for handler, validator, and webhook controller
|
|
139
|
+
|
|
140
|
+
## Integration Spec Template
|
|
141
|
+
Create `docs/integrations/<system>.md` if it doesn't exist:
|
|
142
|
+
```markdown
|
|
143
|
+
# Integration: <System Name>
|
|
144
|
+
|
|
145
|
+
## Connection
|
|
146
|
+
- Base URL: env `<SYSTEM>_API_URL`
|
|
147
|
+
- Auth: Bearer token (env `<SYSTEM>_API_KEY`)
|
|
148
|
+
- Rate limit: N req/min
|
|
149
|
+
|
|
150
|
+
## Endpoints
|
|
151
|
+
| Method | Path | Purpose | Request | Response |
|
|
152
|
+
|--------|------|---------|---------|----------|
|
|
153
|
+
| POST | /api/resource | Create | `{ field: value }` | `{ id, status }` |
|
|
154
|
+
|
|
155
|
+
## Webhooks (inbound)
|
|
156
|
+
- URL: /webhooks/<system>
|
|
157
|
+
- Signature: HMAC-SHA256 in X-Signature header
|
|
158
|
+
- Secret: env `<SYSTEM>_WEBHOOK_SECRET`
|
|
159
|
+
- Events: resource.created, resource.updated
|
|
160
|
+
|
|
161
|
+
## Error Handling
|
|
162
|
+
- 429 → retry after Retry-After header
|
|
163
|
+
- 5xx → circuit breaker (5 consecutive failures → open 30s, 2 retries with exponential backoff)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Rules
|
|
167
|
+
- NEVER hardcode API keys — always `process.env.*`
|
|
168
|
+
- NEVER trust inbound webhooks without signature verification
|
|
169
|
+
- Adapter is the ONLY class that knows the external API — handlers call adapter methods
|
|
170
|
+
- Store external IDs on your entities (`externalId` column) for reconciliation
|
|
171
|
+
- Use idempotency keys on all mutating external calls
|
|
172
|
+
- Log all external calls at debug level (structured JSON) for troubleshooting
|
|
173
|
+
- Circuit breaker on ALL external HTTP calls — prevent cascade failures
|
|
174
|
+
- Each integration gets its own circuit breaker — never share across integrations
|
|
175
|
+
- 4xx responses are never retried and don't count toward circuit-breaker failures
|
|
176
|
+
- All repo access via `getTransactionalRepo()` — never use `this.repo` directly in handlers
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Add Migration
|
|
2
|
+
|
|
3
|
+
## Steps
|
|
4
|
+
1. Modify TypeORM entity classes in `src/<module>/entities/`
|
|
5
|
+
2. Generate migration: `npx typeorm migration:generate src/migrations/<MigrationName>`
|
|
6
|
+
3. Review generated SQL — ensure it handles existing data
|
|
7
|
+
4. Run migration: `npx typeorm migration:run`
|
|
8
|
+
5. Verify with: `npx typeorm migration:revert` (test down migration)
|
|
9
|
+
|
|
10
|
+
## Migration Naming Convention
|
|
11
|
+
Use descriptive names: `AddItemTable`, `AddStatusColumnToOrder`, `CreateIndexOnEmail`.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
- NEVER modify a migration that has been run — create a new one
|
|
15
|
+
- Column names are `"camelCase"` (quoted in SQL) — NOT `snake_case`
|
|
16
|
+
- Use regular `CREATE INDEX` in transactional migrations — NEVER `CREATE INDEX CONCURRENTLY`
|
|
17
|
+
- For large tables needing non-blocking index creation, use a separate migration with `transaction = false`
|
|
18
|
+
- NEVER write manual `CREATE TABLE` for tables with TypeORM entities — use `migration:generate`
|
|
19
|
+
- Test migration both up and down
|
|
20
|
+
- Each module's tables live in its own schema (e.g., `identity.*`, `project.*`)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Add Module (Bounded Context)
|
|
2
|
+
|
|
3
|
+
## Steps
|
|
4
|
+
1. **Create module directory** — `src/<module-name>/`
|
|
5
|
+
2. **Create subdirectories:**
|
|
6
|
+
```
|
|
7
|
+
src/<module-name>/
|
|
8
|
+
├── commands/ # Command classes + handlers + validators
|
|
9
|
+
├── queries/ # Query classes + handlers
|
|
10
|
+
├── entities/ # TypeORM entities
|
|
11
|
+
├── dtos/ # Request/response DTOs
|
|
12
|
+
├── controllers/ # THIN controllers
|
|
13
|
+
├── services/ # Adapters (external integrations)
|
|
14
|
+
└── <module-name>.module.ts
|
|
15
|
+
```
|
|
16
|
+
3. **Create module file:**
|
|
17
|
+
```typescript
|
|
18
|
+
import { Module } from '@nestjs/common';
|
|
19
|
+
import { CqrsModule } from '@nestjs/cqrs';
|
|
20
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
21
|
+
|
|
22
|
+
@Module({
|
|
23
|
+
imports: [
|
|
24
|
+
CqrsModule,
|
|
25
|
+
TypeOrmModule.forFeature([/* entities */]),
|
|
26
|
+
],
|
|
27
|
+
controllers: [/* controllers */],
|
|
28
|
+
providers: [
|
|
29
|
+
...CommandHandlers,
|
|
30
|
+
...QueryHandlers,
|
|
31
|
+
...Validators,
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
export class XxxModule {}
|
|
35
|
+
```
|
|
36
|
+
4. **Register in AppModule** — add to imports array in `src/app.module.ts`
|
|
37
|
+
5. **Create PostgreSQL schema** — migration: `CREATE SCHEMA IF NOT EXISTS <module_name>;`
|
|
38
|
+
6. **Add initial entity** — run `/add-entity`
|
|
39
|
+
|
|
40
|
+
## App Module Checklist
|
|
41
|
+
|
|
42
|
+
When registering a new module in `app.module.ts`, ensure these `.forRoot()` modules are present (import once, never in feature modules):
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
@Module({
|
|
46
|
+
imports: [
|
|
47
|
+
QuanticModule.forRoot({ redis: { url: process.env.REDIS_URL } }),
|
|
48
|
+
QuanticHealthModule.forRoot(), // health probes — auto-detects DB + Redis
|
|
49
|
+
ScheduleModule.forRoot(),
|
|
50
|
+
LoggerModule.forRoot(pinoConfig),
|
|
51
|
+
// ... feature modules
|
|
52
|
+
XxxModule, // your new module
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
export class AppModule {}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Graceful Shutdown
|
|
59
|
+
|
|
60
|
+
If the service has custom resources (queue workers, websockets, outbox publisher), extend `GracefulShutdownService`:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
@Injectable()
|
|
64
|
+
export class AppShutdownService extends GracefulShutdownService {
|
|
65
|
+
constructor(
|
|
66
|
+
@Optional() dataSource: DataSource,
|
|
67
|
+
@Optional() @Inject('REDIS_CLIENT') redis: Redis,
|
|
68
|
+
private readonly queueWorker: Worker,
|
|
69
|
+
) {
|
|
70
|
+
super(dataSource, redis);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected async drainWork(): Promise<void> {
|
|
74
|
+
await this.queueWorker.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Register `AppShutdownService` in `app.module.ts` providers. Base class handles DB and Redis cleanup automatically.
|
|
80
|
+
|
|
81
|
+
## Rules
|
|
82
|
+
- Each module owns its own PostgreSQL schema — no cross-schema queries
|
|
83
|
+
- Modules communicate through `CommandBus`/`QueryBus` — never import another module's services or repositories
|
|
84
|
+
- Only commands, queries, and DTOs are exported from a module
|
|
85
|
+
- Import from `@quanticjs/core` — never import from sibling modules directly
|
|
86
|
+
- Inter-module async communication uses Redis Streams
|
|
87
|
+
- `.forRoot()` modules (ScheduleModule, LoggerModule, QuanticHealthModule, etc.) go ONLY in `app.module.ts`
|
|
88
|
+
- `QuanticHealthModule.forRoot()` is MANDATORY — every service needs health probes
|
|
89
|
+
- Services with queue workers or websockets MUST extend `GracefulShutdownService` and close resources in `drainWork()`
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Add Real-time Feature (Socket.IO)
|
|
2
|
+
|
|
3
|
+
## When to Use
|
|
4
|
+
When you need live updates pushed to the browser — notifications, collaborative editing, live status, real-time feeds.
|
|
5
|
+
|
|
6
|
+
## Steps
|
|
7
|
+
1. **Create WebSocket gateway** in `src/<module>/gateways/<Feature>Gateway.ts`:
|
|
8
|
+
```typescript
|
|
9
|
+
import { WebSocketGateway, WebSocketServer, SubscribeMessage, ConnectedSocket, MessageBody } from '@nestjs/websockets';
|
|
10
|
+
import { Server, Socket } from 'socket.io';
|
|
11
|
+
import { createAdapter } from '@socket.io/redis-adapter';
|
|
12
|
+
import { Injectable, Inject, Optional } from '@nestjs/common';
|
|
13
|
+
import { REDIS_CLIENT } from '@quanticjs/core';
|
|
14
|
+
import type { Redis } from 'ioredis';
|
|
15
|
+
|
|
16
|
+
@WebSocketGateway({
|
|
17
|
+
namespace: '/<feature>',
|
|
18
|
+
cors: {
|
|
19
|
+
origin: (process.env.CORS_ORIGINS || 'http://localhost:5173').split(','),
|
|
20
|
+
credentials: true,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
export class FeatureGateway {
|
|
24
|
+
@WebSocketServer()
|
|
25
|
+
server!: Server;
|
|
26
|
+
|
|
27
|
+
constructor(@Optional() @Inject(REDIS_CLIENT) private readonly redis?: Redis) {}
|
|
28
|
+
|
|
29
|
+
afterInit(server: Server): void {
|
|
30
|
+
if (this.redis) {
|
|
31
|
+
const pubClient = this.redis.duplicate();
|
|
32
|
+
const subClient = this.redis.duplicate();
|
|
33
|
+
server.adapter(createAdapter(pubClient, subClient));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@SubscribeMessage('subscribe')
|
|
38
|
+
handleSubscribe(
|
|
39
|
+
@ConnectedSocket() client: Socket,
|
|
40
|
+
@MessageBody() data: { resourceId: string },
|
|
41
|
+
): void {
|
|
42
|
+
client.join(`<feature>:${data.resourceId}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@SubscribeMessage('unsubscribe')
|
|
46
|
+
handleUnsubscribe(
|
|
47
|
+
@ConnectedSocket() client: Socket,
|
|
48
|
+
@MessageBody() data: { resourceId: string },
|
|
49
|
+
): void {
|
|
50
|
+
client.leave(`<feature>:${data.resourceId}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
emitUpdate(resourceId: string, payload: unknown): void {
|
|
54
|
+
this.server.to(`<feature>:${resourceId}`).emit('update', payload);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. **Register in module** — add gateway to `providers` array
|
|
60
|
+
|
|
61
|
+
3. **Emit from handler** — inject gateway, call `emitUpdate()` after mutation:
|
|
62
|
+
```typescript
|
|
63
|
+
@CommandHandler(UpdateStatusCommand)
|
|
64
|
+
export class UpdateStatusHandler implements ICommandHandler<UpdateStatusCommand> {
|
|
65
|
+
constructor(
|
|
66
|
+
private readonly gateway: FeatureGateway,
|
|
67
|
+
@InjectRepository(Resource) private readonly repo: Repository<Resource>,
|
|
68
|
+
) {}
|
|
69
|
+
|
|
70
|
+
async execute(command: UpdateStatusCommand): Promise<Result<void>> {
|
|
71
|
+
const repo = getTransactionalRepo(this.repo);
|
|
72
|
+
await repo.update(command.id, { status: command.status });
|
|
73
|
+
this.gateway.emitUpdate(command.id, { status: command.status });
|
|
74
|
+
return Result.success();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
4. **Frontend hook** using `@quanticjs/react-query` for cache invalidation:
|
|
80
|
+
```typescript
|
|
81
|
+
import { useEffect } from 'react';
|
|
82
|
+
import { io } from 'socket.io-client';
|
|
83
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
84
|
+
|
|
85
|
+
export function useRealtimeUpdates(resourceId: string, queryKey: string[]) {
|
|
86
|
+
const queryClient = useQueryClient();
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
const socket = io('/<feature>', { withCredentials: true });
|
|
90
|
+
socket.emit('subscribe', { resourceId });
|
|
91
|
+
|
|
92
|
+
socket.on('update', () => {
|
|
93
|
+
queryClient.invalidateQueries({ queryKey });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return () => { socket.disconnect(); };
|
|
97
|
+
}, [resourceId, queryKey, queryClient]);
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Patterns
|
|
102
|
+
- **Room per resource** — `<feature>:<resourceId>`
|
|
103
|
+
- **Redis adapter** — required for multi-instance deployments (horizontal scaling)
|
|
104
|
+
- **Graceful fallback** — if Redis unavailable, in-memory adapter (single instance only)
|
|
105
|
+
- **Invalidate, don't push data** — emit event type + ID, let client refetch via React Query
|
|
106
|
+
|
|
107
|
+
## Frontend Connection
|
|
108
|
+
```typescript
|
|
109
|
+
// Socket.IO uses the BFF cookie automatically
|
|
110
|
+
const socket = io('/<feature>', { withCredentials: true });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Rules
|
|
114
|
+
- ALWAYS use Redis adapter in production (horizontal scaling)
|
|
115
|
+
- ALWAYS use namespace per feature (avoids event collision)
|
|
116
|
+
- ALWAYS disconnect on component unmount (memory leak prevention)
|
|
117
|
+
- NEVER send large payloads via WebSocket — send event + ID, client refetches
|
|
118
|
+
- NEVER use Socket.IO for data that should go through the command bus
|
|
119
|
+
- Frontend connects with `withCredentials: true` — BFF cookie sent on handshake
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Audit Rules Against ADRs
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
`/audit-rules` — compare `.claude/rules/` against the engineering ADR repository and report gaps.
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Rules in `.claude/rules/` are derived from architectural decision records (ADRs) at:
|
|
9
|
+
`/Users/turkel/Desktop/personal/projecets/engineering-adrs`
|
|
10
|
+
|
|
11
|
+
Over time, ADRs get updated or new ones are added without updating rules, and vice versa. This skill detects drift between the two.
|
|
12
|
+
|
|
13
|
+
## ADR-to-Rule Mapping
|
|
14
|
+
|
|
15
|
+
| ADR (Backend) | Rule File |
|
|
16
|
+
|---|---|
|
|
17
|
+
| 001-modular-monolith.md | backend-patterns.md |
|
|
18
|
+
| 002-cqrs-pipeline-behaviors.md | backend-patterns.md |
|
|
19
|
+
| 003-result-error-handling.md | backend-patterns.md, api-patterns.md |
|
|
20
|
+
| 004-bff-authentication.md | auth-patterns.md |
|
|
21
|
+
| 005-multi-tenancy-rls.md | database-patterns.md |
|
|
22
|
+
| 006-typeorm-code-first-migrations.md | database-patterns.md |
|
|
23
|
+
| 007-two-layer-validation.md | backend-patterns.md |
|
|
24
|
+
| 008-redis-streams-events.md | backend-patterns.md |
|
|
25
|
+
| 009-kubernetes-helm-argocd.md | docker-patterns.md |
|
|
26
|
+
| 010-api-documentation.md | api-patterns.md |
|
|
27
|
+
| 011-observability.md | observability-patterns.md |
|
|
28
|
+
| 012-testing-strategy.md | testing-patterns.md |
|
|
29
|
+
| 013-local-development-workflow.md | docker-patterns.md |
|
|
30
|
+
|
|
31
|
+
| ADR (Frontend) | Rule File |
|
|
32
|
+
|---|---|
|
|
33
|
+
| 001-monorepo-tooling.md | frontend-patterns.md |
|
|
34
|
+
| 002-design-tokens-and-component-libraries.md | frontend-patterns.md |
|
|
35
|
+
| 003-state-management.md | frontend-patterns.md |
|
|
36
|
+
| 004-authentication-and-authorization.md | auth-patterns.md |
|
|
37
|
+
| 005-typescript-strict-mode.md | frontend-patterns.md |
|
|
38
|
+
| 006-mobile-secure-storage.md | auth-patterns.md |
|
|
39
|
+
| 007-observability.md | observability-patterns.md |
|
|
40
|
+
| 008-testing-strategy.md | testing-patterns.md |
|
|
41
|
+
| 009-local-development-workflow.md | docker-patterns.md |
|
|
42
|
+
|
|
43
|
+
## Process
|
|
44
|
+
|
|
45
|
+
### Step 1: Pull latest ADRs
|
|
46
|
+
|
|
47
|
+
Run `git -C /Users/turkel/Desktop/personal/projecets/engineering-adrs pull` to ensure you're comparing against the latest ADRs.
|
|
48
|
+
|
|
49
|
+
### Step 2: Read all files
|
|
50
|
+
|
|
51
|
+
Read every ADR file listed in the mapping table above and every rule file in `.claude/rules/`. Do this in parallel using multiple Read calls to be efficient.
|
|
52
|
+
|
|
53
|
+
### Step 3: Compare each ADR-rule pair
|
|
54
|
+
|
|
55
|
+
For each mapping in the table, compare the ADR's decisions against the corresponding rule file. Check for:
|
|
56
|
+
|
|
57
|
+
**Missing rules** — decisions, bans ("NEVER"), or patterns defined in the ADR but absent from the rule file:
|
|
58
|
+
- Code patterns or conventions mandated in the ADR
|
|
59
|
+
- Explicit bans ("What is explicitly banned" sections)
|
|
60
|
+
- Technology choices or library mandates
|
|
61
|
+
- Configuration requirements
|
|
62
|
+
- Mapping tables (e.g., Result-to-HTTP mappings)
|
|
63
|
+
|
|
64
|
+
**Stale rules** — rules that contradict or no longer match the current ADR:
|
|
65
|
+
- Values that changed (e.g., a timeout was 30s in the rule but the ADR now says 60s)
|
|
66
|
+
- Patterns that were replaced by newer decisions
|
|
67
|
+
- Libraries or tools that were swapped
|
|
68
|
+
|
|
69
|
+
**Unmapped ADRs** — any ADR file in the repo that does not appear in the mapping table above. These may need a new rule file or an addition to an existing one.
|
|
70
|
+
|
|
71
|
+
### Step 4: Report
|
|
72
|
+
|
|
73
|
+
Output a structured report with three sections:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
## Audit: Rules vs ADRs
|
|
77
|
+
|
|
78
|
+
### Gaps Found
|
|
79
|
+
For each gap:
|
|
80
|
+
- **ADR:** <filename> — <section or quote>
|
|
81
|
+
- **Rule:** <filename> — what's missing or wrong
|
|
82
|
+
- **Action:** what should be added/changed
|
|
83
|
+
|
|
84
|
+
### Stale Rules
|
|
85
|
+
For each stale rule:
|
|
86
|
+
- **Rule:** <filename> — <the stale content>
|
|
87
|
+
- **ADR:** <filename> — <what it should say now>
|
|
88
|
+
- **Action:** what should change
|
|
89
|
+
|
|
90
|
+
### New ADRs Without Rules
|
|
91
|
+
For each unmapped ADR:
|
|
92
|
+
- **ADR:** <filename> — <summary of decisions>
|
|
93
|
+
- **Suggested rule file:** <which rule file to add it to>
|
|
94
|
+
|
|
95
|
+
### Summary
|
|
96
|
+
- X gaps found
|
|
97
|
+
- X stale rules
|
|
98
|
+
- X unmapped ADRs
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Step 5: Offer to fix
|
|
102
|
+
|
|
103
|
+
After presenting the report, ask the user:
|
|
104
|
+
"Want me to fix these gaps?" using AskUserQuestion with options:
|
|
105
|
+
- **Fix all** — update all rule files to match ADRs
|
|
106
|
+
- **Fix one by one** — go through each gap and ask before fixing
|
|
107
|
+
- **Report only** — no changes, just the audit
|
|
108
|
+
|
|
109
|
+
If the user chooses to fix, update only the rule files (`.claude/rules/`), never the ADRs. ADRs are the source of truth.
|
|
110
|
+
|
|
111
|
+
Also update the mapping table in this skill file if new ADRs were found that need mapping.
|
|
112
|
+
|
|
113
|
+
## Rules
|
|
114
|
+
|
|
115
|
+
- ADRs are the source of truth. Rules derive from ADRs, not the other way around.
|
|
116
|
+
- NEVER modify ADR files — only modify rule files.
|
|
117
|
+
- When reporting gaps, quote the specific ADR section so the user can verify.
|
|
118
|
+
- Ignore stylistic differences (wording, formatting) — only flag semantic gaps.
|
|
119
|
+
- If an ADR section says "planned" or "future", do NOT flag it as missing from rules. Rules only cover current decisions.
|
|
120
|
+
- Check ADR status field — only compare "Accepted" ADRs. Skip "Proposed", "Deprecated", or "Superseded" ADRs.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Debugging
|
|
2
|
+
|
|
3
|
+
## Backend (NestJS)
|
|
4
|
+
|
|
5
|
+
### Logs
|
|
6
|
+
```bash
|
|
7
|
+
docker compose logs backend -f --tail=100 # Live structured logs (Pino)
|
|
8
|
+
docker compose logs backend 2>&1 | grep -i error # Filter errors only
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Health Check
|
|
12
|
+
```bash
|
|
13
|
+
curl http://localhost:3000/health
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Database
|
|
17
|
+
```bash
|
|
18
|
+
docker compose exec postgres psql -U postgres -d autoflux
|
|
19
|
+
npx typeorm migration:show # List migrations and status
|
|
20
|
+
npx typeorm migration:run # Apply pending migrations
|
|
21
|
+
npx typeorm migration:revert # Rollback last migration
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Redis
|
|
25
|
+
```bash
|
|
26
|
+
docker compose exec redis redis-cli PING
|
|
27
|
+
docker compose exec redis redis-cli MONITOR # Watch all commands live
|
|
28
|
+
docker compose exec redis redis-cli KEYS '*' # List all keys
|
|
29
|
+
docker compose exec redis redis-cli XINFO GROUPS <stream> # Stream consumer info
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Running specific tests
|
|
33
|
+
```bash
|
|
34
|
+
npx jest --testPathPattern=CreateItem --verbose
|
|
35
|
+
npx jest --watch # interactive mode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Frontend (React)
|
|
39
|
+
|
|
40
|
+
### Dev Server
|
|
41
|
+
```bash
|
|
42
|
+
cd client && npm run dev # Check terminal for build errors
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Browser DevTools
|
|
46
|
+
1. **Console** — check for React errors, unhandled rejections
|
|
47
|
+
2. **Network** — verify API calls go to `/api/*` (not direct to :3000)
|
|
48
|
+
3. **Application > Cookies** — verify httpOnly session cookie exists after login
|
|
49
|
+
4. **React DevTools** — inspect component state and query cache
|
|
50
|
+
5. **TanStack Query DevTools** — inspect cache state, stale queries, refetch triggers
|
|
51
|
+
|
|
52
|
+
### Running specific tests
|
|
53
|
+
```bash
|
|
54
|
+
cd client && npx vitest run src/pages/ProjectsPage.test.tsx
|
|
55
|
+
cd client && npx playwright test projects.spec.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Common Issues
|
|
59
|
+
|
|
60
|
+
### Auth / 401 Errors
|
|
61
|
+
| Symptom | Cause | Fix |
|
|
62
|
+
|---------|-------|-----|
|
|
63
|
+
| 401 on every request | Session cookie missing/expired | Re-login via `/auth/login` |
|
|
64
|
+
| 401 after refresh | Keycloak session expired | Restart Keycloak: `docker compose restart keycloak` |
|
|
65
|
+
| Login redirect loop | Callback URL mismatch | Check Keycloak client config: Valid Redirect URIs |
|
|
66
|
+
| No cookie set | Vite proxy not configured | Verify `vite.config.ts` has `/auth` proxy |
|
|
67
|
+
|
|
68
|
+
### Database / Migrations
|
|
69
|
+
| Symptom | Cause | Fix |
|
|
70
|
+
|---------|-------|-----|
|
|
71
|
+
| `relation does not exist` | Migration not run | `npx typeorm migration:run` |
|
|
72
|
+
| `column does not exist` | Entity/migration mismatch | `npx typeorm migration:generate src/migrations/Fix` |
|
|
73
|
+
| Migration fails | Conflict with existing data | Check migration SQL, add IF NOT EXISTS |
|
|
74
|
+
| Duplicate key error | Missing unique constraint handling | Add `@DistributedLock` or check-before-insert |
|
|
75
|
+
|
|
76
|
+
### Docker / Services
|
|
77
|
+
| Symptom | Cause | Fix |
|
|
78
|
+
|---------|-------|-----|
|
|
79
|
+
| Connection refused :3000 | Backend not started | `docker compose up backend` |
|
|
80
|
+
| Connection refused :5432 | Trying localhost for DB | Use Docker hostname `postgres` in backend config |
|
|
81
|
+
| Container restarting | OOM or crash loop | `docker compose logs <svc> --tail=20` |
|
|
82
|
+
| Slow hot reload | Volume mount lag | Restart: `docker compose restart backend` |
|
|
83
|
+
|
|
84
|
+
### Redis / Streams
|
|
85
|
+
| Symptom | Cause | Fix |
|
|
86
|
+
|---------|-------|-----|
|
|
87
|
+
| Stream lag growing | Consumer crashed | Check consumer logs, restart service |
|
|
88
|
+
| DLQ filling up | Handler throwing repeatedly | Fix handler bug, then reprocess DLQ |
|
|
89
|
+
| Cache stale | Missing invalidation | Check mutation's cache invalidation logic |
|
|
90
|
+
|
|
91
|
+
### Frontend / API
|
|
92
|
+
| Symptom | Cause | Fix |
|
|
93
|
+
|---------|-------|-----|
|
|
94
|
+
| CORS error | Bypassing Vite proxy | Use relative URLs (`/api/...`), not `http://localhost:3000` |
|
|
95
|
+
| Stale data | Missing `invalidates` | Add `invalidates: [['queryKey']]` to `useApiMutation` |
|
|
96
|
+
| Form errors not showing | Not using `@quanticjs/react-forms` | Use `useForm` — auto-maps server errors |
|
|
97
|
+
| White screen | Unhandled error | Check browser console, add ErrorBoundary |
|
|
98
|
+
|
|
99
|
+
## Quick Diagnostic Script
|
|
100
|
+
```bash
|
|
101
|
+
echo "=== Services ===" && docker compose ps
|
|
102
|
+
echo "=== Backend Health ===" && curl -s http://localhost:3000/health | jq .
|
|
103
|
+
echo "=== Pending Migrations ===" && npx typeorm migration:show 2>&1 | grep -v "✓"
|
|
104
|
+
echo "=== Redis ===" && docker compose exec redis redis-cli PING
|
|
105
|
+
```
|