@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,382 @@
|
|
|
1
|
+
# Layered Architecture Pattern
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Organize code into horizontal layers with clear responsibilities. Common in MVC and enterprise applications.
|
|
5
|
+
|
|
6
|
+
## Layer Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
┌─────────────────────────────────┐
|
|
10
|
+
│ Presentation Layer (UI/API) │ ← User interaction, HTTP/GraphQL
|
|
11
|
+
├─────────────────────────────────┤
|
|
12
|
+
│ Application/Service Layer │ ← Business logic orchestration
|
|
13
|
+
├─────────────────────────────────┤
|
|
14
|
+
│ Domain/Business Layer │ ← Core business rules
|
|
15
|
+
├─────────────────────────────────┤
|
|
16
|
+
│ Data Access Layer (DAL) │ ← Database, external APIs
|
|
17
|
+
└─────────────────────────────────┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Layer Responsibilities
|
|
21
|
+
|
|
22
|
+
### 1. Presentation Layer (Controllers, Views, Components)
|
|
23
|
+
**Handle user input, render UI, expose APIs.**
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// ✅ Controller handles HTTP, delegates to service
|
|
27
|
+
@Controller('/api/products')
|
|
28
|
+
export class ProductController {
|
|
29
|
+
constructor(private productService: ProductService) {}
|
|
30
|
+
|
|
31
|
+
@Get()
|
|
32
|
+
async getAll(@Query() filters: ProductFilters): Promise<ProductDTO[]> {
|
|
33
|
+
const products = await this.productService.findAll(filters);
|
|
34
|
+
return products.map(p => this.toDTO(p));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Post()
|
|
38
|
+
async create(@Body() dto: CreateProductDTO): Promise<ProductDTO> {
|
|
39
|
+
const product = await this.productService.create(dto);
|
|
40
|
+
return this.toDTO(product);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private toDTO(product: Product): ProductDTO {
|
|
44
|
+
return {
|
|
45
|
+
id: product.id,
|
|
46
|
+
name: product.name,
|
|
47
|
+
price: product.price.amount,
|
|
48
|
+
currency: product.price.currency,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Service Layer (Business Logic)
|
|
55
|
+
**Orchestrate business operations, coordinate between domain and data layers.**
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// ✅ Service contains business logic, uses repositories
|
|
59
|
+
export class ProductService {
|
|
60
|
+
constructor(
|
|
61
|
+
private productRepository: ProductRepository,
|
|
62
|
+
private inventoryService: InventoryService,
|
|
63
|
+
private eventBus: EventBus
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
async create(dto: CreateProductDTO): Promise<Product> {
|
|
67
|
+
// Business validation
|
|
68
|
+
if (dto.price <= 0) {
|
|
69
|
+
throw new BusinessError('Price must be positive');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create domain object
|
|
73
|
+
const product = new Product(
|
|
74
|
+
generateId(),
|
|
75
|
+
dto.name,
|
|
76
|
+
new Money(dto.price, dto.currency)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Persist
|
|
80
|
+
await this.productRepository.save(product);
|
|
81
|
+
|
|
82
|
+
// Business side effects
|
|
83
|
+
await this.inventoryService.initializeStock(product.id, dto.initialStock);
|
|
84
|
+
await this.eventBus.publish(new ProductCreatedEvent(product));
|
|
85
|
+
|
|
86
|
+
return product;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async findAll(filters: ProductFilters): Promise<Product[]> {
|
|
90
|
+
// Business logic for filtering
|
|
91
|
+
const products = await this.productRepository.findMany(filters);
|
|
92
|
+
|
|
93
|
+
// Business rule: filter out discontinued products unless explicitly requested
|
|
94
|
+
if (!filters.includeDiscontinued) {
|
|
95
|
+
return products.filter(p => !p.isDiscontinued);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return products;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async updatePrice(id: string, newPrice: Money): Promise<Product> {
|
|
102
|
+
const product = await this.productRepository.findById(id);
|
|
103
|
+
if (!product) {
|
|
104
|
+
throw new NotFoundError('Product not found');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Business rule: price change audit
|
|
108
|
+
if (product.price.amount !== newPrice.amount) {
|
|
109
|
+
await this.auditLog.recordPriceChange(id, product.price, newPrice);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
product.updatePrice(newPrice);
|
|
113
|
+
await this.productRepository.save(product);
|
|
114
|
+
|
|
115
|
+
return product;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 3. Domain Layer (Business Models)
|
|
121
|
+
**Core business entities and rules. Framework-independent.**
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// ✅ Domain model with business rules
|
|
125
|
+
export class Product {
|
|
126
|
+
constructor(
|
|
127
|
+
public readonly id: string,
|
|
128
|
+
private _name: string,
|
|
129
|
+
private _price: Money,
|
|
130
|
+
private _isDiscontinued: boolean = false
|
|
131
|
+
) {}
|
|
132
|
+
|
|
133
|
+
get name(): string {
|
|
134
|
+
return this._name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get price(): Money {
|
|
138
|
+
return this._price;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get isDiscontinued(): boolean {
|
|
142
|
+
return this._isDiscontinued;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
updatePrice(newPrice: Money): void {
|
|
146
|
+
// Business rule: validate price
|
|
147
|
+
if (newPrice.amount <= 0) {
|
|
148
|
+
throw new Error('Price must be positive');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Business rule: prevent large price changes
|
|
152
|
+
const changePercent = Math.abs(
|
|
153
|
+
(newPrice.amount - this._price.amount) / this._price.amount
|
|
154
|
+
);
|
|
155
|
+
if (changePercent > 0.5) {
|
|
156
|
+
throw new Error('Price change exceeds 50% threshold');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this._price = newPrice;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
discontinue(): void {
|
|
163
|
+
// Business rule: cannot discontinue if already discontinued
|
|
164
|
+
if (this._isDiscontinued) {
|
|
165
|
+
throw new Error('Product already discontinued');
|
|
166
|
+
}
|
|
167
|
+
this._isDiscontinued = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
rename(newName: string): void {
|
|
171
|
+
// Business rule: validate name
|
|
172
|
+
if (newName.trim().length < 3) {
|
|
173
|
+
throw new Error('Name must be at least 3 characters');
|
|
174
|
+
}
|
|
175
|
+
this._name = newName;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ✅ Value object
|
|
180
|
+
export class Money {
|
|
181
|
+
constructor(
|
|
182
|
+
public readonly amount: number,
|
|
183
|
+
public readonly currency: string
|
|
184
|
+
) {
|
|
185
|
+
if (amount < 0) {
|
|
186
|
+
throw new Error('Amount cannot be negative');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
add(other: Money): Money {
|
|
191
|
+
if (this.currency !== other.currency) {
|
|
192
|
+
throw new Error('Cannot add different currencies');
|
|
193
|
+
}
|
|
194
|
+
return new Money(this.amount + other.amount, this.currency);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 4. Data Access Layer (Repositories)
|
|
200
|
+
**Database operations, external API calls, caching.**
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// ✅ Repository handles data persistence
|
|
204
|
+
export class ProductRepository {
|
|
205
|
+
constructor(private db: Database) {}
|
|
206
|
+
|
|
207
|
+
async save(product: Product): Promise<void> {
|
|
208
|
+
await this.db.query(
|
|
209
|
+
`INSERT INTO products (id, name, price_amount, price_currency, is_discontinued)
|
|
210
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
211
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
212
|
+
name = $2,
|
|
213
|
+
price_amount = $3,
|
|
214
|
+
price_currency = $4,
|
|
215
|
+
is_discontinued = $5`,
|
|
216
|
+
[product.id, product.name, product.price.amount, product.price.currency, product.isDiscontinued]
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async findById(id: string): Promise<Product | null> {
|
|
221
|
+
const row = await this.db.queryOne(
|
|
222
|
+
'SELECT * FROM products WHERE id = $1',
|
|
223
|
+
[id]
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (!row) return null;
|
|
227
|
+
|
|
228
|
+
return this.toDomain(row);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async findMany(filters: ProductFilters): Promise<Product[]> {
|
|
232
|
+
const query = this.buildFilterQuery(filters);
|
|
233
|
+
const rows = await this.db.query(query.sql, query.params);
|
|
234
|
+
return rows.map(row => this.toDomain(row));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private toDomain(row: any): Product {
|
|
238
|
+
return new Product(
|
|
239
|
+
row.id,
|
|
240
|
+
row.name,
|
|
241
|
+
new Money(row.price_amount, row.price_currency),
|
|
242
|
+
row.is_discontinued
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private buildFilterQuery(filters: ProductFilters): { sql: string; params: any[] } {
|
|
247
|
+
// Build dynamic SQL based on filters
|
|
248
|
+
// ...
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## File Organization
|
|
254
|
+
|
|
255
|
+
### Backend (Node.js/Express/NestJS)
|
|
256
|
+
```
|
|
257
|
+
src/
|
|
258
|
+
├── presentation/ # Controllers, DTOs, validators
|
|
259
|
+
│ ├── controllers/
|
|
260
|
+
│ │ ├── ProductController.ts
|
|
261
|
+
│ │ └── OrderController.ts
|
|
262
|
+
│ ├── dto/
|
|
263
|
+
│ │ ├── CreateProductDTO.ts
|
|
264
|
+
│ │ └── ProductDTO.ts
|
|
265
|
+
│ └── validators/
|
|
266
|
+
│ └── ProductValidator.ts
|
|
267
|
+
│
|
|
268
|
+
├── application/ # Services, use cases
|
|
269
|
+
│ ├── services/
|
|
270
|
+
│ │ ├── ProductService.ts
|
|
271
|
+
│ │ └── OrderService.ts
|
|
272
|
+
│ └── interfaces/
|
|
273
|
+
│ └── IProductRepository.ts
|
|
274
|
+
│
|
|
275
|
+
├── domain/ # Business models, value objects
|
|
276
|
+
│ ├── models/
|
|
277
|
+
│ │ ├── Product.ts
|
|
278
|
+
│ │ └── Order.ts
|
|
279
|
+
│ └── value-objects/
|
|
280
|
+
│ └── Money.ts
|
|
281
|
+
│
|
|
282
|
+
└── infrastructure/ # Repositories, database, external APIs
|
|
283
|
+
├── repositories/
|
|
284
|
+
│ ├── ProductRepository.ts
|
|
285
|
+
│ └── OrderRepository.ts
|
|
286
|
+
├── database/
|
|
287
|
+
│ └── connection.ts
|
|
288
|
+
└── external/
|
|
289
|
+
└── PaymentGateway.ts
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Frontend (React/Vue)
|
|
293
|
+
```
|
|
294
|
+
src/
|
|
295
|
+
├── presentation/ # Components (UI only)
|
|
296
|
+
│ ├── components/
|
|
297
|
+
│ │ ├── ProductCard.tsx
|
|
298
|
+
│ │ └── ProductList.tsx
|
|
299
|
+
│ └── pages/
|
|
300
|
+
│ └── ProductsPage.tsx
|
|
301
|
+
│
|
|
302
|
+
├── application/ # Hooks/Composables (business logic)
|
|
303
|
+
│ ├── hooks/ # React
|
|
304
|
+
│ │ ├── useProducts.ts
|
|
305
|
+
│ │ └── useCart.ts
|
|
306
|
+
│ └── composables/ # Vue
|
|
307
|
+
│ ├── useProducts.ts
|
|
308
|
+
│ └── useCart.ts
|
|
309
|
+
│
|
|
310
|
+
├── domain/ # Business models (client-side)
|
|
311
|
+
│ ├── models/
|
|
312
|
+
│ │ ├── Product.ts
|
|
313
|
+
│ │ └── Cart.ts
|
|
314
|
+
│ └── value-objects/
|
|
315
|
+
│ └── CartItem.ts
|
|
316
|
+
│
|
|
317
|
+
└── infrastructure/ # API clients, storage
|
|
318
|
+
├── api/
|
|
319
|
+
│ └── ProductApiClient.ts
|
|
320
|
+
└── storage/
|
|
321
|
+
└── LocalStorageCart.ts
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Testing Strategy
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// ✅ Test presentation layer (mock service)
|
|
328
|
+
describe('ProductController', () => {
|
|
329
|
+
it('returns all products', async () => {
|
|
330
|
+
const mockService = { findAll: jest.fn().mockResolvedValue([]) };
|
|
331
|
+
const controller = new ProductController(mockService);
|
|
332
|
+
const result = await controller.getAll({});
|
|
333
|
+
expect(result).toEqual([]);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ✅ Test service layer (mock repository)
|
|
338
|
+
describe('ProductService', () => {
|
|
339
|
+
it('creates product and triggers events', async () => {
|
|
340
|
+
const mockRepo = { save: jest.fn() };
|
|
341
|
+
const mockEvents = { publish: jest.fn() };
|
|
342
|
+
const service = new ProductService(mockRepo, mockEvents);
|
|
343
|
+
|
|
344
|
+
await service.create({ name: 'Test', price: 100 });
|
|
345
|
+
|
|
346
|
+
expect(mockRepo.save).toHaveBeenCalled();
|
|
347
|
+
expect(mockEvents.publish).toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ✅ Test domain layer (no mocks - pure logic)
|
|
352
|
+
describe('Product', () => {
|
|
353
|
+
it('prevents negative prices', () => {
|
|
354
|
+
const product = new Product('1', 'Test', new Money(100, 'USD'));
|
|
355
|
+
expect(() => product.updatePrice(new Money(-10, 'USD'))).toThrow();
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ✅ Test data layer (integration test with real DB)
|
|
360
|
+
describe('ProductRepository', () => {
|
|
361
|
+
it('persists and retrieves product', async () => {
|
|
362
|
+
const repo = new ProductRepository(testDb);
|
|
363
|
+
const product = new Product('1', 'Test', new Money(100, 'USD'));
|
|
364
|
+
|
|
365
|
+
await repo.save(product);
|
|
366
|
+
const retrieved = await repo.findById('1');
|
|
367
|
+
|
|
368
|
+
expect(retrieved?.name).toBe('Test');
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Rules Summary
|
|
374
|
+
|
|
375
|
+
- **ALWAYS** keep presentation logic separate from business logic
|
|
376
|
+
- **ALWAYS** put business rules in service or domain layer, not controllers
|
|
377
|
+
- **ALWAYS** make repositories responsible for data access only
|
|
378
|
+
- **ALWAYS** use DTOs to decouple API contracts from domain models
|
|
379
|
+
- **NEVER** access database directly from controllers
|
|
380
|
+
- **NEVER** put business logic in repositories
|
|
381
|
+
- **PREFER** thin controllers, fat services
|
|
382
|
+
- **PREFER** dependency injection for testability
|