😸
Prismaでユニークバリデーションのためのユーティリティを作る
背景
ユーザーからの入力がデータベースに存在すること(しないこと)をチェックしたいときがあります。
例えば、ユーザー登録をするときにユーザー名が重複していないことを確認したいとします。Prismaで愚直に実装すると、以下のような感じになると思います。
async function isUserNameUnique(prisma: PrismaClient, userName: string): Promise<boolean> {
const user = await prisma.user.findFirst({ where: { userName } });
return user !== null;
}
ユニークチェックをする箇所が複数ある場合、このような関数を何度も定義するのは少し面倒です。
ところで、Laravelのバリデーションにはunique
やexists
などのルールがあり、一意性や存在のバリデーションを簡潔に書くことができます。
$request->validate([
// user_nameフィールドの値が、usersテーブルのuser_nameカラムに存在しないことを検証する
'user_name' => 'unique:users'
]);
PrismaでもLaravelのように書きたかったので、ユニークチェックを簡潔に書くためのユーティリティを作ってみました。
作ったもの
modelValidator
メソッドを作りました。
const userValidator = modelValidator(prisma.user);
async function createUser(userName: string) {
// 登録するとき
const isUserNameUnique = await userValidator.isUnique({ userName })
}
async function updateUserName(userId: number, userName: string) {
// 更新するとき(自分自身は除外する)
const isUserNameUnique = await userVaidator.isUnique({ userName }, { id: userId })
}
この例の場合、isUnique
の第1引数はUserWhereInput
で、第2引数はUserWhereUniqueInput
です。更新するときは自分自身をユニークチェックから除外したいので、除外するものを第2引数で指定できるようにしています。
実装(型)
isUnique
の引数の型は、findFirst
とfindUnique
の引数のwhere
から取得しています。
type PrismaModel = {
findFirst: (...args: any[]) => Promise<any>
findUnique: (...args: any[]) => Promise<any>
}
type ModelWhereInput<Model extends PrismaModel> = NonNullable<NonNullable<Parameters<Model['findFirst']>[0]>['where']>
type ModelWhereUniqueInput<Model extends PrismaModel> = NonNullable<
NonNullable<Parameters<Model['findUnique']>[0]>['where']
>
type UniqueValidator<Model extends PrismaModel> = (
fields: ModelWhereInput<Model>,
ignore?: ModelWhereUniqueInput<Model>
) => Promise<boolean>
type ModelValidator<Model extends PrismaModel> = {
isUnique: UniqueValidator<Model>
}
findFirst
のシグネチャはこんな感じなので、NonNullable
でundefined
を除外しながらプロパティにアクセスします。
(args?: { where?: ModelWhereInput }) => (省略)
実装(関数)
findFirst
でレコードを取得し、除外する対象がある場合はその処理をしています。
function createUniqueValidator<Model extends PrismaModel>(model: Model): UniqueValidator<Model> {
const validator: UniqueValidator<Model> = async (fields, ignore) => {
const record = await model.findFirst({ where: fields })
if (record === null) return true
if (ignore) {
const shouldIgnore = Object.entries(ignore).every(([key, value]) => record[key] === value)
if (shouldIgnore) return true
}
return false
}
return validator
}
export function modelValidator<Model extends PrismaModel>(model: Model): ModelValidator<Model> {
return {
isUnique: createUniqueValidator(model),
}
}
存在のバリデーションを実装する機会があれば、exists
メソッドも追加してみようと思います。
Discussion