Open62

GraphQLサーバをつくる(heroku + Express + Apollo Server + Prisma)

high-ghigh-g

https://devcenter.heroku.com/articles/heroku-cli

Heroku CLIをインストール

npm install -g heroku

以下でlogin実行

heroku login

アプリ作成

heroku create

なんか怒られた

Creating app... !
 ▸    To create an app, verify your account by adding payment information.
 ▸    Verify now at https://heroku.com/verify Learn more at
 ▸    https://devcenter.heroku.com/articles/account-verification

アプリを作成しています... !
  ▸ アプリを作成するには、支払い情報を追加してアカウントを確認します。
  ▸ https://heroku.com/verify で今すぐ確認 詳細はこちら
  ▸ https://devcenter.heroku.com/articles/account-verification

カード情報を登録後、再度 heroku createでいけったぽい

high-ghigh-g

上の記事を参考に、nodemon, ts, express, apollo-serverなどを導入し色々整えて、
ローカルでseverを立ち上げられる所まで来た

high-ghigh-g

rimrafインストール

pnpm i -D rimraf

package.jsonのscriptsに以下を追加

    "build": "rimraf ./build && tsc",
    "start": "npm run build && node ./build/index.js",

touch Procfileを作成(heroku側で実行されるファイル)
以下を記述

web: npm start
high-ghigh-g

コードのデプロイ
アプリを Heroku にデプロイするには。次のように git push​ コマンドを使用して、ローカルリポジトリのメインブランチ​から heroku​ リモートにコードをプッシュします。次に例を示します。

git push heroku main

(ログインができていない場合は、heroku login)

途中でエラーが発生してる

remote: Resolving node version 18.x...
remote: Downloading and installing node 18.16.0...
remote: Using default npm version: 9.5.1

1.localのnode, npmバージョンとことなっている

2.以下のts系でエラーが出ている

remote:        > rimraf ./build && tsc
remote:        
remote:        error TS6059: File '/tmp/build_8524c435/vite-project/src/App.tsx' is not under 'rootDir' '/tmp/build_8524c435/src'. 'rootDir' is expected to contain all source files.
remote:          The file is in the program because:
remote:            Matched by default include pattern '**/*'
remote:        error TS6059: File '/tmp/build_8524c435/vite-project/src/main.tsx' is not under 'rootDir' '/tmp/build_8524c435/src'. 'rootDir' is expected to contain all source files.
remote:          The file is in the program because:
remote:            Matched by default include pattern '**/*'
remote:        error TS6059: File '/tmp/build_8524c435/vite-project/vite.config.ts' is not under 'rootDir' '/tmp/build_8524c435/src'. 'rootDir' is expected to contain all source files.
remote:          The file is in the program because:
remote:            Matched by default include pattern '**/*'

https://relativelayout.hatenablog.com/entry/2018/11/08/092143

high-ghigh-g

vite-projectを移動したらデプロイが通った!

vite側の設定に問題があるっぽい

high-ghigh-g

herokuでデプロイ整理

  1. package.jsonのengineに記載があるバージョンのnode, npmでnpm iが実行される(engineの記載がない場合は、ltsバージョンでインストールが実行される)
  2. node_modulesが作られ次第、Procfileに記載しているコードが動作する
  3. 現状 npm run build && node ./build/index.js が実行される為、./build/index.jsがサーバとして動作する様にlocalで組めてればok

ここまでが整えられた状態でherokuのmainリポジトリにプッシュ
git push heroku main

high-ghigh-g

heroku logsでherokuアプリのログが確認できる

heroku logs --tailで監視

↓の様なエラーが表示される

2023-04-27T02:54:14.795435+00:00 heroku[router]: at=error code=H10 desc="App crashed" method=GET path="/favicon.ico" host=afternoon-shore-82089.herokuapp.com request_id=ab4d9fad-c8f1-4b22-99f1-93fb41b37ad5 fwd="218.220.224.82" dyno= connect= service= status=503 bytes= protocol=https
high-ghigh-g

やりたいこと整理

  1. GraphQLサーバをたてる
  2. CORS許可する(様子見て)
  3. ローカルでvite+React環境作成
  4. ローカルからGraphQLサーバへアクセスする
high-ghigh-g

一旦、GraphQLサーバと疎通できるかどうかをlocalでprojectをたてて確認してみる

high-ghigh-g

viteでサクッとプロジェクト作って以下を記述

App.tsx

import { ApolloClient, InMemoryCache, gql } from '@apollo/client'

function App() {
  const client = new ApolloClient({
    uri: 'https://○○/api',
    cache: new InMemoryCache(),
  })

  const clickHandler = () => {
    client
      .query({
        query: gql`
          query Query {
            hello
          }
        `,
      })
      .then((result) => console.log(result))
  }

  return (
    <div>
      <p>ボタンclickするとGraphQLサーバと通信するよ</p>
      <button onClick={clickHandler}>click</button>
    </div>
  )
}

export default App

high-ghigh-g

herokuでDBを扱う場合パターンが2つ

  • herokuのDataでDB設定を行う
  • Supabase等の外部DBと接続する

どちらもPrismaを利用していれば、scheme.prismaのdatasourceを変更するだけになりそう

