Schema changes
Open schema.graphql and add the following types
graphqltype UserStats {tweetCount: Int!followingCount: Int!followerCount: Int!}type Tweet {id: String!body: String!createdAt: String!updatedAt: String!author: Userstats: TweetStats}type TweetStats {favoriteCount: Int!retweetCount: Int!commentCount: Int!}
and make the following changes
difftype Query {currentUser: User!suggestions: [Suggestion!]!+ tweets: [Tweet!]!}
difftype User {id: String!name: String!handle: String!coverUrl: String!avatarUrl: String!createdAt: String!updatedAt: String!+ stats: UserStats}
Run yarn codegen in both the client and server folders to generate updated TypeScript types.
Context Cache
We also need to set up some caches in our context object, to avoid the N+1 DB query problem.
Open server/src/apollo-server.ts and make the following change
diffcontext: () =>({db,+ dbTweetCache: {},+ dbTweetToFavoriteCountMap: {},+ dbUserCache: {},} as TwitterResolverContext),
and in your server/src/resolvers.ts add these new fields to the TwitterResolverContext interface
tsimport Db, { DbTweet, DbUser } from "./db"export interface TwitterResolverContext {db: DbdbTweetCache: Record<string, DbTweet>dbUserCache: Record<string, DbUser>dbTweetToFavoriteCountMap: Record<string, number>}
Resolvers and nested types
Create a file for simple “DB representation” -> “GraphQL representation” transforms server/src/transforms.ts
tsimport { Tweet } from "./resolvers-types.generated"import { DbTweet } from "./db"export const tweetTransform = (t: DbTweet): Omit<Tweet, "author"> => {return {id: t.id,body: t.message,createdAt: t.createdAt,updatedAt: t.updatedAt,}}
In your Query resolver, server/src/resolvers/Query.ts add an import to your “transforms” file
tsimport { tweetTransform } from "../transforms"
and a tweets function that uses it. Note that we’re using our cache objects
tstweets: (_,__,{ db, dbTweetToFavoriteCountMap, dbUserCache, dbTweetCache }) => {db.getAllUsers().forEach((user) => {dbUserCache[user.id] = user;});db.getAllFavorites().forEach((favorite) => {const count = dbTweetToFavoriteCountMap[favorite.tweetId] || 0;dbTweetToFavoriteCountMap[favorite.tweetId] = count + 1;});return db.getAllTweets().map((t) => {dbTweetCache[t.id] = t;return tweetTransform(t);});},
Now use Apollo Explorer to try and run this query
graphqlquery GetTweets {currentUser {stats {followerCount}}tweets {bodyauthor {handle}}}
Note that all the tweet.author fields have a null value, as does the currentUser.stats field.
We can address this with inner resolvers
Inner resolvers
Create a new User resolver module server/src/resolvers/User.ts
tsimport { TwitterResolverContext } from "../resolvers"import { UserResolvers } from "../resolvers-types.generated"const userTwitterResolver: UserResolvers<TwitterResolverContext> ={stats(user, _, { db }) {return {followingCount: 123,followerCount: 456789,tweetCount: db.getUserTweets(user.id).length,}},}export default userTwitterResolver
and a Tweet resolver module server/src/resolvers/Tweet.ts
tsimport { TwitterResolverContext } from "../resolvers"import { TweetResolvers } from "../resolvers-types.generated"const tweetTwitterResolver: TweetResolvers<TwitterResolverContext> ={author(tweet, _, { dbUserCache, dbTweetCache }) {const dbTweet = dbTweetCache[tweet.id]if (!dbTweet)throw new Error("Attempted to find Tweet.author, but the tweet was not found in dbTweetCache")const dbUser = dbUserCache[dbTweet.userId]if (!dbUser)throw new Error("Attempted to find Tweet.author, but the tweet's author (a User) was not found in dbUserCache")return dbUser},stats(tweet, _, { dbTweetToFavoriteCountMap }) {return {commentCount: 99,retweetCount: 1,favoriteCount:dbTweetToFavoriteCountMap[tweet.id] || 0,}},}export default tweetTwitterResolver
Wire these resolvers up in your server/src/resolvers.ts file
diff+import tweetTwitterResolver from './resolvers/Tweet';+import userTwitterResolver from './resolvers/User';export function createResolvers(): Resolvers<TwitterResolverContext> {const resolvers = {Query: queryTwitterResolver,+ Tweet: tweetTwitterResolver,+ User: userTwitterResolver,};return resolvers;}
Consume it in the UI
We’re going to make two frontend changes.
Current User Stats
First, in client/src/App.tsx, add a new “stats” part to the GET_CURRENT_USER GraphQL operation
diffavatarUrlcreatedAt+ stats {+ tweetCount+ followingCount+ followerCount+ }}suggestions {
This should allow you to pass the currentUser value through to the Header component
diff- <Header currentUser={CURRENT_USER} />+ <Header currentUser={currentUser} />
Now you should see non-zero “follower count” and “following count” numbers for the currently logged in user
Tweets
Open client/src/Timeline.tsx and add the following imports
tsimport { gql } from "@apollo/client"import { useGetTimelineTweetsQuery } from "./generated/graphql"
add a new query in top-level module scope
tsexport const GET_TIMELINE_TWEETS = gql`query GetTimelineTweets {tweets {idbodystats {favoriteCountretweetCountcommentCount}createdAtauthor {namehandleavatarUrl}}}`
Because we created a new operation, we’ll need to run yarn codegen within the client sub-project again
to get the new type information.
Within this component, before it returns, add this
tsconst { loading, error, data } = useGetTimelineTweetsQuery()if (loading) return <p>Loading...</p>if (error) return <p>Error: {error}</p>if (!data) return <p>No data!</p>const { tweets } = dataif (!tweets) return <p>No tweets!</p>
And now instead of mapping over the fixture data array, use the data that the GraphQL API returns
diff- {TWEETS.map((t) => {+ {tweets.map((t) => {
You can delete the TWEETS array if you like, we don’t need it anymore.
You should see some new tweets in the feed now!