graphql conditional subquery based on parent field - graphql

What is the approach when your subquery needs some field from parent to resolve?
In my case owner_id field from parent is needed for owner resolver, but what should i do if user will not request it?
Should i somehow force fetch owner_id field or skip fetching owner when owner_id is not in request?
Problematic query:
project(projectId: $projectId) {
name
owner_id <--- what if user didn't fetch this
owner {
nickname
}
}
Graphql type:
type Project {
id: ID!
name: String!
owner_id: String!
owner: User!
}
And resolvers:
export const project = async (_, args, ___, info) => {
return await getProjectById(args.projectId, getAstFields(info))
}
export default {
async owner(parent, __, ___, info) {
return await getUserById(parent.owner_id, getAstFields(info))
},
}
Helper functions that doesn't matter:
export const getUserById = async (userId: string, fields: string[]) => {
const [owner] = await query<any>(`SELECT ${fields.join()} FROM users WHERE id=$1;`, [userId])
return owner
}
export const getAstFields = (info: GraphQLResolveInfo): string[] => {
const { fields } = simplify(parse(info) as any, info.returnType) as { fields: { [key: string]: { name: string } } }
return Object.entries(fields).map(([, v]) => v.name)
}

You can always read owner_id in project type resolver.
In this case ... modify getAstFields function to accept additional parameter, f.e. an array of always required properties. This way you can attach owner_id to fields requested in query (defined in info).
I was thinking that resolver will drop additional props internally.
It will be removed later, from 'final' response [object/tree].

Related

Nested GraphQL query with array argument

I am using apollo-datasource-rest with apollo-server-lambda and trying to figure out how to map a query to a specific resolver. I have the following schema, in which the plan query is supposed to return a list of users (that should be driven by the users query rather than the user query).
type Query {
user(user_id: String, username: String): User
users(user_ids: [String!]): [User!]
plan(plan_id: String): Plan
}
type User {
id: String
username: String
first: String
last: String
image: String
}
type Plan {
title: String
image: String
attending: [User]
}
The plan query resolver datasource is as follows:
planReducer(data) {
return {
image: data.public.info.image,
title: data.public.info.title,
attending: Object.keys(data.public.attending)
}
}
data.public.attending in the planReducer returns an array of user_ids that I would like to then be able to feed into my users query as opposed to my user query.
These are my current resolvers:
user: (_, { username }, { dataSources }) =>
dataSources.userData.getUserByUsername({ username: username }),
users: async (_, { user_ids }, { dataSources }) => {
const usersArray = await dataSources.userData.getUsersByIds({ userIds: user_ids })
return usersArray
},
plan: async (_, { plan_id }, { dataSources }) => {
return dataSources.planData.getPlanById({ planId: plan_id })
}
Your resolver map should look like below:
const resolvers = {
Query: {
plan: async (_parent, { plan_id: planId }, { dataSources }) => (
dataSources.planData.getPlanById({ planId })
)
},
Plan: {
users: async ({ user_ids: userIds }, _variables, { dataSources }) => (
dataSources.userData.getUsersByIds({ userIds })
)
}
}
Every key within Query should be a resolver that corresponds to a query defined within the root Query of your schema. Keys that are direct children of the root, in this case Plan, will be used to resolve their corresponding types when returned from the plan resolver within Query.
If resolvers are not defined, GraphQL will fall back to a default resolver which in this case looks like:
const resolvers = {
Plan: {
title: (parent) => parent.title,
image: (parent) => parent.image,
}
}
By specifying a custom resolver, you are able to compute fields to return to your clients based on the return value of parent resolvers.

GraphQL resolvers only resolving the first type

