@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,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
|