PrismaでWhere条件を動的かつ型安全に実装する方法

# はじめに
WebAPIの仕様としてリクエスト時に与えられたパラメータに応じて、WHERE句を動的に生成するケース(検索など)はよくあるが、それをPrismaで型安全に実装する方法について。

# 環境
- TypeScript：4.7.4
- Prisma：4.8.0
- Provider：MySQL

# テーブル定義
usersテーブルに名前(name)と年齢(age)がカラム定義されている。
```schema.prisma
model User {
  id   Int    @id @default(autoincrement()) @db.UnsignedInt
  name String
  age  Int

  @@map("users")
}
```

# WebAPIとしての抽出条件の仕様
オプショナルで以下の条件が指定可能なユーザー一覧のWebAPIを想定する。
- 名前(name)に特定の文字列が含まれているか？
- 年齢(age)がn歳以上か？
- 年齢(age)がn歳以下か？

参考実装用の仮想定義だがこのような型のオブジェクトが与えられるイメージ
```typescript
type Args = {
  name?: string
  minAge?: number
  maxAge?: number
}
const args: Args = {
  /* optional */
}
```
# シンプルな実装例
`findMany`の`where`キーの値を直接指定するようなケースではこんな感じでシンプルかつ型安全に実装できる。
※`undefined`の場合にはWHERE句には含まれない
```typescript
const users = await this.prisma.user.findMany({
  where: {
    name: {
      contains: args.name
    },
    age: {
      gte: args.minAge,
      lte: args.maxAge
    }
  }
})
```
全て指定されていた場合に実際に発行されるQueryのWHERE句は以下の通り。
```
WHERE (`db`.`users`.`name` LIKE ? AND `db`.`users`.`age` >= ? AND `db`.`users`.`age` <= ?)
```

しかし、WHERE句の対象となるカラムが増えたり条件の制御が複雑になりだすと、`where`キーの値を一度オブジェクト変数として定義して処理の中で追加するような実装が必要になることがある。

# `where`キーに指定する値をオブジェクト変数として定義し後から追加する実装例(not type-safe)
```typescript
const where: any = {
  name: {
    contains: args.name
  },
  age: undefined
}

where.age = { gte: args.minAge, lte: args.maxAge }

const users = await this.prisma.user.findMany({
  where
})
```
あるいはオブジェクトのスプレッド構文を使って...
```typescript
let where

where = {
  name: {
    contains: args.name
  }
}

where = {
  ...where,
  age: { gte: args.minAge, lte: args.maxAge }
}

const users = await this.prisma.user.findMany({
  where
})
```
結果として最初のシンプルな実装例と同様にQueryのWHERE句を動的に生成できるが、いずれも`where`変数は`any`型のため型安全ではなく、以下のような問題が考えられる。
1. ヒューマンエラーによりカラム名や`StringFilter`、`IntFilter`などで定義されているキー名(contains、gte、lteなど)および値の型定義に誤りがあると例外が発生する状態となる
2. `where`で指定している対象カラムのカラム名や型定義を変更してしまうと例外が発生する状態となる

最大の問題は上記の状態でコンパイル(トランスパイル)が通ってしまうため、単体テストなどでしっかりとフォローできていないケースでは問題を検知できない可能性があること。

当然のことながらこれは`where`キーに値を直接指定している場合にはtsコンパイラがエラーを示してくれる。
```typescript
const users = await this.prisma.user.findMany({
  where: {
    hoge: {
      contains: args.name // Type '{ hoge: { contains: string; }; }' is not assignable to type 'UserWhereInput'.
    }
  }
})
```
上記のエラーからも分かるとおり、このエラーは`UserWhereInput`タイプとの型の不一致により発生しているため、
`where`変数を`UserWhereInput`タイプで定義することで型安全に実装することができる。

# 型安全な実装例
[PrismaClientのリファレンス](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference)を確認すると、[examples-7](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#examples-7)のような例を参考にすると型安全に実装できることが分かる。

`$ prisma generate`によって生成されるPrismaClient(node_modules/.prisma/client)には`{Model名}WhereInput`が定義されているので、それをタイプインポートすることで適切な型定義を利用することができる。
```typescript
import type { Prisma } from '@prisma/client'

const where: Prisma.UserWhereInput = {
  name: {
    contains: args.name
  }
  // 型情報にageキーが含まれているため事前にキーを宣言しundefinedとしておく必要もない
  // age: undefined
}

where.age = { gte: args.minAge, lte: args.maxAge }

const users = await this.prisma.user.findMany({
  where
})
```
上記のような実装としておくことで仮に存在しないカラムを指定した場合には`where`キーに直接指定した場合と同様にtsコンパイラがエラーを示してくれるため、動的かつ型安全に実装することができる。
```typescript
const where: Prisma.UserWhereInput = {
  hoge: {
    contains: args.name // Type '{ hoge: { contains: string; }; }' is not assignable to type 'UserWhereInput'.
  }
}
```
※カラム名を変更した場合や、`StringFilter`、`IntFilter`などで定義されているキー名(contains、gte、lteなど)および値の型定義に誤りがある場合も同様

# まとめ
PrismaでWhere条件を動的に実装する必要があり、一度変数に格納するような場合には必ずPrismaClientから型定義情報をタイプインポートし`any`型の使用を避けましょう。

# 参考
https://www.prisma.io/docs/reference/api-reference/prisma-client-reference