Prismaを使うとGraphQLが簡単にできるようなので、やってみました。

前回書いた、Prismaとdbdocsを組み合わせて使ってみたという記事の実質続きみたいな感じです。

ジェネレーターを実装する

GraphQLを扱うためにはリゾルバという実装が必要なようですが、これをスキーマ定義から作成するようにします。

既に構築済みのprismaプロジェクトがある場合、typegraphql-prismaを追加するだけです。

yarn add -D typegraphql typegraphql-prisma

追加したら、schema.prismaに以下のgeneratorセクションを追加します。

generator typegraphql {
  provider = "typegraphql-prisma"
}

追加したら以下のコマンドでリゾルバが作成されます。

npx prisma generate

作成されたリゾルバは、デフォルトではnode_modules/@generated/type-graphqlにあります。

GraphQLのサーバーを構築する

本題です。

Apolloを使用してGraphQLサーバーを構築しました。

実装はこちらを参考にしつつ行いました。

import { PrismaClient } from "@prisma/client";
import { ApolloServer, BaseContext } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { resolvers } from "@generated/type-graphql";
import { buildSchema } from "type-graphql";

(async () => {
    const schema = await buildSchema({
        resolvers,
        validate: false,
    });
    
    const prisma = new PrismaClient();

    interface Context {
        prisma: PrismaClient;
    }
    
    const server = new ApolloServer<Context>({
      schema,
    });

    const { url } = await startStandaloneServer(server, {
        context: async () => ({ prisma }),
        listen: { port: 4000 },
    });
})();

最新のApolloだと、contextの関数がasyncになっているので注意が必要です。

これで起動してみたら、エラーが起きました。

Looks like you've forgot to provide experimental metadata API polyfill. Please read the installation instruction for more details

同じバグに悩んでいる人は見つかりましたが、解決せず。。

experimental metadata API polyfill error when using enums · Issue #970 · MichalLytek/type-graphql · GitHub

ただ、今までts-nodeで直接実行していたので、以下のスクリプトをpackage.jsonに追加してからyarn startで実行するようにしました。

  "scripts": {
    "postinstall": "yarn compile",
    "compile": "tsc",
    "start": "yarn compile && node ./dist/index.js"
  },

一応tsconfig.jsonも公式のものと合わせました。

Get Started with Apollo Server - Apollo GraphQL Docs

{
  "compilerOptions": {
    "rootDirs": ["src"],
    "outDir": "dist",
    "lib": ["es2020"],
    "target": "es2020",
    "module": "esnext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "types": ["node"]
  }
}

これで実行しましたが、まだエラーが出ます。

インポートに失敗している?

yarn run v1.22.15
$ npm run compile && node ./dist/index.js

> dbdocs-playground@1.0.0 compile
> tsc

node_modules/type-graphql/dist/errors/ArgumentValidationError.d.ts:1:38 - error TS2307: Cannot find module 'class-validator' or its corresponding type declarations.

1 import type { ValidationError } from "class-validator";
                                       ~~~~~~~~~~~~~~~~~

node_modules/type-graphql/dist/schema/build-context.d.ts:2:39 - error TS2307: Cannot find module 'class-validator' or its corresponding type declarations.

2 import type { ValidatorOptions } from "class-validator";
                                        ~~~~~~~~~~~~~~~~~

node_modules/type-graphql/dist/utils/emitSchemaDefinitionFile.d.ts:2:10 - error TS2305: Module '"graphql/utilities/printSchema"' has no exported member 'Options'.

2 import { Options as GraphQLPrintSchemaOptions } from "graphql/utilities/printSchema";
           ~~~~~~~


Found 3 errors in 3 files.

Errors  Files
     1  node_modules/type-graphql/dist/errors/ArgumentValidationError.d.ts:1
     1  node_modules/type-graphql/dist/schema/build-context.d.ts:2
     1  node_modules/type-graphql/dist/utils/emitSchemaDefinitionFile.d.ts:2
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

最近prismaがv5.0.0をリリースしたばかりなので、それが原因かもしれません。

一度バージョンを前のメジャーバージョンの最新である4.16.2に落とします。

(typegraphql-prismaもprismaのバージョンに強く依存しているので、落とします。)

yarn add prisma@4.16.2 typegraphql-prisma@0.26.0

一応prisma generateも再実行しておきます。

yarn prisma generate

しかし同じエラーに…

一つのエラーはyarn add class-validatorしてあげたら解決しましたが、もう一つのエラーはどうやらGraphQLのバージョンを15に下げないといけないみたいです。

Module '"graphql/utilities/printSchema"' has no exported member 'Options' · Issue #1135 · MichalLytek/type-graphql · GitHub


yarn add graphql@15.8.0

下げましたが、やはりエラーになってしまいました。

yarn run v1.22.15
$ yarn compile && node ./dist/index.js
$ tsc
src/generated/type-graphql/helpers.ts(2,27): error TS2307: Cannot find module 'graphql-fields' or its corresponding type declarations.
src/generated/type-graphql/models/Post.ts(2,33): error TS2307: Cannot find module 'graphql-scalars' or its corresponding type declarations.
src/generated/type-graphql/models/Post.ts(7,2): error TS1238: Unable to resolve signature of class decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 1.
src/generated/type-graphql/models/Post.ts(9,4): error TS1240: Unable to resolve signature of property decorator when called as an expression.
  Argument of type 'ClassFieldDecoratorContext<Post, number> & { name: "id"; private: false; static: false; }' is not assignable to parameter of type 'string | symbol'.
src/generated/type-graphql/models/Post.ts(14,4): error TS1240: Unable to resolve signature of property decorator when called as an expression.
...長すぎるので以下省略...

