@tonycasey/lisa 0.5.13
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 +42 -0
- package/dist/cli.js +390 -0
- package/dist/lib/interfaces/IDockerClient.js +2 -0
- package/dist/lib/interfaces/IMcpClient.js +2 -0
- package/dist/lib/interfaces/IServices.js +2 -0
- package/dist/lib/interfaces/ITemplateCopier.js +2 -0
- package/dist/lib/mcp.js +35 -0
- package/dist/lib/services.js +57 -0
- package/dist/package.json +36 -0
- package/dist/templates/agents/.sample.env +12 -0
- package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
- package/dist/templates/agents/skills/common/group-id.js +193 -0
- package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
- package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
- package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
- package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
- package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
- package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
- package/dist/templates/agents/skills/memory/SKILL.md +31 -0
- package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
- package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
- package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
- package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
- package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
- package/dist/templates/claude/config.js +40 -0
- package/dist/templates/claude/hooks/README.md +158 -0
- package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
- package/dist/templates/claude/hooks/common/context.js +263 -0
- package/dist/templates/claude/hooks/common/group-id.js +188 -0
- package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
- package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
- package/dist/templates/claude/hooks/common/zep-client.js +175 -0
- package/dist/templates/claude/hooks/session-start.js +401 -0
- package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
- package/dist/templates/claude/hooks/session-stop.js +122 -0
- package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
- package/dist/templates/claude/settings.json +46 -0
- package/dist/templates/docker/.env.lisa.example +17 -0
- package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
- package/dist/templates/rules/shared/clean-architecture.md +333 -0
- package/dist/templates/rules/shared/code-quality-rules.md +469 -0
- package/dist/templates/rules/shared/git-rules.md +64 -0
- package/dist/templates/rules/shared/testing-principles.md +469 -0
- package/dist/templates/rules/typescript/coding-standards.md +751 -0
- package/dist/templates/rules/typescript/testing.md +629 -0
- package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
- package/package.json +64 -0
- package/scripts/postinstall.js +710 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
# TypeScript Coding Standards
|
|
2
|
+
|
|
3
|
+
**Note:** This document covers TypeScript-specific conventions. For universal architecture principles, see `shared/clean-architecture.md`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 📘 IMPORTANT: TypeScript Configuration
|
|
8
|
+
|
|
9
|
+
**Before writing TypeScript code, understand your project's configuration:**
|
|
10
|
+
|
|
11
|
+
See **[TypeScript Configuration Guide](./typescript-config-guide.md)** for:
|
|
12
|
+
- How to configure `tsconfig.json` for development vs. production
|
|
13
|
+
- Common configuration issues and solutions (300+ errors resolved)
|
|
14
|
+
- When to use relaxed vs. strict settings
|
|
15
|
+
- Monorepo configuration best practices
|
|
16
|
+
- When to use `@ts-nocheck` for examples and tests
|
|
17
|
+
|
|
18
|
+
**Key takeaways:**
|
|
19
|
+
- Start with **relaxed settings** during active development (`strict: false`)
|
|
20
|
+
- Enable **strict mode** when stabilizing for production
|
|
21
|
+
- Use separate `tsconfig.test.json` for tests
|
|
22
|
+
- Explicitly exclude examples, tests, and demo files
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🚨 CRITICAL: Strict Null Checks
|
|
27
|
+
|
|
28
|
+
### ALWAYS Handle Undefined/Null Values
|
|
29
|
+
|
|
30
|
+
**TypeScript strict mode will cause compile errors if you don't explicitly handle `undefined` and `null`.**
|
|
31
|
+
|
|
32
|
+
This is the #1 source of TypeScript errors. Every value that might be undefined MUST be checked before use.
|
|
33
|
+
|
|
34
|
+
#### Common Errors and Fixes
|
|
35
|
+
|
|
36
|
+
**❌ ERROR: TS2532 "Object is possibly 'undefined'"**
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// ❌ BAD - Will cause TS2532 error
|
|
40
|
+
function processUser(user: User | undefined) {
|
|
41
|
+
return user.name; // ERROR: Object is possibly 'undefined'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ✅ FIX #1 - Null check
|
|
45
|
+
function processUser(user: User | undefined) {
|
|
46
|
+
if (!user) {
|
|
47
|
+
throw new UserNotFoundError();
|
|
48
|
+
}
|
|
49
|
+
return user.name; // ✅ TypeScript knows user is defined here
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ✅ FIX #2 - Optional chaining
|
|
53
|
+
function processUser(user: User | undefined): string | undefined {
|
|
54
|
+
return user?.name; // Returns undefined if user is undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ✅ FIX #3 - Nullish coalescing
|
|
58
|
+
function processUser(user: User | undefined): string {
|
|
59
|
+
return user?.name ?? 'Unknown'; // Returns 'Unknown' if undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ✅ FIX #4 - Type guard
|
|
63
|
+
function processUser(user: User | undefined): string {
|
|
64
|
+
if (user === undefined) {
|
|
65
|
+
return 'Unknown';
|
|
66
|
+
}
|
|
67
|
+
return user.name; // ✅ user is User here
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**❌ ERROR: TS2345 "Type 'X | undefined' is not assignable to parameter of type 'X'"**
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// ❌ BAD - Passing possibly undefined to function expecting defined value
|
|
75
|
+
function sendEmail(email: string) { /* ... */ }
|
|
76
|
+
|
|
77
|
+
const user: User | undefined = await getUser(id);
|
|
78
|
+
sendEmail(user.email); // ERROR: user is possibly undefined
|
|
79
|
+
|
|
80
|
+
// ✅ FIX #1 - Check before passing
|
|
81
|
+
const user = await getUser(id);
|
|
82
|
+
if (!user) {
|
|
83
|
+
throw new UserNotFoundError(id);
|
|
84
|
+
}
|
|
85
|
+
sendEmail(user.email); // ✅ TypeScript knows user is defined
|
|
86
|
+
|
|
87
|
+
// ✅ FIX #2 - Early return
|
|
88
|
+
const user = await getUser(id);
|
|
89
|
+
if (!user) return;
|
|
90
|
+
sendEmail(user.email); // ✅ TypeScript knows user is defined
|
|
91
|
+
|
|
92
|
+
// ✅ FIX #3 - Guard clause
|
|
93
|
+
const user = await getUser(id);
|
|
94
|
+
if (user === undefined) {
|
|
95
|
+
throw new UserNotFoundError(id);
|
|
96
|
+
}
|
|
97
|
+
sendEmail(user.email); // ✅ TypeScript knows user is User
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Array Operations
|
|
101
|
+
|
|
102
|
+
**❌ BAD - .find() returns undefined**
|
|
103
|
+
```typescript
|
|
104
|
+
const users: User[] = [/* ... */];
|
|
105
|
+
const user = users.find(u => u.id === targetId);
|
|
106
|
+
processUser(user); // ERROR: user might be undefined
|
|
107
|
+
|
|
108
|
+
// ✅ GOOD - Check result
|
|
109
|
+
const user = users.find(u => u.id === targetId);
|
|
110
|
+
if (!user) {
|
|
111
|
+
throw new UserNotFoundError(targetId);
|
|
112
|
+
}
|
|
113
|
+
processUser(user); // ✅ user is User
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Object Property Access
|
|
117
|
+
|
|
118
|
+
**❌ BAD - Accessing nested properties**
|
|
119
|
+
```typescript
|
|
120
|
+
const name = user.profile.name; // ERROR: profile might be undefined
|
|
121
|
+
|
|
122
|
+
// ✅ GOOD - Optional chaining
|
|
123
|
+
const name = user.profile?.name; // Returns undefined if profile is undefined
|
|
124
|
+
const name = user.profile?.name ?? 'Unknown'; // Default value
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Function Return Values
|
|
128
|
+
|
|
129
|
+
**When repository methods return `T | null`, ALWAYS check the result:**
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Repository method signature
|
|
133
|
+
interface IUserRepository {
|
|
134
|
+
getById(id: string): Promise<User | null>; // Can return null!
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ❌ BAD - Not checking for null
|
|
138
|
+
async function getUser(id: string): Promise<User> {
|
|
139
|
+
const user = await userRepository.getById(id);
|
|
140
|
+
return user; // ERROR: Type 'User | null' is not assignable to 'User'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ✅ GOOD - Check and throw
|
|
144
|
+
async function getUser(id: string): Promise<User> {
|
|
145
|
+
const user = await userRepository.getById(id);
|
|
146
|
+
if (!user) {
|
|
147
|
+
throw new UserNotFoundError(id);
|
|
148
|
+
}
|
|
149
|
+
return user; // ✅ TypeScript knows user is User, not null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ✅ GOOD - Return nullable type
|
|
153
|
+
async function getUser(id: string): Promise<User | null> {
|
|
154
|
+
return await userRepository.getById(id); // Explicitly nullable return
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### MANDATORY Checks for Every Potentially Undefined Value
|
|
159
|
+
|
|
160
|
+
**Before using ANY value that might be undefined, you MUST:**
|
|
161
|
+
|
|
162
|
+
1. **Check if it exists**: `if (!value) { ... }`
|
|
163
|
+
2. **Use optional chaining**: `value?.property`
|
|
164
|
+
3. **Provide a default**: `value ?? defaultValue`
|
|
165
|
+
4. **Use a type guard**: `if (value === undefined) { ... }`
|
|
166
|
+
|
|
167
|
+
**No exceptions. TypeScript will not compile otherwise.**
|
|
168
|
+
|
|
169
|
+
## Type Safety
|
|
170
|
+
|
|
171
|
+
### Strict Mode
|
|
172
|
+
|
|
173
|
+
**ALWAYS use TypeScript strict mode:**
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
// tsconfig.json
|
|
177
|
+
{
|
|
178
|
+
"compilerOptions": {
|
|
179
|
+
"strict": true,
|
|
180
|
+
"noImplicitAny": true,
|
|
181
|
+
"strictNullChecks": true,
|
|
182
|
+
"strictFunctionTypes": true,
|
|
183
|
+
"strictPropertyInitialization": true
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Never Use `any`
|
|
189
|
+
|
|
190
|
+
**NEVER use `any` type** - it defeats the purpose of TypeScript.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// ❌ BAD
|
|
194
|
+
function processData(data: any): any {
|
|
195
|
+
return data.something;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ✅ GOOD
|
|
199
|
+
function processData(data: ProductData): ProcessedData {
|
|
200
|
+
return {
|
|
201
|
+
id: data.id,
|
|
202
|
+
name: data.name
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Alternatives to `any`:**
|
|
208
|
+
- `unknown` - for truly unknown types (with type guards)
|
|
209
|
+
- Generic types - `<T>` for reusable code
|
|
210
|
+
- Union types - `string | number`
|
|
211
|
+
- Specific interfaces - define the shape
|
|
212
|
+
|
|
213
|
+
### Explicit Return Types
|
|
214
|
+
|
|
215
|
+
Always specify return types for public functions:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
// ❌ BAD - inferred return type
|
|
219
|
+
export function calculateTotal(items) {
|
|
220
|
+
return items.reduce((sum, item) => sum + item.price, 0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ✅ GOOD - explicit return type
|
|
224
|
+
export function calculateTotal(items: CartItem[]): number {
|
|
225
|
+
return items.reduce((sum, item) => sum + item.price, 0);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Type vs Interface
|
|
230
|
+
|
|
231
|
+
**Prefer `interface` for object shapes:**
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// ✅ GOOD - interface for objects
|
|
235
|
+
export interface IProduct {
|
|
236
|
+
id: string;
|
|
237
|
+
name: string;
|
|
238
|
+
price: number;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Use `type` for:**
|
|
243
|
+
- Unions: `type Status = 'active' | 'inactive'`
|
|
244
|
+
- Intersections: `type Combined = TypeA & TypeB`
|
|
245
|
+
- Mapped types: `type Readonly<T> = { readonly [P in keyof T]: T[P] }`
|
|
246
|
+
- Tuples: `type Point = [number, number]`
|
|
247
|
+
|
|
248
|
+
## Naming Conventions
|
|
249
|
+
|
|
250
|
+
### Interfaces
|
|
251
|
+
|
|
252
|
+
**Prefix interfaces with `I`:**
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// ✅ GOOD
|
|
256
|
+
export interface IProductService {
|
|
257
|
+
getById(id: string): Promise<IProduct>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface IProductRepository {
|
|
261
|
+
save(product: IProduct): Promise<void>;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ❌ BAD
|
|
265
|
+
export interface ProductService { ... } // Missing 'I' prefix
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Classes
|
|
269
|
+
|
|
270
|
+
**PascalCase for classes:**
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// ✅ GOOD
|
|
274
|
+
export class ProductService implements IProductService {
|
|
275
|
+
// ...
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export class ProductRepository implements IProductRepository {
|
|
279
|
+
// ...
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Files
|
|
284
|
+
|
|
285
|
+
**File names must match the primary export:**
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// File: IProductRepository.ts
|
|
289
|
+
export interface IProductRepository { ... }
|
|
290
|
+
|
|
291
|
+
// File: ProductRepository.ts
|
|
292
|
+
export class ProductRepository implements IProductRepository { ... }
|
|
293
|
+
|
|
294
|
+
// File: ProductService.ts
|
|
295
|
+
export class ProductService implements IProductService { ... }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Constants
|
|
299
|
+
|
|
300
|
+
**UPPER_SNAKE_CASE for constants:**
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
export const MAX_RETRY_ATTEMPTS = 3;
|
|
304
|
+
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
305
|
+
export const API_BASE_URL = 'https://api.example.com';
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Variables and Functions
|
|
309
|
+
|
|
310
|
+
**camelCase for variables and functions:**
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const productList: IProduct[] = [];
|
|
314
|
+
const userId: string = '123';
|
|
315
|
+
|
|
316
|
+
function calculateDiscount(price: number): number {
|
|
317
|
+
return price * 0.1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function fetchUserData(id: string): Promise<IUser> {
|
|
321
|
+
// ...
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## File Organization
|
|
326
|
+
|
|
327
|
+
### One Interface Per File
|
|
328
|
+
|
|
329
|
+
Each interface gets its own file:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// ✅ GOOD
|
|
333
|
+
// File: IProductService.ts
|
|
334
|
+
export interface IProductService {
|
|
335
|
+
getById(id: string): Promise<IProduct>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// File: IOrderService.ts
|
|
339
|
+
export interface IOrderService {
|
|
340
|
+
create(order: IOrder): Promise<string>;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// ❌ BAD - multiple interfaces in one file
|
|
346
|
+
// File: Services.ts
|
|
347
|
+
export interface IProductService { ... }
|
|
348
|
+
export interface IOrderService { ... }
|
|
349
|
+
export interface IUserService { ... }
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Exception:** Small, related interfaces can be together:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// File: IPaginationTypes.ts
|
|
356
|
+
export interface IPaginationRequest {
|
|
357
|
+
page: number;
|
|
358
|
+
pageSize: number;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface IPaginationResponse<T> {
|
|
362
|
+
items: T[];
|
|
363
|
+
total: number;
|
|
364
|
+
page: number;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Index Files for Clean Imports
|
|
369
|
+
|
|
370
|
+
Use `AgentEnums.ts` files to export from directories:
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// File: src/domain/interfaces/AgentEnums.ts
|
|
374
|
+
export { IProductRepository } from './IProductRepository';
|
|
375
|
+
export { IOrderRepository } from './IOrderRepository';
|
|
376
|
+
export { IUserRepository } from './IUserRepository';
|
|
377
|
+
|
|
378
|
+
// Now you can import as:
|
|
379
|
+
import { IProductRepository, IOrderRepository } from '@/domain/interfaces';
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Clean Architecture Implementation
|
|
383
|
+
|
|
384
|
+
### Repository Interface (Domain)
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// File: src/domain/interfaces/IProductRepository.ts
|
|
388
|
+
import { IProduct } from '../entities/IProduct';
|
|
389
|
+
|
|
390
|
+
export interface IProductRepository {
|
|
391
|
+
getById(id: string): Promise<IProduct | null>;
|
|
392
|
+
save(product: IProduct): Promise<void>;
|
|
393
|
+
delete(id: string): Promise<void>;
|
|
394
|
+
findByCategory(categoryId: string): Promise<IProduct[]>;
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Repository Implementation (Infrastructure)
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// File: src/infrastructure/repositories/ProductRepository.ts
|
|
402
|
+
import { IProductRepository } from '@/domain/interfaces/IProductRepository';
|
|
403
|
+
import { IProduct } from '@/domain/entities/IProduct';
|
|
404
|
+
import { IDatabaseService } from '../interfaces/IDatabaseService';
|
|
405
|
+
import { ProductNotFoundError } from '@/domain/errors/ProductNotFoundError';
|
|
406
|
+
|
|
407
|
+
export class ProductRepository implements IProductRepository {
|
|
408
|
+
constructor(private readonly db: IDatabaseService) {}
|
|
409
|
+
|
|
410
|
+
async getById(id: string): Promise<IProduct | null> {
|
|
411
|
+
try {
|
|
412
|
+
const doc = await this.db.readDocDataAtPath(`products/${id}`);
|
|
413
|
+
return doc ? (doc as IProduct) : null;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw new RepositoryError(`Failed to fetch product: ${id}`, { originalError: error });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async save(product: IProduct): Promise<void> {
|
|
420
|
+
await this.db.writeDocDataAtPath(`products/${product.id}`, product);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async delete(id: string): Promise<void> {
|
|
424
|
+
await this.db.deleteDocAtPath(`products/${id}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async findByCategory(categoryId: string): Promise<IProduct[]> {
|
|
428
|
+
const docs = await this.db.queryCollection('products', {
|
|
429
|
+
where: ['categoryId', '==', categoryId]
|
|
430
|
+
});
|
|
431
|
+
return docs as IProduct[];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Service (Application)
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// File: src/application/services/ProductService.ts
|
|
440
|
+
import { IProductService } from '../interfaces/IProductService';
|
|
441
|
+
import { IProductRepository } from '@/domain/interfaces/IProductRepository';
|
|
442
|
+
import { IProduct } from '@/domain/entities/IProduct';
|
|
443
|
+
import { ProductNotFoundError } from '@/domain/errors/ProductNotFoundError';
|
|
444
|
+
|
|
445
|
+
export class ProductService implements IProductService {
|
|
446
|
+
constructor(
|
|
447
|
+
private readonly productRepository: IProductRepository,
|
|
448
|
+
private readonly logger: ILogger
|
|
449
|
+
) {}
|
|
450
|
+
|
|
451
|
+
async getProduct(id: string): Promise<IProduct> {
|
|
452
|
+
const product = await this.productRepository.getById(id);
|
|
453
|
+
|
|
454
|
+
if (!product) {
|
|
455
|
+
this.logger.warn('Product not found', { productId: id });
|
|
456
|
+
throw new ProductNotFoundError(id);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
this.logger.info('Product retrieved', { productId: id });
|
|
460
|
+
return product;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async createProduct(data: CreateProductData): Promise<IProduct> {
|
|
464
|
+
// Validation
|
|
465
|
+
this.validateProductData(data);
|
|
466
|
+
|
|
467
|
+
// Create entity
|
|
468
|
+
const product: IProduct = {
|
|
469
|
+
id: generateId(),
|
|
470
|
+
...data,
|
|
471
|
+
createdAt: new Date(),
|
|
472
|
+
updatedAt: new Date()
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Save
|
|
476
|
+
await this.productRepository.save(product);
|
|
477
|
+
|
|
478
|
+
this.logger.info('Product created', { productId: product.id });
|
|
479
|
+
return product;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private validateProductData(data: CreateProductData): void {
|
|
483
|
+
if (!data.name || data.name.trim().length === 0) {
|
|
484
|
+
throw new ValidationError('Product name is required');
|
|
485
|
+
}
|
|
486
|
+
if (data.price <= 0) {
|
|
487
|
+
throw new ValidationError('Product price must be positive');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Constructor Injection
|
|
494
|
+
|
|
495
|
+
**ALWAYS use constructor injection for dependencies:**
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
// ✅ GOOD - Constructor injection
|
|
499
|
+
export class OrderService implements IOrderService {
|
|
500
|
+
constructor(
|
|
501
|
+
private readonly orderRepository: IOrderRepository,
|
|
502
|
+
private readonly productRepository: IProductRepository,
|
|
503
|
+
private readonly logger: ILogger
|
|
504
|
+
) {}
|
|
505
|
+
|
|
506
|
+
async createOrder(data: CreateOrderData): Promise<IOrder> {
|
|
507
|
+
// Use injected dependencies
|
|
508
|
+
const products = await this.productRepository.findByIds(data.productIds);
|
|
509
|
+
// ...
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ❌ BAD - Direct instantiation
|
|
514
|
+
export class OrderService implements IOrderService {
|
|
515
|
+
async createOrder(data: CreateOrderData): Promise<IOrder> {
|
|
516
|
+
const orderRepo = new OrderRepository(); // Hard to test!
|
|
517
|
+
// ...
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Readonly Dependencies
|
|
523
|
+
|
|
524
|
+
Mark injected dependencies as `readonly`:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
constructor(
|
|
528
|
+
private readonly orderRepository: IOrderRepository, // ✅ readonly
|
|
529
|
+
private readonly logger: ILogger // ✅ readonly
|
|
530
|
+
) {}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
This prevents accidental reassignment.
|
|
534
|
+
|
|
535
|
+
## Error Handling
|
|
536
|
+
|
|
537
|
+
### Domain Errors
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// File: src/domain/errors/DomainError.ts
|
|
541
|
+
export class DomainError extends Error {
|
|
542
|
+
constructor(
|
|
543
|
+
message: string,
|
|
544
|
+
public readonly statusCode: number = 500,
|
|
545
|
+
public readonly data?: Record<string, unknown>
|
|
546
|
+
) {
|
|
547
|
+
super(message);
|
|
548
|
+
this.name = this.constructor.name;
|
|
549
|
+
Error.captureStackTrace(this, this.constructor);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// File: src/domain/errors/ProductNotFoundError.ts
|
|
554
|
+
export class ProductNotFoundError extends DomainError {
|
|
555
|
+
constructor(productId: string) {
|
|
556
|
+
super(
|
|
557
|
+
`Product not found: ${productId}`,
|
|
558
|
+
404,
|
|
559
|
+
{ productId }
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Try-Catch with Proper Types
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// ✅ GOOD - Proper error handling
|
|
569
|
+
async function fetchProduct(id: string): Promise<IProduct> {
|
|
570
|
+
try {
|
|
571
|
+
const product = await productRepository.getById(id);
|
|
572
|
+
if (!product) {
|
|
573
|
+
throw new ProductNotFoundError(id);
|
|
574
|
+
}
|
|
575
|
+
return product;
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (error instanceof ProductNotFoundError) {
|
|
578
|
+
throw error; // Re-throw domain errors
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Transform unknown errors
|
|
582
|
+
logger.error('Unexpected error fetching product', {
|
|
583
|
+
error,
|
|
584
|
+
productId: id
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
throw new ServiceError(
|
|
588
|
+
'Failed to fetch product',
|
|
589
|
+
{ productId: id, originalError: error }
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## Async/Await
|
|
596
|
+
|
|
597
|
+
**ALWAYS use async/await over raw Promises:**
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// ✅ GOOD - async/await
|
|
601
|
+
async function getUser(id: string): Promise<IUser> {
|
|
602
|
+
const user = await userRepository.getById(id);
|
|
603
|
+
return user;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ❌ BAD - raw promises
|
|
607
|
+
function getUser(id: string): Promise<IUser> {
|
|
608
|
+
return userRepository.getById(id).then(user => {
|
|
609
|
+
return user;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### Parallel Operations
|
|
615
|
+
|
|
616
|
+
Use `Promise.all()` for parallel async operations:
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// ✅ GOOD - parallel execution
|
|
620
|
+
async function getUserWithOrders(userId: string): Promise<UserWithOrders> {
|
|
621
|
+
const [user, orders] = await Promise.all([
|
|
622
|
+
userRepository.getById(userId),
|
|
623
|
+
orderRepository.findByUser(userId)
|
|
624
|
+
]);
|
|
625
|
+
|
|
626
|
+
return { user, orders };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ❌ BAD - sequential (slower)
|
|
630
|
+
async function getUserWithOrders(userId: string): Promise<UserWithOrders> {
|
|
631
|
+
const user = await userRepository.getById(userId);
|
|
632
|
+
const orders = await orderRepository.findByUser(userId);
|
|
633
|
+
return { user, orders };
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Generics
|
|
638
|
+
|
|
639
|
+
Use generics for reusable, type-safe code:
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// Generic repository interface
|
|
643
|
+
export interface IRepository<T> {
|
|
644
|
+
getById(id: string): Promise<T | null>;
|
|
645
|
+
save(entity: T): Promise<void>;
|
|
646
|
+
delete(id: string): Promise<void>;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Specific implementations
|
|
650
|
+
export interface IProductRepository extends IRepository<IProduct> {
|
|
651
|
+
findByCategory(categoryId: string): Promise<IProduct[]>;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Generic function
|
|
655
|
+
function findById<T extends { id: string }>(
|
|
656
|
+
items: T[],
|
|
657
|
+
id: string
|
|
658
|
+
): T | undefined {
|
|
659
|
+
return items.find(item => item.id === id);
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## Utility Types
|
|
664
|
+
|
|
665
|
+
Use TypeScript utility types:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// Make all properties optional
|
|
669
|
+
type PartialProduct = Partial<IProduct>;
|
|
670
|
+
|
|
671
|
+
// Make all properties required
|
|
672
|
+
type RequiredProduct = Required<IProduct>;
|
|
673
|
+
|
|
674
|
+
// Make all properties readonly
|
|
675
|
+
type ReadonlyProduct = Readonly<IProduct>;
|
|
676
|
+
|
|
677
|
+
// Pick specific properties
|
|
678
|
+
type ProductSummary = Pick<IProduct, 'id' | 'name' | 'price'>;
|
|
679
|
+
|
|
680
|
+
// Omit specific properties
|
|
681
|
+
type ProductWithoutId = Omit<IProduct, 'id'>;
|
|
682
|
+
|
|
683
|
+
// Extract return type of function
|
|
684
|
+
type ProductData = ReturnType<typeof getProduct>;
|
|
685
|
+
|
|
686
|
+
// Extract parameters of function
|
|
687
|
+
type GetProductParams = Parameters<typeof getProduct>;
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
## Code Quality Checklist
|
|
691
|
+
|
|
692
|
+
- [ ] TypeScript strict mode enabled
|
|
693
|
+
- [ ] No `any` types used
|
|
694
|
+
- [ ] Explicit return types on public functions
|
|
695
|
+
- [ ] Interfaces prefixed with 'I'
|
|
696
|
+
- [ ] File names match primary export
|
|
697
|
+
- [ ] One interface per file
|
|
698
|
+
- [ ] Constructor injection used
|
|
699
|
+
- [ ] Dependencies marked as readonly
|
|
700
|
+
- [ ] Proper error handling with domain errors
|
|
701
|
+
- [ ] async/await preferred over raw promises
|
|
702
|
+
- [ ] Generics used where appropriate
|
|
703
|
+
- [ ] Index files for clean imports
|
|
704
|
+
|
|
705
|
+
## Tooling
|
|
706
|
+
|
|
707
|
+
### Recommended Tools
|
|
708
|
+
|
|
709
|
+
- **Compiler**: `tsc` - TypeScript compiler
|
|
710
|
+
- **Linter**: `ESLint` with TypeScript plugin
|
|
711
|
+
- **Formatter**: `Prettier`
|
|
712
|
+
- **Testing**: `Jest` with `ts-jest`
|
|
713
|
+
- **Build**: `tsc` or bundlers (webpack, rollup, esbuild)
|
|
714
|
+
|
|
715
|
+
### ESLint Configuration
|
|
716
|
+
|
|
717
|
+
```javascript
|
|
718
|
+
// .eslintrc.js
|
|
719
|
+
module.exports = {
|
|
720
|
+
parser: '@typescript-eslint/parser',
|
|
721
|
+
plugins: ['@typescript-eslint'],
|
|
722
|
+
extends: [
|
|
723
|
+
'eslint:recommended',
|
|
724
|
+
'plugin:@typescript-eslint/recommended',
|
|
725
|
+
],
|
|
726
|
+
rules: {
|
|
727
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
728
|
+
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|
729
|
+
'@typescript-eslint/no-unused-vars': 'error',
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Pre-commit Checks
|
|
735
|
+
|
|
736
|
+
```bash
|
|
737
|
+
# Run TypeScript compiler
|
|
738
|
+
npx tsc --noEmit
|
|
739
|
+
|
|
740
|
+
# Run linter
|
|
741
|
+
npx eslint "src/**/*.ts"
|
|
742
|
+
|
|
743
|
+
# Run formatter
|
|
744
|
+
npx prettier --check "src/**/*.ts"
|
|
745
|
+
|
|
746
|
+
# Run tests
|
|
747
|
+
npm test
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
For testing specifics, see `languages/typescript/testing.md`.
|
|
751
|
+
For universal architecture principles, see `shared/clean-architecture.md`.
|