Enumerating all fields from a GraphQL query - graphql

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.

Related

Cant Return Array in Graphql Query

I'm pretty new to graphql and I'm working on a project in nodejs where I am trying to return users when a getUsers query is performed. The issue is that when I test this query in graphql studio, I'm getting an error stating: "GraphQLError: Cannot query field \"users\" on type \"User\". I'm really confused as to why I'm having this issue. I've seen a number of examples where people where able to return just an array and didn't have a problem, but every time I've tried this I end up getting a similar error. Due to this, I've only been able to return a value for a query or mutation when I am super specific such as for my user query:
...
const user = await requireAuth(user)
return {
_id: user._id,
username: user.username,
firstName: user.firstName,
email: user.email,
}
Does anyone know why this is happening? I would really appreciate any help or advice. Thank you!
Query getUsers in graphql,
{
getUsers {
users
}
}
Query in user-resolvers.js
getUsers: async(parent, args, context, info) => {
try {
let users = await User.find()
console.log(users)
// console.log(users) shows all of the users in the format found in type Users
return users;
}
catch (error) {
throw error;
}
},
schema.js
export default`
type Users {
_id: ID!
username: String
email: String!
firstName: String
lastName: String
basicInfo: [BasicInfo]!
avatar: String
date: Date
}
type BasicInfo {
birth_date: String!
age: Int!
feet: Int!
inches: Int!
}
...
type Query {
getUsers: [Users]
}
...
schema {
query: Query
mutation: Mutation
}
`;
index.js
import UserResolvers from './user-resolvers.js';
import User from '../../models/User.js';
export default {
Query: {
user: UserResolvers.user,
getUsers: UserResolvers.getUsers,
},
...
};
In the query you specify the fields you want to return and you don't have a field users, you must only specify fields that exist in your schema:
{
getUsers {
id
username
email
...
}
}
More info here

Querying Many-To-Many Relationships in AWS Amplify

I have two models in my graphql schema and the one I am trying to query on, Sessions, has two #belongsTo directives (read on a forum this matters). I can successfully save these models and view them on the AWS AppSync Queries Tab where I can query getSessions successfully BUT when I try to the exact same query locally following these docs:
(https://docs.amplify.aws/lib/graphqlapi/advanced-workflows/q/platform/flutter/#combining-multiple-operations)
I get an error locally:
type "Null" is not a subtype of type 'string'
What am I doing wrong and how do I fix this so I can successfully retrieve my nested query:
Here are my models as a reference:
Sessions:
type Session
#model
#auth(
rules: [
{ allow: public }
{ allow: owner }
{ allow: groups, groups: ["Admin"] }
]
) {
id: ID!
name: String
numPeoplePresent: Int
notes: String
eIdReader: String
weighTiming: String
cows: [Cow] #manyToMany(relationName: "CowSession")
proceduresID: ID
procedures: Procedures #hasOne(fields: ["proceduresID"])
}
Cow:
type Cow
#model
#auth(
rules: [
{ allow: public }
{ allow: owner }
{ allow: groups, groups: ["Admin"] }
]
) {
id: ID!
name: String!
RfId: String
weight: [Float!]!
temperament: [Int]
breed: String
type: String
dateOfBirth: AWSDate
sessions: [Session] #manyToMany(relationName: "CowSession")
procedures: [Procedures] #manyToMany(relationName: "CowProcedures")
}
This is the query that is causing the error:
const getSession = 'getSession';
String graphQLDocument = '''query getSession(\$id: ID!) {
$getSession(id: \$id) {
numPeoplePresent
notes
name
eIdReader
id
owner
proceduresID
updatedAt
weighTiming
cows {
items {
cow {
RfId
}
}
}
}
}''';
final getSessionRequest = GraphQLRequest<Session>(
document: graphQLDocument,
modelType: Session.classType,
variables: <String, String>{'id': sessID}, //parameter of the current session can hardcode to whatever you need here
decodePath: getSession,
);
final response =
await Amplify.API.query(request: getSessionRequest).response;
print('Response: ${response.data}');
The wonderful people at amplify answered this quickly so I will relay the information here:
the problem was the intermediary ids were not included in my local query so it was unable to retrieve the nested Cows. Updated query looks like this:
getSession = 'getSession';
String graphQLDocument = '''query getSession(\$id: ID!) {
$getSession(id: \$id) {
numPeoplePresent
notes
name
eIdReader
id
owner
proceduresID
updatedAt
weighTiming
cows {
items {
id <-- needed this one
cow {
id <-- and this id too
RfId
breed
dateOfBirth
name
type
weight
}
}
}
}
}''';

GraphQL query how to pass variables

