civicship-api アーキテクチャ検証
DDD (ドメイン駆動設計) と Clean Architecture の原則に基づいてコード変更を検証します。
$ARGUMENTS が指定された場合、その特定のパスまたはドメインを検証します。指定がない場合は、現在のブランチの未コミット変更を全て検証します。
重要なアーキテクチャルール
1. レイヤー違反
Resolverレイヤー (controller/resolver.ts)
- •✅ UseCaseのメソッドのみを呼び出す必要がある
- •✅ フィールドリゾルバではDataLoaderを使用する必要がある(N+1問題防止)
- •❌ ビジネスロジックを書いてはいけない
- •❌ Repositoryを直接呼び出してはいけない
UseCaseレイヤー (usecase.ts)
- •✅ Serviceを使ってビジネスフローをオーケストレーションする必要がある
- •✅
ctx.issuer.onlyBelongingCommunity(ctx, async (tx) => {...})でトランザクションを管理する必要がある - •✅ 自ドメインおよび他ドメインのServiceを呼び出せる
- •✅ 結果を返す前にPresenterを呼び出す必要がある
- •❌ 他ドメインのUseCaseを呼び出してはいけない(循環依存の原因)
Serviceレイヤー (service.ts)
- •✅ ビジネスロジックとバリデーションを実装する必要がある
- •✅ Repositoryを呼び出す必要がある
- •✅ 他ドメインのServiceを呼び出せる(読み取り操作のみ)
- •✅
txパラメータを受け取り、Repositoryに渡す必要がある(Service内でif (tx)分岐はしない) - •❌ GraphQL型(
GqlXxx)を返してはいけない - Prisma型のみを返す
Repositoryレイヤー (data/repository.ts)
- •✅ Prismaクエリのみを実行する必要がある
- •✅ Row-Level Security (RLS)のため
ctx.issuerを使用する必要がある - •✅
txパラメータをメソッドタイプに応じて処理する必要がある:- •Mutationメソッド(create/update/delete): 必須の
tx: Prisma.TransactionClientを受け取り、直接使用(if (tx)分岐なし) - •Queryメソッド トランザクション内外両方で呼ばれる可能性: オプショナルの
tx?: Prisma.TransactionClientを受け取り、if (tx)で分岐
- •Mutationメソッド(create/update/delete): 必須の
- •❌ ビジネスロジックを含めてはいけない
Converter (data/converter.ts)
- •✅ 純粋関数である必要がある(GraphQL input → Prisma形式)
- •❌ トランザクションを使用してはいけない
- •❌ データベースクエリを実行してはいけない
Presenter (presenter.ts)
- •✅ 純粋関数である必要がある(Prisma → GraphQL型)
- •❌ ビジネスロジックを含めてはいけない
- •❌ データベースクエリを実行してはいけない
2. トランザクションパターンの検証
パターンA: Mutationメソッド(create/update/delete) - 必須トランザクション
// UseCaseがトランザクションを管理
async managerCreateOpportunity({ input, permission }, ctx) {
return ctx.issuer.onlyBelongingCommunity(ctx, async (tx) => {
const record = await this.service.createOpportunity(ctx, input, permission.communityId, tx);
return OpportunityPresenter.create(record);
});
}
// Serviceがtxを受け取り、Repositoryに渡す
async createOpportunity(ctx, input, communityId, tx) {
const data = this.converter.create(input, communityId);
return await this.repository.create(ctx, data, tx);
}
// Repositoryは必須のtxパラメータを受け取る(分岐なし)
async create(ctx, data, tx: Prisma.TransactionClient) {
return tx.opportunity.create({
data,
select: opportunitySelectDetail,
});
}
パターンB: Queryメソッド - オプショナルトランザクション(柔軟な使用)
// Repositoryはオプショナルのtxパラメータを受け取り、分岐処理
async findCommunityWallet(ctx, communityId, tx?: Prisma.TransactionClient) {
if (tx) {
return tx.wallet.findFirst({
where: { communityId, type: WalletType.COMMUNITY },
select: walletSelectDetail,
});
}
return ctx.issuer.public(ctx, (tx) => {
return tx.wallet.findFirst({
where: { communityId, type: WalletType.COMMUNITY },
select: walletSelectDetail,
});
});
}
違反としてフラグするもの:
- •❌ Serviceレイヤーでトランザクションを開始している(UseCaseのみで行う)
- •❌ ServiceからRepositoryへの
txパラメータ伝播が欠けている - •❌ Mutationメソッドがオプショナルの
tx?を使用している(必須のtxにすべき) - •❌ オプショナルの
tx?を使用しているQueryメソッドでif (tx)分岐がない
3. Row-Level Security (RLS) の検証
必須パターン:
- •
ctx.issuer.public(ctx, tx => {...})- 公開クエリ - •
ctx.issuer.internal(ctx, tx => {...})- 内部/管理者クエリ - •
ctx.issuer.onlyBelongingCommunity(ctx, async tx => {...})- ユーザーの所属コミュニティのみ
違反としてフラグするもの:
- •❌
ctx.issuerラッパーなしの直接Prismaクエリ - •❌
communityId分離チェックの欠如 - •❌ RLSメソッドを使用していないRepository
4. GraphQL型の命名規則
正しい:
- •型:
GqlUser,GqlCommunity - •入力:
GqlCreateUserInput,GqlUpdateCommunityInput - •ペイロード:
GqlUserPayload,GqlCommunityConnection - •Prisma型:
PrismaUser,PrismaCommunity
違反としてフラグするもの:
- •❌
GqlXxx型を返すService - •❌ Service/Repositoryレイヤーで使用されているGraphQL型
- •❌ 誤った命名規則
5. DataLoaderによるN+1問題防止
フィールドリゾルバはDataLoaderを使用する必要がある:
Opportunity: {
community: (parent, _, ctx) => ctx.loaders.community.load(parent.communityId),
createdByUser: (parent, _, ctx) => ctx.loaders.user.load(parent.createdBy)
}
違反としてフラグするもの:
- •❌ データベースへの直接クエリを行うフィールドリゾルバ
- •❌
controller/dataloader.tsのDataLoader定義の欠如
検証プロセス
ステップ1: 検証対象ファイルの特定
$ARGUMENTS が指定された場合:
# 特定のドメインを検証 find src/application/domain/"$ARGUMENTS" -type f \( -name "*.ts" -o -name "*.graphql" \)
指定がない場合:
# 未コミット変更を検証 git status --porcelain | grep -E '^\s*[MA]' | cut -c 4-
ステップ2: ファイルの読み取りと分析
各ファイルに対してレイヤー固有のチェックを実行:
controller/resolver.ts の場合:
- •Repositoryの直接呼び出しを検索(例:
repository.find,repository.create) - •全てのフィールドリゾルバが
ctx.loadersを使用していることを確認
usecase.ts の場合:
- •
ctx.issuer.onlyBelongingCommunity(ctx, async (tx) => ...)パターンを確認 - •他ドメインのUseCaseの呼び出しをチェック(例:
otherDomainUseCase.doSomething()) - •返却前にPresenterを呼び出していることを確認
service.ts の場合:
- •
Gql型のインポートまたは返却を検索 - •
txパラメータが適切に宣言され、渡されていることを確認 - •不適切なクロスドメインUseCaseの呼び出しをチェック
data/repository.ts の場合:
- •全てのクエリが
ctx.issuer.public/internal/onlyBelongingCommunityを使用していることを確認 - •Mutationメソッド(create/update/delete)が必須の
tx: Prisma.TransactionClientを使用していることをチェック - •オプショナルの
tx?を使用するQueryメソッドでif (tx)分岐を実装していることをチェック - •ビジネスロジック(複雑な条件文、バリデーション)を検索
data/converter.ts の場合:
- •
txパラメータがないことを確認 - •Prismaクライアントの使用がないことを確認
presenter.ts の場合:
- •純粋関数のみであることを確認
- •Prismaクライアントの使用がないことを確認
schema/*.graphql の場合:
- •型命名が
Gql*規約に従っていることを確認 - •対応するResolverの実装があることをチェック
ステップ3: 検出結果の報告
構造化されたレポートを生成:
# アーキテクチャ検証レポート ## サマリー - 分析ファイル数: X - 検出違反数: Y - 重大問題数: Z ## 違反 ### レイヤー違反 - [ ] **ファイル**: `src/application/domain/opportunity/controller/resolver.ts:45` - **問題**: ResolverがRepositoryを直接呼び出している - **修正**: UseCaseメソッドを呼び出すように変更 ### トランザクションパターンの問題 - [ ] **ファイル**: `src/application/domain/user/service.ts:120` - **問題**: Repositoryへの `tx` パラメータ伝播が欠けている - **修正**: `tx` パラメータを追加し、Repositoryメソッドに渡す ### Row-Level Securityの問題 - [ ] **ファイル**: `src/application/domain/community/data/repository.ts:78` - **問題**: `ctx.issuer` なしの直接Prismaクエリ - **修正**: クエリを `ctx.issuer.public(ctx, tx => ...)` でラップ ### GraphQL型の違反 - [ ] **ファイル**: `src/application/domain/wallet/service.ts:56` - **問題**: Serviceが `GqlWallet` 型を返している - **修正**: Prisma型を返し、Presenterで変換 ### DataLoaderの欠如 - [ ] **ファイル**: `src/application/domain/opportunity/controller/resolver.ts:89` - **問題**: フィールドリゾルバが直接データベースクエリを実行 - **修正**: `ctx.loaders.community.load(parent.communityId)` を使用 ## 推奨事項 1. 完全なアーキテクチャガイドラインについては @CLAUDE.md を確認 2. リグレッションがないことを確認するため `pnpm test` を実行 3. Service内のビジネスロジックに対するユニットテストの追加を検討
追加チェック
依存性注入の検証
- •全てのService/Repositoryが
src/application/provider.tsに登録されていることを確認 - •
@injectable()および@inject("ServiceName")デコレータをチェック
GraphQLスキーマの同期
- •スキーマ変更後、
pnpm gql:generateが実行されたことを確認 - •TypeScriptコンパイルエラーをチェック
セキュリティチェック
- •パスワードやシークレットがログに出力されていないこと
- •Serviceレイヤーでの入力バリデーション
- •パラメータ化クエリ(Prismaが処理)
終了基準
以下の場合にレポートが完成:
- •✅ スコープ内の全ファイルが分析された
- •✅ 各違反にファイルパス、行番号、修正案が含まれる
- •✅ サマリー統計が正確である
- •✅ 推奨事項が実行可能である
参考資料
完全なアーキテクチャドキュメントと実装パターンについては @CLAUDE.md を参照してください。