Zenn
🧠

Prismaでfull-typedのページネーション関数をつくる

2025/03/24に公開

書こうとしたきっかけ

paginationのresponseを以下のように統一したく、

type PaginationResponse<T> = {
    items: T[],
    total: number,
    cursor: number,
    limit: number,
}

かつ毎回直でprisma.user.findMany()を使ってからresponseを作りたくないため、関数で以上の作業をまとめようとするのが普通だと思いますが、適当に関数を書いてしまうとprismaの強大なtype-systemが利用できなくなるのがすごく嫌。
なのでネットで漁ってみたのですが、この類の記事があまり見当たらないため、自分の書き方のここに残そうとしました。

prisma-extension-paginationを使えばええやんという声もあるかもしれませんが、これだけのために他のライブラリーを入れたくないのでね...

結論

まず結論から言うと、以下のような書き方にしました。
注目すべきはPRISMA_CLIENT_MODEL_NAMESという型とPrisma.Resultを使うこと。
あとPrisma.Resultを使っているため、PaginationResultがitem: Tになっていることかな。

import { MAX_PAGE_SIZE, PRISMA_CLIENT_MODEL_NAMES } from "@/const";

export type PaginationResult<T> = {
  items: T;
  total: number;
  cursor: number;
  limit: number;
};
export async function cursorPaginate<
  TModel extends PrismaClient[PRISMA_CLIENT_MODEL_NAMES],
  TArgs extends Parameters<TModel["findMany"]>[0],
>(
  model: TModel,
  args: TArgs
): Promise<PaginationResult<Prisma.Result<TModel, TArgs, "findMany">>> {
  // Ideally, args will always be passed to this function.
  // But TS does not understand that.
  if (!args) {
    throw new Error("cursorPaginate.args is required");
  }

  if (!args.skip) {
    args.skip = 1;
  }
  if (!args.take) {
    args.take = MAX_PAGE_SIZE
  }
  if (args.take && args.take > process.app.config.db.maxPageSize) {
    args.take = MAX_PAGE_SIZE
  }

  const [items, total] = await Promise.all([
    // @ts-expect-error findMany is a function, but TS does not understand that
    model.findMany(args),
    // @ts-expect-error count is a function, but TS does not understand that
    model.count({ where: args.where }),
  ]);

  return {
    items,
    total,
    cursor: args.skip,
    limit: args.take,
  };
}

PRISMA_CLIENT_MODEL_NAMESはこちらで定義する型で、中には

export type PRISMA_CLIENT_MODEL_NAMES = "user" | "post" | "profile";
export const PRISMA_CLIENT_MODEL_NAMES = {
    user: "user",
    post: "post",
    profile: "profile",
};

などの情報がはいっている。
これをPrismaClientに渡せばモデル関連のメソッドだけ取り出せる。

Prisma.Resultは違うモデルのReturnTypeを推論してくれるもので、Prisma.Result<ModelType, Args, MethodFunction>という記述で使える。
(*実際のコードにはResult<T, A, F extends Operation>と書かれてますが、ここはわかりやすいにするために記述を変更している、具体的な内容は読者各自prismaのコードを読んでください。)

説明

まずPrismaClientはなぜこのような強い型を提供できているかとういうと、prisma.schemaから毎回参照してGenerateしていることは周知の事実だと思います。

そこで私たちがこの型と獲得するためにPrismaClient[user]などのPrismaClient[<modelName>]という書き方をしないといけない。しかしこのmodelNameはprisma generateによって生成されているため、直接PrismaClient一括もらうのは不可能。

そのためPRISMA_CLIENT_MODEL_NAMESが必要。

しかしこれだけでは毎回更新があったら手動でPRISMA_CLIENT_MODEL_NAMES書き換えるしかないのでめんどくさいしHuman-errorが出やすい。なので筆者は毎回prisma generate後scriptを使ってprisma.schemaをパースしてPRISMA_CLIENT_MODEL_NAMES生成するようにしている。

パースと言ってもprisma.schemaの全部を理解する必要がないので、model XXXX {の部分だけ取り出せばよいので、正規表現で簡単に抽出するようにしてます。具体的にはこのような正規表現をつかっている^\s*model\s+(\w+)\s*\{

具体的な実現はpackage.jsonの中に"prisma_generate": "prisma generate && ts-node parse_prisma_schema.ts"など定義して、毎回確実に生成後に呼び出されるようにするなどいくつか方法ありますが、ここは各自のプロジェクトによって違うと思うのでやりやすいやりかたで大丈夫。

以上

Discussion

ログインするとコメントできます