Hi all I have a query where I am trying to get messages from a user with a specific uuid or a role that matches the users role. I am unsure of how to properly use the _ilike or % in this instance. I've tried a myriad of combinations yet the role messages never get returned. My query as it sits now and the hook used in the component are below.
I appreciate any feedback.
Here is my query
query getUserMessages($userId: String!) {
messageReceivers(
where: { _or: [{ userId: { _eq: $userId } }, { message: { roles: { _ilike: "%" } } }] }
) {
messageId
userId
message {
id
audioLink
body
videoLink
user {
firstName
lastName
photo
title
specialty
profession
location
}
}
}
}
Using the lazyquery hook in component
const [getUserMessages, { error, called, loading, data }] = useGetUserMessagesLazyQuery()
const userRole = `%${user.role}%`
useEffect(() => {
getUserMessages({
variables: { userId: user?.id, message: { roles: { _ilike: userRole } } },
})
}, [user])
You are incorrectly passing userRole to the query. To fix it, apply userId's pattern to userRole.
In the query definition, add $userRole in the operation signature (You are currently hardcoding _ilike to % in the query, but you want set it dynamically as $userRole).
In the calling function, send the variables correctly variables: { userId: user?.id, userRole: userRole}.
The GraphQL Variable docs neatly describe how this fits together.
Thanks #fedonev! Though I didn't see your solution you were absolutely correct. I was able to work it out a little differently and I hope this helps someone who's run into the same issue.
By creating the variable $role in the query I was able to use the same syntax as was being used by the userId variable. If anyone has this issue please feel free to comment I will happily help if I can.
Query
query getUserMessages($userId: String!, $role: String = "%") {
messages(
where: {
_or: [{ roles: { _ilike: $role } }, { messageReceivers: { userId: { _eq: $userId } } }]
}
order_by: { createdAt: desc }
) {
createdAt
id
body
audioLink
videoLink
roles
}
Call from in component
useEffect(() => {
getUserMessages({
variables: { userId: user?.id, role: user?.role },
})
}, [user])

GraphQL nested query returns null

I am trying to use a GraphQL nested query (I am 80% sure this is a nested query?) to get information on the listing and the chef (author) of the listing. I can get the listing info just fine, but I am unable to get the chef info.
I was under the impression that the default resolver (user) would fire when getListing(args) returned without a valid User object for the chef. But the default resolver does not appear to be firing.
How do I properly get the nested information?
For example, my query is:
query getListing($listingID: String!) {
getListing(listingID: $listingID) {
name
chef {
firstName
}
}
}
The query returns:
{
"data": {
"getListing": {
"name": "Test",
"chef": {
"firstName": null
}
}
}
}
The function getListing(args) queries the DB and returns:
{
name: 'Test',
chef: 'testUsername',
listingID: 'testListingID'
}
My Schema is:
type Listing {
uuid: String!
name: String!
chef: User!
}
type User {
username: String
firstName: String
}
type Query {
getUser(jwt: String!): User
getListing(listingID: String): Listing
}
And my resolvers are:
const resolvers = {
Query: {
getListing: async (parent, args, context, info) => {
console.log('GET_LISTING');
return getListing(args);
},
getUser: async (parent, args, context, info) => {
console.log('GET_USER');
return getUser(args);
},
},
User: async (parent, args) => {
console.log('USER RESOLVER');
return getUser(args);
},
};
Other Info:
I am using Apollo Server running on AWS Lambda integrating with DynamoDB on the backend.
Resolvers exist only at the field level. You can't resolve a type (i.e. User). You can only resolve a field that has that type (i.e. chef).
const resolvers = {
// ...
Listing: {
chef: (parent, args) => {
return getUser()
},
},
}
It's unclear what sort of parameters getUser accepts, so you'll need to modify the above example accordingly. You won't use args unless you actually specify arguments for the field being resolved in your schema. It looks like the returning listing has a chef property that's the name of the user, so you can access that value with parent.chef.

Can an Apollo Mutation return a custom result object?

The result returned from an Apollo mutation is typically a type or a subset of fields from a type, and this is usually great. So my mutation:
const addUserMutation = gql`
mutation createUser($email: String!, $permissions: [CreatePermissionInput]) {
createUser(input: {email: $email, permissions: $permissions}) {
id
created
lastUpdated
uid
email
permissions {
who
...
}
}
}
`
Which is calling:
extend type Mutation {
createUser(input: CreateUserInput!): User
}
Will return the user with the fields listed.
Problem is, I want to know if the user that we just tried to create already existed or not. So how can I edit the response to include this flag? Can you have a mutation return, say, an object like:
{
exists: true,
user: { ... }
}
So I can do this:
this.props.submit({
variables: {
email,
permissions,
},
}).then(({ result }) => {
console.log(result)
// > { exists: true, user: [USER OBJECT] }
})
I get that this will break the auto cache update but sometimes you need the response from an update to tell you more.
Create an additional type for the return result of mutation
type UserPayLoad {
exists:Boolean
user:User
}
extend type Mutation {
createUser(input: CreateUserInput!): UserPayLoad
}
Just try this. This may help you

Resources