🏭

Prisma で Factory を作るための便利関数を作りました!

2 min read 3

ゴール

次のようなことができる Factory 関数を

// User の必須な値にはデフォルト値が生成されて入っている -> 全ての値を指定する必要がない
const user = await UserFactory.create({
  name: "John Doe"
})

次のようにデフォルト値とモデル名(+いくつかの型)を指定するだけで作れるようにしました。

const defaultAttributes = {
  name: faker.name.firstName(),
  birthday: faker.date.past(),
};

export const userFactory = createFactory<
  Prisma.userCreateInput,
  user
>("user", defaultAttributes);

こちらの動画を参考にして作ってコードを GitHub で検索したり型を補強して作りました。

https://www.youtube.com/watch?v=a5S5thDd7Xg

実装

結論として次のようなものを書きました。

import { getPrismaClient } from "../../src/prisma-client";
import { PrismaClient } from "@prisma/client";

type Awaited<T> = T extends PromiseLike<infer U> ? U : T;

// モデル名を PrismaClient の Class から取るようにしています。こうするとモデルが増えても勝手に型定義もアップデートされるからです。(先頭に "$" が付いているのが PrismaClient の関数でそれ以外に生えてるプロパティが schema.prisma から生成されたモデルの名前です)
type FilterStartsWith<
  Union,
  Prefix extends string
> = Union extends `${Prefix}${infer _Property}` ? never : Union;
type ModelName = FilterStartsWith<keyof Awaited<PrismaClient>, "$">;

/**
 * connect/create が生えてたら include できるようにする
 */
function buildPrismaIncludeFromAttrs(attrs: Record<string, any>) {
  const include = Object.keys(attrs).reduce((prev, curr) => {
    const value = attrs[curr];
    const isObject = typeof value === "object";
    const isRelation =
      isObject && Object.keys(value).find((v) => v.match(/connect|create/));

    if (isRelation) {
      prev[curr] = true;
    }

    return prev;
  }, Object.create(null));

  const hasInclude = Object.keys(include).length;
  return hasInclude ? include : undefined;
}

// ここでモデル名とデフォルトの値を渡すと、それに基づいた Factory 関数を返します。
export const createFactory = <CreateInputType, ModelType>(
  modelName: ModelName,
  defaultAttributes: CreateInputType
) => {
  return {
    create: async (attrs: Partial<CreateInputType>): Promise<ModelType> => {
      const obj: CreateInputType = {
        ...defaultAttributes,
        ...attrs,
      };

      const options: Record<string, any> = {};
      const includes = buildPrismaIncludeFromAttrs(attrs);
      if (includes) options.include = includes;

      const prisma = await getPrismaClient();

      // 型力が足りなかったので妥協しました。猛者を求む
      return await prisma[modelName as string].create({
        data: { ...obj },
        ...options,
      });
    },
  };
};

Discussion

// モデル名を PrismaClient の Class から取るようにしています。こうするとモデルが増えても勝手に型定義もアップデートされるからです。
// これらの関数を除くと schema.prisma から生成したモデルのみになるはずです。
// 本当は自分が型力足りないだけでもっと Cool な方法ある気がするので、もし見つけたらコメントください。

下記のようにTemplateLiteralTypesを使えばCoolに行けるかなと思いました。

type FilterStartsWith<Union, Prefix extends string> = Union extends `${Prefix}${infer _Property}` ? never : Union;
type ModelName = FilterStartsWith<keyof PrismaClient, '$'>;

おおありがとうございます!!!!!
試してみます!

バッチリできました!
TemplateLiteralTypes 実践で使ったことなかったですがバッチリなユースケースでしたね。
勉強になりました。

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