😸

Prismaでユニークバリデーションのためのユーティリティを作る

2023/02/02に公開

背景

ユーザーからの入力がデータベースに存在すること(しないこと)をチェックしたいときがあります。

例えば、ユーザー登録をするときにユーザー名が重複していないことを確認したいとします。Prismaで愚直に実装すると、以下のような感じになると思います。

async function isUserNameUnique(prisma: PrismaClient, userName: string): Promise<boolean> {
  const user = await prisma.user.findFirst({ where: { userName } });
  return user !== null;
}

ユニークチェックをする箇所が複数ある場合、このような関数を何度も定義するのは少し面倒です。

ところで、Laravelのバリデーションにはuniqueexistsなどのルールがあり、一意性や存在のバリデーションを簡潔に書くことができます。

$request->validate([
    // user_nameフィールドの値が、usersテーブルのuser_nameカラムに存在しないことを検証する
    'user_name' => 'unique:users'
]);

バリデーション 9.x Laravel

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の引数の型は、findFirstfindUniqueの引数の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のシグネチャはこんな感じなので、NonNullableundefinedを除外しながらプロパティにアクセスします。

(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メソッドも追加してみようと思います。

GitHubで編集を提案

Discussion