@martinffx/atelier 1.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +95 -220
- package/agents/architect.md +6 -6
- package/agents/oracle.md +1 -1
- package/agents/{recon.md → sentinel.md} +3 -3
- package/dist/atelier.js +167 -26
- package/package.json +2 -1
- package/skills/atelier-orchestrator/SKILL.md +180 -0
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/SKILL.md +2 -2
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +9 -9
- package/skills/code-subagents/SKILL.md +44 -75
- package/skills/code-subagents/references/batch-reviewer-prompt.md +44 -0
- package/skills/code-subagents/references/implementor-prompt.md +10 -11
- package/skills/spec-brainstorm/SKILL.md +21 -16
- package/skills/spec-finish/SKILL.md +14 -14
- package/skills/spec-implement/SKILL.md +42 -42
- package/skills/spec-plan/SKILL.md +72 -26
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/code-subagents/references/code-quality-reviewer-prompt.md +0 -48
- package/skills/code-subagents/references/spec-reviewer-prompt.md +0 -62
- 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/spec-orchestrator/SKILL.md +0 -145
- 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,398 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: typescript-drizzle-orm
|
|
3
|
-
description: Type-safe SQL with Drizzle ORM in TypeScript. Use when defining database schemas, writing queries, setting up relations, running migrations, or working with PostgreSQL/MySQL/SQLite/Cloudflare D1/Durable Objects data layers.
|
|
4
|
-
user-invocable: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Drizzle ORM
|
|
8
|
-
|
|
9
|
-
Lightweight, type-safe ORM with SQL-like and relational query APIs for PostgreSQL, MySQL, SQLite, Cloudflare D1, and Durable Objects.
|
|
10
|
-
|
|
11
|
-
## Quick Start (PostgreSQL)
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import {
|
|
15
|
-
pgTable,
|
|
16
|
-
serial,
|
|
17
|
-
text,
|
|
18
|
-
integer,
|
|
19
|
-
timestamp,
|
|
20
|
-
boolean,
|
|
21
|
-
varchar,
|
|
22
|
-
uuid,
|
|
23
|
-
primaryKey,
|
|
24
|
-
unique,
|
|
25
|
-
index
|
|
26
|
-
} from 'drizzle-orm/pg-core'
|
|
27
|
-
|
|
28
|
-
export const users = pgTable('users', {
|
|
29
|
-
id: serial('id').primaryKey(),
|
|
30
|
-
name: text('name').notNull(),
|
|
31
|
-
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
32
|
-
age: integer('age'),
|
|
33
|
-
isActive: boolean('is_active').default(true),
|
|
34
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
35
|
-
updatedAt: timestamp('updated_at').$onUpdate(() => new Date()),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
export const posts = pgTable('posts', {
|
|
39
|
-
id: serial('id').primaryKey(),
|
|
40
|
-
title: text('title').notNull(),
|
|
41
|
-
content: text('content'),
|
|
42
|
-
authorId: integer('author_id')
|
|
43
|
-
.notNull()
|
|
44
|
-
.references(() => users.id, { onDelete: 'cascade' }),
|
|
45
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
46
|
-
})
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
See [references/postgresql.md](./references/postgresql.md) for detailed PostgreSQL patterns.
|
|
50
|
-
|
|
51
|
-
## Quick Start (SQLite/D1)
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
55
|
-
|
|
56
|
-
export const users = sqliteTable('users', {
|
|
57
|
-
id: text('id').primaryKey(),
|
|
58
|
-
name: text('name').notNull(),
|
|
59
|
-
email: text('email').notNull(),
|
|
60
|
-
isActive: integer('is_active', { mode: 'boolean' }).default(true),
|
|
61
|
-
createdAt: text('created_at').notNull(),
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
export const posts = sqliteTable('posts', {
|
|
65
|
-
id: text('id').primaryKey(),
|
|
66
|
-
title: text('title').notNull(),
|
|
67
|
-
content: text('content'),
|
|
68
|
-
authorId: text('author_id')
|
|
69
|
-
.notNull()
|
|
70
|
-
.references(() => users.id, { onDelete: 'cascade' }),
|
|
71
|
-
createdAt: text('created_at').notNull(),
|
|
72
|
-
})
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
See [references/sqlite.md](./references/sqlite.md) for SQLite patterns and [references/cloudflare.md](./references/cloudflare.md) for D1 and Durable Objects.
|
|
76
|
-
|
|
77
|
-
## Type Inference
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
// Infer types from schema - no manual interfaces needed
|
|
81
|
-
export type User = typeof users.$inferSelect
|
|
82
|
-
export type NewUser = typeof users.$inferInsert
|
|
83
|
-
|
|
84
|
-
export type Post = typeof posts.$inferSelect
|
|
85
|
-
export type NewPost = typeof posts.$inferInsert
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Relations
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { relations } from 'drizzle-orm'
|
|
92
|
-
|
|
93
|
-
export const usersRelations = relations(users, ({ many }) => ({
|
|
94
|
-
posts: many(posts),
|
|
95
|
-
}))
|
|
96
|
-
|
|
97
|
-
export const postsRelations = relations(posts, ({ one }) => ({
|
|
98
|
-
author: one(users, {
|
|
99
|
-
fields: [posts.authorId],
|
|
100
|
-
references: [users.id],
|
|
101
|
-
}),
|
|
102
|
-
}))
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## SQL-like Queries
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
import { eq, and, or, gt, like, isNull, desc, asc } from 'drizzle-orm'
|
|
109
|
-
|
|
110
|
-
// Select all
|
|
111
|
-
const allUsers = await db.select().from(users)
|
|
112
|
-
|
|
113
|
-
// Select specific columns
|
|
114
|
-
const names = await db.select({ name: users.name }).from(users)
|
|
115
|
-
|
|
116
|
-
// Where clause
|
|
117
|
-
const activeUsers = await db
|
|
118
|
-
.select()
|
|
119
|
-
.from(users)
|
|
120
|
-
.where(eq(users.isActive, true))
|
|
121
|
-
|
|
122
|
-
// Multiple conditions
|
|
123
|
-
const filtered = await db
|
|
124
|
-
.select()
|
|
125
|
-
.from(users)
|
|
126
|
-
.where(and(
|
|
127
|
-
eq(users.isActive, true),
|
|
128
|
-
gt(users.age, 18)
|
|
129
|
-
))
|
|
130
|
-
|
|
131
|
-
// Like/pattern matching
|
|
132
|
-
const matching = await db
|
|
133
|
-
.select()
|
|
134
|
-
.from(users)
|
|
135
|
-
.where(like(users.email, '%@example.com'))
|
|
136
|
-
|
|
137
|
-
// Order and limit
|
|
138
|
-
const recent = await db
|
|
139
|
-
.select()
|
|
140
|
-
.from(posts)
|
|
141
|
-
.orderBy(desc(posts.createdAt))
|
|
142
|
-
.limit(10)
|
|
143
|
-
|
|
144
|
-
// Joins
|
|
145
|
-
const postsWithAuthors = await db
|
|
146
|
-
.select({
|
|
147
|
-
postTitle: posts.title,
|
|
148
|
-
authorName: users.name,
|
|
149
|
-
})
|
|
150
|
-
.from(posts)
|
|
151
|
-
.leftJoin(users, eq(posts.authorId, users.id))
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Relational Queries
|
|
155
|
-
|
|
156
|
-
```typescript
|
|
157
|
-
// Requires schema with relations passed to drizzle()
|
|
158
|
-
const db = drizzle(pool, { schema })
|
|
159
|
-
|
|
160
|
-
// Find many with relations
|
|
161
|
-
const usersWithPosts = await db.query.users.findMany({
|
|
162
|
-
with: {
|
|
163
|
-
posts: true,
|
|
164
|
-
},
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Partial columns + nested relations
|
|
168
|
-
const partial = await db.query.users.findMany({
|
|
169
|
-
columns: {
|
|
170
|
-
id: true,
|
|
171
|
-
name: true,
|
|
172
|
-
},
|
|
173
|
-
with: {
|
|
174
|
-
posts: {
|
|
175
|
-
columns: {
|
|
176
|
-
title: true,
|
|
177
|
-
createdAt: true,
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
// Find first
|
|
184
|
-
const user = await db.query.users.findFirst({
|
|
185
|
-
where: eq(users.id, 1),
|
|
186
|
-
with: { posts: true },
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
// Exclude columns
|
|
190
|
-
const withoutEmail = await db.query.users.findMany({
|
|
191
|
-
columns: {
|
|
192
|
-
email: false, // exclude
|
|
193
|
-
},
|
|
194
|
-
})
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Insert
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
// Single insert
|
|
201
|
-
const [newUser] = await db
|
|
202
|
-
.insert(users)
|
|
203
|
-
.values({ name: 'Alice', email: 'alice@example.com' })
|
|
204
|
-
.returning()
|
|
205
|
-
|
|
206
|
-
// Multiple insert
|
|
207
|
-
await db.insert(users).values([
|
|
208
|
-
{ name: 'Bob', email: 'bob@example.com' },
|
|
209
|
-
{ name: 'Carol', email: 'carol@example.com' },
|
|
210
|
-
])
|
|
211
|
-
|
|
212
|
-
// Upsert (on conflict)
|
|
213
|
-
await db
|
|
214
|
-
.insert(users)
|
|
215
|
-
.values({ name: 'Alice', email: 'alice@example.com' })
|
|
216
|
-
.onConflictDoUpdate({
|
|
217
|
-
target: users.email,
|
|
218
|
-
set: { name: 'Alice Updated' },
|
|
219
|
-
})
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Update
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
await db
|
|
226
|
-
.update(users)
|
|
227
|
-
.set({ isActive: false })
|
|
228
|
-
.where(eq(users.id, 1))
|
|
229
|
-
|
|
230
|
-
// Update with returning
|
|
231
|
-
const [updated] = await db
|
|
232
|
-
.update(users)
|
|
233
|
-
.set({ name: 'New Name' })
|
|
234
|
-
.where(eq(users.id, 1))
|
|
235
|
-
.returning()
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## Delete
|
|
239
|
-
|
|
240
|
-
```typescript
|
|
241
|
-
await db.delete(users).where(eq(users.id, 1))
|
|
242
|
-
|
|
243
|
-
// Delete with returning
|
|
244
|
-
const [deleted] = await db
|
|
245
|
-
.delete(users)
|
|
246
|
-
.where(eq(users.id, 1))
|
|
247
|
-
.returning()
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
## Transactions
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
await db.transaction(async (tx) => {
|
|
254
|
-
const [user] = await tx
|
|
255
|
-
.insert(users)
|
|
256
|
-
.values({ name: 'Alice', email: 'alice@example.com' })
|
|
257
|
-
.returning()
|
|
258
|
-
|
|
259
|
-
await tx.insert(posts).values({
|
|
260
|
-
title: 'First Post',
|
|
261
|
-
authorId: user.id,
|
|
262
|
-
})
|
|
263
|
-
})
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
## Entity Pattern
|
|
267
|
-
|
|
268
|
-
Domain entities encapsulate data transformations between API, domain, and database layers.
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
|
|
272
|
-
import type { users } from './schema'
|
|
273
|
-
|
|
274
|
-
type UserRecord = InferSelectModel<typeof users>
|
|
275
|
-
type UserInsert = InferInsertModel<typeof users>
|
|
276
|
-
|
|
277
|
-
class UserEntity {
|
|
278
|
-
public readonly id: string
|
|
279
|
-
public readonly name: string
|
|
280
|
-
public readonly email: string
|
|
281
|
-
public readonly createdAt: Date
|
|
282
|
-
|
|
283
|
-
private constructor(data: UserEntityData) {
|
|
284
|
-
Object.assign(this, data)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// API request → Entity
|
|
288
|
-
static fromRequest(rq: CreateUserRequest, id?: string): UserEntity {
|
|
289
|
-
return new UserEntity({
|
|
290
|
-
id: id ?? crypto.randomUUID(),
|
|
291
|
-
name: rq.name,
|
|
292
|
-
email: rq.email,
|
|
293
|
-
createdAt: new Date(),
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// DB record → Entity
|
|
298
|
-
static fromRecord(record: UserRecord): UserEntity {
|
|
299
|
-
return new UserEntity({
|
|
300
|
-
id: record.id,
|
|
301
|
-
name: record.name,
|
|
302
|
-
email: record.email,
|
|
303
|
-
createdAt: record.createdAt,
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Entity → DB insert
|
|
308
|
-
toRecord(): UserInsert {
|
|
309
|
-
return {
|
|
310
|
-
id: this.id,
|
|
311
|
-
name: this.name,
|
|
312
|
-
email: this.email,
|
|
313
|
-
createdAt: this.createdAt,
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Entity → API response
|
|
318
|
-
toResponse(): UserResponse {
|
|
319
|
-
return {
|
|
320
|
-
id: this.id,
|
|
321
|
-
name: this.name,
|
|
322
|
-
email: this.email,
|
|
323
|
-
createdAt: this.createdAt.toISOString(),
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
See [references/entity-pattern.md](./references/entity-pattern.md) for detailed examples.
|
|
330
|
-
|
|
331
|
-
## Repository Pattern
|
|
332
|
-
|
|
333
|
-
Repositories wrap database access with error handling and business logic.
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
import { eq, and } from 'drizzle-orm'
|
|
337
|
-
import { users } from './schema'
|
|
338
|
-
import { UserEntity } from './entities/UserEntity'
|
|
339
|
-
|
|
340
|
-
class UserRepo {
|
|
341
|
-
constructor(private db: DrizzleDB) {}
|
|
342
|
-
|
|
343
|
-
async getById(id: string): Promise<UserEntity> {
|
|
344
|
-
const record = await this.db.query.users.findFirst({
|
|
345
|
-
where: eq(users.id, id),
|
|
346
|
-
})
|
|
347
|
-
if (!record) throw new NotFoundError('User not found')
|
|
348
|
-
return UserEntity.fromRecord(record)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async create(entity: UserEntity): Promise<UserEntity> {
|
|
352
|
-
try {
|
|
353
|
-
const [record] = await this.db
|
|
354
|
-
.insert(users)
|
|
355
|
-
.values(entity.toRecord())
|
|
356
|
-
.returning()
|
|
357
|
-
return UserEntity.fromRecord(record)
|
|
358
|
-
} catch (error) {
|
|
359
|
-
throw handleDBError(error, { userId: entity.id })
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async update(entity: UserEntity): Promise<UserEntity> {
|
|
364
|
-
const [record] = await this.db
|
|
365
|
-
.update(users)
|
|
366
|
-
.set(entity.toRecord())
|
|
367
|
-
.where(eq(users.id, entity.id))
|
|
368
|
-
.returning()
|
|
369
|
-
if (!record) throw new NotFoundError('User not found')
|
|
370
|
-
return UserEntity.fromRecord(record)
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
See [references/repository-pattern.md](./references/repository-pattern.md) for detailed examples.
|
|
376
|
-
|
|
377
|
-
## Database-Specific Guides
|
|
378
|
-
|
|
379
|
-
For database-specific patterns, connection setup, migrations, and testing:
|
|
380
|
-
|
|
381
|
-
- **[PostgreSQL patterns](./references/postgresql.md)** - Connection, migrations, column types, error codes, optimistic locking
|
|
382
|
-
- **[SQLite patterns](./references/sqlite.md)** - Schema definition, type differences, better-sqlite3 testing
|
|
383
|
-
- **[Cloudflare D1 & Durable Objects](./references/cloudflare.md)** - D1 connection, DO SQLite, testing with vitest-pool-workers, D1 vs DO decision guide
|
|
384
|
-
|
|
385
|
-
## Guidelines
|
|
386
|
-
|
|
387
|
-
1. Define schema in dedicated `schema.ts` file(s)
|
|
388
|
-
2. Use `$inferSelect` and `$inferInsert` for types - don't duplicate
|
|
389
|
-
3. Always define relations for nested queries with `db.query`
|
|
390
|
-
4. Pass `{ schema }` to `drizzle()` to enable relational queries
|
|
391
|
-
5. Use SQL-like API (`db.select()`) for complex joins
|
|
392
|
-
6. Use relational API (`db.query`) for nested data fetching
|
|
393
|
-
7. Foreign keys need explicit `references(() => table.column)`
|
|
394
|
-
8. Use `returning()` to get inserted/updated/deleted rows
|
|
395
|
-
9. Wrap database access in Repository classes for error handling
|
|
396
|
-
10. Use Entity classes for all data transformations (fromRequest, fromRecord, toRecord, toResponse)
|
|
397
|
-
11. Add `lockVersion` column for optimistic locking on mutable resources
|
|
398
|
-
12. Handle DB errors with specific error types (23505=conflict, 23503=not found, 40001/OC000=retry)
|