Validate DDD/Clean Architecture compliance in code changes, checking layer violations, transaction patterns, and Row-Level Security
DDD (ドメイン駆動設計) と Clean Architecture の原則に基づいてコード変更を検証します。
$ARGUMENTS が指定された場合、その特定のパスまたはドメインを検証します。指定がない場合は、現在のブランチの未コミット変更を全て検証します。
Resolverレイヤー (controller/resolver.ts)
UseCaseレイヤー (usecase.ts)
ctx.issuer.onlyBelongingCommunity(ctx, async (tx) => {...}) でトランザクションを管理する必要があるServiceレイヤー (service.ts)
tx パラメータを受け取り、Repositoryに渡す必要がある(Service内で if (tx) 分岐はしない)GqlXxx)を返してはいけない - Prisma型のみを返すRepositoryレイヤー (data/repository.ts)
ctx.issuer を使用する必要があるtx パラメータをメソッドタイプに応じて処理する必要がある:tx: Prisma.TransactionClient を受け取り、直接使用(if (tx) 分岐なし)tx?: Prisma.TransactionClient を受け取り、if (tx) で分岐Converter (data/converter.ts)
Presenter (presenter.ts)
パターン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,
});
});
}
違反としてフラグするもの:
tx パラメータ伝播が欠けているtx? を使用している(必須の tx にすべき)tx? を使用しているQueryメソッドで if (tx) 分岐がない必須パターン:
ctx.issuer.public(ctx, tx => {...}) - 公開クエリctx.issuer.internal(ctx, tx => {...}) - 内部/管理者クエリctx.issuer.onlyBelongingCommunity(ctx, async tx => {...}) - ユーザーの所属コミュニティのみ違反としてフラグするもの:
ctx.issuer ラッパーなしの直接PrismaクエリcommunityId 分離チェックの欠如正しい:
GqlUser, GqlCommunityGqlCreateUserInput, GqlUpdateCommunityInputGqlUserPayload, GqlCommunityConnectionPrismaUser, PrismaCommunity違反としてフラグするもの:
GqlXxx 型を返すServiceフィールドリゾルバはDataLoaderを使用する必要がある:
Opportunity: {
community: (parent, _, ctx) => ctx.loaders.community.load(parent.communityId),
createdByUser: (parent, _, ctx) => ctx.loaders.user.load(parent.createdBy)
}
違反としてフラグするもの:
controller/dataloader.ts のDataLoader定義の欠如$ARGUMENTS が指定された場合:
# 特定のドメインを検証
find src/application/domain/"$ARGUMENTS" -type f \( -name "*.ts" -o -name "*.graphql" \)
指定がない場合:
# 未コミット変更を検証
git status --porcelain | grep -E '^\s*[MA]' | cut -c 4-
各ファイルに対してレイヤー固有のチェックを実行:
controller/resolver.ts の場合:
repository.find, repository.create)ctx.loaders を使用していることを確認usecase.ts の場合:
ctx.issuer.onlyBelongingCommunity(ctx, async (tx) => ...) パターンを確認otherDomainUseCase.doSomething())service.ts の場合:
Gql 型のインポートまたは返却を検索tx パラメータが適切に宣言され、渡されていることを確認data/repository.ts の場合:
ctx.issuer.public/internal/onlyBelongingCommunity を使用していることを確認tx: Prisma.TransactionClient を使用していることをチェックtx? を使用するQueryメソッドで if (tx) 分岐を実装していることをチェックdata/converter.ts の場合:
tx パラメータがないことを確認presenter.ts の場合:
schema/*.graphql の場合:
Gql* 規約に従っていることを確認構造化されたレポートを生成:
# アーキテクチャ検証レポート
## サマリー
- 分析ファイル数: 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内のビジネスロジックに対するユニットテストの追加を検討
src/application/provider.ts に登録されていることを確認@injectable() および @inject("ServiceName") デコレータをチェックpnpm gql:generate が実行されたことを確認以下の場合にレポートが完成:
完全なアーキテクチャドキュメントと実装パターンについては @CLAUDE.md を参照してください。