とりあえずherokuのDataでDB設定を行う」のパターンを試す

high-ghigh-g

DBとバックエンド間の連携にprisma/clientを使う

high-ghigh-g
  • Query, Mutationのschemeは既に記述している
  • Queryに対するresolversを一旦整理する
    • resolversは一旦ローカルのtsファイルを利用する
    • 上記が出来たらPrismaと接続する
high-ghigh-g

アーキテクチャばっかり組んでいて肝心のクエリの書き方を忘れている・・・

クエリ実行時の戻り値がプリミティブな場合、プロパティを書くだけで良い

query Query {
  hello
}

クエリ実行時の戻り値がオブジェクトな場合、Queryに記述したプロパティ + 取得対象のオブジェクトの中で必要なプロパティを記述

query Query {
  books {
    id
    title
  }
}
high-ghigh-g

クエリ実行出来た
typeDefsに記述したスキーマの定義が改めて理解できた
resolversの整理もできたし、それぞれresolversに記述するタイプも理解できた

high-ghigh-g

リゾルバに詳細なタイプの記述ができていない場合、
定義しているクエリ内でオブジェクトを操作する形の記述を行うことになる。

GraphQLを利用している場合、オブジェクトを組み換えて、入れ子構造を作成することは望ましくない為、
入れ子構造が発生した場合は、リゾルバタイプとして切り出すのが良い

high-ghigh-g

input キーワードの利用

Queryで、filterを利用してinput typeのスキーマを接続することが出来る
books(filter: BooksInput): [Book!]!

high-ghigh-g

リゾルバ側の実行文を以下で記述

  books: (_: any, { filter }: argFilterType) => {
    let filteredBooks = books

    if (filter?.isRead) {
      filteredBooks = filteredBooks.filter((book) => {
        return book.isRead
      })
    }

    return filteredBooks
  },

high-ghigh-g

メインのコードは以下だけみたい。
ApolloServerのcontextプロパティにPrismaClientのインスタンスを渡すだけ

import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient()
  const apolloServer = new ApolloServer({
    typeDefs,
    resolvers: {
      Query,
      Mutation,
      Category,
      Book,
    },
    // context: {
    //   books,
    //   categories,
    // },
    context: {
      prisma,
    },
  })
high-ghigh-g

型定義

export type Context = {
  prisma: PrismaClient<Prisma.PrismaClientOptions, never, Prisma.RejectOnNotFound | Prisma.RejectPerOperation>
}

high-ghigh-g

scheme.prismaで定義した型の通りにtypeDefsの型を合わせていく

resolverをprismaに置き換える

high-ghigh-g

そういえばMutationのdelete, updateを作り込んでない

high-ghigh-g

Queryに記述する関数の第1引数がparent, 第2引数がQueryメソッドのfilterで渡される値, 第3引数がcontext

  books: (_: any, { isRead }: { isRead: boolean }, { prisma }: Context) => {
    return prisma.book.findMany({
      where: {
        isRead,
      },
    })
  },
high-ghigh-g

GraphQLのクエリに引数をもたせる場合の書き方

query Book {
  books(isRead:true) {
    id
    title
    isRead
  }
}
high-ghigh-g

GraphQLメソッドメモ

複数一致
findMany

単一一致
findUnique

high-ghigh-g

外部から引数を受け付ける場合

query Book($bookId: Int!) {
  book(id: $bookId) {
    id
    title
    isRead
  }
}
{
  "bookId": 1
}
high-ghigh-g
return books.filter((book) => book.categoryId === id)

ここの解決から

high-ghigh-g
// Category側のidとBook側のcategoryIdの紐づけ
export const Category = {
  books: ({ id }: { id: number }, _: any, { prisma }: Context) => {
    return prisma.book.findMany({
      where: {
        categoryId: id,
      },
    })
  },
}
high-ghigh-g

delete

 deleteBook: async (_: any, { id }: { id: number }, { prisma }: Context): Promise<BookPayload> => {
    const book = await prisma.book.findUnique({
      where: {
        id,
      },
    })

    if (!book) {
      return {
        errors: [{ message: '本が見つかりませんでした' }],
        book: null,
      }
    }

    await prisma.book.delete({
      where: {
        id,
      },
    })

    return {
      errors: [],
      book,
    }
  },
}
high-ghigh-g

update resolve


  updateBook: async (
    _: any,
    { id, input }: { id: number; input: MutationBook['input'] },
    { prisma }: Context
  ): Promise<BookPayload> => {
    const book = await prisma.book.findUnique({
      where: {
        id,
      },
    })

    if (!book) {
      return {
        errors: [{ message: '本が見つかりませんでした' }],
        book: null,
      }
    }

    const updatedBook = await prisma.book.update({
      where: {
        id,
      },
      data: {
        ...input,
      },
    })

    return {
      errors: [],
      book: updatedBook,
    }
  },
high-ghigh-g

useQueryにGraphQLのクエリを投げたら簡単に取れた!

import { gql, useQuery } from '@apollo/client'

const Component = () => {
  const booksData = gql`
    query allBooks {
      books(isRead: true) {
        id
        title
        author
      }
    }
  `

  const { data } = useQuery(booksData)

}