@martinffx/atelier 1.2.2 → 1.4.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/.codex-plugin/plugin.json +27 -0
- package/CHANGELOG.md +33 -0
- package/README.md +51 -59
- package/agents/architect.md +1 -1
- package/agents/oracle.md +1 -1
- package/dist/atelier.js +14171 -12949
- package/package.json +6 -1
- package/skills/code-commit/SKILL.md +77 -77
- package/skills/code-handoff/SKILL.md +1 -1
- package/skills/code-pull-request/SKILL.md +36 -0
- package/skills/code-pull-request/references/common.md +76 -0
- package/skills/code-pull-request/references/create-pr.md +122 -0
- package/skills/code-pull-request/references/default-template.md +25 -0
- package/skills/code-pull-request/references/leave-comment.md +53 -0
- package/skills/code-pull-request/references/merge-pr.md +94 -0
- package/skills/code-pull-request/references/read-comments.md +61 -0
- package/skills/code-pull-request/references/respond-to-comment.md +66 -0
- package/skills/code-review/SKILL.md +21 -12
- package/skills/code-review/references/reviewers.md +142 -41
- package/skills/code-review/references/rq.md +48 -32
- package/skills/oracle-debug/SKILL.md +218 -0
- package/skills/{code-debug → oracle-debug}/references/bug-patterns.md +3 -3
- package/skills/oracle-domain-modelling/SKILL.md +84 -0
- package/skills/oracle-grill-me/SKILL.md +20 -0
- package/skills/spec-brainstorm/SKILL.md +91 -71
- package/skills/spec-finish/SKILL.md +20 -14
- package/skills/spec-implement/SKILL.md +3 -2
- package/skills/spec-orchestrator/SKILL.md +4 -15
- package/skills/spec-plan/SKILL.md +1 -1
- package/agents/scout.md +0 -40
- package/skills/code-commit/references/pr-workflow.md +0 -145
- package/skills/code-debug/SKILL.md +0 -199
- package/skills/code-docs/SKILL.md +0 -171
- package/skills/code-docs/references/api-docs.md +0 -116
- package/skills/code-docs/references/changelog.md +0 -64
- package/skills/code-docs/references/readme-structure.md +0 -94
- package/skills/oracle-architect/SKILL.md +0 -327
- package/skills/oracle-architect/references/api-design.md +0 -345
- package/skills/oracle-architect/references/data-modeling.md +0 -318
- package/skills/oracle-architect/references/ddd-patterns.md +0 -348
- package/skills/oracle-architect/references/interface-design.md +0 -203
- package/skills/oracle-challenge/SKILL.md +0 -126
- package/skills/oracle-doubt/SKILL.md +0 -252
- package/skills/oracle-doubt/references/adversarial-prompt.md +0 -47
- package/skills/oracle-grillme/SKILL.md +0 -160
- package/skills/oracle-security/SKILL.md +0 -225
- package/skills/oracle-security/references/security-patterns.md +0 -236
- package/skills/oracle-security/references/threat-modeling-guide.md +0 -137
- package/skills/oracle-testing/SKILL.md +0 -237
- package/skills/oracle-testing/references/anti-patterns.md +0 -618
- package/skills/oracle-testing/references/boundaries.md +0 -579
- package/skills/oracle-testing/references/core-testing.md +0 -691
- package/skills/oracle-testing/references/edge-testing.md +0 -794
- package/skills/oracle-testing/references/stub-driven-tdd.md +0 -322
- package/skills/oracle-thinkdeep/SKILL.md +0 -164
- /package/skills/{code-debug → oracle-debug}/references/language-tools.md +0 -0
- /package/skills/{code-debug → oracle-debug}/references/techniques.md +0 -0
- /package/skills/{oracle-grillme → oracle-domain-modelling}/references/ADR-FORMAT.md +0 -0
- /package/skills/{oracle-grillme → oracle-domain-modelling}/references/CONTEXT-FORMAT.md +0 -0
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
# API Design Reference
|
|
2
|
-
|
|
3
|
-
REST conventions, request/response contracts, and error handling patterns.
|
|
4
|
-
|
|
5
|
-
## REST Conventions
|
|
6
|
-
|
|
7
|
-
### Resource-Oriented Endpoints
|
|
8
|
-
|
|
9
|
-
**Standard CRUD operations:**
|
|
10
|
-
```
|
|
11
|
-
GET /orders # List orders
|
|
12
|
-
POST /orders # Create order
|
|
13
|
-
GET /orders/:id # Get single order
|
|
14
|
-
PATCH /orders/:id # Update order
|
|
15
|
-
DELETE /orders/:id # Delete order
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
**Nested resources:**
|
|
19
|
-
```
|
|
20
|
-
GET /orders/:orderId/items # List order items
|
|
21
|
-
POST /orders/:orderId/items # Add item to order
|
|
22
|
-
GET /orders/:orderId/items/:itemId # Get order item
|
|
23
|
-
PATCH /orders/:orderId/items/:itemId # Update order item
|
|
24
|
-
DELETE /orders/:orderId/items/:itemId # Delete order item
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Actions (Non-CRUD Operations)
|
|
28
|
-
|
|
29
|
-
**Use POST for state transitions:**
|
|
30
|
-
```
|
|
31
|
-
POST /orders/:id/cancel # Cancel order
|
|
32
|
-
POST /orders/:id/ship # Ship order
|
|
33
|
-
POST /orders/:id/confirm # Confirm order
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Reasoning:** These modify state but aren't CRUD operations. They represent domain actions.
|
|
37
|
-
|
|
38
|
-
**Avoid:**
|
|
39
|
-
```
|
|
40
|
-
PATCH /orders/:id { "action": "cancel" } # Unclear what changed
|
|
41
|
-
GET /orders/:id/cancel # GET shouldn't have side effects
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Request/Response Contracts
|
|
45
|
-
|
|
46
|
-
### Request Types
|
|
47
|
-
|
|
48
|
-
**Create request (POST /resource):**
|
|
49
|
-
```typescript
|
|
50
|
-
interface CreateOrderRequest {
|
|
51
|
-
customerId: string;
|
|
52
|
-
items: Array<{
|
|
53
|
-
productId: string;
|
|
54
|
-
quantity: number;
|
|
55
|
-
}>;
|
|
56
|
-
shippingAddress?: {
|
|
57
|
-
street: string;
|
|
58
|
-
city: string;
|
|
59
|
-
zipCode: string;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**Update request (PATCH /resource/:id):**
|
|
65
|
-
```typescript
|
|
66
|
-
interface UpdateOrderRequest {
|
|
67
|
-
status?: 'pending' | 'confirmed' | 'shipped';
|
|
68
|
-
shippingAddress?: Address;
|
|
69
|
-
// Only fields that can be updated
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**Query parameters:**
|
|
74
|
-
```
|
|
75
|
-
GET /orders?customerId=cust-123&status=pending&limit=20&offset=0
|
|
76
|
-
|
|
77
|
-
interface ListOrdersQuery {
|
|
78
|
-
customerId?: string;
|
|
79
|
-
status?: OrderStatus;
|
|
80
|
-
limit?: number; // Default 20, max 100
|
|
81
|
-
offset?: number; // Pagination offset
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Response Types
|
|
86
|
-
|
|
87
|
-
**Single resource (GET /resource/:id):**
|
|
88
|
-
```typescript
|
|
89
|
-
interface OrderResponse {
|
|
90
|
-
id: string;
|
|
91
|
-
customerId: string;
|
|
92
|
-
items: OrderItem[];
|
|
93
|
-
status: 'pending' | 'confirmed' | 'shipped' | 'delivered';
|
|
94
|
-
total: number;
|
|
95
|
-
createdAt: string; // ISO 8601
|
|
96
|
-
updatedAt: string;
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**List response (GET /resource):**
|
|
101
|
-
```typescript
|
|
102
|
-
interface ListOrdersResponse {
|
|
103
|
-
data: OrderResponse[];
|
|
104
|
-
pagination: {
|
|
105
|
-
total: number;
|
|
106
|
-
limit: number;
|
|
107
|
-
offset: number;
|
|
108
|
-
hasMore: boolean;
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
**Create response (POST /resource):**
|
|
114
|
-
```typescript
|
|
115
|
-
// Return created resource with 201 status
|
|
116
|
-
interface CreateOrderResponse extends OrderResponse {
|
|
117
|
-
id: string; // Generated by server
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Timestamp Conventions
|
|
122
|
-
|
|
123
|
-
**Use ISO 8601 for all timestamps:**
|
|
124
|
-
```json
|
|
125
|
-
{
|
|
126
|
-
"createdAt": "2024-01-21T10:30:45Z",
|
|
127
|
-
"updatedAt": "2024-01-21T10:30:45Z",
|
|
128
|
-
"deletedAt": null
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**Never:** Unix timestamps, custom formats, or timezone abbreviations
|
|
133
|
-
|
|
134
|
-
## Error Handling
|
|
135
|
-
|
|
136
|
-
### HTTP Status Codes
|
|
137
|
-
|
|
138
|
-
**Success:**
|
|
139
|
-
- `200 OK` - GET, PATCH succeeded
|
|
140
|
-
- `201 Created` - POST created resource
|
|
141
|
-
- `204 No Content` - DELETE or update with no response body
|
|
142
|
-
|
|
143
|
-
**Client Errors:**
|
|
144
|
-
- `400 Bad Request` - Invalid input validation
|
|
145
|
-
- `401 Unauthorized` - Missing authentication
|
|
146
|
-
- `403 Forbidden` - Authenticated but insufficient permissions
|
|
147
|
-
- `404 Not Found` - Resource not found
|
|
148
|
-
- `409 Conflict` - State violation (can't transition state)
|
|
149
|
-
- `422 Unprocessable Entity` - Validation failed
|
|
150
|
-
|
|
151
|
-
**Server Errors:**
|
|
152
|
-
- `500 Internal Server Error` - Unhandled exception
|
|
153
|
-
- `503 Service Unavailable` - Temporary outage
|
|
154
|
-
|
|
155
|
-
### Error Response Format
|
|
156
|
-
|
|
157
|
-
**Validation error (400):**
|
|
158
|
-
```typescript
|
|
159
|
-
interface ValidationErrorResponse {
|
|
160
|
-
error: 'Validation failed';
|
|
161
|
-
code: 'VALIDATION_ERROR';
|
|
162
|
-
details: {
|
|
163
|
-
field: string;
|
|
164
|
-
message: string;
|
|
165
|
-
}[];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Example
|
|
169
|
-
{
|
|
170
|
-
"error": "Validation failed",
|
|
171
|
-
"code": "VALIDATION_ERROR",
|
|
172
|
-
"details": [
|
|
173
|
-
{ "field": "items", "message": "Must have at least one item" },
|
|
174
|
-
{ "field": "customerId", "message": "Invalid customer ID format" }
|
|
175
|
-
]
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Business rule violation (409):**
|
|
180
|
-
```typescript
|
|
181
|
-
interface ConflictErrorResponse {
|
|
182
|
-
error: string;
|
|
183
|
-
code: string;
|
|
184
|
-
details?: Record<string, unknown>;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Example
|
|
188
|
-
{
|
|
189
|
-
"error": "Cannot cancel shipped order",
|
|
190
|
-
"code": "ORDER_ALREADY_SHIPPED",
|
|
191
|
-
"details": {
|
|
192
|
-
"status": "shipped",
|
|
193
|
-
"orderId": "order-123"
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
**Not found (404):**
|
|
199
|
-
```json
|
|
200
|
-
{
|
|
201
|
-
"error": "Order not found",
|
|
202
|
-
"code": "ORDER_NOT_FOUND",
|
|
203
|
-
"details": { "id": "order-123" }
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
**Generic error (500):**
|
|
208
|
-
```json
|
|
209
|
-
{
|
|
210
|
-
"error": "Internal server error",
|
|
211
|
-
"code": "INTERNAL_ERROR"
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Error Code Convention
|
|
216
|
-
|
|
217
|
-
Use SCREAMING_SNAKE_CASE for error codes:
|
|
218
|
-
- `VALIDATION_ERROR` - Input validation failed
|
|
219
|
-
- `ORDER_NOT_FOUND` - Resource not found
|
|
220
|
-
- `ORDER_ALREADY_SHIPPED` - State violation
|
|
221
|
-
- `INSUFFICIENT_INVENTORY` - Business rule violation
|
|
222
|
-
- `AUTHENTICATION_REQUIRED` - Auth failed
|
|
223
|
-
- `PERMISSION_DENIED` - Authz failed
|
|
224
|
-
|
|
225
|
-
## Versioning Patterns
|
|
226
|
-
|
|
227
|
-
### URL Versioning
|
|
228
|
-
|
|
229
|
-
**Include version in path:**
|
|
230
|
-
```
|
|
231
|
-
GET /v1/orders
|
|
232
|
-
GET /v2/orders
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**Pros:** Explicit, clear to consumers
|
|
236
|
-
**Cons:** Requires multiple code paths
|
|
237
|
-
|
|
238
|
-
### Accept Header Versioning
|
|
239
|
-
|
|
240
|
-
**Use media type version:**
|
|
241
|
-
```
|
|
242
|
-
GET /orders
|
|
243
|
-
Accept: application/vnd.myapi.v2+json
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Pros:** Cleaner URLs, single endpoint
|
|
247
|
-
**Cons:** Less discoverable
|
|
248
|
-
|
|
249
|
-
### Pragmatic Approach
|
|
250
|
-
|
|
251
|
-
1. Start without versioning (all responses same format)
|
|
252
|
-
2. Add versioning when you need breaking changes
|
|
253
|
-
3. Support at least 2 versions during transition
|
|
254
|
-
4. Provide clear deprecation timeline
|
|
255
|
-
|
|
256
|
-
### Backwards Compatibility
|
|
257
|
-
|
|
258
|
-
**Adding fields (safe):**
|
|
259
|
-
```typescript
|
|
260
|
-
// v1
|
|
261
|
-
interface OrderResponse {
|
|
262
|
-
id: string;
|
|
263
|
-
total: number;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// v2 (backwards compatible)
|
|
267
|
-
interface OrderResponse {
|
|
268
|
-
id: string;
|
|
269
|
-
total: number;
|
|
270
|
-
tax?: number; // New optional field
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
**Removing fields (breaking change):**
|
|
275
|
-
```typescript
|
|
276
|
-
// Requires new API version
|
|
277
|
-
// v1 has 'total'
|
|
278
|
-
// v2 has 'subtotal' and 'tax'
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
## Example: Complete Order API
|
|
282
|
-
|
|
283
|
-
### Endpoints
|
|
284
|
-
|
|
285
|
-
```
|
|
286
|
-
POST /orders
|
|
287
|
-
Create order
|
|
288
|
-
Request: CreateOrderRequest
|
|
289
|
-
Response: 201 OrderResponse
|
|
290
|
-
|
|
291
|
-
GET /orders
|
|
292
|
-
List orders
|
|
293
|
-
Query: ListOrdersQuery
|
|
294
|
-
Response: 200 ListOrdersResponse
|
|
295
|
-
|
|
296
|
-
GET /orders/:id
|
|
297
|
-
Get order
|
|
298
|
-
Response: 200 OrderResponse
|
|
299
|
-
|
|
300
|
-
PATCH /orders/:id
|
|
301
|
-
Update order
|
|
302
|
-
Request: UpdateOrderRequest
|
|
303
|
-
Response: 200 OrderResponse
|
|
304
|
-
|
|
305
|
-
POST /orders/:id/cancel
|
|
306
|
-
Cancel order
|
|
307
|
-
Response: 200 OrderResponse
|
|
308
|
-
|
|
309
|
-
DELETE /orders/:id
|
|
310
|
-
Delete order
|
|
311
|
-
Response: 204 (no body)
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Implementation Pattern
|
|
315
|
-
|
|
316
|
-
```typescript
|
|
317
|
-
// Router handles HTTP concerns
|
|
318
|
-
router.post('/orders', async (req, res) => {
|
|
319
|
-
// 1. Parse request
|
|
320
|
-
const request: CreateOrderRequest = req.body;
|
|
321
|
-
|
|
322
|
-
// 2. Validate input (preliminary)
|
|
323
|
-
if (!request.customerId) {
|
|
324
|
-
return res.status(400).json({
|
|
325
|
-
error: 'Validation failed',
|
|
326
|
-
code: 'VALIDATION_ERROR',
|
|
327
|
-
details: [{ field: 'customerId', message: 'Required' }]
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// 3. Call service
|
|
332
|
-
const result = await orderService.createOrder(request);
|
|
333
|
-
|
|
334
|
-
// 4. Handle result
|
|
335
|
-
if (result.ok) {
|
|
336
|
-
return res.status(201).json(result.value.toResponse());
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// 5. Return error
|
|
340
|
-
res.status(409).json({
|
|
341
|
-
error: result.error,
|
|
342
|
-
code: result.code || 'UNKNOWN_ERROR'
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
```
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
# Data Modeling Reference
|
|
2
|
-
|
|
3
|
-
Entity design principles, schema patterns, and access pattern optimization.
|
|
4
|
-
|
|
5
|
-
## Entity Design
|
|
6
|
-
|
|
7
|
-
### Design Process
|
|
8
|
-
1. Start with domain concepts from product requirements
|
|
9
|
-
2. Identify aggregates and their boundaries
|
|
10
|
-
3. Define attributes and relationships
|
|
11
|
-
4. Add validation rules and invariants
|
|
12
|
-
5. Include transformation methods (fromRequest, toRecord, toResponse)
|
|
13
|
-
|
|
14
|
-
### Entity Structure
|
|
15
|
-
|
|
16
|
-
**Core transformation methods:**
|
|
17
|
-
```typescript
|
|
18
|
-
class Order {
|
|
19
|
-
// Create from external request
|
|
20
|
-
static fromRequest(req: CreateOrderRequest): Order
|
|
21
|
-
|
|
22
|
-
// Create from database record
|
|
23
|
-
static fromRecord(record: OrderRecord): Order
|
|
24
|
-
|
|
25
|
-
// Convert to database record
|
|
26
|
-
toRecord(): OrderRecord
|
|
27
|
-
|
|
28
|
-
// Convert to API response
|
|
29
|
-
toResponse(): OrderResponse
|
|
30
|
-
|
|
31
|
-
// Validate invariants
|
|
32
|
-
validate(): Result<Order>
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Example Entity Design
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
class Order {
|
|
40
|
-
constructor(
|
|
41
|
-
public readonly id: string,
|
|
42
|
-
public readonly customerId: string,
|
|
43
|
-
public readonly items: OrderItem[],
|
|
44
|
-
public readonly status: OrderStatus,
|
|
45
|
-
public readonly total: number,
|
|
46
|
-
public readonly createdAt: Date,
|
|
47
|
-
public readonly metadata: OrderMetadata
|
|
48
|
-
) {}
|
|
49
|
-
|
|
50
|
-
static fromRequest(req: CreateOrderRequest): Order {
|
|
51
|
-
return new Order(
|
|
52
|
-
generateId(),
|
|
53
|
-
req.customerId,
|
|
54
|
-
req.items.map(i => new OrderItem(i)),
|
|
55
|
-
'pending',
|
|
56
|
-
req.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
|
|
57
|
-
new Date(),
|
|
58
|
-
{ source: req.source || 'web' }
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
toRecord(): OrderRecord {
|
|
63
|
-
return {
|
|
64
|
-
id: this.id,
|
|
65
|
-
customer_id: this.customerId,
|
|
66
|
-
items: JSON.stringify(this.items),
|
|
67
|
-
status: this.status,
|
|
68
|
-
total: this.total,
|
|
69
|
-
created_at: this.createdAt.toISOString(),
|
|
70
|
-
metadata: this.metadata
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
toResponse(): OrderResponse {
|
|
75
|
-
return {
|
|
76
|
-
id: this.id,
|
|
77
|
-
customerId: this.customerId,
|
|
78
|
-
items: this.items,
|
|
79
|
-
status: this.status,
|
|
80
|
-
total: this.total,
|
|
81
|
-
createdAt: this.createdAt.toISOString()
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
validate(): Result<Order> {
|
|
86
|
-
if (this.items.length === 0) {
|
|
87
|
-
return Err('Order must have at least one item');
|
|
88
|
-
}
|
|
89
|
-
if (this.total < 0) {
|
|
90
|
-
return Err('Order total cannot be negative');
|
|
91
|
-
}
|
|
92
|
-
return Ok(this);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Schema Patterns
|
|
98
|
-
|
|
99
|
-
### Relational Design
|
|
100
|
-
|
|
101
|
-
**Orders table:**
|
|
102
|
-
```sql
|
|
103
|
-
CREATE TABLE orders (
|
|
104
|
-
id UUID PRIMARY KEY,
|
|
105
|
-
customer_id UUID NOT NULL REFERENCES customers(id),
|
|
106
|
-
status VARCHAR(20) NOT NULL CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered')),
|
|
107
|
-
total DECIMAL(10,2) NOT NULL,
|
|
108
|
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
109
|
-
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
CREATE INDEX idx_orders_customer ON orders(customer_id);
|
|
113
|
-
CREATE INDEX idx_orders_status ON orders(status);
|
|
114
|
-
CREATE INDEX idx_orders_created ON orders(created_at DESC);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Order items (normalized):**
|
|
118
|
-
```sql
|
|
119
|
-
CREATE TABLE order_items (
|
|
120
|
-
id UUID PRIMARY KEY,
|
|
121
|
-
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
|
|
122
|
-
product_id UUID NOT NULL,
|
|
123
|
-
quantity INT NOT NULL CHECK (quantity > 0),
|
|
124
|
-
price DECIMAL(10,2) NOT NULL,
|
|
125
|
-
UNIQUE(order_id, product_id)
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
CREATE INDEX idx_order_items_order ON order_items(order_id);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### DynamoDB Single Table Design
|
|
132
|
-
|
|
133
|
-
**Items structure:**
|
|
134
|
-
```typescript
|
|
135
|
-
{
|
|
136
|
-
PK: 'ORDER#123',
|
|
137
|
-
SK: 'ORDER#123',
|
|
138
|
-
type: 'Order',
|
|
139
|
-
customerId: 'CUST#456',
|
|
140
|
-
status: 'pending',
|
|
141
|
-
total: 99.99,
|
|
142
|
-
items: [...],
|
|
143
|
-
createdAt: '2024-01-21T10:00:00Z',
|
|
144
|
-
|
|
145
|
-
// GSI for customer lookup
|
|
146
|
-
GSI1PK: 'CUST#456',
|
|
147
|
-
GSI1SK: 'ORDER#2024-01-21T10:00:00Z',
|
|
148
|
-
|
|
149
|
-
// GSI for status lookup
|
|
150
|
-
GSI2PK: 'pending',
|
|
151
|
-
GSI2SK: 'ORDER#2024-01-21T10:00:00Z'
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
PK: 'ORDER#123',
|
|
156
|
-
SK: 'ITEM#item-001',
|
|
157
|
-
type: 'OrderItem',
|
|
158
|
-
productId: 'PROD#999',
|
|
159
|
-
quantity: 2,
|
|
160
|
-
price: 49.99
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Key design:**
|
|
165
|
-
- PK = partition key (primary lookup)
|
|
166
|
-
- SK = sort key (range queries)
|
|
167
|
-
- GSI = global secondary index (alternative queries)
|
|
168
|
-
|
|
169
|
-
## Access Patterns
|
|
170
|
-
|
|
171
|
-
### Define Patterns First
|
|
172
|
-
|
|
173
|
-
List all data access patterns **before** designing schema:
|
|
174
|
-
|
|
175
|
-
1. **Find order by ID**
|
|
176
|
-
- Query: `PK = ORDER#123, SK = ORDER#123`
|
|
177
|
-
- Index: Primary key
|
|
178
|
-
|
|
179
|
-
2. **List orders by customer**
|
|
180
|
-
- Query: `GSI1PK = CUST#456`
|
|
181
|
-
- Index: Global secondary index
|
|
182
|
-
|
|
183
|
-
3. **List pending orders by date**
|
|
184
|
-
- Query: `GSI2PK = pending, GSI2SK > date`
|
|
185
|
-
- Index: Global secondary index sorted by date
|
|
186
|
-
|
|
187
|
-
4. **Count orders by status**
|
|
188
|
-
- Query: Scan with filter (or GSI)
|
|
189
|
-
- Performance: Consider status table
|
|
190
|
-
|
|
191
|
-
5. **Get recent orders for customer**
|
|
192
|
-
- Query: `GSI1PK = CUST#456` sorted by date DESC
|
|
193
|
-
- Index: Global secondary index
|
|
194
|
-
|
|
195
|
-
### Query Optimization
|
|
196
|
-
|
|
197
|
-
**N+1 Problem:**
|
|
198
|
-
```typescript
|
|
199
|
-
// BAD: N+1 queries
|
|
200
|
-
const orders = await orderRepo.findByCustomer(customerId);
|
|
201
|
-
const itemsPerOrder = await Promise.all(
|
|
202
|
-
orders.map(o => itemRepo.findByOrder(o.id)) // N queries
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// GOOD: Single query with denormalization
|
|
206
|
-
const orderData = await db.orders
|
|
207
|
-
.where({ customerId })
|
|
208
|
-
.select(['*', 'items']) // Includes denormalized items
|
|
209
|
-
.sort({ createdAt: 'DESC' });
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
**Index Coverage:**
|
|
213
|
-
```sql
|
|
214
|
-
-- Bad: Full table scan
|
|
215
|
-
SELECT * FROM orders WHERE customer_id = $1;
|
|
216
|
-
|
|
217
|
-
-- Good: Index-covered query
|
|
218
|
-
CREATE INDEX idx_orders_customer_status
|
|
219
|
-
ON orders(customer_id, status);
|
|
220
|
-
|
|
221
|
-
SELECT * FROM orders
|
|
222
|
-
WHERE customer_id = $1 AND status = 'pending';
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Data Transformation Patterns
|
|
226
|
-
|
|
227
|
-
### Request → Entity → Record → Response Flow
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
// 1. HTTP Request arrives
|
|
231
|
-
POST /orders
|
|
232
|
-
{
|
|
233
|
-
"customerId": "cust-123",
|
|
234
|
-
"items": [{ "productId": "prod-1", "quantity": 2 }]
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 2. Router parses to request type
|
|
238
|
-
const request: CreateOrderRequest = req.body;
|
|
239
|
-
|
|
240
|
-
// 3. Entity transforms from request (with ID generation)
|
|
241
|
-
const order = Order.fromRequest(request);
|
|
242
|
-
|
|
243
|
-
// 4. Entity validates business rules
|
|
244
|
-
const validation = order.validate();
|
|
245
|
-
if (!validation.ok) return error;
|
|
246
|
-
|
|
247
|
-
// 5. Service performs logic, uses repository
|
|
248
|
-
await repo.save(order.toRecord());
|
|
249
|
-
|
|
250
|
-
// 6. Repository converts to database record
|
|
251
|
-
{
|
|
252
|
-
id: "order-abc",
|
|
253
|
-
customer_id: "cust-123",
|
|
254
|
-
items: "[...]",
|
|
255
|
-
status: "pending",
|
|
256
|
-
created_at: "2024-01-21T10:00:00Z"
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 7. Response converts from record
|
|
260
|
-
const response = order.toResponse();
|
|
261
|
-
|
|
262
|
-
// 8. HTTP Response sends
|
|
263
|
-
{
|
|
264
|
-
"id": "order-abc",
|
|
265
|
-
"customerId": "cust-123",
|
|
266
|
-
"items": [...],
|
|
267
|
-
"status": "pending"
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Transformation Considerations
|
|
272
|
-
|
|
273
|
-
**Immutability:**
|
|
274
|
-
```typescript
|
|
275
|
-
// Bad: Mutation during transform
|
|
276
|
-
toRecord(): OrderRecord {
|
|
277
|
-
this.items.forEach(i => i.status = 'recorded');
|
|
278
|
-
return { items: this.items };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Good: No side effects
|
|
282
|
-
toRecord(): OrderRecord {
|
|
283
|
-
return {
|
|
284
|
-
items: this.items.map(i => ({
|
|
285
|
-
...i,
|
|
286
|
-
// Transform properties as needed
|
|
287
|
-
}))
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
**Versioning:**
|
|
293
|
-
```typescript
|
|
294
|
-
// Handle multiple API versions
|
|
295
|
-
toResponse(version: 'v1' | 'v2'): OrderResponse {
|
|
296
|
-
const base = {
|
|
297
|
-
id: this.id,
|
|
298
|
-
customerId: this.customerId,
|
|
299
|
-
status: this.status
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
if (version === 'v2') {
|
|
303
|
-
return { ...base, total: this.total };
|
|
304
|
-
}
|
|
305
|
-
return base;
|
|
306
|
-
}
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
**Partial Loading:**
|
|
310
|
-
```typescript
|
|
311
|
-
// Only load fields needed for the operation
|
|
312
|
-
async findOrderSummary(id: string): Promise<OrderSummary> {
|
|
313
|
-
const record = await db.orders
|
|
314
|
-
.select(['id', 'status', 'total'])
|
|
315
|
-
.findOne({ id });
|
|
316
|
-
return { id: record.id, status: record.status, total: record.total };
|
|
317
|
-
}
|
|
318
|
-
```
|