sacchiのブログ

勉強したことを中心に書きます

Apollo Server with Firestore【Query編】

この記事は

Next.jsのAPI RoutesにGraphQLサーバーをたてるに書いたクソアプリリプレイス計画のつづきになります。今回はFirestoreからデータをとってくるQueryをつくります。具体的には

query {
  user{
    id
    screenName
    lists(id: "vK5tesguNdF5xNnC4dLs") {
      documentID
      name
      places(id: "ChIJ9e_QSdiMGGAR0dGxo5BGx98") {
        name
        url
      }
    }
  }
}

みたいなクエリを投げられるようにすることです! こまめに書くつもりが重めの内容になってしまったのでぱーっと書きます。

やっていく

Firestore

(余談)

Firestoreは、便利だなーとは思いますが、collection groupが何となく気持ち悪くてあんまり好きじゃないです。でもベストプラクティスを知れば好きになるような気がするので、プロダクションで使っている会社で働いてみたいという気持ちがあります。

あとはたまにしか使わないので、QuerySnapshotとかDocumentReferenceとかが毎回よくわからなくなってしまうので、いつもこの記事にお世話になっています。しかも、今回久々に触ったら、v9になってModular方式というものになっていました。仕方ないので移行しました。

Firestoreの設計

Firestoreはクソアプリのものをほとんどそのまま使います。document.data()でdocumentIDもとれるようにフィールドに追加する処理だけしておきました。 userがlistsをもっていて、listがplacesをもっているという構造になっています。

Apollo Server

shemaをつくる

const typeDefs = gql`
  type User {
    id: String!
    imageName: String!
    screenName: String!
    description: String
    lists(id: String): [List]
  }

  type List {
    documentID: String!
    name: String!
    description: String
    places(id: String): [Place]
  }

  type Place {
    documentID: String!
    name: String!
    url: String!
    types: [String!]!
  }

  type Query {
    users: [User]
    user: User
  }
`;

エンティティの子要素フィールド?(User.listsとか)に引数をもたせることで、ネストしたクエリでも引数を渡せるようになります!

resolverを定義する

type Args = {
  id: string
}

type Context = {
  userID: string
}

const getDocuments = async (collectionPath: string) => {
  const querySnapshot = await getDocs(collection(db, collectionPath));
  const documents = await Promise.all(querySnapshot.docs.map(async (document) => {
    if (!document.data()) {
      throw new UserInputError(`${collectionPath} is invalid path.`);
    }
    return document.data()
  }));
  return documents
}

const getDocument = async (collectionPath: string, documentID: string) => {
  const document = await getDoc(doc(db, collectionPath, documentID));
  if (!document.data()) {
    throw new UserInputError(`${collectionPath}/${documentID} is invalid path.`);
  }
  return document.data();
}

const getChildren = async (collectionPath: string, args: Args) => {
  let children;
  if (args.id) {
    const document = await getDocument(collectionPath, args.id);
    children = [document];
  } else {
    children = await getDocuments(collectionPath);
  }
  return children
}

const resolvers:IResolvers = {
  Query: {
    users: async () => {
      const users = await getDocuments("users");
      return  users as User[]
    },
    user: async (_parent: any, _args: any, context: Context) => {
      if (!context.userID) {
        throw new UserInputError("context userID is required.");
      }
      const userDoc = await getDocument("users", context.userID);
      return  userDoc as User
    }
  },
  User: {
    lists: async (_parent: User, args: Args, context: Context) => {
      const listsRef = `users/${context.userID}/lists`;
      const lists = await getChildren(listsRef, args);
      return  lists as List[]
    }
  },
  List: {
    places: async (parent: List, args: Args, context: Context) => {
      const placesRef = `users/${context.userID}/lists/${parent.documentID}/places`;
      const places = await getChildren(placesRef, args);
      return  places as Place[]
    } 
  }
};

UserやListなどの型はtypes/global.d.tsなどに定義してある前提。

まず、ネストしたクエリをどう実装するかというと、ルートにエンティティの子要素についてのresolverを書くだけ! 今回であればUser.listsについてのresolver、Lists.placesのresolverを書くだけです。

また、resolverはparent, args, context, info(今回はよく調べていない)を引数にとります。Parentは親resolverの結果オブジェクト、argsはクエリから渡された引数、contextには共有引数をとります。contextにはログインユーザーを持たせることが多い気がします。 今回は簡単のために、userIDをというcontextをheaderから渡しています。context.userIDを使うことで、collection groupを使わずにplacesまで取得することができていますが、自分以外の人のリストを表示するときのことを考えていませんでした(これ書きながら気づいた)。この記事によると、リレーションがはれるらしい...! これでできそう。やることが増えた....

上記を設定してapollo serverのインスタンスを作成

export const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    const userID:string =req.headers.userid
    return { userID }
  }
});

これをNext.jsのAPI routeで呼び出して使いました。

残り作業

  • リレーション
  • Firestoreとのすみ分けをする
  • code generatorで型を自動生成する

上記ができたらMutationを実装するかフロントから作るか迷う

感想

  • 日本語の文献もっと多いと思っていた
  • とても便利!!もっと使い慣れていきたい
  • Firestore使う意味はない。ORMがいらないくらい...

おわりに

