I had started out writing verbose GraphQL and switched to graphql-tools and makeExecutableSchema and with the changes it will load queries for user(id: "N"), users, group(id: "N") and groups, however, the nested lists just return "id": null. I feel like I have to have a small mistake somewhere but am not seeing it:
const { makeExecutableSchema } = require('graphql-tools')
const db = require('./db')
const typeDefs = `
type User {
id: String
first_name: String!
last_name: String!
email: String!
friends: [User!]
groups: [Group!]
}
type Group {
id: String
name: String!
}
type Query {
users: [User!]!
user(id: String!): User
groups: [Group!]!
group(id: String!): Group
}
`
const resolvers = {
Query: {
users: db.readAllUsers,
user: (root, args, { loaders }) => loaders.user.load(args.id),
groups: db.readAllGroups,
group: (root, args, { loaders }) => loaders.group.load(args.id)
}
}
module.exports = makeExecutableSchema({ typeDefs, resolvers })
Any help would be greatly appreciated!
Edit: to clarify, here's what the data source looks like
You've defined resolvers for your queries, while are just fields on your Query type. However, you don't have any resolvers for the group field on the User type, so GraphQL falls back to using the default resolver for that field. Since the property field on the Object User resolves to is just an array of ids, GraphQL doesn't know how to make sense of it -- after all, you told it groups would be an array of objects.
You'll need to add a resolver for the groups field and transform that array into a Promise that will resolve to an array of Group objects:
const resolvers = {
Query: {
users: db.readAllUsers,
user: (root, args, { loaders }) => loaders.user.load(args.id),
groups: db.readAllGroups,
group: (root, args, { loaders }) => loaders.group.load(args.id)
},
User: {
groups: ({ groups }, args, { loaders }) => {
return Promise.all(groups.map(id => loaders.group.load(id)))
}
}
}
Related
Started messing around with GraphQL, but I'm stuck with this error. Not sure if it's a problem in the schema definition or in the query.
const express_graphql = require('express-graphql')
const { buildSchema } = require('graphql')
const users = require('../users/translator')
const schema = buildSchema(`
type User {
id: ID
email: String
role: String
}
type Query {
user(id: ID!): User
users: [User]
token(email: String!, password: String!): String!
}
type Mutation {
signup(email: String!, password: String!, role: String!): ID
}`
)
const resolvers = {
users: users.getAll,
user: users.getById,
token: users.login,
signup: users.create,
}
module.exports = app => {
// GraphQL route
app.use('/graphql', express_graphql({
schema,
rootValue: resolvers,
graphiql: true,
}))
}
app is an express.js server while const users holds the logic. I'm able to fetch users and tokens, but when I try to POST a mutation
{
signup(email: "my#email.com", password: "321321", role: "admin")
}
I get the error Cannot query field "signup" on type "Query". By looking at the GraphiQL suggestions after reading the schema from the server, it looks like the signup mutation doesn't even get exported:
Some tutorials say I should export resolvers using
const resolvers = {
query: {
users: users.getAll,
user: users.getById,
token: users.login,
},
mutation: {
signup: users.create,
}
}
But it doesn't work either. Any hints?
You need to specify the operation type (query, mutation or subscription) like this:
mutation {
signup(email: "my#email.com", password: "321321", role: "admin")
}
If the operation type is omitted, the operation is assumed to be a query. This is called "query shorthand notation", but only works if your operation is unnamed and does not include any variable definitions.
It's good practice to always include the operation type regardless.
I was wondering if it was possible to make a "dynamic condition" with sequelize using findOne/findOrCreate etc instead of row SQL query
i use it with graphQL and arguments could be optionnal in graphQL, so what i want to do is :
if i have id and name as argument :
user.findOrCreate({
where: {
id: args.id,
name : args.name,
}
})
and if i have id, name and email :
user.findOrCreate({
where: {
id: args.id,
name : args.name,
email: args.email,
}
})
so it is possible to have a parameter inside the findOrCreate to check if something exist(here, args.email) and if not, doesn't include it inside the query ?
You can create the the function and import that function:
export const findOrCreate = async function (query) {
try {
const user = await user.findOrCreate(query);
return user;
} catch (e) {
throw Error('Error occur while finding or creating the records’);
}
};
wherever you need import that function and call with the required parameters
const query = {
where: { // we search for this user
id: args.id,
name : args.name,
email: args.email,
},
defaults: {
job: 'Technical Lead JavaScript'
} // if it doesn't exist, we create it with this additional data
}
await findOrCreate(query )
I just started using graphql with mysql, i would like to know if it is possible to use a name in the graphql query different from the column name in my data base.
For example i have a table users with the columns userName and password, when i define the type for the schema i have the following:
const unidadesMedidaInternaType = new GraphQLObjectType({
name: 'unidadesMedidaInterna',
fields: () => ({
userName: { type: GraphQLID },
password: { type:GraphQLString }
})
});
the resolver:
resolve (parent, args) {
return pool.query(`SELECT * FROM users`);
}
so i have to query like this:
{
users {
userName,
password
}
}
i would like to have different names in the query like this:
{
users {
Name,
secret
}
}
i tried changing the names of the fields in the type definition but the result of the query is full of nulls values.
In order to have different names in the queries you have 2 options:
Option 1: Use aliases to run the query:
You can run your query with aliases like
{
users {
Name: userName,
secret: password
}
}
In this case you are just renaming the fields name on execution time, so the original names will still be available to query.
Option 2: Map the query result to the GraphQLObject type.
First rename the fields:
const unidadesMedidaInternaType = new GraphQLObjectType({
name: 'unidadesMedidaInterna',
fields: () => ({
Name: { type: GraphQLID },
secret: { type:GraphQLString }
})
});
Then map the result of the query to match the fields:
resolve (parent, args) {
const result = pool.query(`SELECT * FROM users`);
// If the result of the query is an array then you have to map its items
return { Name: result.userName, secret: result.password }
}
I am modularizing my schema for a GraphQL API and trying to merge the resolvers without using any 3rd party libraries.
Is there a simple way to do this without Lodash.merge() or equivalent?
The Apollo Documentation says to use a library such as Lodash to merge() modularized resolvers. (http://dev.apollodata.com/tools/graphql-tools/generate-schema.html#modularizing)
The problem seems to be that by their nature, the resolvers contain functions as properties, so they seem to be omitted when I access them via Object.assign() or even JSON.stringify().
If I console.log them, I see: {"Query":{},"Mutation":{}}
Here is what one of the resolvers looks like:
const productResolvers = {
Query: {
myProducts: (root, { userId }, context) => {
return [
{ id: 1, amount: 100, expiry: '12625383984343', created: '12625383984343' },
{ id: 2, amount: 200, expiry: '12561351347311', created: '12625383984343' },
{ id: 3, amount: 200, expiry: '11346347378333', created: '12625383984343' },
{ id: 4, amount: 350, expiry: '23456234523453', created: '12625383984343' },
];
},
},
Mutation: {
addProduct: (root, { userId }, context) => {
return { id: 350, amount: 100, expiry: '12625383984343', created: '12625383984343' };
},
}
};
Let's assume there is another one virtually identical called widgetResolvers.
Here is a fully functional block of code:
export const schema = makeExecutableSchema({
typeDefs: [queries, mutations, productSchema, widgetSchema],
resolvers
});
Here is what I'm trying to achieve:
export const schema = makeExecutableSchema({
typeDefs: [queries, mutations, productSchema, widgetSchema],
resolvers: Object.assign({}, productResolvers, widgetResolvers)
});
I haven't loaded in ability to use rest spread yet (https://babeljs.io/docs/plugins/transform-object-rest-spread/). I suspect it won't work for the same reason Object.assign() doesn't work.
Oh, and here is why I suspect this merge doesn't work: Why doesn't JSON.stringify display object properties that are functions?
If you're using Object.assign(), your Query and Mutation properties shouldn't end up empty, but you will run into an issue because, unlike lodash's merge(), it's not recursive. Object.assign() only compares the "direct" properties of the objects it's passed -- overriding properties of previous sources as it moves through the list.
Because Query and Mutation are properties of the objects being passed, each subsequent resolver override the previous object's Query and Mutation, with the resulting object only holding the Query and Mutation properties of the last object passed into Object.assign().
It's a lot less neat, but if you're bent on avoiding importing lodash, you could get the expected behavior this way:
const productResolver = {
Query: { ... ✂ ... },
Mutation: { ... ✂ ... }
}
const widgetResolver = {
Query: { ... ✂ ... },
Mutation: { ... ✂ ... }
}
const resolvers = {
Query: Object.assign({}, widgetResolver.Query, productResolver.Query),
Mutation: Object.assign({}, widgetResolver.Mutation, productResolver.Mutation)
}
Got type resolvers too? No problem:
const Widget = { ... ✂ ... }
const Product = { ... ✂ ... }
const resolvers = Object.assign(
{
Query: Object.assign({}, widgetResolver.Query, productResolver.Query),
Mutation: Object.assign({}, widgetResolver.Mutation, productResolver.Mutation)
},
Widget,
Product)
How could I check if user has permission to see or query something? I have no idea how to do this.
In args? How would that even work?
In resolve()? See if user has permission and somehow
eliminate/change some of the args?
Example:
If user is "visitor", he can only see public posts, "admin" can see everything.
const userRole = 'admin'; // Let's say this could be "admin" or "visitor"
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => {
return {
posts: {
type: new GraphQLList(Post),
args: {
id: {
type: GraphQLString
},
title: {
type: GraphQLString
},
content: {
type: GraphQLString
},
status: {
type: GraphQLInt // 0 means "private", 1 means "public"
},
},
// MongoDB / Mongoose magic happens here
resolve(root, args) {
return PostModel.find(args).exec()
}
}
}
}
})
Update - Mongoose model looks something like this:
import mongoose from 'mongoose'
const postSchema = new mongoose.Schema({
title: {
type: String
},
content: {
type: String
},
author: {
type: mongoose.Schema.Types.ObjectId, // From user model/collection
ref: 'User'
},
date: {
type: Date,
default: Date.now
},
status: {
type: Number,
default: 0 // 0 -> "private", 1 -> "public"
},
})
export default mongoose.model('Post', postSchema)
You can check a user's permission in the resolve function or in the model layer. Here are the steps you have to take:
Authenticate the user before executing the query. This is up to your server and usually happens outside of graphql, for example by looking at the cookie that was sent along with the request. See this Medium post for more details on how to do this using Passport.js.
Add the authenticated user object or user id to the context. In express-graphql you can do it via the context argument:
app.use('/graphql', (req, res) => {
graphqlHTTP({ schema: Schema, context: { user: req.user } })(req, res);
}
Use the context inside the resolve function like this:
resolve(parent, args, context){
if(!context.user.isAdmin){
args.isPublic = true;
}
return PostModel.find(args).exec();
}
You can do authorization checks directly in resolve functions, but if you have a model layer, I strongly recommend implementing it there by passing the user object to the model layer. That way your code will be more modular, easier to reuse and you don't have to worry about forgetting some checks in a resolver somewhere.
For more background on authorization, check out this post (also written by myself):
Auth in GraphQL - part 2
One approach that has helped us solve authorization at our company is to think about resolvers as a composition of middleware. The above example is great but it will become unruly at scale especially as your authorization mechanisms get more advanced.
An example of a resolver as a composition of middleware might look something like this:
type ResolverMiddlewareFn =
(fn: GraphQLFieldResolver) => GraphQLFieldResolver;
A ResolverMiddlewareFn is a function that takes a GraphQLFieldResolver and and returns a GraphQLFieldResolver.
To compose our resolver middleware functions we will use (you guessed it) the compose function! Here is an example of compose implemented in javascript, but you can also find compose functions in ramda and other functional libraries. Compose lets us combine simple functions to make more complicated functions.
Going back to the GraphQL permissions problem lets look at a simple example.
Say that we want to log the resolver, authorize the user, and then run the meat and potatoes. Compose lets us combine these three pieces such that we can easily test and re-use them across our application.
const traceResolve =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
const start = new Date().getTime();
const result = await fn(obj, args, context, info);
const end = new Date().getTime();
console.log(`Resolver took ${end - start} ms`);
return result;
};
const isAdminAuthorized =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
if (!context.user.isAdmin) {
throw new Error('User lacks admin authorization.');
}
return await fn(obj, args, context, info);
}
const getPost = (obj: any, args: any, context: any, info: any) => {
return PostModel.find(args).exec();
}
const getUser = (obj: any, args: any, context: any, info: any) => {
return UserModel.find(args).exec();
}
// You can then define field resolve functions like this:
postResolver: compose(traceResolve, isAdminAuthorized)(getPost)
// And then others like this:
userResolver: compose(traceResolve, isAdminAuthorized)(getUser)