@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,59 @@
|
|
|
1
|
+
# Add API Endpoint
|
|
2
|
+
|
|
3
|
+
Wire a CQRS handler to an HTTP endpoint with DTO validation and a thin controller.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
```
|
|
7
|
+
/add-api-endpoint POST /project/items
|
|
8
|
+
/add-api-endpoint GET /identity/users/:id
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
1. **Create handler** — run `/add-handler` for the command/query + validator + handler
|
|
13
|
+
2. **Create DTO** with class-validator decorators (controller-layer validation):
|
|
14
|
+
```typescript
|
|
15
|
+
import { IsString, IsNotEmpty, MaxLength } from 'class-validator';
|
|
16
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
17
|
+
|
|
18
|
+
export class CreateItemDto {
|
|
19
|
+
@ApiProperty({ description: 'Item name', minLength: 1, maxLength: 100 })
|
|
20
|
+
@IsString()
|
|
21
|
+
@IsNotEmpty()
|
|
22
|
+
@MaxLength(100)
|
|
23
|
+
name: string;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
3. **Create response DTO:**
|
|
27
|
+
```typescript
|
|
28
|
+
export class ItemResponseDto {
|
|
29
|
+
@ApiProperty()
|
|
30
|
+
id: string;
|
|
31
|
+
|
|
32
|
+
@ApiProperty()
|
|
33
|
+
name: string;
|
|
34
|
+
|
|
35
|
+
@ApiProperty()
|
|
36
|
+
createdAt: Date;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
4. **Add controller method** — THIN pattern:
|
|
40
|
+
```typescript
|
|
41
|
+
@Post()
|
|
42
|
+
@ApiOperation({ summary: 'Create a new item' })
|
|
43
|
+
@ApiResponse({ status: 201, type: ItemResponseDto })
|
|
44
|
+
@ApiResponse({ status: 400, type: ErrorResponseDto })
|
|
45
|
+
async create(@Body() dto: CreateItemDto): Promise<ItemResponseDto> {
|
|
46
|
+
return this.commandBus.execute(new CreateItemCommand(dto.name, dto.description));
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
5. Register handler in module's `providers` array (if not done in step 1)
|
|
50
|
+
6. **Add backend tests** — run `/write-backend-tests` for handler, validator, and controller
|
|
51
|
+
7. `npm run build && npm run test`
|
|
52
|
+
|
|
53
|
+
## Rules
|
|
54
|
+
- Controller does NOTHING except parse request → commandBus/queryBus → return
|
|
55
|
+
- DTO uses class-validator for shape validation; business rules stay in the Zod validator (created by `/add-handler`)
|
|
56
|
+
- ALL repo access via `getTransactionalRepo()` — UnitOfWork is automatic
|
|
57
|
+
- Every endpoint annotated with `@ApiOperation`, `@ApiResponse`, `@ApiBody`, `@ApiTags`
|
|
58
|
+
- All API responses use typed response DTOs — never raw entity objects
|
|
59
|
+
- Error responses use RFC 9457 problem-details shape
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Add Authenticated Endpoint
|
|
2
|
+
|
|
3
|
+
## Auth Flow (BFF Pattern)
|
|
4
|
+
```
|
|
5
|
+
Browser → httpOnly cookie → NestJS BFF middleware → injects Bearer header → JwtAuthGuard → RolesGuard → Handler
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
The frontend sends **no** Authorization header. The BFF middleware extracts the JWT from the httpOnly session cookie and injects it as a Bearer token before the request reaches the controller.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
1. **Add controller endpoint** with appropriate decorators:
|
|
12
|
+
```typescript
|
|
13
|
+
@Controller('items')
|
|
14
|
+
export class ItemsController {
|
|
15
|
+
constructor(private readonly commandBus: CommandBus) {}
|
|
16
|
+
|
|
17
|
+
// Protected endpoint (default — JwtAuthGuard is global)
|
|
18
|
+
@Post()
|
|
19
|
+
async create(@Body() dto: CreateItemDto, @Req() req: any) {
|
|
20
|
+
return this.commandBus.execute(new CreateItemCommand(dto.name, req.user.keycloakId));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Admin-only endpoint
|
|
24
|
+
@Roles('admin')
|
|
25
|
+
@Get('admin/stats')
|
|
26
|
+
async stats() { ... }
|
|
27
|
+
|
|
28
|
+
// Public endpoint (no auth required)
|
|
29
|
+
@Public()
|
|
30
|
+
@Get('health')
|
|
31
|
+
async health() { ... }
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
2. **Extract user from request** — `req.user` contains:
|
|
35
|
+
```typescript
|
|
36
|
+
{
|
|
37
|
+
keycloakId: string; // JWT sub
|
|
38
|
+
email: string;
|
|
39
|
+
roles: string[]; // from realm_access.roles
|
|
40
|
+
username?: string; // from preferred_username
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
Note: `organizationId` is NOT in the JWT (multi-tenancy/RLS deferred per ADR-005).
|
|
44
|
+
|
|
45
|
+
## Decorators
|
|
46
|
+
| Decorator | Effect |
|
|
47
|
+
|-----------|--------|
|
|
48
|
+
| *(none)* | JwtAuthGuard is global — endpoint is protected by default |
|
|
49
|
+
| `@Public()` | Bypass JwtAuthGuard — endpoint is public |
|
|
50
|
+
| `@Roles('admin')` | Require specific realm role(s) |
|
|
51
|
+
| `@Roles('admin', 'super-admin')` | Require ANY of the listed roles |
|
|
52
|
+
|
|
53
|
+
## BFF Auth Endpoints (in `src/bff/`)
|
|
54
|
+
| Endpoint | Purpose |
|
|
55
|
+
|----------|---------|
|
|
56
|
+
| `GET /auth/login` | Redirect to Keycloak (params: `provider`, `returnTo`) |
|
|
57
|
+
| `GET /auth/callback` | OIDC code exchange → set httpOnly cookie → redirect to SPA |
|
|
58
|
+
| `POST /auth/refresh` | Refresh access token server-side |
|
|
59
|
+
| `POST /auth/logout` | Clear cookie + revoke Keycloak session |
|
|
60
|
+
| `GET /auth/me` | Return current user info from session |
|
|
61
|
+
|
|
62
|
+
## Rules
|
|
63
|
+
- NEVER bypass auth for endpoints that mutate data
|
|
64
|
+
- NEVER return raw tokens to the frontend — only user profile data via `/auth/me`
|
|
65
|
+
- NEVER store tokens in `localStorage`, `sessionStorage`, or non-httpOnly cookies
|
|
66
|
+
- Frontend sends requests with `credentials: 'include'` — cookies are sent automatically
|
|
67
|
+
- NEVER set `Authorization` headers from frontend code
|
|
68
|
+
- NEVER implement token refresh logic in the frontend — BFF middleware handles it
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Add Entity
|
|
2
|
+
|
|
3
|
+
## Steps
|
|
4
|
+
1. **Create entity file** in `src/<module>/entities/<EntityName>.entity.ts`
|
|
5
|
+
2. **Choose base class:**
|
|
6
|
+
- `BaseEntity` — standard base class. Provides `id` (UUID), `createdAt`, `updatedAt`.
|
|
7
|
+
- `TenantBaseEntity` — **only when multi-tenancy is enabled** (currently deferred per ADR-005). Adds `organizationId` column + RLS. Do NOT use until multi-tenancy is re-activated.
|
|
8
|
+
```typescript
|
|
9
|
+
import { BaseEntity } from '@quanticjs/core';
|
|
10
|
+
```
|
|
11
|
+
3. **Define columns:**
|
|
12
|
+
```typescript
|
|
13
|
+
import { BaseEntity } from '@quanticjs/core';
|
|
14
|
+
|
|
15
|
+
@Entity('items')
|
|
16
|
+
export class Item extends BaseEntity {
|
|
17
|
+
@Column({ type: 'varchar', length: 200 })
|
|
18
|
+
name!: string;
|
|
19
|
+
|
|
20
|
+
@Column({ type: 'text', nullable: true })
|
|
21
|
+
description!: string | null;
|
|
22
|
+
|
|
23
|
+
@Column({ type: 'jsonb', default: {} })
|
|
24
|
+
metadata!: Record<string, unknown>;
|
|
25
|
+
|
|
26
|
+
@Column({ type: 'boolean', default: true })
|
|
27
|
+
isActive!: boolean;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
4. **Register in module** — add to `TypeOrmModule.forFeature([...])` in the module
|
|
31
|
+
5. **Generate migration:** `npx typeorm migration:generate src/migrations/<Name>`
|
|
32
|
+
6. **Run migration:** `npx typeorm migration:run`
|
|
33
|
+
|
|
34
|
+
## Column Type Reference
|
|
35
|
+
| TypeScript | PostgreSQL | Notes |
|
|
36
|
+
|------------|------------|-------|
|
|
37
|
+
| `string` | `varchar(N)` | Always set length |
|
|
38
|
+
| `string` | `text` | Unlimited length |
|
|
39
|
+
| `number` | `int` | Integer |
|
|
40
|
+
| `number` | `decimal(10,2)` | Money/precision |
|
|
41
|
+
| `boolean` | `boolean` | |
|
|
42
|
+
| `Date` | `timestamptz` | Always use timestamptz |
|
|
43
|
+
| `Record<string,unknown>` | `jsonb` | Queryable JSON |
|
|
44
|
+
| `string[]` | `varchar[]` | PostgreSQL array |
|
|
45
|
+
| `enum` | `enum` | TypeORM enum type |
|
|
46
|
+
|
|
47
|
+
## Rules
|
|
48
|
+
- Use `BaseEntity` for all entities (multi-tenancy/RLS is deferred per ADR-005)
|
|
49
|
+
- Do NOT use `TenantBaseEntity` or add `organizationId` until multi-tenancy is re-activated
|
|
50
|
+
- `BaseEntity` provides: `id` (UUID v4), `createdAt`, `updatedAt` — never redefine these
|
|
51
|
+
- Use `!` (definite assignment) on all columns — TypeORM sets them
|
|
52
|
+
- Database columns are **camelCase** (TypeORM default naming strategy)
|
|
53
|
+
- NEVER modify a migration that has been run — create a new one
|
|
54
|
+
- Index columns used in WHERE clauses: `@Index(['columnName'])`
|
|
55
|
+
- Import base classes from `@quanticjs/core`
|
|
56
|
+
- Each module owns its own PostgreSQL schema
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Add Domain Event
|
|
2
|
+
|
|
3
|
+
## Event Flow
|
|
4
|
+
```
|
|
5
|
+
Handler → OutboxEvent (same DB transaction) → OutboxPublisherService (poll) → Redis Stream → Consumer
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Events use the **outbox pattern** — the event record is written to the database in the same transaction as the entity mutation, guaranteeing atomicity. A background publisher polls the outbox and pushes to Redis Streams.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
1. **Create domain event in the handler** after successful mutation:
|
|
12
|
+
```typescript
|
|
13
|
+
import { getTransactionalRepo, Result } from '@quanticjs/core';
|
|
14
|
+
import { OutboxEvent, DomainEvent } from '@quanticjs/core';
|
|
15
|
+
|
|
16
|
+
@CommandHandler(CreateItemCommand)
|
|
17
|
+
export class CreateItemHandler implements ICommandHandler<CreateItemCommand> {
|
|
18
|
+
constructor(
|
|
19
|
+
@InjectRepository(Item) private readonly itemRepo: Repository<Item>,
|
|
20
|
+
@InjectRepository(OutboxEvent) private readonly outboxRepo: Repository<OutboxEvent>,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async execute(command: CreateItemCommand): Promise<Result<ItemDto>> {
|
|
24
|
+
const itemRepo = getTransactionalRepo(this.itemRepo);
|
|
25
|
+
const outboxRepo = getTransactionalRepo(this.outboxRepo);
|
|
26
|
+
|
|
27
|
+
const item = itemRepo.create({ name: command.name });
|
|
28
|
+
await itemRepo.save(item);
|
|
29
|
+
|
|
30
|
+
// Domain event — same transaction as the entity write
|
|
31
|
+
const event = new DomainEvent(
|
|
32
|
+
'item.created', // eventType
|
|
33
|
+
item.id, // aggregateId
|
|
34
|
+
{ name: item.name }, // payload (minimal — IDs preferred)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await outboxRepo.save(outboxRepo.create({
|
|
38
|
+
eventType: event.eventType,
|
|
39
|
+
aggregateId: event.aggregateId,
|
|
40
|
+
streamKey: event.streamKey,
|
|
41
|
+
payload: event.payload,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return Result.success(toDto(item));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
2. **Stream key** is derived from eventType: `item.created` → `autoflux:events:items`
|
|
50
|
+
|
|
51
|
+
3. **OutboxPublisherService** (from `@quanticjs/core`) polls pending events:
|
|
52
|
+
- Reads pending OutboxEvents
|
|
53
|
+
- Publishes to Redis Stream
|
|
54
|
+
- Marks as Published or Failed (max 5 retries with exponential backoff → DLQ)
|
|
55
|
+
|
|
56
|
+
## Event Naming Convention
|
|
57
|
+
| Event Type | Stream Key | When |
|
|
58
|
+
|------------|------------|------|
|
|
59
|
+
| `item.created` | `autoflux:events:items` | After entity creation |
|
|
60
|
+
| `item.updated` | `autoflux:events:items` | After entity update |
|
|
61
|
+
| `item.deleted` | `autoflux:events:items` | After soft delete |
|
|
62
|
+
| `project.status.changed` | `autoflux:events:projects` | After status transition |
|
|
63
|
+
|
|
64
|
+
## Consuming Events
|
|
65
|
+
|
|
66
|
+
Extend `RedisStreamConsumer` from `@quanticjs/core`. The base class automatically creates a **dedicated Redis connection** for blocking `XREADGROUP BLOCK` calls, keeping the shared `REDIS_CLIENT` free for non-blocking ops.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { Injectable, Inject, Optional } from '@nestjs/common';
|
|
70
|
+
import { REDIS_CLIENT, RedisStreamConsumer } from '@quanticjs/core';
|
|
71
|
+
import type { Redis } from 'ioredis';
|
|
72
|
+
import { hostname } from 'os';
|
|
73
|
+
|
|
74
|
+
@Injectable()
|
|
75
|
+
export class ItemEventConsumer extends RedisStreamConsumer {
|
|
76
|
+
readonly streamKey = 'autoflux:events:items';
|
|
77
|
+
readonly consumerGroup = 'project-planning';
|
|
78
|
+
readonly consumerName = `planner-${hostname()}-${process.pid}`;
|
|
79
|
+
|
|
80
|
+
constructor(@Optional() @Inject(REDIS_CLIENT) redis: Redis | undefined) {
|
|
81
|
+
super(redis);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected shouldHandle(fields: Record<string, string>): boolean {
|
|
85
|
+
return fields.eventType === 'item.created';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async handleMessage(fields: Record<string, string>): Promise<void> {
|
|
89
|
+
const payload = JSON.parse(fields.payload || '{}');
|
|
90
|
+
// Handle idempotently — events may be delivered more than once
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Lifecycle Hooks
|
|
96
|
+
```typescript
|
|
97
|
+
// Setup in onModuleInit, polling in onApplicationBootstrap
|
|
98
|
+
async onModuleInit() {
|
|
99
|
+
await this.createConsumerGroup(); // XGROUP CREATE
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async onApplicationBootstrap() {
|
|
103
|
+
this.startPolling(); // non-blocking — fires and forgets
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Connection Layout
|
|
108
|
+
| Client | Count | Purpose |
|
|
109
|
+
|--------|-------|---------|
|
|
110
|
+
| `REDIS_CLIENT` (shared) | 1 | Cache, locks, XADD, XGROUP CREATE, XACK |
|
|
111
|
+
| Dedicated blocking client | 1 per consumer | `XREADGROUP BLOCK` only |
|
|
112
|
+
|
|
113
|
+
## Retry & Dead-Letter Policy
|
|
114
|
+
- Max retries: 5 with exponential backoff + jitter
|
|
115
|
+
- Backoff: `min(1s × 2^attempt + random(0,1000ms), 30s)`
|
|
116
|
+
- Dead-letter stream: `{stream}:dlq` with `MAXLEN ~ 100000`
|
|
117
|
+
- Failed events are NEVER silently dropped
|
|
118
|
+
|
|
119
|
+
## Rules
|
|
120
|
+
- ALWAYS use outbox pattern — never publish directly to Redis (data loss on crash)
|
|
121
|
+
- OutboxEvent is saved in the SAME transaction as the entity mutation
|
|
122
|
+
- Event payloads should be minimal — include IDs, not full entities
|
|
123
|
+
- Consumers must be idempotent — events may be delivered more than once
|
|
124
|
+
- NEVER share Redis connection for blocking XREADGROUP reads
|
|
125
|
+
- NEVER start polling in `onModuleInit()` — use `onApplicationBootstrap()`
|
|
126
|
+
- NEVER publish events before the transaction commits
|
|
127
|
+
- All repo access via `getTransactionalRepo()`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Add Feature
|
|
2
|
+
|
|
3
|
+
End-to-end feature implementation orchestrating all sub-skills.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
```
|
|
7
|
+
/add-feature Create project items with priority and status
|
|
8
|
+
/add-feature User profile editing with avatar upload
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
1. **Understand requirements** — read any specs, clarify with user if ambiguous
|
|
13
|
+
2. **Create command/query + validator + handler** — run `/add-handler`
|
|
14
|
+
3. **Create/update entity** — run `/add-entity`
|
|
15
|
+
4. **Create migration** — run `/add-migration`
|
|
16
|
+
5. **Add controller endpoint** — run `/add-api-endpoint` (handler already exists from step 2, so only DTO + controller)
|
|
17
|
+
6. **Add frontend** — run `/add-frontend-page` (skip if purely backend)
|
|
18
|
+
7. **Add backend tests** — run `/write-backend-tests`
|
|
19
|
+
8. **Add UI tests** — run `/write-ui-tests` (skip if no frontend)
|
|
20
|
+
9. **Verify** — `npm run build && npm test`
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Add Frontend Page
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
```
|
|
5
|
+
/add-frontend-page /projects
|
|
6
|
+
/add-frontend-page /settings/profile
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Steps
|
|
10
|
+
1. Create route in React Router config
|
|
11
|
+
2. Create page component in `client/src/pages/` — lazy-loaded with `React.lazy()` + `<Suspense>`
|
|
12
|
+
3. Create data hooks using `@quanticjs/react-query`:
|
|
13
|
+
- `useApiQuery` for data fetching (NOT raw `useQuery`)
|
|
14
|
+
- `useApiMutation` for write operations with `invalidates` option (NOT raw `useMutation`)
|
|
15
|
+
- `usePaginatedQuery` for paginated lists
|
|
16
|
+
4. Use shadcn/ui + `@quanticjs/react-ui` components (Spinner, Skeleton, EmptyState, ErrorBoundary, Dialog, ToastProvider)
|
|
17
|
+
5. Add loading skeleton, empty state, and error boundary
|
|
18
|
+
6. Forms use `useForm` from `@quanticjs/react-forms` + Zod (NOT raw React Hook Form) — auto-maps server validation errors
|
|
19
|
+
7. Support dark mode via CSS variables: `hsl(var(--background))`, `hsl(var(--primary))`
|
|
20
|
+
8. **Add tests** — run `/write-ui-tests` for the new page
|
|
21
|
+
|
|
22
|
+
## Design Standards
|
|
23
|
+
- All async content has loading skeletons (`Skeleton` from `@quanticjs/react-ui`)
|
|
24
|
+
- All lists have empty states (`EmptyState` from `@quanticjs/react-ui`)
|
|
25
|
+
- All forms use `useForm` from `@quanticjs/react-forms` + Zod (auto server-error mapping)
|
|
26
|
+
- WCAG 2.1 AA accessible
|
|
27
|
+
- No hardcoded hex colors or spacing — use design tokens/CSS variables
|
|
28
|
+
- Components accept `className` for extension and forward refs
|
|
29
|
+
|
|
30
|
+
## State Management (per ADR-003)
|
|
31
|
+
| State type | Tool |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Server/remote data | `useApiQuery` / `useApiMutation` from `@quanticjs/react-query` |
|
|
34
|
+
| URL-derived state | `useSearchParams` |
|
|
35
|
+
| Local UI state | `useState` |
|
|
36
|
+
| Shared client state | Zustand store (with selectors) |
|
|
37
|
+
| Form state | `useForm` from `@quanticjs/react-forms` + Zod |
|
|
38
|
+
|
|
39
|
+
## Error Handling — Three Tiers
|
|
40
|
+
|
|
41
|
+
| Tier | Handles | Pattern |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| **Forms** | Validation errors (400/422) | `useForm` auto-maps `ApiError.fieldErrors` to form fields. Non-field errors go to `errors._root`. |
|
|
44
|
+
| **Toast** | Non-form mutations (delete, toggle, actions) | `toast.error(apiError)` — ApiError-aware, extracts title + detail automatically |
|
|
45
|
+
| **ErrorBoundary** | Unhandled render errors | Already wired in app root provider stack. Prefer page-level boundary for granular recovery. |
|
|
46
|
+
|
|
47
|
+
Non-form mutations **must** always provide `onError`:
|
|
48
|
+
```typescript
|
|
49
|
+
const mutation = useApiMutation(
|
|
50
|
+
(api, id: string) => api.delete(`/items/${id}`),
|
|
51
|
+
{
|
|
52
|
+
invalidates: [['items']],
|
|
53
|
+
onError: (error) => toast.error(error),
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**5xx errors:** Never show `error.detail` — may contain stack traces. Show generic "Something went wrong" message.
|
|
59
|
+
Always include `error.correlationId` in error UI for support reporting.
|
|
60
|
+
|
|
61
|
+
## Rules
|
|
62
|
+
- NEVER use raw `useQuery`/`useMutation` — use `useApiQuery`/`useApiMutation` from `@quanticjs/react-query`
|
|
63
|
+
- NEVER use raw React Hook Form — use `useForm` from `@quanticjs/react-forms` (auto server-error mapping)
|
|
64
|
+
- NEVER fetch data with `useEffect` + `useState` — use `useApiQuery`
|
|
65
|
+
- NEVER copy query data into `useState`
|
|
66
|
+
- NEVER put server data in Zustand
|
|
67
|
+
- NEVER manually map server validation errors to form fields — `@quanticjs/react-forms` handles this
|
|
68
|
+
- NEVER omit `onError` on non-form mutations — every mutation failure must be visible to the user
|
|
69
|
+
- NEVER show `error.detail` from 5xx responses — use generic message instead
|
|
70
|
+
- NEVER write `catch (e) { console.log(e) }` on mutations — swallowing errors is a bug
|
|
71
|
+
- NEVER use `any` — use `unknown` and narrow
|
|
72
|
+
- NEVER hardcode hex colors or spacing values
|
|
73
|
+
- NEVER use `NodeJS.Timeout` or other Node.js types in frontend code
|
|
74
|
+
- API errors handled via `ApiError` from `@quanticjs/react-core` — use `.detail`, `.fieldErrors`, `.correlationId`
|
|
75
|
+
- Auth refresh handled automatically by `createClient`'s `auth.refresh` — NEVER implement retry logic on 401
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Add Handler
|
|
2
|
+
|
|
3
|
+
Create a CQRS command or query with validator and handler following the `@quanticjs/core` patterns.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
```
|
|
7
|
+
/add-handler CreateItem in project
|
|
8
|
+
/add-handler GetUserProfile in identity
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. **Create command or query class** in `src/<module>/commands/` or `src/<module>/queries/`
|
|
14
|
+
- Add `@Validate(XxxValidator)` decorator on the command class (MANDATORY)
|
|
15
|
+
- Add optional decorators as needed: `@Cache`, `@DistributedLock`, `@FeatureFlag`
|
|
16
|
+
```typescript
|
|
17
|
+
import { Validate, DistributedLock } from '@quanticjs/core';
|
|
18
|
+
|
|
19
|
+
@Validate(CreateXxxValidator)
|
|
20
|
+
@DistributedLock('create-xxx:{name}') // only if critical section needed
|
|
21
|
+
export class CreateXxxCommand {
|
|
22
|
+
constructor(public readonly name: string, public readonly userId: string) {}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. **Create `.validator.ts`** co-located with the command — ALL validation logic lives here:
|
|
27
|
+
```typescript
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
import { ICommandValidator, validateCommand } from '@quanticjs/core';
|
|
30
|
+
|
|
31
|
+
export class CreateXxxValidator implements ICommandValidator<CreateXxxCommand> {
|
|
32
|
+
private schema = z.object({
|
|
33
|
+
name: z.string().min(1).max(100),
|
|
34
|
+
// Business rules go here as .refine() / .superRefine()
|
|
35
|
+
});
|
|
36
|
+
validate(command: CreateXxxCommand) { return validateCommand(this.schema, command); }
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
3. **Create handler class** implementing `ICommandHandler<T>` or `IQueryHandler<T>`:
|
|
41
|
+
```typescript
|
|
42
|
+
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
|
43
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
44
|
+
import { Repository } from 'typeorm';
|
|
45
|
+
import { getTransactionalRepo, Result } from '@quanticjs/core';
|
|
46
|
+
|
|
47
|
+
@CommandHandler(CreateXxxCommand)
|
|
48
|
+
export class CreateXxxHandler implements ICommandHandler<CreateXxxCommand> {
|
|
49
|
+
constructor(@InjectRepository(Xxx) private readonly xxxRepo: Repository<Xxx>) {}
|
|
50
|
+
|
|
51
|
+
async execute(command: CreateXxxCommand): Promise<Result<XxxDto>> {
|
|
52
|
+
const xxxRepo = getTransactionalRepo(this.xxxRepo); // UnitOfWork
|
|
53
|
+
const entity = xxxRepo.create({ name: command.name });
|
|
54
|
+
await xxxRepo.save(entity);
|
|
55
|
+
return Result.success(toDto(entity));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
4. **Register** handler and validator in module's `providers` array
|
|
61
|
+
5. **Add tests** — run `/write-backend-tests` for the handler and validator
|
|
62
|
+
|
|
63
|
+
## Pipeline Behavior Chain
|
|
64
|
+
**Commands:** `Log (global) → FeatureFlag → Validate → Cache → DistributedLock → Transactional (auto) → Handler`
|
|
65
|
+
**Queries:** `Log (global) → FeatureFlag → Validate → Cache → Handler`
|
|
66
|
+
|
|
67
|
+
## Available Decorators
|
|
68
|
+
| Decorator | When to Use |
|
|
69
|
+
|-----------|-------------|
|
|
70
|
+
| `@Validate(ValidatorClass)` | Every command with external input (MANDATORY) |
|
|
71
|
+
| `@DistributedLock('key:{prop}')` | Commands with critical sections — race conditions, concurrent writes, resource contention |
|
|
72
|
+
| `@Cache('key:{prop}', { ttlSeconds })` | Read-heavy queries |
|
|
73
|
+
| `@FeatureFlag('release-module-feature')` | Feature-gated commands (see naming below) |
|
|
74
|
+
| `@IsolatedTransaction()` | Audit/notification commands that must commit independently |
|
|
75
|
+
|
|
76
|
+
## Feature Flag Naming & Lifecycle
|
|
77
|
+
|
|
78
|
+
When adding `@FeatureFlag`, use the correct naming convention and fallback:
|
|
79
|
+
|
|
80
|
+
| Category | Name format | Lifetime | Fallback |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| **Release** | `release-{module}-{feature}` | Remove within 30 days of full rollout | `throw` (default) |
|
|
83
|
+
| **Kill switch** | `kill-{module}-{feature}` | Permanent | `throw` (default) |
|
|
84
|
+
| **Experiment** | `experiment-{module}-{feature}` | Remove within 90 days | `default` (control variant) |
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
@FeatureFlag('release-billing-invoices') // blocks if disabled
|
|
88
|
+
@FeatureFlag('kill-payments-processing') // blocks if disabled
|
|
89
|
+
@FeatureFlag('experiment-scoring-v2', { fallback: 'default', defaultValue: oldResult })
|
|
90
|
+
@FeatureFlag('release-notifications-email', { fallback: 'skip' }) // silently skips
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If `UNLEASH_URL` is not set, all flags pass — local dev and tests work without Unleash.
|
|
94
|
+
|
|
95
|
+
## Rules
|
|
96
|
+
- Command class MUST have `@Validate(XxxValidator)` — without it, the `.validator.ts` is dead code
|
|
97
|
+
- ALL validation in `.validator.ts` using Zod — NEVER validate inline in handlers
|
|
98
|
+
- Business rules (age >= 18, email unique, date range valid) → Zod `.refine()` / `.superRefine()` in validator
|
|
99
|
+
- Handler uses `getTransactionalRepo()` for all repo access — UnitOfWork is automatic
|
|
100
|
+
- Handlers NEVER contain validation logic — no `if (x < y) return Result.validationError()`
|
|
101
|
+
- Handlers NEVER throw exceptions — return `Result.failure()` / `Result.notFound()` / `Result.conflict()` etc.
|
|
102
|
+
- If handler has a critical section, add `@DistributedLock('key:{prop}')` on the **command class**
|
|
103
|
+
- Return `Result<T>` from handlers — never throw for business errors
|
|
104
|
+
- Feature flags: NEVER nest multiple `@FeatureFlag` on one handler — one handler, one flag
|
|
105
|
+
- Feature flags: NEVER use on infrastructure code (migrations, middleware) — use env vars instead
|