中間発表がおわったと思えばもう本選考の時期になってしまいました。就活とバランス取りながら精進するぞ〜

超TypeScript入門 完全パック(2021)をやっていました

この記事は

最近大学がはじまったり、研究の中間報告の準備で忙しく、あまりブログに書けるようなことをしていませんでした。 ですが、全くお勉強していなかったわけではないので、言い訳っぽい投稿をします。

やったこと

Udemyの超TypeScript入門 完全パック(2021)という講座をこつこつやっていました。 なんと私が所属している大学は、申請すればUdemyが無料で受け放題という特典があります。 夏休みにNext.jsを触る機会があり、雰囲気でTypeScriptを書いていましたが、フロントエンドをやっていきたいならきちんと学ぶ必要があると思っていたので受けてみています。結構長めの講座で(13時間以上)まだ半分しか終わっていないので、引き続きやっていきます。 ちなみに講義動画や衛生授業的なものがめちゃくちゃ苦手でしたが(ひとが耳元でしゃべっているみたいで嫌だった)、コロナ禍で慣れました。

全然書くことなかった。。

Next.jsのAPI RoutesにGraphQLサーバーをたてる

この記事は

前提として私は夏休みに入るまでフロントエンド超初心者でした。そして夏休みを経て初心者に成長しました。

超初心者だったときにVue.js+Firebase(FirestoreとかAuthenticationとか)でサーバーレスなWebアプリをせっせとつくっていたのですが、最近色々学んだ結果、Next.jsとGraphQLで綺麗にパフォーマンスをあげられそうな気がしてきたので、リプレイス計画をたてています。

具体的には以前つくったクソアプリ(当時は一生懸命つくったつもりでした...)には

  • ただのJSで書かれている
  • SSRで高速化できそうなところが多い
  • Firestoreのコレクションがネストしまっくっていてデータの取得のために何度もリクエストをしなくてはいけなかったり、無駄なデータをとってきてしまうことが多い(これがいちばんやだ)

などのクソ要素があります。 この記事はリプレイス計画の一環としてためした「Next.jsのAPI RoutesにGraphQLサーバーをたててみる」の備忘録です。

やっていく

apollo-server-microを使います。 Apollo Serverを名乗るものがたくさんいて困惑しましたが、ひとまず公式に従いました。 やっていくといっても実は公式がサンプルコードを用意してくれています。 なので、やることは以下のステップだけ。

  1. Next.jsのプロジェクトを作成
  2. src/pages/api/graphql.tsにサンプルコードを真似する

お試しなのでダミーデータをベタ書きで持つようにしています。

import { ApolloServer, gql } from "apollo-server-micro";

const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;

const books = [
  {
    title: "Harry Potter and the Chamber of Secrets",
    author: "J.K. Rowling",
  },
  {
    title: "Jurassic Park",
    author: "Michael Crichton",
  },
];

const resolvers = {
  Query: {
    books: () => books,
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

export const config = {
  api: {
    bodyParser: false,
  },
}

export default server.createHandler({ path: "/api/graphql" })

bodyParser: false だけ謎だったのですが、Node.js入門によると、HTTP リクエストのボディ部を読み込み、 req.body にパース (文字列の解析) した結果を設定します。 らしいので、パースせずにそのまま描画したい、、みたいな感じなのでしょうか?

この設定をなくすとPlaygroundで結果が表示されなくなったので、とても大事ということだけわかった()

  1. npx next dev すると

http://localhost:3000/api/graphql にPlaygroundがたちあがる🎉

f:id:hcp-miyuki:20210927211539p:plain

お困りごと

バージョンを指定せずにapollo-server-microをインストールすると、最近版の3.3.0が入ります。 ただ、3系だと2つのエラーに悩まされたので、最終的に一番ダウンロード数が多かった2.25.2を入れ直しました。

エラー1 UnhandledPromiseRejectionWarning: Error: You must `await server.start()` before calling `server.applyMiddleware()` at ApolloServer

これはエラーというよりは3系からこういう仕様になったらしいです。 なので落ち着いて言われたとおりに対応しました。また、Apollo Server v3のドキュメントにも書かれていました。

エラー2 Apollo Server network error: unable to reach server

エラー1を倒したらなんか出てきた...なんとなく、CORS周りな気がします。ブラウザ変えると直るよ!みたいな曖昧なことがstackoverflowに回答として書かれていたのを見て、「疲れたし、公式も2系だからダウングレードするか...」と妥協してしまいました。

おわりに

たいしたことはしていないけど、GraphQLやNext.jsなど、2ヶ月前には全然わかっていなかったことが少しわかっていることに感動しています。 つぎはFirestoreの読み書きの記事を書きたいな〜

ブログ書くぞ

はじめまして

こんにちは。きっとつづかないと思うけど、備忘録的な技術ブログを始めてみようと思います。 これまでインターン先のテックブログや自分のQiitaにたま〜に記事を投稿していました。 ですが、なんとなく「ちゃんとした」記事をかかなくてはと思って気軽に投稿ができなかったので、このブログには雑多にメモ書きみたいな気持ちで書いていこうと思っています。2,30分で書けることしか書かない予定。 いい感じに蓄積させたら綺麗にしたやつを研究室のアドベントカレンダーとかで書けたらいいな! おてやわらかに。