最初の方にgraphql-fieldsとgraphql-scalarsというライブラリが必要ということが書いてあるのでインストールしましたが、解決しませんでした。(むしろエラーが増えた

ここに来て、typegraphql-prismaの公式のサンプルが合ったことを思い出して、これをクローンして実施してみました。

typegraphql-prisma/examples/1-prototyping at main · MichalLytek/typegraphql-prisma · GitHub

インストールとGraphQLの実装を作成します。

yarn && yarn prisma generate

実行します。

yarn start

無事起動すると以下のメッセージが出ます。

GraphQL is listening on 4000!

クライアントから操作してみる

GraphQLクライアントから操作してみます。

クライアントにはAltairを使用します。

Altair GraphQL Client

linuxであればsnapからインストールできます

snap install altair

インストールできたら起動してみます。

altair
起動画面

URLに「http://localhost:4000」と入力し、Queryの部分にはサンプルリポジトリにあるexamples.graphqlを貼り付けます。

query GetAllUsersAndPosts {
  users {
    ...UserData
    posts {
      ...PostData
    }
  }
  posts {
    ...PostData
    author {
      ...UserData
    }
  }
}

query GetSelectedPost {
  post(where: { id: "cl63gazie0009prtpf16vmw0i" }) {
    ...PostData
    author {
      ...UserData
    }
  }
}

query GetSomeUsers {
  users(where: { email: { contains: "prisma" } }, orderBy: { name: desc }) {
    ...UserData
    posts(skip: 1) {
      ...PostData
    }
  }
}

mutation UpdatePost {
  updateOnePost(
    where: { id: "cl63gazie0010prtphdhetz2w" }
    data: { published: { set: true } }
  ) {
    ...PostData
  }
}

mutation AddUser {
  createOneUser(data: { email: "test@test.test", name: "Test" }) {
    ...UserData
  }
}

mutation AddUserWithPost {
  createOneUser(
    data: {
      email: "test2@test.test"
      name: "Test2"
      posts: {
        create: {
          title: "Test post"
          content: "Missing content"
          published: false
        }
      }
    }
  ) {
    ...UserData
    posts {
      ...PostData
    }
  }
}

query GetPrismaPostCount {
  aggregatePost(where: { title: { contains: "Prisma" } }) {
    _count {
      _all
    }
  }
}

fragment UserData on User {
  id
  email
  name
}

fragment PostData on Post {
  id
  createdAt
  updatedAt
  published
  title
  content
}
サンプルクエリを貼り付けた後

準備ができました。

早速クエリを実行してみます。

とりあえずユーザーとポストを作成します。

作成後の画面

作成できました。

続けて取得のためのクエリを投げてみます。

ユーザーとポストの取得結果

バッチリですね!

takeskipという引数を使用して、ページネーションも可能です。

(take:1, skip:0)を追加
(take:1, skip:1)を追加

感想

既存のプロジェクトにtypegraphql-prismaを入れることに苦戦してしまいました。。

GraphQLの利便性とPrismaによる拡張性を兼ね備えることで最強になりました。

とはいえ、独自クエリなどを入れたいケースは追加で実装が必要になると思いますが、基本的にはこれだけで割と網羅できるんではないでしょうか。

こういった技術を活用して仕事を効率化していきたいですね。

追記: 2023-08-03

package.jsonの内容とtsconfig.jsonの内容を変更して、どうにか動かせるようにしました。

tsconfig.json

{
    "compilerOptions": {
        "target": "es2018",
        "module": "commonjs",
        "lib": ["es2018", "esnext.asynciterable"],
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "esModuleInterop": true,
    }
}

package.json

{
  "private": true,
  "scripts": {
    "start": "ts-node --transpile-only ./index.ts",
    "generate": "prisma generate",
    "seed": "ts-node --transpile-only ./prisma/seed.ts"
  },
  "dependencies": {
    "@apollo/server": "^4.9.0",
    "@prisma/client": "5.1.0",
    "@types/graphql-fields": "^1.3.4",
    "graphql": "^16.6.0",
    "graphql-fields": "^2.0.3",
    "graphql-scalars": "^1.20.1",
    "reflect-metadata": "^0.1.13",
    "type-graphql": "2.0.0-beta.1"
  },
  "devDependencies": {
    "@types/node": "^18.11.18",
    "prisma": "^5.1.0",
    "prisma-dbml-generator": "^0.10.0",
    "ts-node": "^10.9.1",
    "typegraphql-prisma": "0.27.0",
    "typescript": "^4.9.4"
  }
}

apolloも最新のバージョンに対応しました。

現状、実装のはじめにリフレクションするためのライブラリを宣言しておかないと動かないみたいですね。

import "reflect-metadata";
import { buildSchema } from "type-graphql";
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import path from "path";
import { PrismaClient } from "@prisma/client";

import { resolvers } from "./src/generated/type-graphql";

interface Context {
  prisma: PrismaClient;
}

async function main() {
  const schema = await buildSchema({
    resolvers,
    emitSchemaFile: path.resolve(__dirname, "./generated-schema.graphql"),
    validate: false,
  });

  const prisma = new PrismaClient();
  await prisma.$connect();

  const server = new ApolloServer({
    schema,
  });
  const { url } = await startStandaloneServer(server, {
    context: async (): Promise<Context> => ({ prisma }),
    listen: { port: 4000 },
});
  console.log(`GraphQL is listening on ${url}!`);
}

main().catch(console.error);

参考

Prismaを使ってサクッとGraphQLのバックエンドを作成する! - FLINTERS Engineer's Blog