@lousy-agents/cli 1.1.0 → 2.0.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/README.md +11 -12
- package/api/copilot-with-fastify/.devcontainer/devcontainer.json +90 -0
- package/api/copilot-with-fastify/.editorconfig +16 -0
- package/api/copilot-with-fastify/.github/ISSUE_TEMPLATE/feature-to-spec.yml +55 -0
- package/api/copilot-with-fastify/.github/copilot-instructions.md +387 -0
- package/api/copilot-with-fastify/.github/instructions/pipeline.instructions.md +149 -0
- package/api/copilot-with-fastify/.github/instructions/software-architecture.instructions.md +430 -0
- package/api/copilot-with-fastify/.github/instructions/spec.instructions.md +411 -0
- package/api/copilot-with-fastify/.github/instructions/test.instructions.md +268 -0
- package/api/copilot-with-fastify/.github/specs/README.md +84 -0
- package/api/copilot-with-fastify/.github/workflows/assign-copilot.yml +59 -0
- package/api/copilot-with-fastify/.github/workflows/ci.yml +88 -0
- package/api/copilot-with-fastify/.nvmrc +1 -0
- package/api/copilot-with-fastify/.vscode/extensions.json +14 -0
- package/api/copilot-with-fastify/.vscode/launch.json +30 -0
- package/api/copilot-with-fastify/.vscode/mcp.json +19 -0
- package/api/copilot-with-fastify/.yamllint +18 -0
- package/api/copilot-with-fastify/biome.json +31 -0
- package/api/copilot-with-fastify/package.json +37 -0
- package/api/copilot-with-fastify/tsconfig.json +34 -0
- package/api/copilot-with-fastify/vitest.config.ts +21 -0
- package/api/copilot-with-fastify/vitest.integration.config.ts +18 -0
- package/api/copilot-with-fastify/vitest.setup.ts +5 -0
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -45
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/config.d.ts +6 -5
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +186 -6
- package/dist/lib/config.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: ".github/workflows/*.{yml,yaml}"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Pipeline Instructions for REST API
|
|
6
|
+
|
|
7
|
+
## MANDATORY: After Modifying Workflows
|
|
8
|
+
|
|
9
|
+
Run these validation commands in order:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm run lint:workflows # Validate GitHub Actions workflows with actionlint
|
|
13
|
+
npm run lint:yaml # Validate YAML syntax with yamllint
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Workflow Structure Requirements
|
|
17
|
+
|
|
18
|
+
1. Every workflow MUST include test and lint jobs.
|
|
19
|
+
2. Reference Node.js version from `.nvmrc` using `actions/setup-node` with `node-version-file` input.
|
|
20
|
+
3. Use official setup actions: `actions/checkout`, `actions/setup-node`, `actions/cache`.
|
|
21
|
+
4. Integration tests require Docker for Testcontainers.
|
|
22
|
+
|
|
23
|
+
## Action Pinning Format
|
|
24
|
+
|
|
25
|
+
Pin ALL third-party actions to exact commit SHA with version comment:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
# CORRECT format:
|
|
29
|
+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
30
|
+
|
|
31
|
+
# INCORRECT formats (do NOT use):
|
|
32
|
+
uses: actions/checkout@v4 # ❌ version tag only
|
|
33
|
+
uses: actions/checkout@v4.1.1 # ❌ version tag only
|
|
34
|
+
uses: actions/checkout@main # ❌ branch reference
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Before adding any action:
|
|
38
|
+
1. Check GitHub for the LATEST stable version
|
|
39
|
+
2. Find the full commit SHA for that version
|
|
40
|
+
3. Add both SHA and version comment
|
|
41
|
+
|
|
42
|
+
## Runner Requirements
|
|
43
|
+
|
|
44
|
+
| Workflow | Runner |
|
|
45
|
+
|----------|--------|
|
|
46
|
+
| Default (all workflows) | `ubuntu-latest` |
|
|
47
|
+
| `copilot-setup-steps.yml` | May use different runners as needed |
|
|
48
|
+
|
|
49
|
+
## Integration Tests with Testcontainers
|
|
50
|
+
|
|
51
|
+
Testcontainers manages Docker containers automatically. No explicit Docker service configuration is needed in the workflow, but Docker must be available on the runner.
|
|
52
|
+
|
|
53
|
+
### Example CI Workflow with Integration Tests
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
name: CI
|
|
57
|
+
|
|
58
|
+
on:
|
|
59
|
+
push:
|
|
60
|
+
branches: [main]
|
|
61
|
+
pull_request:
|
|
62
|
+
branches: [main]
|
|
63
|
+
|
|
64
|
+
jobs:
|
|
65
|
+
lint:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
69
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
70
|
+
with:
|
|
71
|
+
node-version-file: '.nvmrc'
|
|
72
|
+
cache: 'npm'
|
|
73
|
+
- run: npm ci
|
|
74
|
+
- run: npx biome check
|
|
75
|
+
|
|
76
|
+
unit-tests:
|
|
77
|
+
runs-on: ubuntu-latest
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
80
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
81
|
+
with:
|
|
82
|
+
node-version-file: '.nvmrc'
|
|
83
|
+
cache: 'npm'
|
|
84
|
+
- run: npm ci
|
|
85
|
+
- run: npm test
|
|
86
|
+
|
|
87
|
+
integration-tests:
|
|
88
|
+
runs-on: ubuntu-latest
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
91
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
92
|
+
with:
|
|
93
|
+
node-version-file: '.nvmrc'
|
|
94
|
+
cache: 'npm'
|
|
95
|
+
- run: npm ci
|
|
96
|
+
- run: npm run test:integration
|
|
97
|
+
env:
|
|
98
|
+
# Testcontainers configuration
|
|
99
|
+
TESTCONTAINERS_RYUK_DISABLED: false
|
|
100
|
+
|
|
101
|
+
build:
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
needs: [lint, unit-tests, integration-tests]
|
|
104
|
+
steps:
|
|
105
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
106
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
107
|
+
with:
|
|
108
|
+
node-version-file: '.nvmrc'
|
|
109
|
+
cache: 'npm'
|
|
110
|
+
- run: npm ci
|
|
111
|
+
- run: npm run build
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Testcontainers in GitHub Actions
|
|
115
|
+
|
|
116
|
+
Testcontainers works out of the box on `ubuntu-latest` runners since they have Docker pre-installed.
|
|
117
|
+
|
|
118
|
+
### Environment Variables
|
|
119
|
+
|
|
120
|
+
| Variable | Description | Default |
|
|
121
|
+
|----------|-------------|---------|
|
|
122
|
+
| `TESTCONTAINERS_RYUK_DISABLED` | Disable Ryuk container cleanup | `false` |
|
|
123
|
+
| `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE` | Override Docker socket path | auto-detected |
|
|
124
|
+
|
|
125
|
+
### Troubleshooting
|
|
126
|
+
|
|
127
|
+
If container startup is slow:
|
|
128
|
+
- Use `TESTCONTAINERS_RYUK_DISABLED=true` to skip Ryuk (manual cleanup needed)
|
|
129
|
+
- Consider caching Docker images in a separate step
|
|
130
|
+
|
|
131
|
+
If containers fail to start:
|
|
132
|
+
- Ensure Docker is available on the runner
|
|
133
|
+
- Check container logs for startup errors
|
|
134
|
+
- Increase test timeouts for container startup
|
|
135
|
+
|
|
136
|
+
## Database Migrations in CI
|
|
137
|
+
|
|
138
|
+
Run migrations as part of the integration test setup:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// In test setup
|
|
142
|
+
beforeAll(async () => {
|
|
143
|
+
container = await new PostgreSqlContainer().start();
|
|
144
|
+
db = createConnection(container);
|
|
145
|
+
await runMigrations(db); // Apply schema before tests
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Do not run migrations as a separate CI step for integration tests - let Testcontainers handle the isolated database setup.
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "src/**/*.ts"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Clean Architecture Instructions for REST API
|
|
6
|
+
|
|
7
|
+
## The Dependency Rule
|
|
8
|
+
|
|
9
|
+
Dependencies point inward only. Outer layers depend on inner layers, never the reverse.
|
|
10
|
+
|
|
11
|
+
**Layers (innermost to outermost):**
|
|
12
|
+
1. Entities — Enterprise business rules
|
|
13
|
+
2. Use Cases — Application business rules
|
|
14
|
+
3. Adapters — Interface converters (routes, repositories, gateways)
|
|
15
|
+
4. Infrastructure — Frameworks, drivers, composition root
|
|
16
|
+
|
|
17
|
+
## Directory Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
├── entities/ # Layer 1: Business domain entities
|
|
22
|
+
├── use-cases/ # Layer 2: Application business rules
|
|
23
|
+
├── gateways/ # Layer 3: Database and external service adapters
|
|
24
|
+
├── routes/ # Layer 3: Fastify route handlers
|
|
25
|
+
├── plugins/ # Layer 3: Fastify plugins
|
|
26
|
+
├── db/ # Layer 3: Database configuration
|
|
27
|
+
│ ├── connection.ts # Database connection factory
|
|
28
|
+
│ ├── migrations/ # Kysely migrations
|
|
29
|
+
│ └── types.ts # Database schema types
|
|
30
|
+
├── lib/ # Layer 3: Configuration and utilities
|
|
31
|
+
└── index.ts # Layer 4: Composition root
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Layer 1: Entities
|
|
35
|
+
|
|
36
|
+
**Location:** `src/entities/`
|
|
37
|
+
|
|
38
|
+
- MUST NOT import from any other layer
|
|
39
|
+
- MUST NOT depend on frameworks or infrastructure
|
|
40
|
+
- MUST NOT use non-deterministic or side-effect-producing global APIs (e.g., `crypto.randomUUID()`, `Date.now()`)
|
|
41
|
+
- MUST be plain TypeScript objects/classes with business logic
|
|
42
|
+
- MAY contain validation and business rules
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// src/entities/user.ts
|
|
46
|
+
export interface User {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly email: string;
|
|
50
|
+
readonly createdAt: Date;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CreateUserInput {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly email: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isValidEmail(email: string): boolean {
|
|
59
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Note: currentDate is passed in to avoid non-deterministic Date() in entities
|
|
63
|
+
export function canUserBeDeleted(user: User, currentDate: Date): boolean {
|
|
64
|
+
const oneWeekAgo = new Date(currentDate);
|
|
65
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
66
|
+
return user.createdAt < oneWeekAgo;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Violations:**
|
|
71
|
+
- Importing Fastify, Kysely, or any framework
|
|
72
|
+
- Importing from `src/use-cases/`, `src/gateways/`, `src/routes/`, or `src/lib/`
|
|
73
|
+
- Database operations or HTTP calls
|
|
74
|
+
- Using non-deterministic global APIs like `crypto.randomUUID()` or `Date.now()`
|
|
75
|
+
|
|
76
|
+
## Layer 2: Use Cases
|
|
77
|
+
|
|
78
|
+
**Location:** `src/use-cases/`
|
|
79
|
+
|
|
80
|
+
- MUST only import from entities and ports (interfaces)
|
|
81
|
+
- MUST define input/output DTOs
|
|
82
|
+
- MUST define ports for external dependencies
|
|
83
|
+
- MUST NOT import concrete implementations
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// src/use-cases/create-user.ts
|
|
87
|
+
import type { User, CreateUserInput } from '../entities/user.js';
|
|
88
|
+
import { isValidEmail } from '../entities/user.js';
|
|
89
|
+
|
|
90
|
+
export interface CreateUserOutput {
|
|
91
|
+
user: User;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Port - interface for the repository
|
|
95
|
+
export interface UserRepository {
|
|
96
|
+
create(id: string, input: CreateUserInput): Promise<User>;
|
|
97
|
+
findByEmail(email: string): Promise<User | null>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Port - interface for ID generation
|
|
101
|
+
export interface IdGenerator {
|
|
102
|
+
generate(): string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createUserUseCase(
|
|
106
|
+
repository: UserRepository,
|
|
107
|
+
idGenerator: IdGenerator
|
|
108
|
+
) {
|
|
109
|
+
return {
|
|
110
|
+
async execute(input: CreateUserInput): Promise<CreateUserOutput> {
|
|
111
|
+
if (!input.name || input.name.trim().length === 0) {
|
|
112
|
+
throw new Error('Name is required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!isValidEmail(input.email)) {
|
|
116
|
+
throw new Error('Invalid email format');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const existingUser = await repository.findByEmail(input.email);
|
|
120
|
+
if (existingUser) {
|
|
121
|
+
throw new Error('Email already exists');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const id = idGenerator.generate();
|
|
125
|
+
const user = await repository.create(id, input);
|
|
126
|
+
return { user };
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Violations:**
|
|
133
|
+
- Importing Fastify, Kysely, or any framework
|
|
134
|
+
- Importing from `gateways/`, `routes/`, or `lib/`
|
|
135
|
+
- Making database queries or HTTP calls directly
|
|
136
|
+
|
|
137
|
+
## Layer 3: Adapters
|
|
138
|
+
|
|
139
|
+
**Location:** `src/gateways/`, `src/routes/`, `src/plugins/`, and `src/db/`
|
|
140
|
+
|
|
141
|
+
### Database Gateway with Kysely
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// src/gateways/user-repository.ts
|
|
145
|
+
import type { Kysely } from 'kysely';
|
|
146
|
+
import type { User, CreateUserInput } from '../entities/user.js';
|
|
147
|
+
import type { UserRepository } from '../use-cases/create-user.js';
|
|
148
|
+
import type { Database } from '../db/types.js';
|
|
149
|
+
|
|
150
|
+
export function createUserRepository(db: Kysely<Database>): UserRepository {
|
|
151
|
+
return {
|
|
152
|
+
async create(id: string, input: CreateUserInput): Promise<User> {
|
|
153
|
+
const result = await db
|
|
154
|
+
.insertInto('users')
|
|
155
|
+
.values({
|
|
156
|
+
id,
|
|
157
|
+
name: input.name,
|
|
158
|
+
email: input.email,
|
|
159
|
+
created_at: new Date(),
|
|
160
|
+
})
|
|
161
|
+
.returning(['id', 'name', 'email', 'created_at as createdAt'])
|
|
162
|
+
.executeTakeFirstOrThrow();
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
168
|
+
const result = await db
|
|
169
|
+
.selectFrom('users')
|
|
170
|
+
.select(['id', 'name', 'email', 'created_at as createdAt'])
|
|
171
|
+
.where('email', '=', email)
|
|
172
|
+
.executeTakeFirst();
|
|
173
|
+
|
|
174
|
+
return result ?? null;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Database Types
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// src/db/types.ts
|
|
184
|
+
export interface Database {
|
|
185
|
+
users: UsersTable;
|
|
186
|
+
posts: PostsTable;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface UsersTable {
|
|
190
|
+
id: string;
|
|
191
|
+
name: string;
|
|
192
|
+
email: string;
|
|
193
|
+
created_at: Date;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface PostsTable {
|
|
197
|
+
id: string;
|
|
198
|
+
user_id: string;
|
|
199
|
+
title: string;
|
|
200
|
+
content: string;
|
|
201
|
+
created_at: Date;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Database Connection with Postgres.js
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// src/db/connection.ts
|
|
209
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
210
|
+
import postgres from 'postgres';
|
|
211
|
+
import type { Database } from './types.js';
|
|
212
|
+
|
|
213
|
+
export interface DatabaseConfig {
|
|
214
|
+
host: string;
|
|
215
|
+
port: number;
|
|
216
|
+
database: string;
|
|
217
|
+
username: string;
|
|
218
|
+
password: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function createDatabase(config: DatabaseConfig): Kysely<Database> {
|
|
222
|
+
const sql = postgres({
|
|
223
|
+
host: config.host,
|
|
224
|
+
port: config.port,
|
|
225
|
+
database: config.database,
|
|
226
|
+
username: config.username,
|
|
227
|
+
password: config.password,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return new Kysely<Database>({
|
|
231
|
+
dialect: new PostgresDialect({ pool: sql }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Fastify Route Handlers
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// src/routes/user-routes.ts
|
|
240
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
241
|
+
import { z } from 'zod';
|
|
242
|
+
import type { createUserUseCase } from '../use-cases/create-user.js';
|
|
243
|
+
|
|
244
|
+
const CreateUserBodySchema = z.object({
|
|
245
|
+
name: z.string().min(1),
|
|
246
|
+
email: z.string().email(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
type CreateUserBody = z.infer<typeof CreateUserBodySchema>;
|
|
250
|
+
|
|
251
|
+
export function createUserRoutes(
|
|
252
|
+
createUser: ReturnType<typeof createUserUseCase>
|
|
253
|
+
) {
|
|
254
|
+
return async function userRoutes(app: FastifyInstance) {
|
|
255
|
+
app.post<{ Body: CreateUserBody }>(
|
|
256
|
+
'/users',
|
|
257
|
+
{
|
|
258
|
+
schema: {
|
|
259
|
+
body: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
required: ['name', 'email'],
|
|
262
|
+
properties: {
|
|
263
|
+
name: { type: 'string', minLength: 1 },
|
|
264
|
+
email: { type: 'string', format: 'email' },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
async (request: FastifyRequest<{ Body: CreateUserBody }>, reply: FastifyReply) => {
|
|
270
|
+
try {
|
|
271
|
+
const validated = CreateUserBodySchema.parse(request.body);
|
|
272
|
+
const result = await createUser.execute(validated);
|
|
273
|
+
return reply.status(201).send(result.user);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error instanceof Error && error.message === 'Email already exists') {
|
|
276
|
+
return reply.status(409).send({ error: error.message });
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Violations:**
|
|
287
|
+
- Business logic (validation rules, authorization decisions)
|
|
288
|
+
- Domain decisions that should be in entities or use cases
|
|
289
|
+
- Direct SQL strings without Kysely query builder
|
|
290
|
+
|
|
291
|
+
## Layer 4: Infrastructure
|
|
292
|
+
|
|
293
|
+
**Location:** `src/index.ts` (composition root)
|
|
294
|
+
|
|
295
|
+
The composition root wires all dependencies together.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// src/index.ts
|
|
299
|
+
import Fastify from 'fastify';
|
|
300
|
+
import sensible from '@fastify/sensible';
|
|
301
|
+
import cors from '@fastify/cors';
|
|
302
|
+
import { createDatabase } from './db/connection.js';
|
|
303
|
+
import { createUserRepository } from './gateways/user-repository.js';
|
|
304
|
+
import { createUserUseCase } from './use-cases/create-user.js';
|
|
305
|
+
import { createUserRoutes } from './routes/user-routes.js';
|
|
306
|
+
|
|
307
|
+
async function main() {
|
|
308
|
+
const app = Fastify({
|
|
309
|
+
logger: {
|
|
310
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Register plugins
|
|
315
|
+
await app.register(sensible);
|
|
316
|
+
await app.register(cors, { origin: true });
|
|
317
|
+
|
|
318
|
+
// Create database connection
|
|
319
|
+
const db = createDatabase({
|
|
320
|
+
host: process.env.DB_HOST || 'localhost',
|
|
321
|
+
port: Number(process.env.DB_PORT) || 5432,
|
|
322
|
+
database: process.env.DB_NAME || 'app',
|
|
323
|
+
username: process.env.DB_USER || 'postgres',
|
|
324
|
+
password: process.env.DB_PASSWORD || 'postgres',
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Create repositories
|
|
328
|
+
const userRepository = createUserRepository(db);
|
|
329
|
+
|
|
330
|
+
// Create ID generator
|
|
331
|
+
const idGenerator = { generate: () => crypto.randomUUID() };
|
|
332
|
+
|
|
333
|
+
// Create use cases
|
|
334
|
+
const createUser = createUserUseCase(userRepository, idGenerator);
|
|
335
|
+
|
|
336
|
+
// Register routes
|
|
337
|
+
await app.register(createUserRoutes(createUser));
|
|
338
|
+
|
|
339
|
+
// Health check
|
|
340
|
+
app.get('/health', async () => ({ status: 'ok' }));
|
|
341
|
+
|
|
342
|
+
// Start server
|
|
343
|
+
const port = Number(process.env.PORT) || 3000;
|
|
344
|
+
await app.listen({ port, host: '0.0.0.0' });
|
|
345
|
+
|
|
346
|
+
console.log(`Server listening on port ${port}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
main().catch(console.error);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Dependency Injection Patterns
|
|
353
|
+
|
|
354
|
+
### Factory Functions (Preferred)
|
|
355
|
+
|
|
356
|
+
Factory functions create adapter instances with injected dependencies.
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// ✅ Good - Factory function with dependency injection
|
|
360
|
+
export function createUserRepository(db: Kysely<Database>): UserRepository {
|
|
361
|
+
return {
|
|
362
|
+
async findById(id: string): Promise<User | null> {
|
|
363
|
+
const result = await db
|
|
364
|
+
.selectFrom('users')
|
|
365
|
+
.where('id', '=', id)
|
|
366
|
+
.selectAll()
|
|
367
|
+
.executeTakeFirst();
|
|
368
|
+
return result ?? null;
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Usage in composition root
|
|
374
|
+
const db = createDatabase(config);
|
|
375
|
+
const userRepository = createUserRepository(db);
|
|
376
|
+
const createUser = createUserUseCase(userRepository, idGenerator);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Constructor Injection for Classes
|
|
380
|
+
|
|
381
|
+
Use constructor injection when classes are preferred.
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// ✅ Good - Constructor injection
|
|
385
|
+
export class UserRepositoryImpl implements UserRepository {
|
|
386
|
+
constructor(private readonly db: Kysely<Database>) {}
|
|
387
|
+
|
|
388
|
+
async findById(id: string): Promise<User | null> {
|
|
389
|
+
const result = await this.db
|
|
390
|
+
.selectFrom('users')
|
|
391
|
+
.where('id', '=', id)
|
|
392
|
+
.selectAll()
|
|
393
|
+
.executeTakeFirst();
|
|
394
|
+
return result ?? null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Import Rules Summary
|
|
400
|
+
|
|
401
|
+
| From | Entities | Use Cases | Gateways/Routes/DB | Index (Root) |
|
|
402
|
+
|------|----------|-----------|-------------------|--------------|
|
|
403
|
+
| Entities | ✓ | ✗ | ✗ | ✗ |
|
|
404
|
+
| Use Cases | ✓ | ✓ | ✗ | ✗ |
|
|
405
|
+
| Gateways/Routes/DB | ✓ | ✓ | ✓ | ✗ |
|
|
406
|
+
| Index (Root) | ✓ | ✓ | ✓ | ✓ |
|
|
407
|
+
|
|
408
|
+
## Anti-Patterns
|
|
409
|
+
|
|
410
|
+
> ⚠️ **CRITICAL: The following anti-patterns MUST ALWAYS be avoided. Violating these patterns will result in code review rejection.**
|
|
411
|
+
|
|
412
|
+
**🚫 Anemic Domain Model:** Entities as data-only containers with logic in services. Put business rules in entities. **NEVER** create entities without behavior.
|
|
413
|
+
|
|
414
|
+
**🚫 Leaky Abstractions:** Repositories exposing Kysely types or raw SQL. Return domain entities only. **NEVER** expose database implementation details outside the gateway layer.
|
|
415
|
+
|
|
416
|
+
**🚫 Business Logic in Routes:** Authorization checks or validation in route handlers. Move to entities/use cases. **NEVER** put business rules in the route layer.
|
|
417
|
+
|
|
418
|
+
**🚫 Direct Database Access in Use Cases:** Use cases making Kysely queries directly. Use repository ports. **NEVER** import database libraries in use case files.
|
|
419
|
+
|
|
420
|
+
**🚫 Raw SQL Strings:** Using template strings for SQL. Always use Kysely query builder for type safety. **NEVER** use string interpolation for SQL queries.
|
|
421
|
+
|
|
422
|
+
## Code Review Checklist
|
|
423
|
+
|
|
424
|
+
- Entities have zero imports from other layers
|
|
425
|
+
- Use cases define ports for all external dependencies
|
|
426
|
+
- Repositories implement ports, contain no business logic
|
|
427
|
+
- Route handlers validate input with Zod, delegate to use cases
|
|
428
|
+
- Only composition root instantiates concrete implementations
|
|
429
|
+
- Use cases testable with simple mocks (no database, no HTTP)
|
|
430
|
+
- All database queries use Kysely query builder
|