@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +94 -253
  3. package/package.json +1 -1
  4. package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
  5. package/skills/atelier-setup/SKILL.md +80 -0
  6. package/skills/code-review/references/reviewers.md +3 -3
  7. package/skills/code-review/references/rq.md +6 -6
  8. package/skills/spec-brainstorm/SKILL.md +5 -5
  9. package/skills/spec-implement/SKILL.md +2 -3
  10. package/skills/spec-plan/SKILL.md +21 -29
  11. package/skills/spec-plan/references/plan_template.md +77 -0
  12. package/skills/python-architecture/SKILL.md +0 -148
  13. package/skills/python-architecture/references/data-modeling.md +0 -558
  14. package/skills/python-architecture/references/ddd.md +0 -569
  15. package/skills/python-architecture/references/functional-core.md +0 -337
  16. package/skills/python-build-tools/SKILL.md +0 -153
  17. package/skills/python-build-tools/references/basedpyright.md +0 -321
  18. package/skills/python-build-tools/references/ruff.md +0 -225
  19. package/skills/python-build-tools/references/uv.md +0 -257
  20. package/skills/python-fastapi/SKILL.md +0 -184
  21. package/skills/python-fastapi/references/api-design.md +0 -563
  22. package/skills/python-fastapi/references/dependencies.md +0 -479
  23. package/skills/python-fastapi/references/middleware.md +0 -475
  24. package/skills/python-fastapi/references/validation.md +0 -497
  25. package/skills/python-modern-python/SKILL.md +0 -201
  26. package/skills/python-modern-python/references/async.md +0 -76
  27. package/skills/python-modern-python/references/pattern-matching.md +0 -89
  28. package/skills/python-modern-python/references/typing.md +0 -96
  29. package/skills/python-monorepo/SKILL.md +0 -146
  30. package/skills/python-monorepo/references/docker.md +0 -515
  31. package/skills/python-monorepo/references/namespace-packages.md +0 -426
  32. package/skills/python-monorepo/references/workspace-config.md +0 -341
  33. package/skills/python-sqlalchemy/SKILL.md +0 -119
  34. package/skills/python-sqlalchemy/references/async.md +0 -51
  35. package/skills/python-sqlalchemy/references/models.md +0 -67
  36. package/skills/python-sqlalchemy/references/queries.md +0 -57
  37. package/skills/python-temporal/SKILL.md +0 -128
  38. package/skills/python-temporal/references/patterns.md +0 -87
  39. package/skills/python-temporal/references/testing.md +0 -54
  40. package/skills/python-testing/SKILL.md +0 -161
  41. package/skills/python-testing/references/boundaries.md +0 -449
  42. package/skills/python-testing/references/mocking.md +0 -449
  43. package/skills/python-testing/references/pytest.md +0 -524
  44. package/skills/typescript-api-design/SKILL.md +0 -396
  45. package/skills/typescript-api-design/references/error-responses.md +0 -469
  46. package/skills/typescript-build-tools/SKILL.md +0 -447
  47. package/skills/typescript-build-tools/references/biome.md +0 -436
  48. package/skills/typescript-build-tools/references/bun.md +0 -432
  49. package/skills/typescript-build-tools/references/turborepo.md +0 -426
  50. package/skills/typescript-build-tools/references/vitest.md +0 -518
  51. package/skills/typescript-drizzle-orm/SKILL.md +0 -398
  52. package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
  53. package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
  54. package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
  55. package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
  56. package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
  57. package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
  58. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
  59. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
  60. package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
  61. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
  62. package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
  63. package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
  64. package/skills/typescript-effect-ts/SKILL.md +0 -341
  65. package/skills/typescript-effect-ts/references/core.md +0 -412
  66. package/skills/typescript-effect-ts/references/error-handling.md +0 -468
  67. package/skills/typescript-effect-ts/references/resources.md +0 -476
  68. package/skills/typescript-effect-ts/references/schema.md +0 -536
  69. package/skills/typescript-effect-ts/references/services.md +0 -461
  70. package/skills/typescript-fastify/SKILL.md +0 -405
  71. package/skills/typescript-fastify/references/plugins.md +0 -329
  72. package/skills/typescript-fastify/references/typeid.md +0 -315
  73. package/skills/typescript-functional-patterns/SKILL.md +0 -402
  74. package/skills/typescript-functional-patterns/references/adts.md +0 -497
  75. package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
  76. package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
  77. package/skills/typescript-functional-patterns/references/option-result.md +0 -585
  78. package/skills/typescript-testing/SKILL.md +0 -353
  79. package/skills/typescript-testing/references/mocking.md +0 -675
  80. package/skills/typescript-testing/references/msw.md +0 -852
  81. package/skills/typescript-testing/references/snapshot-testing.md +0 -774
