@martinffx/atelier 2.0.0 → 3.0.1
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/CHANGELOG.md +21 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
# Error Handling Patterns
|
|
2
|
-
|
|
3
|
-
Comprehensive error handling for DynamoDB operations with domain error mapping.
|
|
4
|
-
|
|
5
|
-
## DynamoDB Error Types
|
|
6
|
-
|
|
7
|
-
### ConditionalCheckFailedException
|
|
8
|
-
|
|
9
|
-
Thrown when a conditional expression evaluates to false.
|
|
10
|
-
|
|
11
|
-
**Common Causes:**
|
|
12
|
-
- Create operation: Item already exists (`exists: false` failed)
|
|
13
|
-
- Update operation: Item doesn't exist (`exists: true` failed)
|
|
14
|
-
- Custom conditions: Business rule validation failed
|
|
15
|
-
|
|
16
|
-
**From AWS SDK:**
|
|
17
|
-
```typescript
|
|
18
|
-
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb'
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### DynamoDBToolboxError
|
|
22
|
-
|
|
23
|
-
Thrown by dynamodb-toolbox for schema validation errors.
|
|
24
|
-
|
|
25
|
-
**Common Causes:**
|
|
26
|
-
- Required field missing
|
|
27
|
-
- Type mismatch (string vs number)
|
|
28
|
-
- Validation function returned false
|
|
29
|
-
- Invalid attribute name
|
|
30
|
-
|
|
31
|
-
**Properties:**
|
|
32
|
-
- `path`: Field path that failed validation
|
|
33
|
-
- `message`: Error description
|
|
34
|
-
|
|
35
|
-
### TransactionCanceledException
|
|
36
|
-
|
|
37
|
-
Thrown when a transaction fails.
|
|
38
|
-
|
|
39
|
-
**Key Property:**
|
|
40
|
-
- `CancellationReasons`: Array explaining why each operation failed
|
|
41
|
-
|
|
42
|
-
## Domain Error Classes
|
|
43
|
-
|
|
44
|
-
Define domain-specific errors that clients can handle meaningfully.
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
// src/shared/errors.ts
|
|
48
|
-
|
|
49
|
-
export class DuplicateEntityError extends Error {
|
|
50
|
-
public readonly entityType: string
|
|
51
|
-
public readonly entityKey: string
|
|
52
|
-
|
|
53
|
-
constructor(entityType: string, entityKey: string) {
|
|
54
|
-
super(`${entityType} '${entityKey}' already exists`)
|
|
55
|
-
this.name = "DuplicateEntityError"
|
|
56
|
-
this.entityType = entityType
|
|
57
|
-
this.entityKey = entityKey
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export class EntityNotFoundError extends Error {
|
|
62
|
-
public readonly entityType: string
|
|
63
|
-
public readonly entityKey: string
|
|
64
|
-
|
|
65
|
-
constructor(entityType: string, entityKey: string) {
|
|
66
|
-
super(`${entityType} '${entityKey}' not found`)
|
|
67
|
-
this.name = "EntityNotFoundError"
|
|
68
|
-
this.entityType = entityType
|
|
69
|
-
this.entityKey = entityKey
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export class ValidationError extends Error {
|
|
74
|
-
public readonly field: string
|
|
75
|
-
|
|
76
|
-
constructor(field: string, message: string) {
|
|
77
|
-
super(`Validation failed for '${field}': ${message}`)
|
|
78
|
-
this.name = "ValidationError"
|
|
79
|
-
this.field = field
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
## Create Operation Error Handling
|
|
85
|
-
|
|
86
|
-
**Context:** Creating a new entity with duplicate prevention
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
import { PutItemCommand } from 'dynamodb-toolbox'
|
|
90
|
-
import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb'
|
|
91
|
-
import { DynamoDBToolboxError } from 'dynamodb-toolbox'
|
|
92
|
-
|
|
93
|
-
async create(user: UserEntity): Promise<UserEntity> {
|
|
94
|
-
try {
|
|
95
|
-
const result = await this.entity
|
|
96
|
-
.build(PutItemCommand)
|
|
97
|
-
.item(user.toRecord())
|
|
98
|
-
.options({
|
|
99
|
-
condition: { attr: "PK", exists: false } // Prevent duplicates
|
|
100
|
-
})
|
|
101
|
-
.send()
|
|
102
|
-
|
|
103
|
-
return UserEntity.fromRecord(result.ToolboxItem)
|
|
104
|
-
} catch (error: unknown) {
|
|
105
|
-
// Pattern: ConditionalCheckFailed → DuplicateEntityError
|
|
106
|
-
if (error instanceof ConditionalCheckFailedException) {
|
|
107
|
-
throw new DuplicateEntityError("UserEntity", user.username)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Pattern: Schema validation → ValidationError
|
|
111
|
-
if (error instanceof DynamoDBToolboxError) {
|
|
112
|
-
throw new ValidationError(
|
|
113
|
-
error.path ?? "entity",
|
|
114
|
-
error.message
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Unknown errors: re-throw
|
|
119
|
-
throw error
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Key Points:**
|
|
125
|
-
- `ConditionalCheckFailedException` → Item already exists
|
|
126
|
-
- Map to `DuplicateEntityError` with entity type and key
|
|
127
|
-
- Client can catch and show user-friendly message: "Username 'alice' already taken"
|
|
128
|
-
|
|
129
|
-
## Update Operation Error Handling
|
|
130
|
-
|
|
131
|
-
**Context:** Updating an existing entity with existence check
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
async update(user: UserEntity): Promise<UserEntity> {
|
|
135
|
-
try {
|
|
136
|
-
const result = await this.entity
|
|
137
|
-
.build(PutItemCommand)
|
|
138
|
-
.item(user.toRecord())
|
|
139
|
-
.options({
|
|
140
|
-
condition: { attr: "PK", exists: true } // Must exist
|
|
141
|
-
})
|
|
142
|
-
.send()
|
|
143
|
-
|
|
144
|
-
return UserEntity.fromRecord(result.ToolboxItem)
|
|
145
|
-
} catch (error: unknown) {
|
|
146
|
-
// Pattern: ConditionalCheckFailed → EntityNotFoundError
|
|
147
|
-
if (error instanceof ConditionalCheckFailedException) {
|
|
148
|
-
throw new EntityNotFoundError("UserEntity", user.username)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (error instanceof DynamoDBToolboxError) {
|
|
152
|
-
throw new ValidationError(error.path ?? "entity", error.message)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
throw error
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
**Same Exception, Different Meaning:**
|
|
161
|
-
- Create: `ConditionalCheckFailed` → "already exists"
|
|
162
|
-
- Update: `ConditionalCheckFailed` → "not found"
|
|
163
|
-
|
|
164
|
-
Context determines the domain error mapping.
|
|
165
|
-
|
|
166
|
-
## Transaction Error Handling
|
|
167
|
-
|
|
168
|
-
**Context:** Multi-entity transaction (create issue, check repo exists)
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
import { TransactionCanceledException } from '@aws-sdk/client-dynamodb'
|
|
172
|
-
|
|
173
|
-
async createIssue(issue: IssueEntity): Promise<IssueEntity> {
|
|
174
|
-
try {
|
|
175
|
-
// Transaction: PutTransaction + ConditionCheck
|
|
176
|
-
await execute(putIssueTransaction, repoCheckTransaction)
|
|
177
|
-
|
|
178
|
-
return await this.get(/* ... */)
|
|
179
|
-
} catch (error: unknown) {
|
|
180
|
-
handleTransactionError(error, {
|
|
181
|
-
entityType: "IssueEntity",
|
|
182
|
-
entityKey: issue.getEntityKey(),
|
|
183
|
-
parentEntityType: "RepositoryEntity",
|
|
184
|
-
parentEntityKey: issue.getParentEntityKey(),
|
|
185
|
-
operationName: "issue",
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Transaction Error Handler
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
function handleTransactionError(
|
|
195
|
-
error: unknown,
|
|
196
|
-
options: {
|
|
197
|
-
entityType: string
|
|
198
|
-
entityKey: string
|
|
199
|
-
parentEntityType: string
|
|
200
|
-
parentEntityKey: string
|
|
201
|
-
operationName: string
|
|
202
|
-
}
|
|
203
|
-
): never {
|
|
204
|
-
if (error instanceof TransactionCanceledException) {
|
|
205
|
-
const reasons = error.CancellationReasons || []
|
|
206
|
-
|
|
207
|
-
// Validate we have expected number of cancellation reasons
|
|
208
|
-
if (reasons.length < 2) {
|
|
209
|
-
throw new ValidationError(
|
|
210
|
-
"transaction",
|
|
211
|
-
`Transaction failed with unexpected cancellation reason count: ${reasons.length}`
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Index 0: The entity create/put operation
|
|
216
|
-
if (reasons[0]?.Code === "ConditionalCheckFailed") {
|
|
217
|
-
throw new DuplicateEntityError(options.entityType, options.entityKey)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Index 1: The parent existence check
|
|
221
|
-
if (reasons[1]?.Code === "ConditionalCheckFailed") {
|
|
222
|
-
throw new EntityNotFoundError(
|
|
223
|
-
options.parentEntityType,
|
|
224
|
-
options.parentEntityKey
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Unknown transaction failure
|
|
229
|
-
throw new ValidationError(
|
|
230
|
-
options.operationName,
|
|
231
|
-
`Failed to create ${options.operationName} due to transaction conflict`
|
|
232
|
-
)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (error instanceof DynamoDBToolboxError) {
|
|
236
|
-
throw new ValidationError(error.path ?? "entity", error.message)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
throw error
|
|
240
|
-
}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
**CancellationReasons Structure:**
|
|
244
|
-
```typescript
|
|
245
|
-
[
|
|
246
|
-
{ Code: "ConditionalCheckFailed" }, // Index 0: PutTransaction failed
|
|
247
|
-
{ Code: "ConditionalCheckFailed" }, // Index 1: ConditionCheck failed
|
|
248
|
-
]
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
**Mapping:**
|
|
252
|
-
- `reasons[0]` failed → Entity already exists (DuplicateEntityError)
|
|
253
|
-
- `reasons[1]` failed → Parent doesn't exist (EntityNotFoundError)
|
|
254
|
-
|
|
255
|
-
**Order Matters:** CancellationReasons array matches the order operations were passed to `execute()`.
|
|
256
|
-
|
|
257
|
-
## Three-Way Transaction Error Handling
|
|
258
|
-
|
|
259
|
-
**Context:** Create star with user + repo validation
|
|
260
|
-
|
|
261
|
-
```typescript
|
|
262
|
-
async createStar(star: StarEntity): Promise<StarEntity> {
|
|
263
|
-
try {
|
|
264
|
-
// Transaction: PutTransaction + UserCheck + RepoCheck
|
|
265
|
-
await execute(putStarTransaction, userCheck, repoCheck)
|
|
266
|
-
|
|
267
|
-
return await this.get(/* ... */)
|
|
268
|
-
} catch (error: unknown) {
|
|
269
|
-
handleThreeWayTransactionError(error, star)
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function handleThreeWayTransactionError(
|
|
274
|
-
error: unknown,
|
|
275
|
-
star: StarEntity
|
|
276
|
-
): never {
|
|
277
|
-
if (error instanceof TransactionCanceledException) {
|
|
278
|
-
const reasons = error.CancellationReasons || []
|
|
279
|
-
|
|
280
|
-
if (reasons.length < 3) {
|
|
281
|
-
throw new ValidationError(
|
|
282
|
-
"transaction",
|
|
283
|
-
`Expected 3 cancellation reasons, got ${reasons.length}`
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Index 0: Star creation
|
|
288
|
-
if (reasons[0]?.Code === "ConditionalCheckFailed") {
|
|
289
|
-
throw new DuplicateEntityError(
|
|
290
|
-
"StarEntity",
|
|
291
|
-
star.getEntityKey()
|
|
292
|
-
)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Index 1: User existence check
|
|
296
|
-
if (reasons[1]?.Code === "ConditionalCheckFailed") {
|
|
297
|
-
throw new EntityNotFoundError("UserEntity", star.username)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Index 2: Repo existence check
|
|
301
|
-
if (reasons[2]?.Code === "ConditionalCheckFailed") {
|
|
302
|
-
throw new EntityNotFoundError(
|
|
303
|
-
"RepositoryEntity",
|
|
304
|
-
`${star.repoOwner}/${star.repoName}`
|
|
305
|
-
)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
throw new ValidationError(
|
|
309
|
-
"star",
|
|
310
|
-
"Failed to create star due to transaction conflict"
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
throw error
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
## Reusable Error Handlers
|
|
319
|
-
|
|
320
|
-
Create utility functions for common patterns:
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// src/repos/utils.ts
|
|
324
|
-
|
|
325
|
-
export function handleCreateError(
|
|
326
|
-
error: unknown,
|
|
327
|
-
entityType: string,
|
|
328
|
-
entityKey: string
|
|
329
|
-
): never {
|
|
330
|
-
if (error instanceof ConditionalCheckFailedException) {
|
|
331
|
-
throw new DuplicateEntityError(entityType, entityKey)
|
|
332
|
-
}
|
|
333
|
-
if (error instanceof DynamoDBToolboxError) {
|
|
334
|
-
throw new ValidationError(error.path ?? "entity", error.message)
|
|
335
|
-
}
|
|
336
|
-
throw error
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export function handleUpdateError(
|
|
340
|
-
error: unknown,
|
|
341
|
-
entityType: string,
|
|
342
|
-
entityKey: string
|
|
343
|
-
): never {
|
|
344
|
-
if (error instanceof ConditionalCheckFailedException) {
|
|
345
|
-
throw new EntityNotFoundError(entityType, entityKey)
|
|
346
|
-
}
|
|
347
|
-
if (error instanceof DynamoDBToolboxError) {
|
|
348
|
-
throw new ValidationError(error.path ?? "entity", error.message)
|
|
349
|
-
}
|
|
350
|
-
throw error
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**Usage:**
|
|
355
|
-
|
|
356
|
-
```typescript
|
|
357
|
-
async create(user: UserEntity): Promise<UserEntity> {
|
|
358
|
-
try {
|
|
359
|
-
const result = await this.entity
|
|
360
|
-
.build(PutItemCommand)
|
|
361
|
-
.item(user.toRecord())
|
|
362
|
-
.options({ condition: { attr: "PK", exists: false } })
|
|
363
|
-
.send()
|
|
364
|
-
|
|
365
|
-
return UserEntity.fromRecord(result.ToolboxItem)
|
|
366
|
-
} catch (error: unknown) {
|
|
367
|
-
handleCreateError(error, "UserEntity", user.username)
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
## Testing Error Handling
|
|
373
|
-
|
|
374
|
-
### Test Duplicate Creation
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
it("should throw DuplicateEntityError for duplicate username", async () => {
|
|
378
|
-
const user1 = createUserEntity({ username: "alice" })
|
|
379
|
-
const user2 = createUserEntity({ username: "alice" })
|
|
380
|
-
|
|
381
|
-
await userRepo.create(user1)
|
|
382
|
-
|
|
383
|
-
await expect(userRepo.create(user2)).rejects.toThrow(DuplicateEntityError)
|
|
384
|
-
await expect(userRepo.create(user2)).rejects.toThrow("already exists")
|
|
385
|
-
})
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### Test Update Non-Existent
|
|
389
|
-
|
|
390
|
-
```typescript
|
|
391
|
-
it("should throw EntityNotFoundError when updating non-existent user", async () => {
|
|
392
|
-
const user = createUserEntity({ username: "nonexistent" })
|
|
393
|
-
|
|
394
|
-
await expect(userRepo.update(user)).rejects.toThrow(EntityNotFoundError)
|
|
395
|
-
await expect(userRepo.update(user)).rejects.toThrow("not found")
|
|
396
|
-
})
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### Test Transaction Validation
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
it("should throw EntityNotFoundError when creating issue for non-existent repo", async () => {
|
|
403
|
-
const issue = IssueEntity.fromRequest({
|
|
404
|
-
owner: "alice",
|
|
405
|
-
repo_name: "nonexistent",
|
|
406
|
-
title: "Test Issue",
|
|
407
|
-
author: "bob",
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
await expect(issueRepo.create(issue)).rejects.toThrow(EntityNotFoundError)
|
|
411
|
-
await expect(issueRepo.create(issue)).rejects.toThrow("RepositoryEntity")
|
|
412
|
-
})
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
## Error Response Formatting (API Layer)
|
|
416
|
-
|
|
417
|
-
Convert domain errors to HTTP responses:
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
// src/routes/users.ts (Fastify example)
|
|
421
|
-
|
|
422
|
-
app.post("/users", async (request, reply) => {
|
|
423
|
-
try {
|
|
424
|
-
const user = UserEntity.fromRequest(request.body)
|
|
425
|
-
const created = await userService.create(user)
|
|
426
|
-
return reply.status(201).send(created.toResponse())
|
|
427
|
-
} catch (error) {
|
|
428
|
-
if (error instanceof DuplicateEntityError) {
|
|
429
|
-
return reply.status(409).send({
|
|
430
|
-
error: "Conflict",
|
|
431
|
-
message: error.message,
|
|
432
|
-
entityType: error.entityType,
|
|
433
|
-
entityKey: error.entityKey,
|
|
434
|
-
})
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (error instanceof ValidationError) {
|
|
438
|
-
return reply.status(400).send({
|
|
439
|
-
error: "Bad Request",
|
|
440
|
-
message: error.message,
|
|
441
|
-
field: error.field,
|
|
442
|
-
})
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (error instanceof EntityNotFoundError) {
|
|
446
|
-
return reply.status(404).send({
|
|
447
|
-
error: "Not Found",
|
|
448
|
-
message: error.message,
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Unknown errors: 500
|
|
453
|
-
console.error("Unexpected error:", error)
|
|
454
|
-
return reply.status(500).send({
|
|
455
|
-
error: "Internal Server Error",
|
|
456
|
-
message: "An unexpected error occurred",
|
|
457
|
-
})
|
|
458
|
-
}
|
|
459
|
-
})
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
**HTTP Status Code Mapping:**
|
|
463
|
-
- `DuplicateEntityError` → 409 Conflict
|
|
464
|
-
- `EntityNotFoundError` → 404 Not Found
|
|
465
|
-
- `ValidationError` → 400 Bad Request
|
|
466
|
-
- Unknown → 500 Internal Server Error
|
|
467
|
-
|
|
468
|
-
## Best Practices
|
|
469
|
-
|
|
470
|
-
### ✓ Do
|
|
471
|
-
|
|
472
|
-
- Always catch `ConditionalCheckFailedException` and map to domain errors
|
|
473
|
-
- Include entity type and key in error messages
|
|
474
|
-
- Inspect `CancellationReasons` array order for transactions
|
|
475
|
-
- Log unexpected errors before re-throwing
|
|
476
|
-
- Test error handling paths explicitly
|
|
477
|
-
|
|
478
|
-
### ✗ Don't
|
|
479
|
-
|
|
480
|
-
- Don't expose DynamoDB error details to clients
|
|
481
|
-
- Don't assume transaction error order - always check CancellationReasons
|
|
482
|
-
- Don't ignore `DynamoDBToolboxError` - it indicates schema issues
|
|
483
|
-
- Don't retry blindly on transaction errors (may have partial state)
|
|
484
|
-
|
|
485
|
-
## Error Handling Checklist
|
|
486
|
-
|
|
487
|
-
For each repository operation:
|
|
488
|
-
|
|
489
|
-
- [ ] **Create operations** - Catch `ConditionalCheckFailed` → `DuplicateEntityError`
|
|
490
|
-
- [ ] **Update operations** - Catch `ConditionalCheckFailed` → `EntityNotFoundError`
|
|
491
|
-
- [ ] **Transactions** - Parse `CancellationReasons` by index
|
|
492
|
-
- [ ] **Schema validation** - Catch `DynamoDBToolboxError` → `ValidationError`
|
|
493
|
-
- [ ] **Unknown errors** - Re-throw with logging
|
|
494
|
-
- [ ] **API layer** - Map domain errors to HTTP status codes
|
|
495
|
-
- [ ] **Tests** - Verify error handling for each failure scenario
|