Open100

GraphQL Nexus から Pothos GraphQL に乗り換える

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

このスクラップについて

GraphQL Nexus を実務で使っているが最近開発があまり活発ではないことに懸念を抱いている。

気になって調べていると下記の記事を見つけて Pothos のことを知った。

https://tmokmss.hatenablog.com/entry/20230109/1673237629

なんかすごく良さそうなので手を動かして試してみて良かったら GraphQL Nexus から乗り換えたい。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

仕事に追われているが

明日納期の仕事があって調べている時間も惜しいくらいなのだが好奇心には勝てない。

たまには自分の喜びのために時間を使おう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ワークスペース作成

コマンド
mkdir hello-pothos
cd hello-pothos
npm init -y
npm install --save @pothos/core graphql-yoga
npm install --save-dev @types/node ts-node
touch tsconfig.json hello-world.ts
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

TypeScript 設定

Pothos is designed to be as type-safe as possible, to ensure everything works correctly, make sure that your tsconfig.json has strict mode set to true:

全てが正しく動くことを確かにするために tsconfig.json のコンパイラオプションの strict モードが有効になっていることを確認してくださいとのこと。

tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

まずは上記の内容だけでどこまでできるかを試してみよう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

hello-world.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      hello: t.string({
        args: {
          name: t.arg.string(),
        },
        resolve: (parent, { name }) => `hello, ${name || "World"}`,
      }),
    }),
  });

  const schema = builder.toSchema();

  const yoga = createYoga({
    schema: builder.toSchema(),
  });

  const server = createServer(yoga);
  server.listen(3000);
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

クエリ作成には Explorer が便利

左側にある上から 3 つ目のアイコンをクリックすると Explorer なるものが起動してクエリを作成するのに便利。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

クエリ実行

▶︎ ボタンを押すかエディタで Command + R を押すとクエリが実行される。

期待通りの結果が表示された。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Giraffe クラス作成

コマンド
touch giraffe.ts
giraffe.ts
export class Giraffe {
  name: string;
  birthday: Date;
  heightsInMeters: number;

  constructor(name: string, birthday: Date, heightsInMeters: number) {
    this.name = name;
    this.birthday = birthday;
    this.heightsInMeters = heightsInMeters;
  }
}

Using classes is completely optional, but it's a good place to start, since it makes it easy to show all the different ways that you can tie the shape of your data to a new object type.

Pothos を使うのにクラスが必須という訳ではない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Object Type 定義

コード例は下記の通り。

コード例
const builder = new SchemaBuilder({});

builder.objectType(Giraffe, {
  name: 'Giraffe',
  description: 'Long necks, cool patterns, taller than you.',
  fields: (t) => ({}),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

Schema ドキュメントを見るためにサーバーを起動する。

objects.ts
import SchemaBuilder from "@pothos/core";
import { Giraffe } from "./giraffe";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder({});

  builder.objectType(Giraffe, {
    name: "Giraffe",
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({}),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000);
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Query Type 定義

適当な Query Type を追加した。

objects.ts(抜粋)
  builder.queryType({
    fields: (t) => ({
      ok: t.boolean({
        resolve: () => true,
      }),
    }),
  });

エラーが下記だけになった。

Error: Type Giraffe must define one or more fields.

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

フィールド追加

objects.ts(抜粋)
  builder.objectType(Giraffe, {
    name: "Giraffe",
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({
      name: t.exposeString("name", {}),
    }),
  });

フィールドを追加したらエラーが消えた。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

スキーマを確認する方法

コマンド
npm install -g get-graphql-schema
get-graphql-schema http://localhost:3000/graphql
実行結果
"""Exposes a URL that specifies the behavior of this scalar."""
directive @specifiedBy(
  """The URL that specifies the behavior of this scalar."""
  url: String!
) on SCALAR

"""Long necks, cool patterns, taller than you."""
type Giraffe {
  name: String!
}

type Query {
  ok: Boolean!
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

公式ドキュメントにあった

https://pothos-graphql.dev/docs/guide/printing-schemas

コマンド
npm install --save graphql

schema.graphql に出力するようにコードを書き換える。

objects.ts
import SchemaBuilder from "@pothos/core";
import { Giraffe } from "./giraffe";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { lexicographicSortSchema, printSchema } from "graphql";
import { writeFile } from "fs/promises";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      ok: t.boolean({
        resolve: () => true,
      }),
    }),
  });

  builder.objectType(Giraffe, {
    name: "Giraffe",
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({
      name: t.exposeString("name", {}),
    }),
  });

  const schema = builder.toSchema();
  const schemaAsString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaAsString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000);
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

