@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,291 +0,0 @@
1
- # SQLite Patterns with Drizzle ORM
2
-
3
- SQLite-specific patterns, schema definition, and local testing with better-sqlite3.
4
-
5
- ## Schema Definition
6
-
7
- ```typescript
8
- import {
9
- sqliteTable,
10
- text,
11
- integer,
12
- blob,
13
- index,
14
- uniqueIndex,
15
- } from 'drizzle-orm/sqlite-core'
16
-
17
- export const users = sqliteTable('users', {
18
- id: text('id').primaryKey(),
19
- name: text('name').notNull(),
20
- email: text('email').notNull(),
21
- age: integer('age'),
22
- isActive: integer('is_active', { mode: 'boolean' }).default(true),
23
- createdAt: text('created_at').notNull(), // ISO 8601 string
24
- updatedAt: text('updated_at').notNull(),
25
- }, (table) => [
26
- uniqueIndex('idx_users_email').on(table.email),
27
- ])
28
-
29
- export const posts = sqliteTable('posts', {
30
- id: text('id').primaryKey(),
31
- title: text('title').notNull(),
32
- content: text('content'),
33
- authorId: text('author_id')
34
- .notNull()
35
- .references(() => users.id, { onDelete: 'cascade' }),
36
- createdAt: text('created_at').notNull(),
37
- }, (table) => [
38
- index('idx_posts_author').on(table.authorId),
39
- ])
40
- ```
41
-
42
- ## Key Differences from PostgreSQL
43
-
44
- | Feature | PostgreSQL | SQLite |
45
- |---------|-----------|--------|
46
- | Primary Key | `serial('id').primaryKey()` | `integer('id').primaryKey({ autoIncrement: true })` or `text('id').primaryKey()` |
47
- | UUID | `uuid('id').defaultRandom()` | `text('id')` (generate in app) |
48
- | Timestamp | `timestamp('created_at').defaultNow()` | `text('created_at')` (ISO 8601) or `integer({ mode: 'timestamp' })` |
49
- | Boolean | `boolean('is_active')` | `integer('is_active', { mode: 'boolean' })` |
50
- | JSON | `jsonb('metadata')` | `text('metadata')` or `blob('metadata', { mode: 'json' })` |
51
- | Enum | `pgEnum()` | `text('type', { enum: ['A', 'B'] })` |
52
- | Binary | `bytea('data')` | `blob('data')` |
53
-
54
- ## Column Types
55
-
56
- ### Integer Modes
57
-
58
- ```typescript
59
- // Default (number)
60
- age: integer('age')
61
-
62
- // Boolean
63
- isActive: integer('is_active', { mode: 'boolean' })
64
-
65
- // Unix timestamp (seconds)
66
- createdAt: integer('created_at', { mode: 'timestamp' })
67
-
68
- // Unix timestamp (milliseconds)
69
- createdAt: integer('created_at', { mode: 'timestamp_ms' })
70
- ```
71
-
72
- ### Text Enums
73
-
74
- ```typescript
75
- // Type-safe enum with inference
76
- type: text('type', { enum: ['ASSET', 'LIABILITY', 'INCOME', 'EXPENSE'] }).notNull()
77
-
78
- // Inferred as: "ASSET" | "LIABILITY" | "INCOME" | "EXPENSE"
79
- ```
80
-
81
- ### Blob Types
82
-
83
- ```typescript
84
- // Buffer mode (default)
85
- data: blob('data')
86
-
87
- // JSON mode
88
- metadata: blob('metadata', { mode: 'json' }).$type<{ key: string }>()
89
-
90
- // BigInt mode
91
- bigNumber: blob('big_number', { mode: 'bigint' })
92
- ```
93
-
94
- ## Indexes and Constraints
95
-
96
- ```typescript
97
- export const accounts = sqliteTable('accounts', {
98
- id: text('id').primaryKey(),
99
- code: text('code').notNull(),
100
- name: text('name').notNull(),
101
- parentId: text('parent_id'),
102
- }, (table) => [
103
- // Unique index
104
- uniqueIndex('idx_accounts_code').on(table.code),
105
- // Regular index
106
- index('idx_accounts_parent').on(table.parentId),
107
- // Composite index
108
- index('idx_accounts_code_parent').on(table.code, table.parentId),
109
- ])
110
-
111
- // Composite primary key
112
- export const postTags = sqliteTable('post_tags', {
113
- postId: text('post_id').notNull().references(() => posts.id),
114
- tagId: text('tag_id').notNull().references(() => tags.id),
115
- }, (table) => [
116
- primaryKey({ columns: [table.postId, table.tagId] }),
117
- ])
118
- ```
119
-
120
- ## Migrations Configuration
121
-
122
- ```typescript
123
- // drizzle.config.ts
124
- import { defineConfig } from 'drizzle-kit'
125
-
126
- export default defineConfig({
127
- schema: './src/db/schema.ts',
128
- out: './drizzle',
129
- dialect: 'sqlite',
130
- dbCredentials: {
131
- url: './local.db', // File path for local SQLite
132
- },
133
- })
134
- ```
135
-
136
- ```bash
137
- # Generate migration
138
- npx drizzle-kit generate
139
-
140
- # Apply migrations
141
- npx drizzle-kit migrate
142
-
143
- # Open Drizzle Studio
144
- npx drizzle-kit studio
145
- ```
146
-
147
- ## Local Testing with better-sqlite3
148
-
149
- For D1 apps or local development, use `better-sqlite3` for fast in-memory testing:
150
-
151
- ```typescript
152
- import Database from 'better-sqlite3'
153
- import { drizzle } from 'drizzle-orm/better-sqlite3'
154
- import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
155
- import * as schema from './schema'
156
-
157
- // In-memory database (fast, isolated tests)
158
- const sqlite = new Database(':memory:')
159
- const db = drizzle(sqlite, { schema })
160
-
161
- // Run migrations
162
- migrate(db, { migrationsFolder: './drizzle' })
163
-
164
- // Use in tests
165
- test('user creation', async () => {
166
- const [user] = await db.insert(schema.users).values({
167
- id: crypto.randomUUID(),
168
- name: 'Alice',
169
- email: 'alice@example.com',
170
- createdAt: new Date().toISOString(),
171
- updatedAt: new Date().toISOString(),
172
- }).returning()
173
-
174
- expect(user.name).toBe('Alice')
175
- })
176
- ```
177
-
178
- ### File-Based Database
179
-
180
- ```typescript
181
- // Persistent database file
182
- const sqlite = new Database('./test.db')
183
- const db = drizzle(sqlite, { schema })
184
- ```
185
-
186
- ### Test Setup Helper
187
-
188
- ```typescript
189
- import { beforeEach, afterEach } from 'vitest'
190
- import Database from 'better-sqlite3'
191
- import { drizzle } from 'drizzle-orm/better-sqlite3'
192
- import * as schema from './schema'
193
-
194
- let sqlite: Database.Database
195
- let db: ReturnType<typeof drizzle>
196
-
197
- beforeEach(() => {
198
- sqlite = new Database(':memory:')
199
- db = drizzle(sqlite, { schema })
200
- migrate(db, { migrationsFolder: './drizzle' })
201
- })
202
-
203
- afterEach(() => {
204
- sqlite.close()
205
- })
206
-
207
- export { db }
208
- ```
209
-
210
- ## Date Handling
211
-
212
- SQLite doesn't have native date types. Use ISO 8601 strings or Unix timestamps:
213
-
214
- ```typescript
215
- // ISO 8601 strings (recommended)
216
- createdAt: text('created_at').notNull()
217
- // Store: new Date().toISOString()
218
- // Parse: new Date(record.createdAt)
219
-
220
- // Unix timestamps (integer)
221
- createdAt: integer('created_at', { mode: 'timestamp' }).notNull()
222
- // Store: Math.floor(Date.now() / 1000)
223
- // Parse: new Date(record.createdAt * 1000)
224
- ```
225
-
226
- ## Schema Design Patterns
227
-
228
- ### UUID Primary Keys
229
-
230
- ```typescript
231
- export const users = sqliteTable('users', {
232
- id: text('id').primaryKey(), // Generate with crypto.randomUUID()
233
- name: text('name').notNull(),
234
- })
235
-
236
- // In your entity or repository
237
- const id = crypto.randomUUID()
238
- await db.insert(users).values({ id, name: 'Alice' })
239
- ```
240
-
241
- ### Auto-Increment Integer Keys
242
-
243
- ```typescript
244
- export const users = sqliteTable('users', {
245
- id: integer('id').primaryKey({ autoIncrement: true }),
246
- name: text('name').notNull(),
247
- })
248
-
249
- // SQLite generates the ID
250
- const [user] = await db.insert(users).values({ name: 'Alice' }).returning()
251
- console.log(user.id) // 1, 2, 3, ...
252
- ```
253
-
254
- ### Hierarchical Data
255
-
256
- ```typescript
257
- export const categories = sqliteTable('categories', {
258
- id: text('id').primaryKey(),
259
- name: text('name').notNull(),
260
- parentId: text('parent_id'),
261
- }, (table) => [
262
- index('idx_categories_parent').on(table.parentId),
263
- ])
264
-
265
- export const categoriesRelations = relations(categories, ({ one, many }) => ({
266
- parent: one(categories, {
267
- fields: [categories.parentId],
268
- references: [categories.id],
269
- relationName: 'parentChild',
270
- }),
271
- children: many(categories, {
272
- relationName: 'parentChild',
273
- }),
274
- }))
275
- ```
276
-
277
- ## When to Use SQLite
278
-
279
- - **Embedded applications** - Desktop, mobile, edge computing
280
- - **Single-writer scenarios** - One process writes, multiple read
281
- - **Cloudflare D1** - Managed SQLite for Workers
282
- - **Durable Objects** - Per-object isolated storage
283
- - **Local development** - Fast, zero-config testing
284
- - **Small to medium datasets** - Under 1TB
285
-
286
- ## Limitations
287
-
288
- - No concurrent writers (one writer at a time)
289
- - Limited network access (file-based)
290
- - No stored procedures
291
- - No built-in replication (use D1 or Turso for distributed)
@@ -1,370 +0,0 @@
1
- ---
2
- name: typescript-dynamodb-toolbox
3
- description: DynamoDB single-table design using dynamodb-toolbox v2. Use when creating entities, defining key patterns, designing GSIs, writing queries, implementing pagination, or working with any DynamoDB data layer in TypeScript projects.
4
- user-invocable: false
5
- ---
6
-
7
- # DynamoDB with dynamodb-toolbox v2
8
-
9
- Type-safe DynamoDB interactions with Entity and Table abstractions for single-table design.
10
-
11
- ## When to Use DynamoDB
12
-
13
- ✓ **Use when:**
14
- - Access patterns are known upfront and stable
15
- - Need predictable sub-10ms performance at scale
16
- - Microservice with clear data boundaries
17
- - Willing to commit to single-table design
18
-
19
- ✗ **Avoid when:**
20
- - Prototyping with fluid requirements
21
- - Need ad-hoc analytical queries
22
- - Team lacks DynamoDB expertise
23
- - GraphQL resolvers drive access patterns
24
-
25
- > DynamoDB inverts the relational paradigm: design for known access patterns, not flexible querying.
26
-
27
- ## Modeling Checklist
28
-
29
- Before implementing:
30
- 1. **Define Entity Relationships** - Create ERD with all entities and relationships
31
- 2. **Create Entity Chart** - Map each entity to PK/SK patterns
32
- 3. **Design GSI Strategy** - Plan secondary access patterns
33
- 4. **Document Access Patterns** - List every query the application needs
34
-
35
- See [references/modeling.md](references/modeling.md) for detailed methodology.
36
-
37
- ## Table Configuration
38
-
39
- ```typescript
40
- import { Table } from 'dynamodb-toolbox/table'
41
-
42
- const AppTable = new Table({
43
- name: process.env.TABLE_NAME || "AppTable",
44
- partitionKey: { name: "PK", type: "string" },
45
- sortKey: { name: "SK", type: "string" },
46
- indexes: {
47
- GSI1: {
48
- type: "global",
49
- partitionKey: { name: "GSI1PK", type: "string" },
50
- sortKey: { name: "GSI1SK", type: "string" },
51
- },
52
- GSI2: {
53
- type: "global",
54
- partitionKey: { name: "GSI2PK", type: "string" },
55
- sortKey: { name: "GSI2SK", type: "string" },
56
- },
57
- GSI3: {
58
- type: "global",
59
- partitionKey: { name: "GSI3PK", type: "string" },
60
- sortKey: { name: "GSI3SK", type: "string" },
61
- },
62
- },
63
- entityAttributeSavedAs: "_et", // default, customize if needed
64
- });
65
- ```
66
-
67
- ### Index Purpose
68
-
69
- | Index | Purpose |
70
- |-------|---------|
71
- | Main Table (PK/SK) | Primary entity access |
72
- | GSI1 | Collection queries (issues by repo, members by org) |
73
- | GSI2 | Entity-specific queries and relationships (forks) |
74
- | GSI3 | Hierarchical queries with temporal sorting (repos by owner) |
75
-
76
- ## Entity Definition (v2 syntax)
77
-
78
- ### Basic Pattern with Linked Keys
79
-
80
- ```typescript
81
- import { Entity } from 'dynamodb-toolbox/entity'
82
- import { item } from 'dynamodb-toolbox/schema/item'
83
- import { string } from 'dynamodb-toolbox/schema/string'
84
-
85
- const UserEntity = new Entity({
86
- name: "USER",
87
- table: AppTable,
88
- schema: item({
89
- // Business attributes
90
- username: string().required().key(),
91
- email: string().required(),
92
- bio: string().optional(),
93
- }).and(_schema => ({
94
- // Computed keys (PK/SK/GSI keys derived from business attributes)
95
- PK: string().key().link<typeof _schema>(
96
- ({ username }) => `ACCOUNT#${username}`
97
- ),
98
- SK: string().key().link<typeof _schema>(
99
- ({ username }) => `ACCOUNT#${username}`
100
- ),
101
- GSI1PK: string().link<typeof _schema>(
102
- ({ username }) => `ACCOUNT#${username}`
103
- ),
104
- GSI1SK: string().link<typeof _schema>(
105
- ({ username }) => `ACCOUNT#${username}`
106
- ),
107
- })),
108
- });
109
- ```
110
-
111
- ### With Validation
112
-
113
- ```typescript
114
- const RepoEntity = new Entity({
115
- name: "REPO",
116
- table: AppTable,
117
- schema: item({
118
- owner: string()
119
- .required()
120
- .validate((value: string) => /^[a-zA-Z0-9_-]+$/.test(value))
121
- .key(),
122
- repo_name: string()
123
- .required()
124
- .validate((value: string) => /^[a-zA-Z0-9_-]+$/.test(value))
125
- .key(),
126
- description: string().optional(),
127
- is_private: boolean().default(false),
128
- }).and(_schema => ({
129
- PK: string().key().link<typeof _schema>(
130
- ({ owner, repo_name }) => `REPO#${owner}#${repo_name}`
131
- ),
132
- SK: string().key().link<typeof _schema>(
133
- ({ owner, repo_name }) => `REPO#${owner}#${repo_name}`
134
- ),
135
- // GSI3 for temporal sorting (repos by owner, newest first)
136
- GSI3PK: string().link<typeof _schema>(
137
- ({ owner }) => `ACCOUNT#${owner}`
138
- ),
139
- GSI3SK: string()
140
- .default(() => `#${new Date().toISOString()}`)
141
- .savedAs("GSI3SK"),
142
- })),
143
- });
144
- ```
145
-
146
- ## Entity Chart (Key Patterns)
147
-
148
- | Entity | PK | SK | Purpose |
149
- |--------|----|----|---------|
150
- | User | `ACCOUNT#{username}` | `ACCOUNT#{username}` | Direct access |
151
- | Repository | `REPO#{owner}#{name}` | `REPO#{owner}#{name}` | Direct access |
152
- | Issue | `ISSUE#{owner}#{repo}#{padded_num}` | Same as PK | Direct access + enumeration |
153
- | Comment | `REPO#{owner}#{repo}` | `ISSUE#{padded_num}#COMMENT#{id}` | Comments under issue |
154
- | Star | `ACCOUNT#{username}` | `STAR#{owner}#{repo}#{timestamp}` | Adjacency list pattern |
155
-
156
- **Key Pattern Rules:**
157
- - `ENTITY#{id}` - Simple identifier
158
- - `PARENT#{id}#CHILD#{id}` - Hierarchy
159
- - `TYPE#{category}#{identifier}` - Categorization
160
- - `#{timestamp}` - Temporal sorting (# prefix ensures ordering)
161
-
162
- ## Type Safety
163
-
164
- ```typescript
165
- import { type InputItem, type FormattedItem } from 'dynamodb-toolbox/entity'
166
-
167
- // Type exports
168
- type UserRecord = typeof UserEntity
169
- type UserInput = InputItem<typeof UserEntity> // For writes
170
- type UserFormatted = FormattedItem<typeof UserEntity> // For reads
171
-
172
- // Usage in entities
173
- class User {
174
- static fromRecord(record: UserFormatted): User { /* ... */ }
175
- toRecord(): UserInput { /* ... */ }
176
- }
177
- ```
178
-
179
- See [references/entity-layer.md](references/entity-layer.md) for transformation patterns.
180
-
181
- ## Repository Pattern
182
-
183
- ```typescript
184
- import { PutItemCommand, GetItemCommand, DeleteItemCommand } from 'dynamodb-toolbox'
185
-
186
- class UserRepository {
187
- constructor(private entity: UserRecord) {}
188
-
189
- // CREATE with duplicate check
190
- async create(user: User): Promise<User> {
191
- try {
192
- const result = await this.entity
193
- .build(PutItemCommand)
194
- .item(user.toRecord())
195
- .options({
196
- condition: { attr: "PK", exists: false }, // Prevent duplicates
197
- })
198
- .send()
199
-
200
- return User.fromRecord(result.ToolboxItem)
201
- } catch (error) {
202
- if (error instanceof ConditionalCheckFailedException) {
203
- throw new DuplicateEntityError("User", user.username)
204
- }
205
- throw error
206
- }
207
- }
208
-
209
- // GET by key
210
- async get(username: string): Promise<User | undefined> {
211
- const result = await this.entity
212
- .build(GetItemCommand)
213
- .key({ username })
214
- .send()
215
-
216
- return result.Item ? User.fromRecord(result.Item) : undefined
217
- }
218
-
219
- // UPDATE with existence check
220
- async update(user: User): Promise<User> {
221
- try {
222
- const result = await this.entity
223
- .build(PutItemCommand)
224
- .item(user.toRecord())
225
- .options({
226
- condition: { attr: "PK", exists: true }, // Must exist
227
- })
228
- .send()
229
-
230
- return User.fromRecord(result.ToolboxItem)
231
- } catch (error) {
232
- if (error instanceof ConditionalCheckFailedException) {
233
- throw new EntityNotFoundError("User", user.username)
234
- }
235
- throw error
236
- }
237
- }
238
-
239
- // DELETE
240
- async delete(username: string): Promise<void> {
241
- await this.entity.build(DeleteItemCommand).key({ username }).send()
242
- }
243
- }
244
- ```
245
-
246
- See [references/error-handling.md](references/error-handling.md) for error patterns.
247
-
248
- ## Query Patterns
249
-
250
- ### Query GSI
251
-
252
- ```typescript
253
- import { QueryCommand } from 'dynamodb-toolbox/table/actions/query'
254
-
255
- // List issues for a repository using GSI1
256
- async listIssues(owner: string, repoName: string): Promise<Issue[]> {
257
- const result = await this.table
258
- .build(QueryCommand)
259
- .entities(this.issueEntity)
260
- .query({
261
- partition: `ISSUE#${owner}#${repoName}`,
262
- index: "GSI1",
263
- })
264
- .send()
265
-
266
- return result.Items?.map(item => Issue.fromRecord(item)) || []
267
- }
268
- ```
269
-
270
- ### Query with Range Filter
271
-
272
- ```typescript
273
- // List by status using beginsWith on SK
274
- async listOpenIssues(owner: string, repoName: string): Promise<Issue[]> {
275
- const result = await this.table
276
- .build(QueryCommand)
277
- .entities(this.issueEntity)
278
- .query({
279
- partition: `ISSUE#${owner}#${repoName}`,
280
- index: "GSI4",
281
- range: {
282
- beginsWith: "ISSUE#OPEN#", // Filter to open issues only
283
- },
284
- })
285
- .send()
286
-
287
- return result.Items?.map(item => Issue.fromRecord(item)) || []
288
- }
289
- ```
290
-
291
- ### Pagination
292
-
293
- ```typescript
294
- // Encode/decode pagination tokens
295
- function encodePageToken(lastEvaluated?: Record<string, unknown>): string | undefined {
296
- return lastEvaluated
297
- ? Buffer.from(JSON.stringify(lastEvaluated)).toString("base64")
298
- : undefined
299
- }
300
-
301
- function decodePageToken(token?: string): Record<string, unknown> | undefined {
302
- return token ? JSON.parse(Buffer.from(token, "base64").toString()) : undefined
303
- }
304
-
305
- // Query with pagination
306
- async listReposByOwner(owner: string, limit = 50, offset?: string) {
307
- const result = await this.table
308
- .build(QueryCommand)
309
- .entities(this.repoEntity)
310
- .query({
311
- partition: `ACCOUNT#${owner}`,
312
- index: "GSI3",
313
- range: { lt: "ACCOUNT#" }, // Filter to only repos (not account itself)
314
- })
315
- .options({
316
- reverse: true, // Newest first
317
- exclusiveStartKey: decodePageToken(offset), // Continue from cursor
318
- limit,
319
- })
320
- .send()
321
-
322
- return {
323
- items: result.Items?.map(item => Repo.fromRecord(item)) || [],
324
- nextOffset: encodePageToken(result.LastEvaluatedKey),
325
- }
326
- }
327
- ```
328
-
329
- ## Transactions
330
-
331
- See [references/transactions.md](references/transactions.md) for:
332
- - Multi-entity transactions (PutTransaction + ConditionCheck)
333
- - Atomic counters with `$add(1)`
334
- - TransactionCanceledException handling
335
-
336
- ## Testing
337
-
338
- See [references/testing.md](references/testing.md) for:
339
- - DynamoDB Local setup
340
- - Test fixtures and factories
341
- - Concurrency and temporal sorting tests
342
-
343
- ## Quick Reference
344
-
345
- **Schema:**
346
- - Use `item({})` for schema definition
347
- - Mark key attributes with `.key()`
348
- - Separate business attributes from computed keys using `.and()`
349
- - Use `.link<typeof _schema>()` to compute PK/SK/GSI keys
350
- - Use `.validate()` for field validation
351
- - Use `.savedAs()` when DynamoDB name differs from schema name
352
-
353
- **Types:**
354
- - `InputItem<T>` for writes (excludes computed attributes)
355
- - `FormattedItem<T>` for reads (includes all attributes)
356
-
357
- **Repository:**
358
- - Use `PutItemCommand` with `{ attr: "PK", exists: false }` for creates
359
- - Use `PutItemCommand` with `{ attr: "PK", exists: true }` for updates
360
- - Use `GetItemCommand` with `.key()` for reads
361
- - Use `QueryCommand` with `.entities()` for type-safe queries
362
-
363
- **Errors:**
364
- - `ConditionalCheckFailedException` → DuplicateEntityError (create) or EntityNotFoundError (update)
365
- - Always catch and convert to domain errors
366
-
367
- **Testing:**
368
- - Use unique IDs per test run (timestamp-based)
369
- - Clean up test data after each test
370
- - Use DynamoDB Local for development