Assume you have a schema where a resource can be identified by its ID or a path (/userName/projectName):
type User {
id: ID!
name: String!
}
type Project {
id: ID!
name: String!
owner: User!
}
To query a project, we need two resolvers:
findProjectByID(id: String!): Project!
findProjectByPath(userName: String!, projectName: String!): Project!
Ideally, we'd have just one resolver that accepts both kinds of identifiers:
findProject(???): Project!
These are the ideas I can come up with, but none of them seem very neat:
# Idea 1
findProject(idOrPath: String!): Project!
# Idea 2
findProject(id: String, userName: String, projectName: String): Project!
# Idea 3
findProject(identifier: ProjectIdentifier): Project!
union ProjectIdentifier = ProjectID | ProjectPath
type ProjectID { id: String! }
type ProjectPath { userName: String!, projectName: String! }
What would be the most GraphQL-y approach? Is there a convention that applies here?
Related
I just tried to implement the Relay in Frontend for this graphql tutorial, In that tutorial, they created graphql server to store URL(Link) bookmarks with the User who posted those URLs.
The relationship between the link and the users is:
Link belongs_to :user,
User has_many :links.
And I listed out all the Links with Users in Frontend, at the time I got the below error.
Warning: RelayResponseNormalizer: Invalid record 1. Expected __typename to be consistent, but the record was assigned conflicting types Link and User. The GraphQL server likely violated the globally unique id requirement by returning the same id for different objects
I'm not aware of how much it will impact the application. because I got the expected result from Frontend.
Frontend View of Query.
I read this relay official blog for this kind of error, but there is no example to know how exactly to resolve this. so can someone help to resolve this?
Relay Query
graphql`
query LinkListQuery {
allLinks {
id,
description,
url,
postedBy {
id,
name
}
}
}`
Schema:
input AUTH_PROVIDER_CREDENTIALS {
email: String!
password: String!
}
input AuthProviderSignupData {
credentials: AUTH_PROVIDER_CREDENTIALS
}
type Link implements Node {
description: String!
id: ID!
postedBy: User
url: String!
votes: [Vote!]!
}
input LinkFilter {
OR: [LinkFilter!]
descriptionContains: String
urlContains: String
}
type Mutation {
createLink(description: String!, url: String!): Link!
createUser(name: String!, authProvider: AuthProviderSignupData): User!
createVote(linkId: ID): Vote!
signinUser(credentials: AUTH_PROVIDER_CREDENTIALS): SignInUserPayload
}
"""An object with an ID."""
interface Node {
"""ID of the object."""
id: ID!
}
type Query {
allLinks(filter: LinkFilter, first: Int, skip: Int): [Link]!
"""Fetches an object given its ID."""
node(
"""ID of the object."""
id: ID!
): Node
}
"""Autogenerated return type of SignInUser"""
type SignInUserPayload {
token: String
user: User
}
type User implements Node {
email: String!
id: ID!
links: [Link!]!
name: String!
votes: [Vote!]!
}
type Vote {
id: ID!
link: Link!
user: User!
}
Below, I'm attempting to create a type Customer which will include and array of type Box.
In my addCustomer mutation, I need to create an empty boxes array which will later have Boxes pushed into to it. I'm getting this error:
Error: The type of Mutation.addCustomer(boxes:) must be Input Type but got: [Box].
Any ideas?
const { gql } = require('apollo-server-express');
const typeDefs = gql`
type Customer {
_id: ID
firstName: String
lastName: String
email: String
password: String
address: String
city: String
state: String
zip: String
phone: String
boxes: [Box]
}
type Auth {
token: ID!
customer: Customer
}
type Box {
_id: ID!
boxSize: String!
sendToCustomer: Boolean!
getFromCustomer: Boolean!
}
type Mutation {
addCustomer(
firstName: String!,
lastName: String!,
email: String!,
password: String!,
address: String!,
city: String!,
state: String!,
zip: String!,
phone: String!
boxes: [Box]
): Auth
login(email: String!, password: String!): Auth
createBox(
boxSize: String!,
sendToCustomer: Boolean!,
getFromCustomer: Boolean!
): Box
addBoxToCustomer(
customerId: ID!,
boxSize: String!,
sendToCustomer: Boolean!,
getFromCustomer: Boolean!
): Customer
}
In GraphQL Input Object Types and Object Types are not compatible. This means that you can't use an output object type as an input object type, even if they have the same structure.
For customer you have circumvented the problem by defining every field as an argument to the mutation. First, I would create an Box input type. I like to call these input types drafts and postfix them with Draft but many people in the community postfix with Input.
input BoxDraft {
boxSize: String!
sendToCustomer: Boolean!
getFromCustomer: Boolean!
}
Notice how we can leave out the _id field, as I assume it is automatically generated by your server implementation. Then you can reference this type in your mutation.
type Mutation {
addCustomer(
firstName: String!,
lastName: String!,
email: String!,
password: String!,
address: String!,
city: String!,
state: String!,
zip: String!,
phone: String!
boxes: [BoxDraft]
): Auth
# ...
}
I would go even one step further and also define a draft type for Customer:
input CustomerDraft {
firstName: String!
lastName: String!
email: String!
password: String!
address: String!
city: String!
state: String!
zip: String!
phone: String!
boxes: [BoxDraft]
}
type Mutation {
addCustomer(draft: CustomerDraft!): Auth
}
I have a User type with many fields on it. I want to expose different fields on it depending on who is querying information about the User. What is a good way to organize this without having many many different types each representing a slightly different view of a user? Here is an example with 4 different types representing different views of the same user. Is there a better way to organize this?
Of course I can make all the fields nullable but that doesn't seem helpful to the developer querying the data.
type UserForSelf {
id: ID!
username: String!
avatarUrl: String!
email: String!
mailingAddress: Address!
team: Team!
lastLogin: DateTime!
}
type UserForPublic {
id: ID!
username: String!
avatarUrl: String!
}
type UserForAdmin {
id: ID!
username: String!
avatarUrl: String!
email: String!
team: Team!
lastLogin: DateTime!
}
type UserForTeamMember {
id: ID!
username: String!
avatarUrl: String!
email: String!
team: Team!
}
You should consider using Schema Directives for this use case.
That basically allows you to only resolve some specific field if the user has permission for it. Otherwise you can return null or throw an error.
So in the end you would have a single type User like this:
directive #hasRole(role: String) on FIELD_DEFINITION
type User {
id: ID!
username: String!
avatarUrl: String!
email: String! #hasRole(role: "USER")
mailingAddress: Address! #hasRole(role: "USER")
team: Team! #hasRole(role: "USER")
lastLogin: DateTime! #hasRole(role: "USER")
}
Then you can have a directive resolver kinda like this:
const directiveResolvers = {
...,
hasRole: (next, source, {role}, ctx) => {
const user = getUser()
if (role === user.role) return next();
throw new Error(`Must have role: ${role}, you have role: ${user.role}`)
},
...
}
If you have a field that only ADMIN can query, you would just use the #hasRole(role: "USER") directive.
Then your service layer (or your resolver if you don't have a service layer) would be responsible to define which User to fetch (if your own user or some user based on ID as long as you have permission).
You can use directives for a lot of different use cases. Here are a few good references:
https://www.prisma.io/blog/graphql-directive-permissions-authorization-made-easy-54c076b5368e
https://www.apollographql.com/docs/apollo-server/schema/directives/
I'm following official Lighthouse documentation on how to setup GraphQl into my project.
Unfortunately, I'm stacked with the following error:
No directive found for `bcrypt`
Following schema I have created so far:
type Query {
users: [User!]! #paginate(defaultCount: 10)
user(id: ID #eq): User #find
}
type User {
id: ID!
name: String!
email: String!
created_at: DateTime!
updated_at: DateTime!
}
type Mutation {
createUser(
name: String!,
email: String! #rules(apply: ["email", "unique:users"])
password: String! #bcrypt
): User #create
}
The query I'm trying to execute is following:
mutation {
createUser(
name:"John Doe"
email:"john#test.com"
password: "somesupersecret"
) {
id
email
}
}
Okay dug up - there is no more #brypt directory in \vendor\nuwave\lighthouse\src\Schema\Directives\ instead, there is #hash directive which is working like a charm and uses driver you specify in config/hashing.php configuration file
I'm building a small blog using GraphQL, Apollo Express and MongoDB with Mongoose.
Currently, articles are fetched by their IDs and visitors can browse an article with the id of let's say "123" here: example.com/articles/123
Instead, I would like to use slugs, so visitors can go to example.com/articles/same-article-as-above
My resolver so far:
import { gql } from 'apollo-server-express';
export default gql`
extend type Query {
articles: [Article!]
article(id: ID!): Article
}
type Article {
id: ID!
slug: String!
title: String!
desription: String!
text: String!
}
`;
I could just add another query:
articleBySlug(slug: String!): Article
This would work perfectly fine. However, this doesn't look very elegant to me and I feel like I am missing some basic understanding. Do I really have to add a new query to my resolvers each time I am trying to fetch an article by its title, text, description or whatever? I would end up with a lot of queries like "articleByTitle", "articleByDate", and so on. Can someone please give me a hint, an example or some best practices (or just confirm that I do have to add more and more queries☺)?
A common way to do this is to add all inputs to the same query, and make them optional:
export default gql`
extend type Query {
articles: [Article!]
article(id: ID, slug: String, date: String, search: String): Article
}
type Article {
id: ID!
slug: String!
title: String!
description: String!
text: String!
}
`;
Then, in the resolver just check that exactly one of id, slug or date is provided, and return an error if not.
Another option is to use a search string similar to what Gmail uses (eg id:x before:2012-12-12) that you then parse in the resolver.
export default gql`
extend type Query {
articles: [Article!]
article(search: String): Article
}
type Article {
id: ID!
slug: String!
title: String!
description: String!
text: String!
}
`;
A third option is to set up a separate search query that can return several types:
export default gql`
extend type Query {
articles: [Article!]
search(query: String!, type: SearchType): SearchResult
}
union SearchResult = Article | User
enum SearchType {
ARTICLE
USER
}
type Article {
id: ID!
slug: String!
title: String!
description: String!
text: String!
}
type User {
id: ID!
email: String!
name: String!
}
`;