I have a user.gql - file with some queries, mutations, inputs etc.
I get content of user.gql, using gql(), then filter an document.definition to find needed a query. After filter
document.definitions
.filter(item => item.kind === 'OperationDefinition')
.filter(item => item.operation === 'query')
, one request object remains. smth like
{
kind: "OperationDefinition",
name: {kind: 'Name', value: 'authenticate'},
operation: "query",
selectionSet: {...}
variableDefinitions: [
...
]
}
How to convert it back to string?
In the file this code is written like this
query authenticate($login: String!, $password: Password!) {
user(login: $login, password: $password) {
token,
name,
additional
}
}
Related
I'm just getting to grips with GraphQL,
I have set up the following query:
type: UserType,
args: {
id: { name: 'id', type: new GraphQLNonNull(GraphQLID) },
email: { name: 'email', type: new GraphQLNonNull(GraphQLString) }
},
resolve: (root, { id, email }, { db: { User } }, fieldASTs) => {
...
}
I would like to be able to pass either an 'id' or 'email' to the query, however, with this setup it requires both an id and email to be passed.
Is there a way to set up the query so only one argument is required, either id or email, but not both?
There's no built-in way to do that in GraphQL. You need to make your arguments nullable (by removing the GraphQLNonNull wrapper type from both of them) and then, inside your resolver, you can just do a check like:
resolve: (root, { id, email }, { db: { User } }, fieldASTs) => {
if (!id && !email) return Promise.reject(new Error('Must pass in either an id or email'))
if (id && email) return Promise.reject(new Error('Must pass in either an id or email, but not both.'))
// the rest of your resolver
}
Define an interface credentials and have that implemented as id or email.
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.
This is closely related to my last question here. In short, I have 2 schemas, dbPosts and dbAuthors. They look somewhat like this (I've omitted some fields here for the sake of brevity):
dbPosts
id: mongoose.Schema.Types.ObjectId,
title: { type: String },
content: { type: String },
excerpt: { type: String },
slug: { type: String },
author: {
id: { type: String },
fname: { type: String },
lname: { type: String },
}
dbAuthors
id: mongoose.Schema.Types.ObjectId,
fname: { type: String },
lname: { type: String },
posts: [
id: { type: String },
title: { type: String }
]
I'm resolving my post queries like this:
const mongoose = require('mongoose');
const graphqlFields = require('graphql-fields');
const fawn = require('fawn');
const dbPost = require('../../../models/dbPost');
const dbUser = require('../../../models/dbUser');
fawn.init(mongoose);
module.exports = {
// Queries
Query: {
posts: (root, args, context) => {
return dbPost.find({});
},
post: (root, args, context) => {
return dbPost.findById(args.id);
},
},
Post: {
author: (parent, args, context, ast) => {
// Retrieve fields being queried
const queriedFields = Object.keys(graphqlFields(ast));
console.log('-------------------------------------------------------------');
console.log('from Post:author resolver');
console.log('queriedFields', queriedFields);
// Retrieve fields returned by parent, if any
const fieldsInParent = Object.keys(parent.author);
console.log('fieldsInParent', fieldsInParent);
// Check if queried fields already exist in parent
const available = queriedFields.every((field) => fieldsInParent.includes(field));
console.log('available', available);
if(parent.author && available) {
return parent.author;
} else {
return dbUser.findOne({'posts.id': parent.id});
}
},
},
};
And I'm resolving all author queries like this:
const mongoose = require('mongoose');
const graphqlFields = require('graphql-fields');
const dbUser = require('../../../models/dbUser');
const dbPost = require('../../../models/dbPost');
module.exports = {
// Queries
Query: {
authors: (parent, root, args, context) => {
return dbUser.find({});
},
author: (root, args, context) => {
return dbUser.findById(args.id);
},
},
Author: {
posts: (parent, args, context, ast) => {
// Retrieve fields being queried
const queriedFields = Object.keys(graphqlFields(ast));
console.log('-------------------------------------------------------------');
console.log('from Author:posts resolver');
console.log('queriedFields', queriedFields);
// Retrieve fields returned by parent, if any
const fieldsInParent = Object.keys(parent.posts[0]._doc);
console.log('fieldsInParent', fieldsInParent);
// Check if queried fields already exist in parent
const available = queriedFields.every((field) => fieldsInParent.includes(field));
console.log('available', available);
if(parent.posts && available) {
// If parent data is available and includes queried fields, no need to query db
return parent.posts;
} else {
// Otherwise, query db and retrieve data
return dbPost.find({'author.id': parent.id, 'published': true});
}
},
},
};
Again, I've left out bits not relevant to this question, such as mutations, in the interest of brevity. My objective is to make all queries work recursively while also optimizing database lookups. But somehow I'm unable to accomplish this. Here's one query I'm running, for instance:
{
posts{
id
title
author{
first_name
last_name
id
posts{
id
title
}
}
}
}
And it returns this:
{
"errors": [
{
"message": "Cannot return null for non-nullable field Post.author.",
"locations": [
{
"line": 5,
"column": 5
}
],
"path": [
"posts",
1,
"author"
]
}
],
"data": {
"posts": [
{
"id": "5ba1f3e7cc546723422e62a4",
"title": "A Title!",
"author": {
"first_name": "Bill",
"last_name": "Erby",
"id": "5ba130271c9d440000ac8fc4",
"posts": [
{
"id": "5ba1f3e7cc546723422e62a4",
"title": "A Title!"
}
]
}
},
null
]
}
}
If you notice, this query does return all values requested, but also adds an error message against the post.author query! What could be causing this?
I haven't included the entire codebase so as not to make things confusing, but should you wish to take a look, it's up on Github and a GraphiQL interface is up at https://graph.schandillia.com should you wish to see the results for yourself.
Thank you so much for your time, if you've come this far. Would really appreciate any pointer in the right direction!"
P.S.: If you notice, I'm logging the values of 3 variables in each resolver for debugging purposes:
queriedFields: An array of all fields being queried
fieldsInParent: An array of all fields being returned in the resolver's parent property
available: A boolean showing if all queriedFields members exist in fieldsInParent
And when I run a simple query like this:
{
posts{
id
author{
id
posts{
id
}
}
}
}
This is what gets logged:
-------------------------------------------------------------
from Post:author resolver
queriedFields [ 'id', 'posts' ]
fieldsInParent [ '$init', 'id', 'first_name', 'last_name' ]
available false
-------------------------------------------------------------
from Post:author resolver
queriedFields [ 'id', 'posts' ]
fieldsInParent [ '$init', 'id', 'first_name', 'last_name' ]
available false
-------------------------------------------------------------
from Author:posts resolver
queriedFields [ 'id' ]
fieldsInParent [ 'id', 'title' ]
available true
Shouldn't the post:author resolver execute only once? Also, it's funny how in the first 2 logs, fieldsInParent is missing the posts field even when the schema for author includes such a field.
Your query result does not in fact include all the requested data. The posts query resolves to an array that includes one Post object and a null. The null is there because GraphQL tried to fully resolve the other Post object and could not -- it encountered a validation error, namely that the post's author resolved to null.
You can change your schema to make the author field nullable, which would get rid of the error but would still leave you with the null post. Presumably, if a post exists, it should have an author (although with MongoDB I guess it's very possible you just have some bad data). If you look inside your resolver, there's two return statements -- one of them (probably the db call) is returning null for that second post.
As an aside, as a client, you probably don't want to deal with nulls inside the array and want an empty array instead of a null for the whole field. When using lists (arrays), you may want to make them both non-nullable and make each item in that list non-nullable as well. You do so like this:
posts: [Post!]!
You still need to ensure your resolver logic prevents those nulls from happening, but adding the validation can help you catch that sort of behavior more easily.
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.
I'm just getting to grips with GraphQL,
I have set up the following query:
type: UserType,
args: {
id: { name: 'id', type: new GraphQLNonNull(GraphQLID) },
email: { name: 'email', type: new GraphQLNonNull(GraphQLString) }
},
resolve: (root, { id, email }, { db: { User } }, fieldASTs) => {
...
}
I would like to be able to pass either an 'id' or 'email' to the query, however, with this setup it requires both an id and email to be passed.
Is there a way to set up the query so only one argument is required, either id or email, but not both?
There's no built-in way to do that in GraphQL. You need to make your arguments nullable (by removing the GraphQLNonNull wrapper type from both of them) and then, inside your resolver, you can just do a check like:
resolve: (root, { id, email }, { db: { User } }, fieldASTs) => {
if (!id && !email) return Promise.reject(new Error('Must pass in either an id or email'))
if (id && email) return Promise.reject(new Error('Must pass in either an id or email, but not both.'))
// the rest of your resolver
}
Define an interface credentials and have that implemented as id or email.