@el-j/magic-helix-plugins 4.0.0-beta.2 → 4.0.0-beta.4
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/architecture/codeowners.md +123 -0
- package/dist/architecture/monorepo.md +146 -0
- package/dist/architecture/nx.md +122 -0
- package/dist/architecture/turborepo.md +114 -0
- package/dist/ci/github-actions.md +268 -0
- package/dist/ci/gitlab-ci.md +330 -0
- package/dist/containers/docker-multistage.md +120 -0
- package/dist/containers/kubernetes-deploy.md +210 -0
- package/dist/cpp/index.cjs +79 -0
- package/dist/cpp/index.mjs +209 -0
- package/dist/csharp/index.cjs +2 -2
- package/dist/csharp/{index.js → index.mjs} +17 -11
- package/dist/csharp/templates/framework-aspnetcore.md +205 -0
- package/dist/csharp/templates/framework-blazor.md +271 -0
- package/dist/csharp/templates/lang-csharp.md +162 -0
- package/dist/devops/docker-compose.md +111 -0
- package/dist/devops/docker-dockerfile.md +94 -0
- package/dist/devops/github-actions.md +160 -0
- package/dist/devops/gitlab-ci.md +210 -0
- package/dist/generic/lang-typescript.md +57 -0
- package/dist/generic/state-redux.md +21 -0
- package/dist/generic/state-rxjs.md +6 -0
- package/dist/generic/style-mui.md +23 -0
- package/dist/generic/style-tailwind.md +76 -0
- package/dist/generic/test-cypress.md +21 -0
- package/dist/generic/test-jest.md +20 -0
- package/dist/generic/test-playwright.md +21 -0
- package/dist/generic/test-vitest.md +131 -0
- package/dist/go/index.cjs +3 -3
- package/dist/go/{index.js → index.mjs} +18 -15
- package/dist/go/templates/lang-go.md +571 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +24 -0
- package/dist/java/index.cjs +2 -2
- package/dist/java/{index.js → index.mjs} +25 -19
- package/dist/java/templates/build-gradle.md +102 -0
- package/dist/java/templates/build-maven.md +86 -0
- package/dist/java/templates/framework-spring-boot.md +179 -0
- package/dist/java/templates/lang-java.md +78 -0
- package/dist/java/templates/lang-kotlin.md +88 -0
- package/dist/meta/magic-helix-meta.md +213 -0
- package/dist/meta/meta-debug.md +459 -0
- package/dist/meta/meta-implement.md +450 -0
- package/dist/meta/meta-roadmap.md +265 -0
- package/dist/nodejs/templates/angular-core.md +19 -0
- package/dist/nodejs/templates/lang-typescript.md +57 -0
- package/dist/nodejs/templates/nestjs-core.md +7 -0
- package/dist/nodejs/templates/react-core.md +677 -0
- package/dist/nodejs/templates/react-zustand.md +7 -0
- package/dist/nodejs/templates/state-redux.md +21 -0
- package/dist/nodejs/templates/state-rxjs.md +6 -0
- package/dist/nodejs/templates/style-primevue.md +6 -0
- package/dist/nodejs/templates/style-quasar.md +22 -0
- package/dist/nodejs/templates/style-tailwind.md +76 -0
- package/dist/nodejs/templates/test-cypress.md +21 -0
- package/dist/nodejs/templates/test-jest.md +20 -0
- package/dist/nodejs/templates/test-playwright.md +21 -0
- package/dist/nodejs/templates/test-vitest.md +131 -0
- package/dist/nodejs/templates/vue-core.md +108 -0
- package/dist/nodejs/templates/vue-pinia.md +5 -0
- package/dist/patterns/architecture/clean-architecture.md +469 -0
- package/dist/patterns/architecture/dependency-injection.md +517 -0
- package/dist/patterns/architecture/domain-driven-design.md +621 -0
- package/dist/patterns/architecture/layered-architecture.md +382 -0
- package/dist/patterns/architecture/repository-pattern.md +408 -0
- package/dist/patterns/domain-expertise/nextjs-rules.md +115 -0
- package/dist/patterns/domain-expertise/react-patterns.md +181 -0
- package/dist/patterns/domain-expertise/server-components.md +212 -0
- package/dist/patterns/domain-expertise/shadcn-ui.md +52 -0
- package/dist/patterns/domain-expertise/tailwind-patterns.md +52 -0
- package/dist/patterns/environment/container-awareness.md +17 -0
- package/dist/patterns/environment/ide-features.md +17 -0
- package/dist/patterns/environment/os-commands.md +17 -0
- package/dist/patterns/organization/heading-hierarchy.md +103 -0
- package/dist/patterns/organization/sequential-workflows.md +102 -0
- package/dist/patterns/organization/xml-rule-groups.md +64 -0
- package/dist/patterns/reasoning/agent-loop.md +151 -0
- package/dist/patterns/reasoning/confirmation-gates.md +141 -0
- package/dist/patterns/reasoning/dependency-analysis.md +132 -0
- package/dist/patterns/reasoning/one-tool-per-iteration.md +152 -0
- package/dist/patterns/reasoning/preview-before-action.md +194 -0
- package/dist/patterns/reasoning/reflection-checkpoints.md +166 -0
- package/dist/patterns/reasoning/result-verification.md +157 -0
- package/dist/patterns/reasoning/subtask-breakdown.md +131 -0
- package/dist/patterns/reasoning/thinking-tags.md +100 -0
- package/dist/patterns/role-definition/capability-declarations.md +72 -0
- package/dist/patterns/role-definition/expert-identity.md +45 -0
- package/dist/patterns/role-definition/scope-boundaries.md +61 -0
- package/dist/patterns/safety/code-safety-rules.md +17 -0
- package/dist/patterns/safety/credential-handling.md +17 -0
- package/dist/patterns/safety/destructive-warnings.md +17 -0
- package/dist/patterns/safety/refusal-messages.md +17 -0
- package/dist/patterns/tone/adaptive-tone.md +17 -0
- package/dist/patterns/tone/concise-communication.md +17 -0
- package/dist/patterns/tone/forbidden-phrases.md +17 -0
- package/dist/patterns/tool-guidelines/function-schemas.md +143 -0
- package/dist/patterns/tool-guidelines/parameter-examples.md +137 -0
- package/dist/patterns/tool-guidelines/usage-policies.md +105 -0
- package/dist/php/index.cjs +2 -2
- package/dist/php/{index.js → index.mjs} +12 -6
- package/dist/php/templates/framework-laravel.md +112 -0
- package/dist/php/templates/lang-php.md +94 -0
- package/dist/python/index.cjs +4 -4
- package/dist/python/{index.js → index.mjs} +10 -7
- package/dist/python/templates/lang-python.md +508 -0
- package/dist/ruby/index.cjs +2 -2
- package/dist/ruby/{index.js → index.mjs} +16 -10
- package/dist/ruby/templates/framework-rails.md +309 -0
- package/dist/ruby/templates/framework-sinatra.md +227 -0
- package/dist/ruby/templates/lang-ruby.md +216 -0
- package/dist/rust/index.cjs +3 -3
- package/dist/rust/{index.js → index.mjs} +24 -18
- package/dist/rust/templates/lang-rust.md +89 -0
- package/dist/swift/index.cjs +32 -0
- package/dist/swift/index.mjs +112 -0
- package/dist/swift/templates/framework-vapor.md +352 -0
- package/dist/swift/templates/lang-swift.md +291 -0
- package/package.json +31 -21
- package/dist/index.js +0 -20
- /package/dist/nodejs/{index.js → index.mjs} +0 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# Clean Architecture Pattern
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Enforce separation of concerns through concentric layers, keeping business logic independent of frameworks, UI, and external dependencies.
|
|
5
|
+
|
|
6
|
+
## Core Principles
|
|
7
|
+
|
|
8
|
+
### Layer Dependency Rule
|
|
9
|
+
**Inner layers NEVER depend on outer layers. Dependencies point inward only.**
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────────────────────────┐
|
|
13
|
+
│ Frameworks & Drivers (UI, DB) │ ← Outermost layer
|
|
14
|
+
├─────────────────────────────────────┤
|
|
15
|
+
│ Interface Adapters (Controllers)│
|
|
16
|
+
├─────────────────────────────────────┤
|
|
17
|
+
│ Application Business Rules │
|
|
18
|
+
│ (Use Cases / Interactors) │
|
|
19
|
+
├─────────────────────────────────────┤
|
|
20
|
+
│ Enterprise Business Rules │
|
|
21
|
+
│ (Entities / Domain Models) │ ← Innermost layer
|
|
22
|
+
└─────────────────────────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Layer Responsibilities
|
|
26
|
+
|
|
27
|
+
#### 1. Entities (Domain Models) - Innermost
|
|
28
|
+
**Pure business logic. No framework dependencies. Use plain objects + pure functions.**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// ✅ Modern: Domain entity as immutable data + pure functions
|
|
32
|
+
export type User = {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly email: string;
|
|
35
|
+
readonly passwordHash: string;
|
|
36
|
+
readonly createdAt: Date;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Factory function to create user
|
|
40
|
+
export function createUser(email: string, password: string): User {
|
|
41
|
+
if (!isValidEmail(email)) {
|
|
42
|
+
throw new Error('Invalid email format');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: generateId(),
|
|
47
|
+
email,
|
|
48
|
+
passwordHash: hashPassword(password),
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pure functions for business logic
|
|
54
|
+
export function validatePassword(user: User, plainPassword: string): boolean {
|
|
55
|
+
return hashPassword(plainPassword) === user.passwordHash;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function changeEmail(user: User, newEmail: string): User {
|
|
59
|
+
if (!isValidEmail(newEmail)) {
|
|
60
|
+
throw new Error('Invalid email format');
|
|
61
|
+
}
|
|
62
|
+
// Immutable update - return new object
|
|
63
|
+
return { ...user, email: newEmail };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isValidEmail(email: string): boolean {
|
|
67
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function hashPassword(plain: string): string {
|
|
71
|
+
// Business rule for password hashing
|
|
72
|
+
return /* hashing logic */;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### 2. Use Cases (Application Business Rules)
|
|
77
|
+
**Orchestrate entities. Define application-specific business rules. Framework-independent.**
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// ✅ Modern: Repository as plain interface (no class)
|
|
81
|
+
export type UserRepository = {
|
|
82
|
+
findById: (id: string) => Promise<User | null>;
|
|
83
|
+
findByEmail: (email: string) => Promise<User | null>;
|
|
84
|
+
save: (user: User) => Promise<void>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type EmailService = {
|
|
88
|
+
sendWelcomeEmail: (email: string) => Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ✅ Use case as pure function with dependencies injected as parameters
|
|
92
|
+
export async function registerUser(
|
|
93
|
+
email: string,
|
|
94
|
+
password: string,
|
|
95
|
+
deps: {
|
|
96
|
+
userRepository: UserRepository;
|
|
97
|
+
emailService: EmailService;
|
|
98
|
+
}
|
|
99
|
+
): Promise<User> {
|
|
100
|
+
// Application business rule
|
|
101
|
+
const existingUser = await deps.userRepository.findByEmail(email);
|
|
102
|
+
if (existingUser) {
|
|
103
|
+
throw new Error('User already exists');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const user = createUser(email, password);
|
|
107
|
+
await deps.userRepository.save(user);
|
|
108
|
+
await deps.emailService.sendWelcomeEmail(email);
|
|
109
|
+
|
|
110
|
+
return user;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Alternative: Factory pattern for partial application
|
|
114
|
+
export function makeRegisterUser(deps: {
|
|
115
|
+
userRepository: UserRepository;
|
|
116
|
+
emailService: EmailService;
|
|
117
|
+
}) {
|
|
118
|
+
return async (email: string, password: string) => {
|
|
119
|
+
return registerUser(email, password, deps);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### 3. Interface Adapters (Controllers, Presenters, Gateways)
|
|
125
|
+
**Convert data between use cases and external layers. Adapt external formats to internal formats.**
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ✅ Modern: Controller as pure function (no class)
|
|
129
|
+
export async function registerUserHandler(
|
|
130
|
+
req: Request,
|
|
131
|
+
res: Response,
|
|
132
|
+
deps: { registerUser: typeof registerUser }
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
try {
|
|
135
|
+
const { email, password } = req.body;
|
|
136
|
+
|
|
137
|
+
// Adapt HTTP input to use case input
|
|
138
|
+
const user = await deps.registerUser(email, password, deps);
|
|
139
|
+
|
|
140
|
+
// Adapt use case output to HTTP response
|
|
141
|
+
res.status(201).json({
|
|
142
|
+
id: user.id,
|
|
143
|
+
email: user.email,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
res.status(400).json({ error: error.message });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ✅ Repository adapter implements type interface
|
|
151
|
+
export function createPostgresUserRepository(db: Database): UserRepository {
|
|
152
|
+
return {
|
|
153
|
+
async findById(id: string): Promise<User | null> {
|
|
154
|
+
const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
155
|
+
if (!row) return null;
|
|
156
|
+
|
|
157
|
+
// Adapt database row to domain entity
|
|
158
|
+
return {
|
|
159
|
+
id: row.id,
|
|
160
|
+
email: row.email,
|
|
161
|
+
passwordHash: row.password_hash,
|
|
162
|
+
createdAt: new Date(row.created_at),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
167
|
+
const row = await db.query('SELECT * FROM users WHERE email = $1', [email]);
|
|
168
|
+
if (!row) return null;
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
id: row.id,
|
|
172
|
+
email: row.email,
|
|
173
|
+
passwordHash: row.password_hash,
|
|
174
|
+
createdAt: new Date(row.created_at),
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async save(user: User): Promise<void> {
|
|
179
|
+
// Adapt domain entity to database row
|
|
180
|
+
await db.query(
|
|
181
|
+
'INSERT INTO users (id, email, password_hash, created_at) VALUES ($1, $2, $3, $4)',
|
|
182
|
+
[user.id, user.email, user.passwordHash, user.createdAt]
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### 4. Frameworks & Drivers (UI, Database, External Services)
|
|
190
|
+
**Implementation details. Frameworks, libraries, databases.**
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// ✅ Modern: Framework-specific setup with functional composition
|
|
194
|
+
import express from 'express';
|
|
195
|
+
import { createDatabase } from './infrastructure/database';
|
|
196
|
+
import { createPostgresUserRepository } from './adapters/repositories/user-repository';
|
|
197
|
+
import { createSendGridEmailService } from './adapters/services/email-service';
|
|
198
|
+
import { makeRegisterUser } from './usecases/register-user';
|
|
199
|
+
import { registerUserHandler } from './adapters/controllers/user-controller';
|
|
200
|
+
|
|
201
|
+
const app = express();
|
|
202
|
+
const db = createDatabase();
|
|
203
|
+
|
|
204
|
+
// Wire up dependencies (Dependency Injection via composition)
|
|
205
|
+
const userRepository = createPostgresUserRepository(db);
|
|
206
|
+
const emailService = createSendGridEmailService(process.env.SENDGRID_KEY!);
|
|
207
|
+
|
|
208
|
+
// Create use case with dependencies
|
|
209
|
+
const registerUser = makeRegisterUser({ userRepository, emailService });
|
|
210
|
+
|
|
211
|
+
// Register route with handler
|
|
212
|
+
app.post('/api/users', (req, res) =>
|
|
213
|
+
registerUserHandler(req, res, { registerUser })
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
app.listen(3000);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## File Organization
|
|
220
|
+
|
|
221
|
+
### Recommended Structure
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
src/
|
|
225
|
+
├── domain/ # Entities layer (innermost)
|
|
226
|
+
│ ├── entities/
|
|
227
|
+
│ │ ├── User.ts
|
|
228
|
+
│ │ ├── Order.ts
|
|
229
|
+
│ │ └── Product.ts
|
|
230
|
+
│ └── value-objects/
|
|
231
|
+
│ ├── Email.ts
|
|
232
|
+
│ └── Money.ts
|
|
233
|
+
│
|
|
234
|
+
├── usecases/ # Application business rules
|
|
235
|
+
│ ├── user/
|
|
236
|
+
│ │ ├── RegisterUser.ts
|
|
237
|
+
│ │ ├── LoginUser.ts
|
|
238
|
+
│ │ └── interfaces/ # Interfaces for repositories, services
|
|
239
|
+
│ │ ├── IUserRepository.ts
|
|
240
|
+
│ │ └── IEmailService.ts
|
|
241
|
+
│ └── order/
|
|
242
|
+
│ ├── CreateOrder.ts
|
|
243
|
+
│ └── interfaces/
|
|
244
|
+
│ └── IOrderRepository.ts
|
|
245
|
+
│
|
|
246
|
+
├── adapters/ # Interface adapters layer
|
|
247
|
+
│ ├── controllers/ # HTTP/API controllers
|
|
248
|
+
│ │ ├── UserController.ts
|
|
249
|
+
│ │ └── OrderController.ts
|
|
250
|
+
│ ├── presenters/ # Format output for UI
|
|
251
|
+
│ │ └── UserPresenter.ts
|
|
252
|
+
│ ├── repositories/ # Database implementations
|
|
253
|
+
│ │ ├── PostgresUserRepository.ts
|
|
254
|
+
│ │ └── PostgresOrderRepository.ts
|
|
255
|
+
│ └── services/ # External service implementations
|
|
256
|
+
│ └── SendGridEmailService.ts
|
|
257
|
+
│
|
|
258
|
+
└── infrastructure/ # Frameworks & drivers (outermost)
|
|
259
|
+
├── database/
|
|
260
|
+
│ └── postgres.ts
|
|
261
|
+
├── http/
|
|
262
|
+
│ └── express-app.ts
|
|
263
|
+
└── config/
|
|
264
|
+
└── env.ts
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Testing Strategy
|
|
268
|
+
|
|
269
|
+
### Test Each Layer Independently
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// ✅ Test domain entity (no mocks needed - pure logic)
|
|
273
|
+
describe('User', () => {
|
|
274
|
+
it('validates password correctly', () => {
|
|
275
|
+
const user = new User('1', 'test@example.com', 'hashedPass');
|
|
276
|
+
expect(user.validatePassword('wrongPass')).toBe(false);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ✅ Test use case (mock repository and service interfaces)
|
|
281
|
+
describe('RegisterUserUseCase', () => {
|
|
282
|
+
it('registers new user and sends welcome email', async () => {
|
|
283
|
+
const mockRepo: IUserRepository = {
|
|
284
|
+
findById: jest.fn().mockResolvedValue(null),
|
|
285
|
+
save: jest.fn(),
|
|
286
|
+
};
|
|
287
|
+
const mockEmail: IEmailService = {
|
|
288
|
+
sendWelcomeEmail: jest.fn(),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const useCase = new RegisterUserUseCase(mockRepo, mockEmail);
|
|
292
|
+
await useCase.execute('new@example.com', 'password');
|
|
293
|
+
|
|
294
|
+
expect(mockRepo.save).toHaveBeenCalled();
|
|
295
|
+
expect(mockEmail.sendWelcomeEmail).toHaveBeenCalledWith('new@example.com');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ✅ Test controller (mock use case)
|
|
300
|
+
describe('UserController', () => {
|
|
301
|
+
it('returns 201 on successful registration', async () => {
|
|
302
|
+
const mockUseCase = {
|
|
303
|
+
execute: jest.fn().mockResolvedValue(new User('1', 'test@example.com', 'hash')),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const controller = new UserController(mockUseCase);
|
|
307
|
+
const req = { body: { email: 'test@example.com', password: 'pass' } };
|
|
308
|
+
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
|
309
|
+
|
|
310
|
+
await controller.register(req, res);
|
|
311
|
+
|
|
312
|
+
expect(res.status).toHaveBeenCalledWith(201);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Migration Strategy
|
|
318
|
+
|
|
319
|
+
### Step 1: Identify Current Layers
|
|
320
|
+
- What's your business logic? (→ Entities)
|
|
321
|
+
- What are your application operations? (→ Use Cases)
|
|
322
|
+
- What talks to the outside world? (→ Adapters)
|
|
323
|
+
- What are framework implementations? (→ Infrastructure)
|
|
324
|
+
|
|
325
|
+
### Step 2: Extract Entities First
|
|
326
|
+
```typescript
|
|
327
|
+
// ❌ Before: Mixed concerns
|
|
328
|
+
class UserService {
|
|
329
|
+
async register(req: Request): Promise<Response> {
|
|
330
|
+
const user = await db.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
|
|
331
|
+
if (user) throw new Error('Exists');
|
|
332
|
+
await db.query('INSERT INTO users...');
|
|
333
|
+
await sendEmail(req.body.email);
|
|
334
|
+
return res.json({ success: true });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ✅ After: Extracted entity
|
|
339
|
+
class User {
|
|
340
|
+
constructor(public id: string, public email: string) {}
|
|
341
|
+
// Business logic here
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Step 3: Create Use Cases
|
|
346
|
+
```typescript
|
|
347
|
+
// ✅ Extract application logic
|
|
348
|
+
class RegisterUserUseCase {
|
|
349
|
+
async execute(email: string, password: string): Promise<User> {
|
|
350
|
+
// Application business rules
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Step 4: Create Adapters
|
|
356
|
+
```typescript
|
|
357
|
+
// ✅ Separate database adapter
|
|
358
|
+
class PostgresUserRepository implements IUserRepository {
|
|
359
|
+
// Database-specific code
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Test-Driven Development (TDD)
|
|
364
|
+
|
|
365
|
+
**ALWAYS write tests before implementation. Follow the Red-Green-Refactor cycle.**
|
|
366
|
+
|
|
367
|
+
### TDD Workflow
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// 1. RED: Write failing test first
|
|
371
|
+
describe('createUser', () => {
|
|
372
|
+
it('should create user with valid email and hashed password', () => {
|
|
373
|
+
const user = createUser('test@example.com', 'password123');
|
|
374
|
+
|
|
375
|
+
expect(user.email).toBe('test@example.com');
|
|
376
|
+
expect(user.passwordHash).not.toBe('password123'); // Should be hashed
|
|
377
|
+
expect(user.id).toBeDefined();
|
|
378
|
+
expect(user.createdAt).toBeInstanceOf(Date);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should throw error for invalid email', () => {
|
|
382
|
+
expect(() => createUser('invalid-email', 'password123'))
|
|
383
|
+
.toThrow('Invalid email format');
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// 2. GREEN: Write minimal code to pass
|
|
388
|
+
export function createUser(email: string, password: string): User {
|
|
389
|
+
if (!isValidEmail(email)) {
|
|
390
|
+
throw new Error('Invalid email format');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
id: generateId(),
|
|
395
|
+
email,
|
|
396
|
+
passwordHash: hashPassword(password),
|
|
397
|
+
createdAt: new Date(),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 3. REFACTOR: Clean up while keeping tests green
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Why Test-First?
|
|
405
|
+
|
|
406
|
+
- **Design pressure**: Tests force you to think about interfaces before implementation
|
|
407
|
+
- **Testable code**: Code written to be tested is naturally more modular and decoupled
|
|
408
|
+
- **Living documentation**: Tests show how code is meant to be used
|
|
409
|
+
- **Confidence**: Refactor freely knowing tests will catch breakage
|
|
410
|
+
- **No test debt**: Tests are written, not "TODO"
|
|
411
|
+
|
|
412
|
+
### TDD Best Practices
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// ✅ DO: Test behavior, not implementation
|
|
416
|
+
it('should return active users', () => {
|
|
417
|
+
const users = filterActiveUsers(allUsers);
|
|
418
|
+
expect(users.every(u => u.isActive)).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ❌ DON'T: Test internal implementation details
|
|
422
|
+
it('should call isActive method', () => {
|
|
423
|
+
expect(mockUser.isActive).toHaveBeenCalled(); // Fragile!
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// ✅ DO: Use descriptive test names that explain business rules
|
|
427
|
+
it('should reject orders when inventory is insufficient', () => {
|
|
428
|
+
// ...
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ❌ DON'T: Use vague test names
|
|
432
|
+
it('should work correctly', () => {
|
|
433
|
+
// What does "correctly" mean?
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ✅ DO: Arrange-Act-Assert (AAA) pattern
|
|
437
|
+
it('should calculate total with tax', () => {
|
|
438
|
+
// Arrange
|
|
439
|
+
const items = [{ price: 100 }, { price: 200 }];
|
|
440
|
+
const taxRate = 0.1;
|
|
441
|
+
|
|
442
|
+
// Act
|
|
443
|
+
const total = calculateTotal(items, taxRate);
|
|
444
|
+
|
|
445
|
+
// Assert
|
|
446
|
+
expect(total).toBe(330); // (100 + 200) * 1.1
|
|
447
|
+
});
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Test Coverage Goals
|
|
451
|
+
|
|
452
|
+
- **Entities/Domain**: 100% coverage - critical business logic
|
|
453
|
+
- **Use Cases**: 90%+ coverage - application logic paths
|
|
454
|
+
- **Adapters**: 80%+ coverage - focus on error handling
|
|
455
|
+
- **Integration**: Key user journeys and edge cases
|
|
456
|
+
|
|
457
|
+
## Rules Summary
|
|
458
|
+
|
|
459
|
+
- **ALWAYS** write tests BEFORE writing implementation code (TDD)
|
|
460
|
+
- **ALWAYS** keep entities framework-free
|
|
461
|
+
- **ALWAYS** define interfaces in use cases, implement in adapters
|
|
462
|
+
- **ALWAYS** point dependencies inward (outer layers depend on inner)
|
|
463
|
+
- **ALWAYS** test each layer independently with appropriate coverage
|
|
464
|
+
- **NEVER** import frameworks in entities or use cases
|
|
465
|
+
- **NEVER** let inner layers know about outer layers
|
|
466
|
+
- **NEVER** skip writing tests (no test debt)
|
|
467
|
+
- **PREFER** dependency injection over direct instantiation
|
|
468
|
+
- **PREFER** small, focused use cases over large service classes
|
|
469
|
+
- **PREFER** testing behavior over implementation details
|