@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,479 +0,0 @@
|
|
|
1
|
-
# Repository Pattern
|
|
2
|
-
|
|
3
|
-
Repositories wrap database access, providing a clean abstraction layer with error handling, validation, and business logic.
|
|
4
|
-
|
|
5
|
-
## Core Concept
|
|
6
|
-
|
|
7
|
-
Repositories:
|
|
8
|
-
- Encapsulate all database operations for an entity
|
|
9
|
-
- Transform between records and entities using entity methods
|
|
10
|
-
- Handle database errors and convert to domain errors
|
|
11
|
-
- Enforce multi-tenancy and authorization boundaries
|
|
12
|
-
- Manage optimistic locking and retries
|
|
13
|
-
|
|
14
|
-
## Basic Repository
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
import { eq, and } from 'drizzle-orm'
|
|
18
|
-
import type { DrizzleDB } from '../db'
|
|
19
|
-
import { users } from '../schema'
|
|
20
|
-
import { UserEntity } from '../entities/UserEntity'
|
|
21
|
-
import { NotFoundError } from '../errors'
|
|
22
|
-
|
|
23
|
-
export class UserRepo {
|
|
24
|
-
constructor(private db: DrizzleDB) {}
|
|
25
|
-
|
|
26
|
-
async getById(id: string): Promise<UserEntity> {
|
|
27
|
-
const record = await this.db.query.users.findFirst({
|
|
28
|
-
where: eq(users.id, id),
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
if (!record) {
|
|
32
|
-
throw new NotFoundError('User not found', { userId: id })
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return UserEntity.fromRecord(record)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async list(): Promise<UserEntity[]> {
|
|
39
|
-
const records = await this.db.query.users.findMany({
|
|
40
|
-
orderBy: desc(users.createdAt),
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
return records.map(UserEntity.fromRecord)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async create(entity: UserEntity): Promise<UserEntity> {
|
|
47
|
-
const [record] = await this.db
|
|
48
|
-
.insert(users)
|
|
49
|
-
.values(entity.toRecord())
|
|
50
|
-
.returning()
|
|
51
|
-
|
|
52
|
-
return UserEntity.fromRecord(record)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async update(entity: UserEntity): Promise<UserEntity> {
|
|
56
|
-
const [record] = await this.db
|
|
57
|
-
.update(users)
|
|
58
|
-
.set(entity.toRecord())
|
|
59
|
-
.where(eq(users.id, entity.id))
|
|
60
|
-
.returning()
|
|
61
|
-
|
|
62
|
-
if (!record) {
|
|
63
|
-
throw new NotFoundError('User not found', { userId: entity.id })
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return UserEntity.fromRecord(record)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async delete(id: string): Promise<void> {
|
|
70
|
-
const result = await this.db
|
|
71
|
-
.delete(users)
|
|
72
|
-
.where(eq(users.id, id))
|
|
73
|
-
.returning()
|
|
74
|
-
|
|
75
|
-
if (result.length === 0) {
|
|
76
|
-
throw new NotFoundError('User not found', { userId: id })
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Repository with Error Handling
|
|
83
|
-
|
|
84
|
-
Convert database errors to domain errors:
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
import { eq } from 'drizzle-orm'
|
|
88
|
-
import type { DrizzleDB } from '../db'
|
|
89
|
-
import { users } from '../schema'
|
|
90
|
-
import { UserEntity } from '../entities/UserEntity'
|
|
91
|
-
import { handleDBError, NotFoundError } from '../errors'
|
|
92
|
-
|
|
93
|
-
export class UserRepo {
|
|
94
|
-
constructor(private db: DrizzleDB) {}
|
|
95
|
-
|
|
96
|
-
async create(entity: UserEntity): Promise<UserEntity> {
|
|
97
|
-
try {
|
|
98
|
-
const [record] = await this.db
|
|
99
|
-
.insert(users)
|
|
100
|
-
.values(entity.toRecord())
|
|
101
|
-
.returning()
|
|
102
|
-
|
|
103
|
-
return UserEntity.fromRecord(record)
|
|
104
|
-
} catch (error) {
|
|
105
|
-
// Maps DB errors (23505, 23503, etc) to domain errors
|
|
106
|
-
throw handleDBError(error, { userId: entity.id })
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async update(entity: UserEntity): Promise<UserEntity> {
|
|
111
|
-
try {
|
|
112
|
-
const [record] = await this.db
|
|
113
|
-
.update(users)
|
|
114
|
-
.set(entity.toRecord())
|
|
115
|
-
.where(eq(users.id, entity.id))
|
|
116
|
-
.returning()
|
|
117
|
-
|
|
118
|
-
if (!record) {
|
|
119
|
-
throw new NotFoundError('User not found', { userId: entity.id })
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return UserEntity.fromRecord(record)
|
|
123
|
-
} catch (error) {
|
|
124
|
-
throw handleDBError(error, { userId: entity.id })
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Error handler in errors.ts
|
|
130
|
-
type ErrorContext = {
|
|
131
|
-
userId?: string
|
|
132
|
-
resourceId?: string
|
|
133
|
-
[key: string]: unknown
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function handleDBError(error: unknown, context: ErrorContext = {}): never {
|
|
137
|
-
const code = (error as { code?: string }).code
|
|
138
|
-
|
|
139
|
-
switch (code) {
|
|
140
|
-
case '23505': // unique_violation
|
|
141
|
-
throw new ConflictError('Resource already exists', context)
|
|
142
|
-
case '23503': // foreign_key_violation
|
|
143
|
-
throw new NotFoundError('Referenced resource not found', context)
|
|
144
|
-
case '40001': // serialization_failure (Postgres)
|
|
145
|
-
throw new ServiceUnavailableError('Transaction conflict - please retry', {
|
|
146
|
-
retryable: true,
|
|
147
|
-
...context,
|
|
148
|
-
})
|
|
149
|
-
case 'OC000': // occ_conflict (AWS DSQL)
|
|
150
|
-
throw new ServiceUnavailableError('Optimistic concurrency conflict', {
|
|
151
|
-
retryable: true,
|
|
152
|
-
...context,
|
|
153
|
-
})
|
|
154
|
-
default:
|
|
155
|
-
throw error
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Repository with Multi-Tenancy
|
|
161
|
-
|
|
162
|
-
Enforce organization-level isolation:
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
import { TypeID } from 'typeid-js'
|
|
166
|
-
import { eq, and } from 'drizzle-orm'
|
|
167
|
-
import type { DrizzleDB } from '../db'
|
|
168
|
-
import { ledgers } from '../schema'
|
|
169
|
-
import { LedgerEntity } from '../entities/LedgerEntity'
|
|
170
|
-
import { NotFoundError } from '../errors'
|
|
171
|
-
|
|
172
|
-
type OrgID = TypeID<'org'>
|
|
173
|
-
type LedgerID = TypeID<'lgr'>
|
|
174
|
-
|
|
175
|
-
export class LedgerRepo {
|
|
176
|
-
constructor(private db: DrizzleDB) {}
|
|
177
|
-
|
|
178
|
-
// ALWAYS include orgId in queries for multi-tenancy
|
|
179
|
-
async getById(orgId: OrgID, ledgerId: LedgerID): Promise<LedgerEntity> {
|
|
180
|
-
const record = await this.db.query.ledgers.findFirst({
|
|
181
|
-
where: and(
|
|
182
|
-
eq(ledgers.id, ledgerId.toString()),
|
|
183
|
-
eq(ledgers.organizationId, orgId.toString()) // Multi-tenancy check
|
|
184
|
-
),
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
if (!record) {
|
|
188
|
-
throw new NotFoundError('Ledger not found', {
|
|
189
|
-
organizationId: orgId.toString(),
|
|
190
|
-
ledgerId: ledgerId.toString(),
|
|
191
|
-
})
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return LedgerEntity.fromRecord(record)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async list(orgId: OrgID): Promise<LedgerEntity[]> {
|
|
198
|
-
const records = await this.db.query.ledgers.findMany({
|
|
199
|
-
where: eq(ledgers.organizationId, orgId.toString()),
|
|
200
|
-
orderBy: desc(ledgers.created),
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
return records.map(LedgerEntity.fromRecord)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async create(entity: LedgerEntity): Promise<LedgerEntity> {
|
|
207
|
-
try {
|
|
208
|
-
const [record] = await this.db
|
|
209
|
-
.insert(ledgers)
|
|
210
|
-
.values(entity.toRecord())
|
|
211
|
-
.returning()
|
|
212
|
-
|
|
213
|
-
return LedgerEntity.fromRecord(record)
|
|
214
|
-
} catch (error) {
|
|
215
|
-
throw handleDBError(error, {
|
|
216
|
-
organizationId: entity.organizationId.toString(),
|
|
217
|
-
ledgerId: entity.id.toString(),
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async update(orgId: OrgID, entity: LedgerEntity): Promise<LedgerEntity> {
|
|
223
|
-
try {
|
|
224
|
-
const [record] = await this.db
|
|
225
|
-
.update(ledgers)
|
|
226
|
-
.set(entity.toRecord())
|
|
227
|
-
.where(and(
|
|
228
|
-
eq(ledgers.id, entity.id.toString()),
|
|
229
|
-
eq(ledgers.organizationId, orgId.toString()) // Multi-tenancy check
|
|
230
|
-
))
|
|
231
|
-
.returning()
|
|
232
|
-
|
|
233
|
-
if (!record) {
|
|
234
|
-
throw new NotFoundError('Ledger not found', {
|
|
235
|
-
organizationId: orgId.toString(),
|
|
236
|
-
ledgerId: entity.id.toString(),
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return LedgerEntity.fromRecord(record)
|
|
241
|
-
} catch (error) {
|
|
242
|
-
throw handleDBError(error, {
|
|
243
|
-
organizationId: orgId.toString(),
|
|
244
|
-
ledgerId: entity.id.toString(),
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async delete(orgId: OrgID, ledgerId: LedgerID): Promise<void> {
|
|
250
|
-
const result = await this.db
|
|
251
|
-
.delete(ledgers)
|
|
252
|
-
.where(and(
|
|
253
|
-
eq(ledgers.id, ledgerId.toString()),
|
|
254
|
-
eq(ledgers.organizationId, orgId.toString()) // Multi-tenancy check
|
|
255
|
-
))
|
|
256
|
-
.returning()
|
|
257
|
-
|
|
258
|
-
if (result.length === 0) {
|
|
259
|
-
throw new NotFoundError('Ledger not found', {
|
|
260
|
-
organizationId: orgId.toString(),
|
|
261
|
-
ledgerId: ledgerId.toString(),
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
## Repository with Optimistic Locking
|
|
269
|
-
|
|
270
|
-
Handle concurrent updates safely:
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
import { eq, and, sql } from 'drizzle-orm'
|
|
274
|
-
import type { DrizzleDB } from '../db'
|
|
275
|
-
import { users } from '../schema'
|
|
276
|
-
import { UserEntity } from '../entities/UserEntity'
|
|
277
|
-
import { ConflictError, NotFoundError } from '../errors'
|
|
278
|
-
|
|
279
|
-
export class UserRepo {
|
|
280
|
-
constructor(private db: DrizzleDB) {}
|
|
281
|
-
|
|
282
|
-
async update(entity: UserEntity): Promise<UserEntity> {
|
|
283
|
-
try {
|
|
284
|
-
const result = await this.db
|
|
285
|
-
.update(users)
|
|
286
|
-
.set({
|
|
287
|
-
...entity.toRecord(),
|
|
288
|
-
lockVersion: sql`${users.lockVersion} + 1`, // Increment version
|
|
289
|
-
})
|
|
290
|
-
.where(and(
|
|
291
|
-
eq(users.id, entity.id),
|
|
292
|
-
eq(users.lockVersion, entity.lockVersion) // Version check
|
|
293
|
-
))
|
|
294
|
-
.returning()
|
|
295
|
-
|
|
296
|
-
if (result.length === 0) {
|
|
297
|
-
// Either not found or version mismatch
|
|
298
|
-
const exists = await this.db.query.users.findFirst({
|
|
299
|
-
where: eq(users.id, entity.id),
|
|
300
|
-
columns: { id: true, lockVersion: true },
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
if (!exists) {
|
|
304
|
-
throw new NotFoundError('User not found', { userId: entity.id })
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Version mismatch - resource was modified
|
|
308
|
-
throw new ConflictError({
|
|
309
|
-
message: 'Resource was modified by another transaction',
|
|
310
|
-
retryable: true, // Service layer can retry
|
|
311
|
-
context: {
|
|
312
|
-
userId: entity.id,
|
|
313
|
-
expectedVersion: entity.lockVersion,
|
|
314
|
-
actualVersion: exists.lockVersion,
|
|
315
|
-
},
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return UserEntity.fromRecord(result[0])
|
|
320
|
-
} catch (error) {
|
|
321
|
-
throw handleDBError(error, { userId: entity.id })
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## Repository with Transactions
|
|
328
|
-
|
|
329
|
-
Complex operations across multiple tables:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
import { TypeID } from 'typeid-js'
|
|
333
|
-
import { eq, and, inArray, sql } from 'drizzle-orm'
|
|
334
|
-
import type { DrizzleDB } from '../db'
|
|
335
|
-
import {
|
|
336
|
-
ledgerTransactions,
|
|
337
|
-
ledgerTransactionEntries,
|
|
338
|
-
ledgerAccounts,
|
|
339
|
-
} from '../schema'
|
|
340
|
-
import { LedgerTransactionEntity } from '../entities/LedgerTransactionEntity'
|
|
341
|
-
import { LedgerAccountEntity } from '../entities/LedgerAccountEntity'
|
|
342
|
-
import { ConflictError } from '../errors'
|
|
343
|
-
|
|
344
|
-
type OrgID = TypeID<'org'>
|
|
345
|
-
type LedgerTransactionID = TypeID<'ltr'>
|
|
346
|
-
|
|
347
|
-
export class LedgerTransactionRepo {
|
|
348
|
-
constructor(private db: DrizzleDB) {}
|
|
349
|
-
|
|
350
|
-
async createTransaction(
|
|
351
|
-
transaction: LedgerTransactionEntity
|
|
352
|
-
): Promise<LedgerTransactionEntity> {
|
|
353
|
-
// 1. Fetch all affected accounts OUTSIDE transaction
|
|
354
|
-
const accountIds = transaction.entries.map(e => e.accountId.toString())
|
|
355
|
-
const accountRecords = await this.db.select().from(ledgerAccounts)
|
|
356
|
-
.where(inArray(ledgerAccounts.id, accountIds))
|
|
357
|
-
|
|
358
|
-
const accountsById = new Map(
|
|
359
|
-
accountRecords.map(r => [r.id, LedgerAccountEntity.fromRecord(r)])
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
// 2. Calculate balance updates in-memory using entity business logic
|
|
363
|
-
const updatedAccounts = transaction.entries.map(entry => {
|
|
364
|
-
const account = accountsById.get(entry.accountId.toString())
|
|
365
|
-
if (!account) {
|
|
366
|
-
throw new NotFoundError('Account not found', {
|
|
367
|
-
accountId: entry.accountId.toString(),
|
|
368
|
-
})
|
|
369
|
-
}
|
|
370
|
-
// Entity contains the double-entry accounting logic
|
|
371
|
-
return account.applyEntry(entry)
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
// 3. Write atomically in single DB transaction
|
|
375
|
-
return await this.db.transaction(async tx => {
|
|
376
|
-
// Insert transaction record (with upsert for idempotency)
|
|
377
|
-
const [txRecord] = await tx
|
|
378
|
-
.insert(ledgerTransactions)
|
|
379
|
-
.values(transaction.toRecord())
|
|
380
|
-
.onConflictDoUpdate({
|
|
381
|
-
target: ledgerTransactions.idempotencyKey,
|
|
382
|
-
set: { updated: new Date() },
|
|
383
|
-
})
|
|
384
|
-
.returning()
|
|
385
|
-
|
|
386
|
-
// Insert transaction entries
|
|
387
|
-
await tx.insert(ledgerTransactionEntries).values(
|
|
388
|
-
transaction.entries.map(e => e.toRecord())
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
// Update account balances with optimistic locking
|
|
392
|
-
for (const account of updatedAccounts) {
|
|
393
|
-
const result = await tx
|
|
394
|
-
.update(ledgerAccounts)
|
|
395
|
-
.set({
|
|
396
|
-
...account.toRecord(),
|
|
397
|
-
lockVersion: sql`${ledgerAccounts.lockVersion} + 1`,
|
|
398
|
-
})
|
|
399
|
-
.where(and(
|
|
400
|
-
eq(ledgerAccounts.id, account.id.toString()),
|
|
401
|
-
eq(ledgerAccounts.lockVersion, account.lockVersion)
|
|
402
|
-
))
|
|
403
|
-
.returning()
|
|
404
|
-
|
|
405
|
-
if (result.length === 0) {
|
|
406
|
-
// Optimistic lock failure - another transaction modified this account
|
|
407
|
-
throw new ConflictError({
|
|
408
|
-
message: `Account ${account.id} was modified by another transaction`,
|
|
409
|
-
retryable: true, // Service layer will retry entire operation
|
|
410
|
-
context: {
|
|
411
|
-
accountId: account.id.toString(),
|
|
412
|
-
transactionId: transaction.id.toString(),
|
|
413
|
-
},
|
|
414
|
-
})
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return LedgerTransactionEntity.fromRecord(txRecord)
|
|
419
|
-
})
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
## Repository with Pagination
|
|
425
|
-
|
|
426
|
-
Cursor-based pagination for large datasets:
|
|
427
|
-
|
|
428
|
-
```typescript
|
|
429
|
-
import { gt, desc } from 'drizzle-orm'
|
|
430
|
-
import type { DrizzleDB } from '../db'
|
|
431
|
-
import { posts } from '../schema'
|
|
432
|
-
import { PostEntity } from '../entities/PostEntity'
|
|
433
|
-
|
|
434
|
-
interface PaginatedResult<T> {
|
|
435
|
-
items: T[]
|
|
436
|
-
nextCursor?: string
|
|
437
|
-
hasMore: boolean
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export class PostRepo {
|
|
441
|
-
constructor(private db: DrizzleDB) {}
|
|
442
|
-
|
|
443
|
-
async list(
|
|
444
|
-
limit: number = 20,
|
|
445
|
-
cursor?: string
|
|
446
|
-
): Promise<PaginatedResult<PostEntity>> {
|
|
447
|
-
const queryLimit = limit + 1 // Fetch one extra to check hasMore
|
|
448
|
-
|
|
449
|
-
const records = await this.db.query.posts.findMany({
|
|
450
|
-
where: cursor ? gt(posts.id, cursor) : undefined,
|
|
451
|
-
orderBy: desc(posts.createdAt),
|
|
452
|
-
limit: queryLimit,
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
const hasMore = records.length > limit
|
|
456
|
-
const items = records.slice(0, limit).map(PostEntity.fromRecord)
|
|
457
|
-
const nextCursor = hasMore ? records[limit - 1].id : undefined
|
|
458
|
-
|
|
459
|
-
return {
|
|
460
|
-
items,
|
|
461
|
-
nextCursor,
|
|
462
|
-
hasMore,
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
## Guidelines
|
|
469
|
-
|
|
470
|
-
1. **Constructor injection** - Pass `db` instance to constructor
|
|
471
|
-
2. **Return entities** - Always convert records to entities before returning
|
|
472
|
-
3. **Error context** - Include relevant IDs in error context for debugging
|
|
473
|
-
4. **Multi-tenancy** - Always filter by `organizationId` when applicable
|
|
474
|
-
5. **Optimistic locking** - Use `lockVersion` for resources with concurrent updates
|
|
475
|
-
6. **Transactions** - Use `db.transaction()` for multi-step operations
|
|
476
|
-
7. **Idempotency** - Use `onConflictDoUpdate` with idempotency keys where applicable
|
|
477
|
-
8. **Check returning** - Always check if `.returning()` is empty (means not found)
|
|
478
|
-
9. **Pagination** - Use cursor-based pagination for large result sets
|
|
479
|
-
10. **Error handling** - Wrap DB operations in try/catch and use `handleDBError()`
|