so im working on a project of mine and i seem to hit a dead end with my abilities.
I am working on a GraphQL backend that is supposed to fetch some data from a MySQL database. I already got the resolvers working so i can fetch all users etc. but i am not able to fetch nested types. For example:
query {
ways {
id
distance
duration
purpose
User {
id
dob
}
}
}
This only returns all the ways from my database but the User returns null
schema.ts
export const typeDefs= gql`
schema {
query: Query
}
type Query {
ways: [Way]
users: [User]
getUser(id: String!): User
getWay(id: String!):Way
}
type Way{
id:String
distance:Float
duration:Float
stages:[Stage]
purpose:String
User:User
}
type User{
id:String
firstname:String
lastname:String
sex:String
dob:String
annualTicket:Boolean
Ways:[Way]
}
resolver.ts
export const resolvers = {
Query: {
ways: async(parent, args, context, info) => {
console.log("ways")
const answer=await getAllWays();
return answer;
},
users: async(parent, args, context, info) => {
console.log("users")
const answer=await getAllUser();
return answer;
},
getUser: async(parent, args, context, info) =>{
console.log("getUser")
const answer=await getUserPerID(args.id);
return answer;
},
getWay:async(parent, args, context, info) =>{
console.log("getWay")
const answer=await getWayPerID(args.id);
return answer;
}
},
User:async(parent, args, context, info) =>{
console.log("User: id="+args.id)
ways: User => getWaysPerUserID(args.id)
},
Way:async(parent, args, context, info) => {
console.log("Way: id="+parent.userID)
user: Ways => getUserPerID(parent.userID)
}
I would expect the outcome to include the user and its data too, using the above mentioned query.
Any advice is much appreciated.
Only fields, not types, can have resolvers. Each key in your resolvers object should map to another object, not a function. So instead of
Way:async(parent, args, context, info) => {
console.log("Way: id="+parent.userID)
user: Ways => getUserPerID(parent.userID)
}
You need to write:
Way: {
// We capitalize the field name User because that's what it is in your schema
User: (parent) => getUserPerID(parent.userID)
}

How to add multiple resolvers in a type (Apollo-server)

I have used express-graphql and there i used to do something like this.
const SubCategoryType = new ObjectType({
name: 'SubCategory',
fields: () => ({
id: { type: IDType },
name: { type: StringType },
category: {
type: CategoryType,
resolve: parentValue => getCategoryBySubCategory(parentValue.id)
},
products: {
type: List(ProductType),
resolve: parentValue => getProductsBySubCategory(parentValue.id)
}
})
});
Here I have multiple resolvers, id and name are fetched directly from the result. and the category and products have there own database operation. and so on.
Now I am working on apollo-server and I can't find a way to replicate this.
for example I have a type
type Test {
something: String
yo: String
comment: Comment
}
type Comment {
text: String
createdAt: String
author: User
}
and in my resolver I want to split it up, for example something like this
text: {
something: 'value',
yo: 'value',
comment: getComments();
}
NOTE: this is just a representation of what I need.
You can add type-specific resolvers to handle specific fields. Let's say you have the following schema (based on your example):
type Query {
getTest: Test
}
type Test {
id: Int!
something: String
yo: String
comment: Comment
}
type Comment {
id: Int!
text: String
createdAt: String
author: User
}
type User {
id: Int!
name: String
email: String
}
Let's also assume you have the following DB methods:
getTest() returns an object with fields something, yo and
commentId
getComment(id) returns an object with fields id, text, createdAt and userId
getUser(id) returns an object with fields id, name and email
Your resolver will be something like the following:
const resolver = {
// root Query resolver
Query: {
getTest: (root, args, ctx, info) => getTest()
},
// Test resolver
Test: {
// resolves field 'comment' on Test
// the 'parent' arg contains the result from the parent resolver (here, getTest on root)
comment: (parent, args, ctx, info) => getComment(parent.commentId)
},
// Comment resolver
Comment: {
// resolves field 'author' on Comment
// the 'parent' arg contains the result from the parent resolver (here, comment on Test)
author: (parent, args, ctx, info) => getUser(parent.userId)
},
}
Hope this helps.

Unable to use Fragments on GraphQL-yoga with Primsa

I am using graphql-yoga with Prisma and Prisma-Bindings
I'm trying to add a fragment to my resolver so that a specific field (id in this situation) is always fetched when the user asks for a custom field, costsToDate.
This is so i can make some additional queries needed to build the result for that field, and i need the ID of the object for that.
Unfortunatley i can't seem to get it to work, and the documentations seems a little lacking on the specifics with graphql-yoga and Prisma.
Here is the definition of the type:
type Job {
id: ID!
projectNumber: String!
client: Client!
name: String!
description: String
quoteNumber: String
workshopDaysQuoted: String!
quoted: String!
targetSpend: String!
costs: [JobCost!]!
estimatedCompletion: DateTime
completed: Boolean!
costTotal: String
workshopDaysUsed: String
costsToDate: String
}
And here is the resolver for the query:
const jobs = {
fragment: `fragment description on Jobs { id }`,
resolve: jobsResolver
}
async function jobsResolver(root, args, context, info) {
await validatePermission(args,context,info,['admin','user','appAuth'])
const {showCompleted} = args
const completedFilter = (typeof showCompleted === 'boolean') ? { completed: showCompleted } : {}
const jobIDS = await context.db.query.jobs({ where: completedFilter }, `{ id }`)
//console.log(jobIDS);
let jobs = await context.db.query.jobs({
where: completedFilter
}, info)
return await getAllJobCostsToDateList(jobs)
}
I am applying the the fragmentReplacements as per below.
const fragmentReplacements = extractFragmentReplacements(resolvers)
console.log(fragmentReplacements)
const port = process.env.PORT || 3010
const graphQLServer = new GraphQLServer({
typeDefs: './src/schema.graphql',
resolvers,
resolverValidationOptions: {
requireResolversForResolveType: false
},
context: req => ({
...req,
db: new Prisma({
typeDefs: `src/generated/prisma.graphql`,
fragmentReplacements,
endpoint: PRISMA_ENDPOINT,
secret: PRISMA_KEY,
debug: false
})
})
})
If i console.log the fragmentReplacements object i get the following, so it does seem to be picking up the fragments.
[ { field: 'job', fragment: 'fragment costsToDate on Job { id }' },
{ field: 'jobs',
fragment: 'fragment costsToDate on Jobs { id }' } ]
So my expectation here is that if i make a query against jobs or job that asks for the costsToDate field that it will also fetch the id for the job/each job.
However if i make the following query.
query{
jobs{
description
costsToDate
}
}
But i see no id fetched, and nothing in the root parameter on the resolver function.
Apologies as i am probably barking up completely the wrong tree here, seems like a somewhat simple requirement, but i can't quite work it out. Sure i'm missing something fundamental.
Thanks!
Gareth
A fragment is used to always retrieve given fields on a given type.
It follows the following format:
fragment NameOfYourFragment on YourType { ... }
You currently can't apply a given fragment conditionally as it is always applied.
Moreover, you specified a fragment on Jobs, but the type name used by Prisma is Job (even if you have the job and jobs resolvers)
You probably only need the following resolver:
const job = {
fragment: `fragment JobId on Job { id }`,
resolve: jobsResolver
}

Enumerating all fields from a GraphQL query

Given a GraphQL schema and resolvers for Apollo Server, and a GraphQL query, is there a way to create a collection of all requested fields (in an Object or a Map) in the resolver function?
For a simple query, it's easy to recreate this collection from the info argument of the resolver.
Given a schema:
type User {
id: Int!
username: String!
roles: [Role!]!
}
type Role {
id: Int!
name: String!
description: String
}
schema {
query: Query
}
type Query {
getUser(id: Int!): User!
}
and a resolver:
Query: {
getUser: (root, args, context, info) => {
console.log(infoParser(info))
return db.Users.findOne({ id: args.id })
}
}
with a simple recursive infoParser function like this:
function infoParser (info) {
const fields = {}
info.fieldNodes.forEach(node => {
parseSelectionSet(node.selectionSet.selections, fields)
})
return fields
}
function parseSelectionSet (selections, fields) {
selections.forEach(selection => {
const name = selection.name.value
fields[name] = selection.selectionSet
? parseSelectionSet(selection.selectionSet.selections, {})
: true
})
return fields
}
The following query results in this log:
{
getUser(id: 1) {
id
username
roles {
name
}
}
}
=> { id: true, username: true, roles: { name: true } }
Things get pretty ugly pretty soon, for example when you use fragments in the query:
fragment UserInfo on User {
id
username
roles {
name
}
}
{
getUser(id: 1) {
...UserInfo
username
roles {
description
}
}
}
GraphQL engine correctly ignores duplicates, (deeply) merges etc. queried fields on execution, but it is not reflected in the info argument. When you add unions and inline fragments it just gets hairier.
Is there a way to construct a collection of all fields requested in a query, taking in account advanced querying capabilities of GraphQL?
Info about the info argument can be found on the Apollo docs site and in the graphql-js Github repo.
I know it has been a while but in case anyone ends up here, there is an npm package called graphql-list-fields by Jake Pusareti that does this. It handles fragments and skip and include directives.
you can also check the code here.

Resources