Building a GraphQL Resolver For Specific Fields
With the root-level `listing` field prepared in our GraphQL API, we'll construct the resolver function for this field to attempt to query for the appropriate listing from the "listings" collection in our database.
Building the Listing Resolvers#
With the root-level listing
field prepared in our GraphQL API, we'll construct the resolver function for this field to attempt to query for the appropriate listing from the listings collection in our database. Similar to how the user
query field queried for a user from our database with a certain ID, the listing
query field will query for a certain listing based on the ID provided.
As a result, we'll update the listing
field in our GraphQL type definitions and state it expects a defined argument of id
of type GraphQL ID. In addition, the listing
field when resolved should return the appropriate Listing
GraphQL object.
type Query {
authUrl: String!
user(id: ID!): User!
listing(id: ID!): Listing!
}
This Listing
GraphQL object has been created in the last module and has fields to describe the certain listing - such as it's title
, description
, host
, etc.
listing()
#
We'll now modify the resolver function for the listing
field to state that it is to accept an id
input from our client and return a Listing
object when resolved. First, we'll construct the interface of the expected arguments for this field in a types.ts
file kept within the src/graphql/resolvers/Listing
folder. We'll create a ListingArgs
interface that is to have an id
field of type string
.
export interface ListingArgs {
id: string;
}
In our listingResolvers
map within the src/graphql/resolvers/Listing/index.ts
file, we'll import a few things we'll need for our listing()
resolver function. We'll first import the Database
and Listing
interfaces that have been defined in the src/lib/types.ts
file. We'll also import the recently created ListingArgs
interface from the types.ts
file adjacent to this file.
// ...
import { Database, Listing } from "../../../lib/types";
import { ListingArgs } from "./types";
We'll now update the listing()
resolver function to query for a certain listing from the database.
We'll define the
listing()
resolver function as anasync
function.In the
listing()
resolver function, we'll access theid
argument passed in and thedb
object available in the context of our resolver.When the
listing()
resolver function is to be complete, it should return aPromise
that when resolved is an object of typeListing
.
// ...
export const listingResolvers: IResolvers = {
Query: {
listing: async (
_root: undefined,
{ id }: ListingArgs,
{ db, req }: { db: Database; req: Request }
): Promise<Listing> => {}
},
Listing: {
// ...
}
};
The listing()
resolver function will be fairly straightforward to implement. We'll use Mongo's findOne()
method to find a listing document from the listings
collection where the _id
field is the ObjectId
representation of the id
argument passed in. If this listing document doesn't exist, we'll throw a new Error
. If the listing document does exist, we'll return the listing
document that's been found. We'll have this implementation be kept in a try
block while in a catch
statement - we'll catch an error if ever to arise and have it thrown within a new error message.
// ...
import { ObjectId } from "mongodb";
// ...
export const listingResolvers: IResolvers = {
Query: {
listing: async (
_root: undefined,
{ id }: ListingArgs,
{ db, req }: { db: Database; req: Request }
): Promise<Listing> => {
try {
const listing = await db.listings.findOne({ _id: new ObjectId(id) });
if (!listing) {
throw new Error("listing can't be found");
}
return listing;
} catch (error) {
throw new Error(`Failed to query listing: ${error}`);
}
}
},
Listing: {
// ...
}
};
listing()
authorize#
The listing
object contains a series of fields where we'll need to define explicit resolver functions for a certain number of them. In the last lesson, we mentioned that the bookings
field within a listing object should be authorized and shown only to the user who owns the listing. When we define the resolver for the listing booking
field, we'll need to check if the listing query is authorized.
We'll follow a similar format to what we did for the User
module and simply get the viewer details with the authorize()
function available in the src/lib/utils/
folder. Within the listing()
resolver function, we'll have an if
statement to check if the viewer id matches that of the listing host
field which will determine the viewer is querying for their own listing. If this is true
, we'll set an authorized
field in the listing
object to be true
.
With that said, the first thing we'll do is add the authorized
field to the Listing
TypeScript interface in the src/lib/types.ts
file and state that it is to be of type boolean
when defined.
export interface Listing {
_id: ObjectId;
title: string;
description: string;
image: string;
host: string;
type: ListingType;
address: string;
country: string;
admin: string;
city: string;
bookings: ObjectId[];
bookingsIndex: BookingsIndexYear;
price: number;
numOfGuests: number;
authorized?: boolean;
}
In our listingResolvers
map file, we'll import the authorize()
function from the src/lib/utils/
folder. We'll also import the Request
interface from express
.
In the listing()
resolver function, we'll access the req
object available as part of context in all our resolvers. Within the function, we'll have the authorize()
function be run and pass in the db
and req
objects it expects, and we'll do this after the listing document has already been found. With the viewer
obtained from the authorize()
function, we'll then state that if viewer._id
matches the listing.host
field, we'll set the authorized
value of the listing
object to true
.
With these changes, the listing()
resolver function will be finalized as follows:
export const listingResolvers: IResolvers = {
Query: {
listing: async (
_root: undefined,
{ id }: ListingArgs,
{ db, req }: { db: Database; req: Request }
): Promise<Listing> => {
try {
const listing = await db.listings.findOne({ _id: new ObjectId(id) });
if (!listing) {
throw new Error("listing can't be found");
}
const viewer = await authorize(db, req);
if (viewer && viewer._id === listing.host) {
listing.authorized = true;
}
return listing;
} catch (error) {
throw new Error(`Failed to query listing: ${error}`);
}
}
},
// ...
};
Note: The
host
field within thelisting
document object is anid
of the host of the listing (i.e. the user who owns the listing).
We'll now create explicit resolver functions for the fields in the Listing
object that we want to be resolved differently than the value being kept in the database. We already have the id()
resolver set-up to resolve the _id
of the listing document to an id
string representation when queried from the client.
host()
#
The host
field in a listing
document in the database is a string ID of the user that owns the listing. When the client queries for this field, we'll want the client to receive object information of the host
. Since we want to resolve the host field to a User
object value, we'll import the User
interface from the src/lib/types.ts
file.
We'll define a resolver function for the host()
field in the Listing
object within our listingResolvers
map, and we'll use MongoDB's findOne()
method to find the host from the listing.host
id value.
// ...
import { Database, Listing, User } from "../../../lib/types";
// ...
export const listingResolvers: IResolvers = {
Query: {
// ...
},
Listing: {
// ...,
host: async (
listing: Listing,
_args: {},
{ db }: { db: Database }
): Promise<User> => {
const host = await db.users.findOne({ _id: listing.host });
if (!host) {
throw new Error("host can't be found");
}
return host;
}
}
};
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two