@@ -1,631 +0,0 @@
1
- # Repository Layer Patterns
2
-
3
- The repository layer encapsulates all DynamoDB operations, providing a clean interface between domain entities and the database.
4
-
5
- ## Repository Responsibilities
6
-
7
- **What Repositories Do:**
8
- - Execute DynamoDB commands (Get, Put, Update, Delete, Query, Scan)
9
- - Enforce business constraints via conditional writes
10
- - Handle pagination and filtering
11
- - Convert between DynamoDB records and domain entities
12
- - Map DynamoDB errors to domain errors
13
-
14
- **What Repositories Don't Do:**
15
- - Business logic (belongs in Service layer)
16
- - Entity transformations (handled by Entity classes)
17
- - HTTP request/response handling (belongs in Router layer)
18
- - Direct attribute manipulation (use Entity methods)
19
-
20
- ## Constructor Patterns
21
-
22
- ### Single Entity Repository
23
-
24
- For simple CRUD operations on one entity type.
25
-
26
- ```typescript
27
- import { type UserRecord } from './schema'
28
-
29
- class UserRepository {
30
- constructor(private readonly entity: UserRecord) {}
31
-
32
- async get(username: string): Promise<UserEntity | undefined> {
33
- const result = await this.entity
34
- .build(GetItemCommand)
35
- .key({ username })
36
- .send()
37
-
38
- return result.Item ? UserEntity.fromRecord(result.Item) : undefined
39
- }
40
- }
41
- ```
42
-
43
- **When to use:**
44
- - Entity has no dependencies on other entities
45
- - All operations are simple CRUD
46
- - No transaction validation needed
47
-
48
- ### Multi-Entity Repository
49
-
50
- For operations requiring validation or queries across entities.
51
-
52
- ```typescript
53
- import { type Table } from 'dynamodb-toolbox/table'
54
- import { type IssueRecord, type RepoRecord, type CounterRecord } from './schema'
55
-
56
- class IssueRepository {
57
- constructor(
58
- private readonly table: Table,
59
- private readonly issueRecord: IssueRecord,
60
- private readonly counterRecord: CounterRecord,
61
- private readonly repoRecord: RepoRecord
62
- ) {}
63
-
64
- async create(issue: IssueEntity): Promise<IssueEntity> {
65
- // Uses counterRecord to generate issue number
66
- // Uses repoRecord to validate parent exists
67
- // Uses table to execute cross-entity queries
68
- }
69
- }
70
- ```
71
-
72
- **When to use:**
73
- - Need to validate foreign keys (transactions)
74
- - Need to query related entities
75
- - Need atomic counters for sequential IDs
76
- - Need to query GSIs with multiple entity types
77
-
78
- **Key Point:** Pass `Table` instance for cross-entity queries, individual `Entity` instances for specific operations.
79
-
80
- ## CRUD Operations
81
-
82
- ### Create with Duplicate Prevention
83
-
84
- ```typescript
85
- import { PutItemCommand } from 'dynamodb-toolbox'
86
- import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb'
87
-
88
- async create(user: UserEntity): Promise<UserEntity> {
89
- try {
90
- const result = await this.entity
91
- .build(PutItemCommand)
92
- .item(user.toRecord())
93
- .options({
94
- condition: { attr: "PK", exists: false } // Prevent duplicates
95
- })
96
- .send()
97
-
98
- return UserEntity.fromRecord(result.ToolboxItem)
99
- } catch (error: unknown) {
100
- if (error instanceof ConditionalCheckFailedException) {
101
- throw new DuplicateEntityError("UserEntity", user.username)
102
- }
103
- throw error
104
- }
105
- }
106
- ```
107
-
108
- **Key Points:**
109
- - Use `PutItemCommand` (replaces entire item)
110
- - Condition: `{ attr: "PK", exists: false }`
111
- - Return entity from `result.ToolboxItem`
112
- - Map `ConditionalCheckFailedException` to `DuplicateEntityError`
113
-
114
- ### Read by Primary Key
115
-
116
- ```typescript
117
- import { GetItemCommand } from 'dynamodb-toolbox'
118
-
119
- async get(username: string): Promise<UserEntity | undefined> {
120
- const result = await this.entity
121
- .build(GetItemCommand)
122
- .key({ username }) // Provide key attributes only
123
- .send()
124
-
125
- return result.Item ? UserEntity.fromRecord(result.Item) : undefined
126
- }
127
- ```
128
-
129
- **Key Points:**
130
- - Use `GetItemCommand`
131
- - Pass only key attributes to `.key()`
132
- - Return `undefined` if not found (don't throw)
133
- - Always convert via `Entity.fromRecord()`
134
-
135
- ### Update with Existence Check
136
-
137
- ```typescript
138
- async update(user: UserEntity): Promise<UserEntity> {
139
- try {
140
- const result = await this.entity
141
- .build(PutItemCommand)
142
- .item(user.toRecord())
143
- .options({
144
- condition: { attr: "PK", exists: true } // Must exist
145
- })
146
- .send()
147
-
148
- return UserEntity.fromRecord(result.ToolboxItem)
149
- } catch (error: unknown) {
150
- if (error instanceof ConditionalCheckFailedException) {
151
- throw new EntityNotFoundError("UserEntity", user.username)
152
- }
153
- throw error
154
- }
155
- }
156
- ```
157
-
158
- **Key Points:**
159
- - Use `PutItemCommand` (replaces entire item)
160
- - Condition: `{ attr: "PK", exists: true }`
161
- - Map `ConditionalCheckFailedException` to `EntityNotFoundError`
162
-
163
- ### Delete
164
-
165
- ```typescript
166
- import { DeleteItemCommand } from 'dynamodb-toolbox'
167
-
168
- async delete(username: string): Promise<void> {
169
- await this.entity
170
- .build(DeleteItemCommand)
171
- .key({ username })
172
- .send()
173
-
174
- // No return value - succeeds silently even if item doesn't exist
175
- }
176
- ```
177
-
178
- **Key Points:**
179
- - Use `DeleteItemCommand`
180
- - No condition needed (idempotent by default)
181
- - Add condition if you need to verify existence
182
-
183
- ## UpdateItemCommand vs PutItemCommand
184
-
185
- ### Use PutItemCommand (Replace Entire Item)
186
-
187
- ```typescript
188
- // Replace entire item with entity state
189
- await this.entity
190
- .build(PutItemCommand)
191
- .item(user.toRecord()) // Full entity
192
- .options({ condition: { attr: "PK", exists: true } })
193
- .send()
194
- ```
195
-
196
- **When to use:**
197
- - Creating entities (with `exists: false`)
198
- - Updating entities (with `exists: true`)
199
- - Working with Entity objects (full state)
200
- - Need to replace entire item
201
-
202
- **Pros:**
203
- - Works with Entity pattern (full state)
204
- - Simple and predictable
205
- - Matches domain model
206
-
207
- **Cons:**
208
- - Replaces entire item (not partial update)
209
-
210
- ### Use UpdateItemCommand (Partial Update)
211
-
212
- ```typescript
213
- import { UpdateItemCommand, $add } from 'dynamodb-toolbox'
214
-
215
- // Atomic counter increment
216
- await this.entity
217
- .build(UpdateItemCommand)
218
- .item({
219
- org_id: orgId,
220
- repo_id: repoId,
221
- current_value: $add(1), // Only update this field
222
- })
223
- .options({ returnValues: "ALL_NEW" })
224
- .send()
225
- ```
226
-
227
- **When to use:**
228
- - Atomic operations (`$add`, `$subtract`, `$append`)
229
- - Partial updates without loading full entity
230
- - Updating single attributes
231
- - Counter increments
232
-
233
- **Atomic Update Operators:**
234
- - `$add(n)` - Add to number (creates with n if doesn't exist)
235
- - `$subtract(n)` - Subtract from number
236
- - `$append([items])` - Append to list
237
- - `$remove()` - Remove attribute
238
-
239
- **Pros:**
240
- - Atomic operations (no race conditions)
241
- - Efficient (don't need to load entity first)
242
- - Can return updated values
243
-
244
- **Cons:**
245
- - Doesn't work with full Entity objects
246
- - More complex API
247
- - Bypasses entity validation
248
-
249
- **Best Practice:** Use `PutItemCommand` for entity-based operations, `UpdateItemCommand` for atomic operations.
250
-
251
- ## Query Patterns
252
-
253
- ### Query Main Table by Partition Key
254
-
255
- ```typescript
256
- async listByPartition(username: string): Promise<Star[]> {
257
- const result = await this.table
258
- .build(QueryCommand)
259
- .entities(this.starRecord) // Type-safe formatting
260
- .query({
261
- partition: `ACCOUNT#${username}`,
262
- })
263
- .send()
264
-
265
- return result.Items?.map(item => StarEntity.fromRecord(item)) || []
266
- }
267
- ```
268
-
269
- **Key Points:**
270
- - Use `Table.build(QueryCommand)` for queries
271
- - Use `.entities(this.record)` for type-safe attribute mapping
272
- - Specify partition key value
273
- - All items with that partition key returned
274
-
275
- ### Query GSI
276
-
277
- ```typescript
278
- async listByRepo(owner: string, repoName: string): Promise<Issue[]> {
279
- const result = await this.table
280
- .build(QueryCommand)
281
- .entities(this.issueRecord)
282
- .query({
283
- partition: `ISSUE#${owner}#${repoName}`,
284
- index: "GSI1", // Specify GSI name
285
- })
286
- .send()
287
-
288
- return result.Items?.map(item => IssueEntity.fromRecord(item)) || []
289
- }
290
- ```
291
-
292
- **Key Points:**
293
- - Specify `index: "GSI1"`
294
- - Query GSI partition key (GSI1PK)
295
- - Returns all items in that partition
296
-
297
- ### Query with Range Key Filter
298
-
299
- ```typescript
300
- async listComments(
301
- owner: string,
302
- repoName: string,
303
- issueNumber: number
304
- ): Promise<Comment[]> {
305
- const paddedNumber = String(issueNumber).padStart(8, "0")
306
-
307
- const result = await this.table
308
- .build(QueryCommand)
309
- .entities(this.commentRecord)
310
- .query({
311
- partition: `REPO#${owner}#${repoName}`,
312
- range: {
313
- beginsWith: `ISSUE#${paddedNumber}#COMMENT#`
314
- }
315
- })
316
- .send()
317
-
318
- return result.Items?.map(item => CommentEntity.fromRecord(item)) || []
319
- }
320
- ```
321
-
322
- **Range Key Operators:**
323
- - `beginsWith: "prefix"` - SK starts with prefix
324
- - `between: ["start", "end"]` - SK between values
325
- - `gt: "value"` - SK greater than
326
- - `gte: "value"` - SK greater than or equal
327
- - `lt: "value"` - SK less than
328
- - `lte: "value"` - SK less than or equal
329
-
330
- ### Query with Status Filter (Smart SK Design)
331
-
332
- ```typescript
333
- async listOpenIssues(owner: string, repoName: string): Promise<Issue[]> {
334
- const result = await this.table
335
- .build(QueryCommand)
336
- .entities(this.issueRecord)
337
- .query({
338
- partition: `ISSUE#${owner}#${repoName}`,
339
- index: "GSI4",
340
- range: {
341
- beginsWith: "ISSUE#OPEN#" // Filters to open issues only
342
- }
343
- })
344
- .send()
345
-
346
- return result.Items?.map(item => IssueEntity.fromRecord(item)) || []
347
- }
348
- ```
349
-
350
- **Pattern:** Encode status in GSI sort key for efficient filtering.
351
-
352
- ## Pagination
353
-
354
- ### Pagination Helper Functions
355
-
356
- ```typescript
357
- // Encode LastEvaluatedKey as opaque token
358
- function encodePageToken(
359
- lastEvaluated?: Record<string, unknown>
360
- ): string | undefined {
361
- return lastEvaluated
362
- ? Buffer.from(JSON.stringify(lastEvaluated)).toString("base64")
363
- : undefined
364
- }
365
-
366
- // Decode token back to LastEvaluatedKey
367
- function decodePageToken(
368
- token?: string
369
- ): Record<string, unknown> | undefined {
370
- return token
371
- ? JSON.parse(Buffer.from(token, "base64").toString())
372
- : undefined
373
- }
374
- ```
375
-
376
- ### Paginated Query
377
-
378
- ```typescript
379
- interface PaginatedResponse<T> {
380
- items: T[]
381
- nextOffset?: string
382
- }
383
-
384
- async listReposByOwner(
385
- owner: string,
386
- limit = 50,
387
- offset?: string
388
- ): Promise<PaginatedResponse<RepositoryEntity>> {
389
- const result = await this.table
390
- .build(QueryCommand)
391
- .entities(this.repoRecord)
392
- .query({
393
- partition: `ACCOUNT#${owner}`,
394
- index: "GSI3",
395
- range: { lt: "ACCOUNT#" }, // Filter repos only
396
- })
397
- .options({
398
- reverse: true, // Newest first
399
- exclusiveStartKey: decodePageToken(offset), // Continue from cursor
400
- limit, // Page size
401
- })
402
- .send()
403
-
404
- return {
405
- items: result.Items?.map(item =>
406
- RepositoryEntity.fromRecord(item)
407
- ) || [],
408
- nextOffset: encodePageToken(result.LastEvaluatedKey),
409
- }
410
- }
411
- ```
412
-
413
- **Key Points:**
414
- - Use `exclusiveStartKey` to continue from previous page
415
- - Use `limit` to control page size
416
- - Encode `LastEvaluatedKey` as opaque token for clients
417
- - Use `reverse: true` for descending order
418
-
419
- **Client Usage:**
420
- ```typescript
421
- // First page
422
- const page1 = await repo.listReposByOwner("alice", 10)
423
-
424
- // Next page
425
- const page2 = await repo.listReposByOwner("alice", 10, page1.nextOffset)
426
-
427
- // Continue until nextOffset is undefined
428
- ```
429
-
430
- ## Table.build vs Entity.build
431
-
432
- ### Entity.build (Single Entity Operations)
433
-
434
- ```typescript
435
- // Use Entity.build for operations on single entity type
436
- const result = await this.entity
437
- .build(GetItemCommand)
438
- .key({ username })
439
- .send()
440
- ```
441
-
442
- **Commands:**
443
- - `GetItemCommand` - Read single item
444
- - `PutItemCommand` - Create/update item
445
- - `UpdateItemCommand` - Partial update
446
- - `DeleteItemCommand` - Delete item
447
- - `PutTransaction` - Transaction put
448
- - `ConditionCheck` - Transaction condition
449
-
450
- ### Table.build (Multi-Entity Queries)
451
-
452
- ```typescript
453
- // Use Table.build for queries across multiple entity types
454
- const result = await this.table
455
- .build(QueryCommand)
456
- .entities(this.issueRecord) // Specify which entity type
457
- .query({ partition: "ISSUE#..." })
458
- .send()
459
- ```
460
-
461
- **Commands:**
462
- - `QueryCommand` - Query partition
463
- - `ScanCommand` - Full table scan (avoid in production)
464
-
465
- **Critical:** Always use `.entities(this.record)` when querying table to ensure proper attribute mapping from raw DynamoDB names (`_ct`, `_md`) to entity names (`created`, `modified`).
466
-
467
- ## Type-Safe Queries with .entities()
468
-
469
- ### Without .entities() (Raw DynamoDB Attributes)
470
-
471
- ```typescript
472
- // ❌ WRONG - Returns raw DynamoDB attributes
473
- const result = await this.table
474
- .build(QueryCommand)
475
- .query({ partition: "ACCOUNT#alice" })
476
- .send()
477
-
478
- // result.Items has _ct and _md instead of created and modified
479
- ```
480
-
481
- ### With .entities() (Formatted Attributes)
482
-
483
- ```typescript
484
- // ✓ RIGHT - Returns formatted attributes
485
- const result = await this.table
486
- .build(QueryCommand)
487
- .entities(this.userRecord) // Formats through entity schema
488
- .query({ partition: "ACCOUNT#alice" })
489
- .send()
490
-
491
- // result.Items has created and modified (properly formatted)
492
- ```
493
-
494
- **Rule:** Always use `.entities()` when querying Table to get type-safe, formatted results.
495
-
496
- ## Common Patterns from gh-ddb
497
-
498
- ### Pattern: List Collection by Parent
499
-
500
- ```typescript
501
- // List all issues for a repository
502
- async list(owner: string, repoName: string): Promise<IssueEntity[]> {
503
- const result = await this.table
504
- .build(QueryCommand)
505
- .entities(this.issueRecord)
506
- .query({
507
- partition: `ISSUE#${owner}#${repoName}`,
508
- index: "GSI1",
509
- })
510
- .send()
511
-
512
- return result.Items?.map(item => IssueEntity.fromRecord(item)) || []
513
- }
514
- ```
515
-
516
- ### Pattern: Query with Temporal Sorting
517
-
518
- ```typescript
519
- // List repos by owner, newest first
520
- async listByOwner(
521
- owner: string,
522
- options: ListOptions = {}
523
- ): Promise<PaginatedResponse<RepositoryEntity>> {
524
- const { limit = 50, offset } = options
525
-
526
- const result = await this.table
527
- .build(QueryCommand)
528
- .entities(this.repoRecord)
529
- .query({
530
- partition: `ACCOUNT#${owner}`,
531
- index: "GSI3",
532
- range: { lt: "ACCOUNT#" }, // Exclude account record itself
533
- })
534
- .options({
535
- reverse: true, // Newest first (GSI3SK has #timestamp)
536
- exclusiveStartKey: decodePageToken(offset),
537
- limit,
538
- })
539
- .send()
540
-
541
- return {
542
- items: result.Items?.map(item =>
543
- RepositoryEntity.fromRecord(item)
544
- ) || [],
545
- offset: encodePageToken(result.LastEvaluatedKey),
546
- }
547
- }
548
- ```
549
-
550
- ### Pattern: Adjacency List Query (Both Directions)
551
-
552
- ```typescript
553
- // User's starred repos (main table)
554
- async getStarsByUser(username: string): Promise<StarEntity[]> {
555
- const result = await this.table
556
- .build(QueryCommand)
557
- .entities(this.starRecord)
558
- .query({
559
- partition: `ACCOUNT#${username}`,
560
- range: { beginsWith: "STAR#" }
561
- })
562
- .send()
563
-
564
- return result.Items?.map(item => StarEntity.fromRecord(item)) || []
565
- }
566
-
567
- // Repo's stargazers (GSI1 - inverted)
568
- async getStarsByRepo(
569
- owner: string,
570
- repoName: string
571
- ): Promise<StarEntity[]> {
572
- const result = await this.table
573
- .build(QueryCommand)
574
- .entities(this.starRecord)
575
- .query({
576
- partition: `REPO#${owner}#${repoName}`,
577
- index: "GSI1",
578
- range: { beginsWith: "STAR#" }
579
- })
580
- .send()
581
-
582
- return result.Items?.map(item => StarEntity.fromRecord(item)) || []
583
- }
584
- ```
585
-
586
- ## Repository Best Practices
587
-
588
- ### ✓ Do
589
-
590
- - Keep repositories focused on their primary entity
591
- - Use conditional writes for create/update
592
- - Always convert via `Entity.fromRecord()`
593
- - Return `undefined` for not found (don't throw on Get)
594
- - Map DynamoDB errors to domain errors
595
- - Use `.entities()` for type-safe queries
596
- - Encode pagination cursors as opaque tokens
597
- - Pass `Table` instance for cross-entity operations
598
-
599
- ### ✗ Don't
600
-
601
- - Don't include business logic (use Service layer)
602
- - Don't return raw DynamoDB records
603
- - Don't query without `.entities()` (loses type safety)
604
- - Don't use Scan in production (performance cost)
605
- - Don't retry blindly on errors (may have partial state)
606
- - Don't expose DynamoDB error details to clients
607
- - Don't create empty Sets (DynamoDB rejects them)
608
-
609
- ## Repository Testing
610
-
611
- See [testing.md](testing.md) for comprehensive testing patterns including:
612
-
613
- - DynamoDB Local setup
614
- - CRUD operation tests
615
- - Query and pagination tests
616
- - Concurrency tests
617
- - Error handling tests
618
-
619
- ## Repository Checklist
620
-
621
- For each repository:
622
-
623
- - [ ] **Constructor** - Single entity or multi-entity pattern
624
- - [ ] **Create** - PutItemCommand with `exists: false` condition
625
- - [ ] **Get** - GetItemCommand returning `undefined` if not found
626
- - [ ] **Update** - PutItemCommand with `exists: true` condition
627
- - [ ] **Delete** - DeleteItemCommand (idempotent)
628
- - [ ] **Queries** - Use `.entities()` for type-safe results
629
- - [ ] **Pagination** - Encode/decode cursors, return nextOffset
630
- - [ ] **Error Handling** - Map to domain errors
631
- - [ ] **Testing** - Comprehensive tests for all operations