フィールド追加

objects.ts(抜粋)
  builder.objectType(Giraffe, {
    name: "Giraffe",
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({
      name: t.exposeString("name", {}),
      age: t.int({
        resolve: (parent) => {
          const ageDifMs = Date.now() - parent.birthday.getTime();
          const ageDate = new Date(ageDifMs);
          return Math.abs(ageDate.getUTCFullYear() - 1970);
        },
      }),
      height: t.float({
        resolve: (parent) => parent.heightsInMeters,
      }),
    }),
  });
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

この時点のスキーマ

サーバーを再起動すると schema.graphql が更新される。

schema.graphql
"""Long necks, cool patterns, taller than you."""
type Giraffe {
  age: Int!
  height: Float!
  name: String!
}

type Query {
  ok: Boolean!
}

Nexus のように nonNull を指定しなくてもデフォルトで nonNull になる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Query 追加

objects.ts(抜粋)
  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        type: Giraffe,
        resolve: () =>
          new Giraffe("James", new Date(Date.UTC(2012, 11, 12)), 5.2),
      }),
    }),
  });

resolve() の戻り値は Giraffe である必要がある。

GraphQL の type Giraffe と同じである必要はない = name, age, height が無くても良い。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

objects.ts
import SchemaBuilder from "@pothos/core";
import { Giraffe } from "./giraffe";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { lexicographicSortSchema, printSchema } from "graphql";
import { writeFile } from "fs/promises";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder<{ Objects: { Giraffe: Giraffe } }>({}); // この行を変更しました。

  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        type: "Giraffe", // この行を変更しました。
        resolve: () =>
          new Giraffe("James", new Date(Date.UTC(2012, 11, 12)), 5.2),
      }),
    }),
  });

  builder.objectType("Giraffe", { // この行を変更しました。
    // name: "Giraffe", // この行をコメントアウトしました。
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({
      name: t.exposeString("name", {}),
      age: t.int({
        resolve: (parent) => {
          const ageDifMs = Date.now() - parent.birthday.getTime();
          const ageDate = new Date(ageDifMs);
          return Math.abs(ageDate.getUTCFullYear() - 1970);
        },
      }),
      height: t.float({
        resolve: (parent) => parent.heightsInMeters,
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaAsString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaAsString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000);
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

VSCode コード補完

ソースコードだけでは伝えきれないが "Giraffe" と入力する時にコード補完が効くので安心できる。

仮にタイプミスがある場合はエラーメッセージを表示してくれる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

SchemaTypes の使い所

This is ideal when you want to list out all the types for your schema in one place, or you have interfaces/types that define your data rather than classes, and means you won't have to import anything when referencing the object type in other parts of the schema.

一つの場所にスキーマに関するすべてのタイプをリストアップしたい時に最適のようだ。

またクラスではなくインタフェースや型の時にも適している。

メリットは後から文字列で参照できるのでインポートが不要な点。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ObjectRef を使う方法

ObjectRefs are useful when you don't want to define all the types in a single place (SchemaTypes) and your data is not represented as classes. Regardless of how you define your object types, builder.objectType returns an ObjectRef that can be used as a type parameter in other parts of the schema.

ObjectRef は SchemaTypes とは異なり、一つの場所にスキーマに関するすべてのタイプをリストアップしたくない時に便利のようだ。

一方で SchemaTypes と同様にデータがクラスとして表現されていない時に適している。

build.objectTypes() の戻り値の型は ObjectRef であり、他の Object Type を定義する時などに使うことができる。

objects.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { lexicographicSortSchema, printSchema } from "graphql";
import { writeFile } from "fs/promises";
import { join } from "path";

type GiraffeShape = {
  name: string;
  birthday: Date;
  heightsInMeters: number;
};

async function main() {
  const builder = new SchemaBuilder({});

  const Giraffe = builder.objectRef<GiraffeShape>("Giraffe");

  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        type: Giraffe,
        resolve: () => ({
          name: "James",
          birthday: new Date(Date.UTC(2012, 11, 12)),
          heightsInMeters: 5.2,
        }),
      }),
    }),
  });

  builder.objectType(Giraffe, {
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({
      name: t.exposeString("name", {}),
      age: t.int({
        resolve: (parent) => {
          const ageDifMs = Date.now() - parent.birthday.getTime();
          const ageDate = new Date(ageDifMs);
          return Math.abs(ageDate.getUTCFullYear() - 1970);
        },
      }),
      height: t.float({
        resolve: (parent) => parent.heightsInMeters,
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaAsString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaAsString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000);
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Object Ref の implement() メソッド

Object Ref とその実装をまとめて書くこともできる。

  const Giraffe = builder.objectRef<GiraffeShape>("Giraffe").implement({
    description: "Long necks, cool patterns, taller than you.",
    fields: (t) => ({
      name: t.exposeString("name", {}),
      age: t.int({
        resolve: (parent) => {
          const ageDifMs = Date.now() - parent.birthday.getTime();
          const ageDate = new Date(ageDifMs);
          return Math.abs(ageDate.getUTCFullYear() - 1970);
        },
      }),
      height: t.float({
        resolve: (parent) => parent.heightsInMeters,
      }),
    }),
  });
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

どの書き方を使う?

ケースバイケースだがクラスがある場合はクラスを使うのが一番楽な気がする。

クラスがなくてインタフェースや型がある場合は ObjectRef を使うと良いかも。

SchemaTypes は使い所がピンと来ないがスキーマがあまり大きくない場合は便利に使えるかも知れない。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

今日はここまで(2 回目)

とりあえず Object Types のページを終わらせることができてよかった。

Pothos では Nexus とは異なって Object Types のそれぞれにクラス/インタフェース/型が必要になる。

その点 Nexus はスキーマから型を自動生成してくれるのである意味で便利だった。

アプリ開発の初期段階で API のモックアップを作っている時などはどうすれば良いのだろう?

ドキュメントを眺めていた所、モック用のプラグインを見つけたので使えるかも知れない。

https://pothos-graphql.dev/docs/plugins/mocks

次回は ShcemaBuilder について理解を深めよう。

https://pothos-graphql.dev/docs/guide/schema-builder

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Backing models

色々と大切なことが説明されているが重要な点は下記に集約されている気がする。

To put it simply, backing models are the types that describe the data as it flows through your application, which may be substantially different than the types described in your GraphQL schema.

Backing models はアプリケーション内で受け渡されるデータの型であり、GraphQL スキーマの型とは異なる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

fields.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      name: t.field({
        description: "Name field",
        type: "String",
        resolve: () => "Gina",
      }),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

便利なメソッド

FieldBuilder の field() メソッドは万能だが一般的なフィールド向けに便利なメソッドが用意されている。

builder.queryType({
  fields: (t) => ({
    id: t.id({ resolve: () => '123' }),
    int: t.int({ resolve: () => 123 }),
    float: t.float({ resolve: () => 1.23 }),
    boolean: t.boolean({ resolve: () => false }),
    string: t.string({ resolve: () => 'abc' }),
    idList: t.idList({ resolve: () => ['123'] }),
    intList: t.intList({ resolve: () => [123] }),
    floatList: t.floatList({ resolve: () => [1.23] }),
    booleanList: t.booleanList({ resolve: () => [false] }),
    stringList: t.stringList({ resolve: () => ['abc'] }),
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

書き換えてみる。

1 行だけ短くなった。

fields.ts(抜粋)
  builder.queryType({
    fields: (t) => ({
      // name: t.field({
      //   description: "Name field",
      //   type: "String",
      //   resolve: () => "Gina",
      // }),
      name: t.string({
        description: "Name field",
        resolve: () => "Gina",
      }),
    }),
  });
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Object や Interfaces

文字列や整数などスカラーの場合は便利なメソッドが用意されているが、Object / Interfaces の場合は field() メソッドを使う必要がある。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

fields.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder<{
    Objects: {
      Giraffe: { name: string };
    };
  }>({});

  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        description: "A giraffe",
        type: "Giraffe",
        resolve: () => ({
          name: "Gina",
        }),
      }),
    }),
  });

  builder.objectType("Giraffe", {
    fields: (t) => ({
      name: t.exposeString("name"),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Ref を使う

SchemaTypes の代わりに Ref を使うこともできる

コード例
const LengthUnit = builder.enumType('LengthUnit', {
  values: { Feet: {}, Meters: {} },
});

builder.objectType('Giraffe', {
  fields: (t) => ({
    preferredNeckLengthUnit: t.field({
      type: LengthUnit,
      resolve: () => 'Feet',
    }),
  }),
});

builder.queryType({
  fields: (t) => ({
    giraffe: t.field({
      type: 'Giraffe',
      resolve: () => ({ name: 'Gina' }),
    }),
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

fields.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder<{
    Objects: {
      Giraffe: { name: string };
    };
  }>({});

  const LengthUnit = builder.enumType("LengthUnit", {
    values: {
      Feet: {},
      Meters: {},
    },
  });

  builder.objectType("Giraffe", {
    fields: (t) => ({
      preferredNeckLengthUnit: t.field({
        type: LengthUnit,
        resolve: () => "Feet" as const,
      }),
    }),
  });

  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        description: "A giraffe",
        type: "Giraffe",
        resolve: () => ({
          name: "Gina",
        }),
      }),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リストフィールド

リストを使うには型(クラス、文字列、Ref)を [] で囲む。

コード例
builder.queryType({
  fields: t => ({
    giraffes: t.field({
      description: 'multiple giraffes'
      type: ['Giraffe'],
      resolve: () => [{ name: 'Gina' }, { name: 'James' }],
    }),
    giraffeNames: t.field({
      type: ['String'],
      resolve: () => ['Gina', 'James'],
    })
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

fields.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder<{
    Objects: {
      Giraffe: { name: string };
    };
  }>({});

  const LengthUnit = builder.enumType("LengthUnit", {
    values: {
      Feet: {},
      Meters: {},
    },
  });

  builder.objectType("Giraffe", {
    fields: (t) => ({
      preferredNeckLengthUnit: t.field({
        type: LengthUnit,
        resolve: () => "Feet" as const,
      }),
    }),
  });

  builder.queryType({
    fields: (t) => ({
      giraffes: t.field({
        description: "mutiple giraffes",
        type: ["Giraffe"],
        resolve: () => [{ name: "Gina" }, { name: "James" }],
      }),
      giraffeNames: t.field({
        type: ["String"],
        resolve: () => ["Gina", "James"],
      }),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Nullable

Pothos ではデフォルトで non-nullable になっており、nullable にするには明示的に指定する必要がある。

コード例
builder.queryType({
  fields: (t) => ({
    nullableField: t.field({
      type: 'String',
      nullable: true,
      resolve: () => null,
    }),
    nullableString: t.string({
      nullable: true,
      resolve: () => null,
    }),
    nullableList: t.field({
      type: ['String'],
      nullable: true,
      resolve: () => null,
    }),
    spareseList: t.field({
      type: ['String'],
      nullable: {
        list: false,
        items: true,
      },
      resolve: () => [null],
    }),
  }),
});

SchemaBuilder の設定によってデフォルトを nullable にすることもできるそうだ。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

コーディング

fields
import SchemaBuilder from "@pothos/core";
import { writeFile } from "fs/promises";
import { lexicographicSortSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      nullableField: t.field({
        type: "String",
        nullable: true,
        resolve: () => null,
      }),
      nullableString: t.string({
        nullable: true,
        resolve: () => null,
      }),
      nullableList: t.field({
        type: ["String"],
        nullable: true,
        resolve: () => null,
      }),
      spareseList: t.field({
        type: ["String"],
        nullable: {
          list: false,
          items: true,
        },
        resolve: () => [null],
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

Exposing fields

多くのフィールドは backing model のフィールドをそのまま返すだけなので、それを便利にするためのメソッドが用意されている。

  • exposeString
  • exposeInt
  • exposeFloat
  • exposeBoolean
  • exposeID
  • exposeStringList
  • exposeIntList
  • exposeFloatList
  • exposeBooleanList
  • exposeIDList

使い方は下記の通り。

コード例
const builder = new SchemaBuilder<{
  Objects: { Giraffe: { name: string } };
}>({});

builder.objectType('Giraffe', {
  fields: (t) => ({
    name: t.exposeString('name', {}),
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に使ってみる

fields.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder<{
    Objects: {
      Giraffe: { name: string };
    };
  }>({});

  builder.objectType("Giraffe", {
    fields: (t) => ({
      name: t.exposeString("name"),
    }),
  });

  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        type: "Giraffe",
        resolve: () => ({ name: "Gino" }),
      }),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

引数

args オプションを使うことで引数を指定できる。

コード例
builder.queryType({
  fields: (t) => ({
    giraffeByName: t.field({
      type: 'Giraffe',
      args: {
        name: t.arg.string({ required: true }),
      },
      resolve: (root, args) => {
        if (args.name !== 'Gina') {
          throw new NotFoundError(`Unknown Giraffe ${name}`);
        }

        return { name: 'Gina' };
      },
    }),
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に使ってみる

fields.ts
import SchemaBuilder from "@pothos/core";
import { writeFile } from "fs/promises";
import { lexicographicSortSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder<{
    Objects: {
      Giraffe: { name: string };
    };
  }>({});

  builder.objectType("Giraffe", {
    fields: (t) => ({
      name: t.exposeString("name"),
    }),
  });

  builder.queryType({
    fields: (t) => ({
      giraffe: t.field({
        type: "Giraffe",
        args: {
          name: t.arg.string({ required: true }),
        },
        resolve: (root, args) => {
          if (args.name !== "Gina") {
            throw new TypeError(`Unkonwn Giraffe ${args.name}`);
          }

          return { name: "Gina" };
        },
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

フィールド追加

queryType() や objectType() の代わりに queryFields() や objectFields() を使うことでフィールドを追加できる。

コード例
builder.queryFields((t) => ({
  giraffe: t.field({
    type: Giraffe,
    resolve: () => new Giraffe('James', new Date(Date.UTC(2012, 11, 12)), 5.2),
  }),
}));

builder.objectField(Giraffe, 'ageInDogYears', (t) =>
  t.int({
    resolve: (parent) => parent.age * 7,
  }),
);
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に使ってみる

fields.ts
import SchemaBuilder from "@pothos/core";
import { writeFile } from "fs/promises";
import { lexicographicSortSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder<{
    Objects: {
      Giraffe: { name: string };
    };
  }>({});

  builder.objectType("Giraffe", {});
  builder.objectFields("Giraffe", (t) => ({
    name: t.exposeString("name"),
  }));

  builder.queryType();
  builder.queryFields((t) => ({
    giraffe: t.field({
      type: "Giraffe",
      resolve: (root, args) => ({ name: "Gina" }),
    }),
  }));

  const schema = builder.toSchema();
  const schemaString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));

objectFields() や queryFields() だけでは使えないのは少し残念。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ネストされたリスト

コード例
const Query = builder.queryType({
  fields: (t) => ({
    example: t.field({
      type: t.listRef(
        t.listRef('String'),
        // items are non-nullable by default, this can be overridden
        // by passing `nullable: true`
        { nullable: true },
      ),
      resolve: (parent, args) => {
        return [['a', 'b'], ['c', 'd'], null];
      },
    }),
  }),
});

listRef() メソッドを使うことで実現できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に使ってみる

fields.ts
import SchemaBuilder from "@pothos/core";
import { writeFile } from "fs/promises";
import { lexicographicSortSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      example: t.field({
        type: t.listRef(t.listRef("String"), { nullable: true }),
        resolve: () => [["a", "b"], ["c", "d"], null],
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("Access to http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

引数の定義方法

FieldBuilder の arg() メソッドを使う。

コード例
  builder.queryType({
    fields: (t) => ({
      string: t.string({
        args: {
          string: t.arg({
            type: "String",
            description: "String arg",
            required: true,
          }),
        },
        resolve: (parent, args) => args.string,
      }),
    }),
  });
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に試してみる

コマンド
toush args.ts
args.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      string: t.string({
        args: {
          string: t.arg({
            type: "String",
            description: "String arg",
            required: true,
          }),
        },
        resolve: (parent, args) => args.string,
      }),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

便利なメソッド

スカラー用に便利なメソッドが用意されていてタイピングを節約できる。

コード例
const Query = builder.queryType({
  fields: (t) => ({
    withArgs: t.stringList({
      args: {
        id: t.arg.id(),
        int: t.arg.int(),
        float: t.arg.float(),
        boolean: t.arg.boolean(),
        string: t.arg.string(),
        idList: t.arg.idList(),
        intList: t.arg.intList(),
        floatList: t.arg.floatList(),
        booleanList: t.arg.booleanList(),
        stringList: t.arg.stringList(),
      },
      resolve: (root, args) => Object.keys(args),
    }),
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に試してみる

args.ts
import SchemaBuilder from "@pothos/core";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder({});

  builder.queryType({
    fields: (t) => ({
      withArgs: t.stringList({
        args: {
          id: t.arg.id(),
          int: t.arg.int(),
          float: t.arg.float(),
          boolean: t.arg.boolean(),
          string: t.arg.string(),
        },
        resolve: (parent, args) => Object.keys(args),
      }),
    }),
  });

  const schema = builder.toSchema();
  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

スキーマが気になる

表示させてみたら下記の通りだった。

schema.graphql
type Query {
  withArgs(
    boolean: Boolean
    float: Float
    id: ID
    int: Int
    string: String
  ): [String!]!
}

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

スカラー以外の引数

FieldBuilder の arg() メソッドを使う。

コード例
const LengthUnit = builder.enumType('LengthUnit', {
  values: { Feet: {}, Meters: {} },
});

const Giraffe = builder.objectType('Giraffe', {
  fields: t => ({
    height: t.float({
      args: {
        unit: t.arg({
          type: LengthUnit,
        }),
      },
      resolve: (parent, args) =>
        args.unit === 'Feet' ? parent.heightInMeters * 3.281 : parent.heightInMeters,
    }),
  }),
}));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に使ってみる

args.ts
import SchemaBuilder from "@pothos/core";
import { writeFile } from "fs/promises";
import { lexicographicSortSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder({});

  const LengthUnit = builder.enumType("LengthUnit", {
    values: { Feet: {}, Meters: {} },
  });

  builder.queryType({
    fields: (t) => ({
      height: t.float({
        args: {
          unit: t.arg({
            type: LengthUnit,
          }),
        },
        resolve: (parent, args) => (args.unit === "Feet" ? 3.281 : 1),
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

必須の引数

引数はデフォルトではオプショナルになる。

つまり null か undefined でも OK になる。

必須にするには required オプションを true に設定する。

コード例
const Query = builder.queryType({
  fields: (t) => ({
    nullableArgs: t.stringList({
      args: {
        optional: t.arg.string(),
        required: t.arg.string({ required: true }),
        requiredList: t.arg.stringList({ required: true }),
        sparseList: t.stringList({
          required: {
            list: true,
            items: false,
          },
        }),
      },
      resolve: (parent, args) => Object.keys(args),
    }),
  }),
});

リストの場合はリスト自体はオプショナルだがリストの要素は必須になるようだ。

引数をデフォルトで必須にすることも SchemaBuilder のオプションで設定できる。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に試してみる

args.ts
import SchemaBuilder from "@pothos/core";
import { writeFile } from "fs/promises";
import { lexicographicSortSchema, printSchema } from "graphql";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";
import { join } from "path";

async function main() {
  const builder = new SchemaBuilder({});

  const LengthUnit = builder.enumType("LengthUnit", {
    values: { Feet: {}, Meters: {} },
  });

  builder.queryType({
    fields: (t) => ({
      nullableArgs: t.stringList({
        args: {
          optional: t.arg.string(),
          required: t.arg.string({ required: true }),
          requiredList: t.arg.stringList({ required: true }),
          sparseList: t.arg.stringList({
            required: {
              list: true,
              items: false,
            },
          }),
        },
        resolve: (parent, args) => Object.keys(args),
      }),
    }),
  });

  const schema = builder.toSchema();
  const schemaString = printSchema(lexicographicSortSchema(schema));

  await writeFile(join(process.cwd(), "schema.graphql"), schemaString);

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => {
    console.info("http://localhost:3000/graphql");
  });
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リスト引数は必ずしもリストで無くても良い

下記のクエリが普通に成功する、知らなかった。

クエリ
query MyQuery {
  nullableArgs(required: "2", requiredList: "3", sparseList: "4", optional: "1")
}
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

リスト

リストを使うには type を [] で囲む。

コード例
const Query = builder.queryType({
  fields: t => ({
    giraffeNameChecker: t.booleanList({
      {
        args: {
          names: t.arg.stringList({
            required: true,
          })
          moreNames: t.arg({
            type: ['String'],
            required: true
          })
        },
      },
      resolve: (parent, args) => {
        return [...args.names, ...args.moreNames].filter(name => ['Gina', 'James'].includes(name)),
      }
    })
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

ネストしたリスト

FieldBuilder の arg.listRef() メソッドを使う。

コード例
const Query = builder.queryType({
  fields: t => ({
    example: t.boolean({
      {
        args: {
          listOfListOfStrings: t.arg({
            type: t.arg.listRef(t.arg.listRef('String')),
          }),
          listOfListOfNullableStrings: t.arg({
            type: t.arg.listRef(
              // By default listRef creates a list of Non-null items
              // This can be overridden by passing in required: false
              t.arg.listRef('String', { required: false }),
              { required: true }),
          })
        },
      },
      resolve: (parent, args) => {
        return true
      }
    })
  }),
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

飽きてきたのでプラグインを試す

ガイドを一通り読んでおくことは重要だと認識しつつも単調なので飽きてきた。

ここらへんでプラグインを試してみて新鮮さを取り戻そう。

薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

プラグインの使い方

SchemaBuilder を作成する時にプラグインを指定する。

コード例
import MocksPlugin from '@pothos/plugin-mocks';
const builder = new SchemaBuilder({
  plugins: [MocksPlugin],
});

Query Type などの resolve() ではエラーを投げるようにしておき、toSchema() を呼び出す時に mocks オプションを指定する。

コード例
builder.queryType({
  fields: (t) => ({
    someField: t.string({
      resolve: () => {
        throw new Error('Not implemented');
      },
    }),
  }),
});

builder.toSchema({
  mocks: {
    Query: {
      someField: (parent, args, context, info) => 'Mock result!',
    },
  },
});
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

実際に使ってみる

コマンド
touch mocks.ts
mocks.ts
import SchemaBuilder from "@pothos/core";
import MocksPlugin from "@pothos/plugin-mocks";
import { createYoga } from "graphql-yoga";
import { createServer } from "http";

async function main() {
  const builder = new SchemaBuilder({
    plugins: [MocksPlugin],
  });

  builder.queryType({
    fields: (t) => ({
      someField: t.string({
        resolve: () => {
          throw new Error("Not implemented");
        },
      }),
    }),
  });

  const schema = builder.toSchema({
    mocks: {
      Query: {
        someField: () => "Mock result!",
      },
    },
  });

  const yoga = createYoga({ schema });
  const server = createServer(yoga);

  server.listen(3000, () => console.info("http://localhost:3000/graphql"));
}

main().catch((err) => console.error(err));
薄田達哉 / tatsuyasusukida薄田達哉 / tatsuyasusukida

モックプラグインの所感

正直あまりコード補完などが効かないので微妙。

resolve() にダミーデータを書くのとあまり変わらない気がする。

resolve() の方がコード補完が効くので使いやすそう。