From a8498093bddc758189a283e8a6c2eb7b79603a74 Mon Sep 17 00:00:00 2001 From: Mel-906 Date: Fri, 27 Mar 2026 00:06:45 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20AsyncLocalStorage=E3=82=92=E7=94=A8?= =?UTF-8?q?=E3=81=84=E3=81=9FUnit=20of=20Work=E5=9F=BA=E7=9B=A4=E3=82=92?= =?UTF-8?q?=E5=B0=8E=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 複数の集約をまたぐDB操作をトランザクションで安全にまとめるための仕組みを追加。 AsyncLocalStorageによりRepository側のコード変更なしでトランザクションを共有できる。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/application/UnitOfWork.ts | 7 ++++ src/application/index.ts | 1 + src/application/usecase/base.ts | 24 ++++++++++++++ .../drizzle/DrizzleUnitOfWork.ts | 12 +++++++ src/infrastructure/drizzle/client.ts | 32 +++++++++++++++++-- src/infrastructure/drizzle/index.ts | 1 + 6 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/application/UnitOfWork.ts create mode 100644 src/infrastructure/drizzle/DrizzleUnitOfWork.ts diff --git a/src/application/UnitOfWork.ts b/src/application/UnitOfWork.ts new file mode 100644 index 0000000..2987d89 --- /dev/null +++ b/src/application/UnitOfWork.ts @@ -0,0 +1,7 @@ +/** + * 一連の操作をまとめて扱うためのインターフェース + * 具体的な実装(DBトランザクション等)はInfrastructure層が担当する + */ +export interface UnitOfWork { + run(fn: () => Promise): Promise; +} diff --git a/src/application/index.ts b/src/application/index.ts index 6a2038e..5e4e322 100644 --- a/src/application/index.ts +++ b/src/application/index.ts @@ -1,3 +1,4 @@ +export * from "./UnitOfWork"; export * from "./usecase/member"; export * from "./usecase/event"; export * from "./usecase/eventParticipation"; diff --git a/src/application/usecase/base.ts b/src/application/usecase/base.ts index 9704b84..c6f3eac 100644 --- a/src/application/usecase/base.ts +++ b/src/application/usecase/base.ts @@ -1,8 +1,32 @@ +import type { UnitOfWork } from "../UnitOfWork"; + /** * ユースケースの基底抽象クラス * TInputはユースケースへの入力型 * TOutputはユースケースからの出力型 */ export abstract class IUseCase { + /** + * ユースケースのビジネスロジックを実行する + * トランザクションなしで実行されるため、読み取り専用のUseCaseはこちらを使う + * 複数のDB書き込みを安全にまとめたい場合は run() を使うこと + */ abstract execute(input: Input): Promise; + + /** + * UnitOfWorkで囲んでexecuteを実行する + * トランザクション内で全てのDB操作がまとめて成功/失敗する + * 複数の集約をまたぐ書き込み操作がある場合に使用する + * + * 注意: + * - execute()内で外部API呼び出し(Discord API等)を行うと、 + * その応答待ちの間トランザクションが開きっぱなしになる + * 外部連携はトランザクション完了後に行うこと + * - トランザクションがロールバックされてもメモリ上のドメインオブジェクトは + * 元に戻らない。execute()内で取得・変更したドメインオブジェクトを + * ロールバック後に再利用しないこと + */ + async run(input: Input, unitOfWork: UnitOfWork): Promise { + return unitOfWork.run(() => this.execute(input)); + } } diff --git a/src/infrastructure/drizzle/DrizzleUnitOfWork.ts b/src/infrastructure/drizzle/DrizzleUnitOfWork.ts new file mode 100644 index 0000000..0c3bd27 --- /dev/null +++ b/src/infrastructure/drizzle/DrizzleUnitOfWork.ts @@ -0,0 +1,12 @@ +import type { UnitOfWork } from "#application/UnitOfWork"; +import { runInTransaction } from "./client"; + +/** + * UnitOfWorkのDrizzle実装 + * AsyncLocalStorageを利用して、トランザクションを処理の流れ全体で共有する + */ +export class DrizzleUnitOfWork implements UnitOfWork { + async run(fn: () => Promise): Promise { + return runInTransaction(fn); + } +} diff --git a/src/infrastructure/drizzle/client.ts b/src/infrastructure/drizzle/client.ts index 12ecc0b..f8364ba 100644 --- a/src/infrastructure/drizzle/client.ts +++ b/src/infrastructure/drizzle/client.ts @@ -1,3 +1,4 @@ +import { AsyncLocalStorage } from "node:async_hooks"; import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; import * as schema from "./schema"; @@ -15,8 +16,35 @@ function getPool(): Pool { return pool; } -export function getDb() { +function createDb() { return drizzle(getPool(), { schema }); } -export type DrizzleDb = ReturnType; +export type DrizzleDb = ReturnType; + +const transactionContext = new AsyncLocalStorage(); + +/** + * DB接続を取得する + * トランザクション中であればそのトランザクションを返し、 + * そうでなければ新しいDB接続を返す + */ +export function getDb(): DrizzleDb { + const tx = transactionContext.getStore(); + if (tx) return tx; + return createDb(); +} + +/** + * トランザクション内で処理を実行する + * すでにトランザクション中であればそのまま実行する(ネストしない) + */ +export function runInTransaction(fn: () => Promise): Promise { + if (transactionContext.getStore()) { + return fn(); + } + const db = createDb(); + return db.transaction(async (tx) => { + return transactionContext.run(tx as unknown as DrizzleDb, fn); + }); +} diff --git a/src/infrastructure/drizzle/index.ts b/src/infrastructure/drizzle/index.ts index a84e17e..8c90ff3 100644 --- a/src/infrastructure/drizzle/index.ts +++ b/src/infrastructure/drizzle/index.ts @@ -2,3 +2,4 @@ export { DrizzleDiscordAccountRepository } from "./DrizzleDiscordAccountReposito export { DrizzleEventRepository } from "./DrizzleEventRepository"; export { DrizzleKarteRepository } from "./DrizzleKarteRepository"; export { DrizzleMemberRepository } from "./DrizzleMemberRepository"; +export { DrizzleUnitOfWork } from "./DrizzleUnitOfWork"; From 91008a2202fb3dcd0a3e4030b05a6163a5916e5b Mon Sep 17 00:00:00 2001 From: Mel-906 Date: Sat, 28 Mar 2026 23:15:00 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20getDb/createDb/DrizzleDb=20?= =?UTF-8?q?=E3=82=92=20getClient/createClient/DrizzleClient=20=E3=81=AB?= =?UTF-8?q?=E3=83=AA=E3=83=8D=E3=83=BC=E3=83=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 生成・取得しているのはDBそのものではなくDrizzleクライアントであるため、 実態に合った命名に修正。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../DrizzleDiscordAccountRepository.ts | 10 ++++----- .../drizzle/DrizzleEventRepository.ts | 22 +++++++++---------- .../drizzle/DrizzleKarteRepository.ts | 8 +++---- .../drizzle/DrizzleMemberRepository.ts | 10 ++++----- src/infrastructure/drizzle/client.ts | 18 +++++++-------- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts b/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts index 9aa1173..d31ec0f 100644 --- a/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts +++ b/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts @@ -8,7 +8,7 @@ import { discordId, memberId, } from "#domain"; -import { getDb } from "./client"; +import { getClient } from "./client"; import { discordAccountDomainEvents, discordAccounts } from "./schema"; import { serializeDiscordAccountEventPayload } from "./serializeDiscordAccountEvent"; @@ -20,7 +20,7 @@ function toDomain(row: DiscordAccountRow): DiscordAccount { export class DrizzleDiscordAccountRepository implements DiscordAccountRepository { async findByDiscordId(id: DiscordId): Promise { - const db = getDb(); + const db = getClient(); const row = await db.query.discordAccounts.findFirst({ where: eq(discordAccounts.discordId, id as string), }); @@ -29,7 +29,7 @@ export class DrizzleDiscordAccountRepository implements DiscordAccountRepository } async findByMemberId(id: MemberId): Promise { - const db = getDb(); + const db = getClient(); const rows = await db.query.discordAccounts.findMany({ where: eq(discordAccounts.memberId, id as string), }); @@ -37,7 +37,7 @@ export class DrizzleDiscordAccountRepository implements DiscordAccountRepository } async save(account: DiscordAccount): Promise { - const db = getDb(); + const db = getClient(); const now = new Date().toISOString(); const events = account.getDomainEvents(); @@ -74,7 +74,7 @@ export class DrizzleDiscordAccountRepository implements DiscordAccountRepository } async delete(id: DiscordId): Promise { - const db = getDb(); + const db = getClient(); await db.delete(discordAccounts).where(eq(discordAccounts.discordId, id as string)); } } diff --git a/src/infrastructure/drizzle/DrizzleEventRepository.ts b/src/infrastructure/drizzle/DrizzleEventRepository.ts index 9451f0d..432c098 100644 --- a/src/infrastructure/drizzle/DrizzleEventRepository.ts +++ b/src/infrastructure/drizzle/DrizzleEventRepository.ts @@ -14,7 +14,7 @@ import { exhibitId, memberId, } from "#domain"; -import { type DrizzleDb, getDb } from "./client"; +import { type DrizzleClient, getClient } from "./client"; import { events, exhibits, lightningTalks, memberEvents, memberExhibits } from "./schema"; // ============================================================================ @@ -101,7 +101,7 @@ export class DrizzleEventRepository implements EventRepository { // ========================================================================== private async persistEvent(event: Event): Promise { - const db = getDb(); + const db = getClient(); const snapshot = event.toSnapshot(); const now = new Date().toISOString(); const dateStr = snapshot.date instanceof Date ? snapshot.date.toISOString() : snapshot.date; @@ -143,7 +143,7 @@ export class DrizzleEventRepository implements EventRepository { } private async deleteObsoleteExhibits( - db: DrizzleDb, + db: DrizzleClient, eventId: EventId, keptExhibitIds: ExhibitId[], ): Promise { @@ -164,7 +164,7 @@ export class DrizzleEventRepository implements EventRepository { } private async upsertExhibit( - db: DrizzleDb, + db: DrizzleClient, eventId: EventId, ex: ReturnType["exhibits"][number], ): Promise { @@ -220,7 +220,7 @@ export class DrizzleEventRepository implements EventRepository { } private async syncMemberEvents( - db: DrizzleDb, + db: DrizzleClient, eventId: EventId, memberIds: MemberId[], ): Promise { @@ -241,7 +241,7 @@ export class DrizzleEventRepository implements EventRepository { } private async syncMemberExhibits( - db: DrizzleDb, + db: DrizzleClient, exhibitId: ExhibitId, memberIds: MemberId[], ): Promise { @@ -266,7 +266,7 @@ export class DrizzleEventRepository implements EventRepository { // ========================================================================== async findById(id: EventId): Promise { - const db = getDb(); + const db = getClient(); const record = await db.query.events.findFirst({ where: eq(events.id, id), with: { @@ -285,7 +285,7 @@ export class DrizzleEventRepository implements EventRepository { } async findByParticipantMemberId(memberId: MemberId): Promise { - const db = getDb(); + const db = getClient(); const participations = await db .select({ eventId: memberEvents.eventId }) @@ -312,7 +312,7 @@ export class DrizzleEventRepository implements EventRepository { } async findByExhibitId(exhibitId: ExhibitId): Promise { - const db = getDb(); + const db = getClient(); const exhibit = await db .select({ eventId: exhibits.eventId }) @@ -325,7 +325,7 @@ export class DrizzleEventRepository implements EventRepository { } async findAll(): Promise { - const db = getDb(); + const db = getClient(); const records = await db.query.events.findMany({ with: { memberEvents: true, @@ -346,7 +346,7 @@ export class DrizzleEventRepository implements EventRepository { } async delete(eventId: EventId): Promise { - const db = getDb(); + const db = getClient(); const exhibitRecords = await db .select({ id: exhibits.id }) diff --git a/src/infrastructure/drizzle/DrizzleKarteRepository.ts b/src/infrastructure/drizzle/DrizzleKarteRepository.ts index 86fc967..0366285 100644 --- a/src/infrastructure/drizzle/DrizzleKarteRepository.ts +++ b/src/infrastructure/drizzle/DrizzleKarteRepository.ts @@ -21,7 +21,7 @@ import { type Resolution, type SupportRecord, } from "#domain"; -import { getDb } from "./client"; +import { getClient } from "./client"; import { karteAssignees, kartes } from "./schema"; // ============================================================================ @@ -250,7 +250,7 @@ function consultedAtToPrecision(r: Recorded): ConsultedAt["precisio export class DrizzleKarteRepository implements KarteRepository { async findById(id: KarteId): Promise { - const db = getDb(); + const db = getClient(); const row = await db.query.kartes.findFirst({ where: eq(kartes.id, id as string), with: { @@ -263,7 +263,7 @@ export class DrizzleKarteRepository implements KarteRepository { } async findAll(): Promise { - const db = getDb(); + const db = getClient(); const rows = await db.query.kartes.findMany({ with: { karteAssignees: true, @@ -274,7 +274,7 @@ export class DrizzleKarteRepository implements KarteRepository { } async save(karte: Karte): Promise { - const db = getDb(); + const db = getClient(); const clientCols = clientToColumns(karte.client); const resCols = resolutionToColumns(karte.supportRecord.resolution); diff --git a/src/infrastructure/drizzle/DrizzleMemberRepository.ts b/src/infrastructure/drizzle/DrizzleMemberRepository.ts index 7f77ec7..144c8d1 100644 --- a/src/infrastructure/drizzle/DrizzleMemberRepository.ts +++ b/src/infrastructure/drizzle/DrizzleMemberRepository.ts @@ -16,7 +16,7 @@ import { type MemberRepository, type Recorded, } from "#domain"; -import { getDb } from "./client"; +import { getClient } from "./client"; import { memberDomainEvents, members } from "./schema"; import { serializeMemberEventPayload } from "./serializeMemberEvent"; @@ -100,7 +100,7 @@ function toInsertValues(member: Member): MemberInsert { export class DrizzleMemberRepository implements MemberRepository { async findById(id: MemberId): Promise { - const db = getDb(); + const db = getClient(); const row = await db.query.members.findFirst({ where: eq(members.id, id as string), }); @@ -109,7 +109,7 @@ export class DrizzleMemberRepository implements MemberRepository { } async findByEmail(email: UniversityEmail): Promise { - const db = getDb(); + const db = getClient(); const row = await db.query.members.findFirst({ where: eq(members.email, email.getValue()), }); @@ -118,13 +118,13 @@ export class DrizzleMemberRepository implements MemberRepository { } async findAll(): Promise { - const db = getDb(); + const db = getClient(); const rows = await db.query.members.findMany(); return rows.map(toDomain); } async save(member: Member): Promise { - const db = getDb(); + const db = getClient(); const values = toInsertValues(member); const events = member.getDomainEvents(); diff --git a/src/infrastructure/drizzle/client.ts b/src/infrastructure/drizzle/client.ts index f8364ba..81fecee 100644 --- a/src/infrastructure/drizzle/client.ts +++ b/src/infrastructure/drizzle/client.ts @@ -16,23 +16,23 @@ function getPool(): Pool { return pool; } -function createDb() { +function createClient() { return drizzle(getPool(), { schema }); } -export type DrizzleDb = ReturnType; +export type DrizzleClient = ReturnType; -const transactionContext = new AsyncLocalStorage(); +const transactionContext = new AsyncLocalStorage(); /** - * DB接続を取得する + * Drizzleクライアントを取得する * トランザクション中であればそのトランザクションを返し、 - * そうでなければ新しいDB接続を返す + * そうでなければ新しいクライアントを返す */ -export function getDb(): DrizzleDb { +export function getClient(): DrizzleClient { const tx = transactionContext.getStore(); if (tx) return tx; - return createDb(); + return createClient(); } /** @@ -43,8 +43,8 @@ export function runInTransaction(fn: () => Promise): Promise { if (transactionContext.getStore()) { return fn(); } - const db = createDb(); + const db = createClient(); return db.transaction(async (tx) => { - return transactionContext.run(tx as unknown as DrizzleDb, fn); + return transactionContext.run(tx as unknown as DrizzleClient, fn); }); } From 6d5f1a67f776206f3a82ce6e84f77fd80b4d9759 Mon Sep 17 00:00:00 2001 From: Mel-906 Date: Sat, 28 Mar 2026 23:32:34 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20DrizzleClient=E3=82=92PgDatabas?= =?UTF-8?q?e=E5=9E=8B=E3=81=A7=E5=AE=9A=E7=BE=A9=E3=81=97=E3=83=88?= =?UTF-8?q?=E3=83=A9=E3=83=B3=E3=82=B6=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E5=BC=B7=E5=88=B6=E3=82=AD=E3=83=A3=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E9=99=A4=E5=8E=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PgTransactionはPgDatabaseを継承しているため、DrizzleClientを PgDatabaseで定義することで DB接続とトランザクションを同じ型として扱えるようになった。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/infrastructure/drizzle/client.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/infrastructure/drizzle/client.ts b/src/infrastructure/drizzle/client.ts index 81fecee..6deb308 100644 --- a/src/infrastructure/drizzle/client.ts +++ b/src/infrastructure/drizzle/client.ts @@ -1,8 +1,12 @@ import { AsyncLocalStorage } from "node:async_hooks"; +import type { NodePgQueryResultHKT } from "drizzle-orm/node-postgres"; import { drizzle } from "drizzle-orm/node-postgres"; +import type { PgDatabase } from "drizzle-orm/pg-core"; import { Pool } from "pg"; import * as schema from "./schema"; +export type DrizzleClient = PgDatabase; + let pool: Pool | null = null; function getPool(): Pool { @@ -16,12 +20,10 @@ function getPool(): Pool { return pool; } -function createClient() { +function createClient(): DrizzleClient { return drizzle(getPool(), { schema }); } -export type DrizzleClient = ReturnType; - const transactionContext = new AsyncLocalStorage(); /** @@ -45,6 +47,6 @@ export function runInTransaction(fn: () => Promise): Promise { } const db = createClient(); return db.transaction(async (tx) => { - return transactionContext.run(tx as unknown as DrizzleClient, fn); + return transactionContext.run(tx, fn); }); } From 44cd95026e5bc9dd0783327ddd04d4193dd73c4b Mon Sep 17 00:00:00 2001 From: Mel-906 Date: Sun, 29 Mar 2026 00:58:41 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20Repository=E5=86=85=E3=83=88?= =?UTF-8?q?=E3=83=A9=E3=83=B3=E3=82=B6=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=82=92runInTransaction/getClient=E7=B5=8C=E7=94=B1=E3=81=AB?= =?UTF-8?q?=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Member・DiscordAccount・KarteのRepository.save()でdb.transaction(tx)を 直接使っていた箇所をrunInTransaction + getClient()に置き換え、 UoWトランザクションが開始されていればそれに乗るようにした。 EventRepository.persistEvent()はトランザクションなしだった5つのDB操作を runInTransaction()で囲み、集約単位の原子性を確保した。 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../DrizzleDiscordAccountRepository.ts | 10 +-- .../drizzle/DrizzleEventRepository.ts | 68 ++++++++++--------- .../drizzle/DrizzleKarteRepository.ts | 13 ++-- .../drizzle/DrizzleMemberRepository.ts | 10 +-- 4 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts b/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts index df80da4..9f77a9b 100644 --- a/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts +++ b/src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts @@ -8,7 +8,7 @@ import { discordId, memberId, } from "#domain"; -import { getClient } from "./client"; +import { getClient, runInTransaction } from "./client"; import { discordAccountDomainEvents, discordAccounts } from "./schema"; import { serializeDiscordAccountEventPayload } from "./serializeDiscordAccountEvent"; @@ -43,12 +43,12 @@ export class DrizzleDiscordAccountRepository implements DiscordAccountRepository } async save(account: DiscordAccount): Promise { - const db = getClient(); const now = new Date().toISOString(); const events = account.getDomainEvents(); - await db.transaction(async (tx) => { - await tx + await runInTransaction(async () => { + const client = getClient(); + await client .insert(discordAccounts) .values({ discordId: account.discordId as string, @@ -65,7 +65,7 @@ export class DrizzleDiscordAccountRepository implements DiscordAccountRepository }); if (events.length > 0) { - await tx.insert(discordAccountDomainEvents).values( + await client.insert(discordAccountDomainEvents).values( events.map((event) => ({ id: uuid(), discordId: event.discordId as string, diff --git a/src/infrastructure/drizzle/DrizzleEventRepository.ts b/src/infrastructure/drizzle/DrizzleEventRepository.ts index 432c098..b15e1e1 100644 --- a/src/infrastructure/drizzle/DrizzleEventRepository.ts +++ b/src/infrastructure/drizzle/DrizzleEventRepository.ts @@ -14,7 +14,7 @@ import { exhibitId, memberId, } from "#domain"; -import { type DrizzleClient, getClient } from "./client"; +import { type DrizzleClient, getClient, runInTransaction } from "./client"; import { events, exhibits, lightningTalks, memberEvents, memberExhibits } from "./schema"; // ============================================================================ @@ -101,45 +101,47 @@ export class DrizzleEventRepository implements EventRepository { // ========================================================================== private async persistEvent(event: Event): Promise { - const db = getClient(); - const snapshot = event.toSnapshot(); - const now = new Date().toISOString(); - const dateStr = snapshot.date instanceof Date ? snapshot.date.toISOString() : snapshot.date; - - // 1) Event upsert - await db - .insert(events) - .values({ - id: snapshot.id, - name: snapshot.name, - date: dateStr, - updatedAt: now, - }) - .onConflictDoUpdate({ - target: events.id, - set: { + await runInTransaction(async () => { + const client = getClient(); + const snapshot = event.toSnapshot(); + const now = new Date().toISOString(); + const dateStr = snapshot.date instanceof Date ? snapshot.date.toISOString() : snapshot.date; + + // 1) Event upsert + await client + .insert(events) + .values({ + id: snapshot.id, name: snapshot.name, date: dateStr, updatedAt: now, - }, - }); + }) + .onConflictDoUpdate({ + target: events.id, + set: { + name: snapshot.name, + date: dateStr, + updatedAt: now, + }, + }); - // 2) Find obsolete exhibits and clean up - const snapshotExhibitIds = snapshot.exhibits.map((ex) => ex.id); - await this.deleteObsoleteExhibits(db, snapshot.id, snapshotExhibitIds); + // 2) Find obsolete exhibits and clean up + const snapshotExhibitIds = snapshot.exhibits.map((ex) => ex.id); + await this.deleteObsoleteExhibits(client, snapshot.id, snapshotExhibitIds); - // 3) Upsert exhibits - for (const ex of snapshot.exhibits) { - await this.upsertExhibit(db, snapshot.id, ex); - } + // 3) Upsert exhibits + for (const ex of snapshot.exhibits) { + await this.upsertExhibit(client, snapshot.id, ex); + } - // 4) Sync member events - await this.syncMemberEvents(db, snapshot.id, event.getMemberIds()); + // 4) Sync member events + await this.syncMemberEvents(client, snapshot.id, event.getMemberIds()); - // 5) Sync member exhibits - for (const exhibitDomain of event.getExhibits()) { - await this.syncMemberExhibits(db, exhibitDomain.id, exhibitDomain.getMemberIds()); - } + // 5) Sync member exhibits + for (const exhibitDomain of event.getExhibits()) { + await this.syncMemberExhibits(client, exhibitDomain.id, exhibitDomain.getMemberIds()); + } + }); } private async deleteObsoleteExhibits( diff --git a/src/infrastructure/drizzle/DrizzleKarteRepository.ts b/src/infrastructure/drizzle/DrizzleKarteRepository.ts index 0366285..4736ecb 100644 --- a/src/infrastructure/drizzle/DrizzleKarteRepository.ts +++ b/src/infrastructure/drizzle/DrizzleKarteRepository.ts @@ -21,7 +21,7 @@ import { type Resolution, type SupportRecord, } from "#domain"; -import { getClient } from "./client"; +import { getClient, runInTransaction } from "./client"; import { karteAssignees, kartes } from "./schema"; // ============================================================================ @@ -274,8 +274,6 @@ export class DrizzleKarteRepository implements KarteRepository { } async save(karte: Karte): Promise { - const db = getClient(); - const clientCols = clientToColumns(karte.client); const resCols = resolutionToColumns(karte.supportRecord.resolution); @@ -305,15 +303,16 @@ export class DrizzleKarteRepository implements KarteRepository { const { id: _, ...updateValues } = values; - await db.transaction(async (tx) => { + await runInTransaction(async () => { + const client = getClient(); // Upsert karte - await tx.insert(kartes).values(values).onConflictDoUpdate({ + await client.insert(kartes).values(values).onConflictDoUpdate({ target: kartes.id, set: updateValues, }); // Sync assignees (delete-all-then-insert) - await tx.delete(karteAssignees).where(eq(karteAssignees.karteId, karte.id as string)); + await client.delete(karteAssignees).where(eq(karteAssignees.karteId, karte.id as string)); if (karte.supportRecord.assignees.type === "recorded") { const assigneeRows = karte.supportRecord.assignees.value.map((assignee) => ({ @@ -322,7 +321,7 @@ export class DrizzleKarteRepository implements KarteRepository { memberId: assignee.type === "resolved" ? (assignee.memberId as string) : null, assigneeName: assignee.type === "unresolved" ? assignee.name : null, })); - await tx.insert(karteAssignees).values(assigneeRows); + await client.insert(karteAssignees).values(assigneeRows); } }); } diff --git a/src/infrastructure/drizzle/DrizzleMemberRepository.ts b/src/infrastructure/drizzle/DrizzleMemberRepository.ts index 144c8d1..a80fd68 100644 --- a/src/infrastructure/drizzle/DrizzleMemberRepository.ts +++ b/src/infrastructure/drizzle/DrizzleMemberRepository.ts @@ -16,7 +16,7 @@ import { type MemberRepository, type Recorded, } from "#domain"; -import { getClient } from "./client"; +import { getClient, runInTransaction } from "./client"; import { memberDomainEvents, members } from "./schema"; import { serializeMemberEventPayload } from "./serializeMemberEvent"; @@ -124,12 +124,12 @@ export class DrizzleMemberRepository implements MemberRepository { } async save(member: Member): Promise { - const db = getClient(); const values = toInsertValues(member); const events = member.getDomainEvents(); - await db.transaction(async (tx) => { - await tx + await runInTransaction(async () => { + const client = getClient(); + await client .insert(members) .values(values) .onConflictDoUpdate({ @@ -146,7 +146,7 @@ export class DrizzleMemberRepository implements MemberRepository { }); if (events.length > 0) { - await tx.insert(memberDomainEvents).values( + await client.insert(memberDomainEvents).values( events.map((event) => ({ id: uuid(), memberId: event.id as string,