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がいらないくらい...

おわりに

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