Open62

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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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でいけったぽい

フロントエンドえんじにゃーフロントエンドえんじにゃー

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
フロントエンドえんじにゃーフロントエンドえんじにゃー

コードのデプロイ
アプリを 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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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
フロントエンドえんじにゃーフロントエンドえんじにゃー

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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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

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

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

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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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

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

query Query {
  hello
}

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

query Query {
  books {
    id
    title
  }
}
フロントエンドえんじにゃーフロントエンドえんじにゃー

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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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

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

フロントエンドえんじにゃーフロントエンドえんじにゃー

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

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

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

    return filteredBooks
  },

フロントエンドえんじにゃーフロントエンドえんじにゃー

メインのコードは以下だけみたい。
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,
    },
  })
フロントエンドえんじにゃーフロントエンドえんじにゃー

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

  books: (_: any, { isRead }: { isRead: boolean }, { prisma }: Context) => {
    return prisma.book.findMany({
      where: {
        isRead,
      },
    })
  },
フロントエンドえんじにゃーフロントエンドえんじにゃー

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,
    }
  },
}
フロントエンドえんじにゃーフロントエンドえんじにゃー

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,
    }
  },
フロントエンドえんじにゃーフロントエンドえんじにゃー

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)

}