This past year I converted an application to use Graphql. Its been great so far, during the conversion I essentially ported all my services that backed my REST endpoints to back grapqhl queries and mutations. The app is working well but would like to continue to evolve my object graph.
Lets consider I have the following relationships.
User -> Team -> Boards -> Lists -> Cards -> Comments
I currently have two different nested schema: User -> team:
type User {
id: ID!
email: String!
role: String!
name: String!
resetPasswordToken: String
team: Team!
lastActiveAt: Date
}
type Team {
id: ID!
inviteToken: String!
owner: String!
name: String!
archived: Boolean!
members: [String]
}
Then I have Boards -> Lists -> Cards -> Comments
type Board {
id: ID!
name: String!
teamId: String!
lists: [List]
createdAt: Date
updatedAt: Date
}
type List {
id: ID!
name: String!
order: Int!
description: String
backgroundColor: String
cardColor: String
archived: Boolean
boardId: String!
ownerId: String!
teamId: String!
cards: [Card]
}
type Card {
id: ID!
text: String!
order: Int
groupCards: [Card]
type: String
backgroundColor: String
votes: [String]
boardId: String
listId: String
ownerId: String
teamId: String!
comments: [Comment]
createdAt: Date
updatedAt: Date
}
type Comment {
id: ID!
text: String!
archived: Boolean
boardId: String!
ownerId: String
teamId: String!
cardId: String!
createdAt: Date
updatedAt: Date
}
Which works great. But I'm curious how nested I can truly make my schema. If I added the rest to make the graph complete:
type Team {
id: ID!
inviteToken: String!
owner: String!
name: String!
archived: Boolean!
members: [String]
**boards: [Board]**
}
This would achieve a much much deeper graph. However I worried how much complicated mutations would be. Specifically for the board schema downwards I need to publish subscription updates for all actions. Which if I add a comment, publish the entire board update is incredibly inefficient. While built a subscription logic for each create/update of every nested schema seems like a ton of code to achieve something simple.
Any thoughts on what the right depth is in object graphs? With keeping in mind the every object beside a user needs to be broadcast to multiple users.
Thanks
GraphQL's purpose is to avoid a couple of queries, so I'm sure that making the nested structure is the right way. With security in mind, add some GraphQL depth limit libraries.
GraphQL style guides suggest you have all complex structures in separate Object Types ( as you have, Comment, Team, Board... ).
Then making a complex query/mutation is up to you.
I'd like you to expand this sentence
Which if I add a comment, publish the entire board update is
incredibly inefficient
I'm not sure about this as you have your id of the Card. So adding new comment will trigger mutation which will create new Comment record and update Card with the new comment.
So your structure of data on the backend will define the way you fetch it but not so much the way you mutate it.
Take a look at the GitHub GraphQL API for example:
each of the mutations is a small function for updating/creating piece of the complex tree even if they have nested structure of types on the backend.
In addition for general knowledge of what are approaches for designing the mutations, I'd suggest this article.
You can use nesting in GraphQL like
type NestedObject {
title: String
content: String
}
type MainObject {
id: ID!
myObject: [NestedObject]
}
In the above code, the type definition of NestObject gets injected into the myObject array. To understand better you can see it as:
type MainObject {
id: ID!
myobject: [
{
title: String
content: String
}
]
}
I Hope this solves your problem!
Related
am trying to persist a new node in graphql using Apollo client. This new node should attach to an existing node. Not quite sure how to go about it. Suppose I have these type definitions:
type Person{
personId: ID!
name: String!
user: User #relationship(type: "LOGS_IN_AS", direction: OUT)
}
type User{
userId: ID!
username: String!
password: String!
person: Person!
}
Now assuming I already have a person in the database as follows:
{
personId: 1,
name: "John Doe"
}
How do I mutate a corresponding User node for this Person and ensure the necessary relationship is created using Apollo's auto generated mutations? Am using a neo4j backend by the way.
Thanks in advance
Don't know how you set up neo4j database but supposedly User should have a personId to link with Person.
You should probably defined a seperate schema type called UpsertUserInput or something like below for the mutation so neo4j can use it to link with the person
type UpsertUserInput{
userId: ID!
personId: ID!
username: String!
password: String!
}
the Appollo schema is not responsible for defining where data comes from or how it's stored. It is entirely implementation-agnostic.
https://www.apollographql.com/docs/apollo-server/schema/schema/#the-schema-definition-language
I see there is a solution, just needed to look a little deeper into the auto-generated mutations in the studio. Solution along the lines of...
useMutation(CREATE_USER, {
variables:{
input:{
userName: "johndoe#example.com",
password: "jdoespassword",
person:{
connect:{
where:{
node:{
personId: 1
}
}
}
}
}
}
})
I'm following a tutorial where the teacher is using a String type for his createdAt fields and suggested that if we wanted, to use a custom scalar type for a stronger typed DateTime field so I'm trying to do just that.
I'm getting the following error: Error: Unknown type "GraphQLDateTime".
Here is the offending code:
const { gql } = require('apollo-server')
const { GraphQLDateTime } = require('graphql-iso-date')
module.exports = gql`
type Post {
id: ID!
username: String!
body: String!
createdAt: GraphQLDateTime!
}
type User {
id: ID!
email: String!
token: String!
username: String!
createdAt: GraphQLDateTime!
}
input RegisterInput {
username: String!
password: String!
confirmPassword: String!
email: String!
}
type Query {
getPosts: [Post]
getPost(postId: ID!): Post
}
type Mutation {
register(registerInput: RegisterInput): User
login(username: String!, password: String!): User!
createPost(body: String!): Post!
deletePost(postId: ID!): String!
}
`
I have added the graphql-iso-date library and VSCode's intellisense is picking that up so I know that's not the issue. It's also indicating that GraphQLDateTime is not being used anywhere in the file even though I'm referencing it.
I know this is probably an easy fix but I'm still new to NodeJS and GraphQL in the context of NodeJS. Any idea what I'm doing wrong? Also is there another DateTime scalar that might be more preferable (Best practices are always a good idea.) Thanks!
There's two steps to adding a custom scalar using apollo-server or graphql-tools:
Add the scalar definition to your type definitions:
scalar DateTime
Add the actual GraphQLScalar to your resolver map:
const { GraphQLDateTime } = require('graphql-iso-date')
const resolvers = {
/* your other resolvers */
DateTime: GraphQLDateTime,
}
Note that the key in the resolver map (here I used DateTime) has to match whatever you used as the scalar name in Step 1. This will also be the name you use in your type definitions. The name itself is arbitrary, but it needs to match.
See the docs for more details.
I’m using a prisma server and graphql-yoga (graphql server on top of node.js) and I’m working for the first time with a type that has an other type as property. It’s one way so no relation I believe. In my case, a Product can be in a FavoriteList (so belong?) but a FavoriteList doesn’t have a relation the other way around.
type Product {
id: ID!
name: String!
releaseDate: DateTime!
brand: Brand!
createdAt: DateTime!
image: Image!
barcode: String
}
type FavoriteList {
id: ID!
products: [Product]
}
On my prisma admin I can add a record to FavoriteList with connecting [Product] and in the playground I can query for that by using the FavoriteLists query. But when I create a query (and resolver) for my graphql-yoga server, products are resolving as null. The query I’m using:
# prisma server & graphql server
query favoriteList {
favoriteLists {
id
products {
id
name
}
}
}
My setup for the graphql-server
type Query {
// ... other queries
favoriteLists: [FavoriteList]
}
// resovler in Query.js
async favoriteLists(root, args, context, info) {
return context.prisma.favoriteLists({}, info)
},
So having the same query, I am wondering if I have to do something else in my graphql-server? From what I can see the Product is not resolving on the FavoriteList type. Thanks in advance.
I would disagree and say you should have a relationship between Product and FavouriteList. When you break it down, one Product "belongs to" one or many FavouriteList and one FavouriteList "has" many Product. So I'd define your datamodel like this:
type Product {
id: ID!
name: String!
releaseDate: DateTime!
brand: Brand!
createdAt: DateTime!
image: Image!
barcode: String
favouriteLists: [FavouriteList!]! #relation(name: "ProductToFavouriteList")
}
type FavoriteList {
id: ID!
products: [Product!]! #relation(name: "ProductToFavouriteList")
}
Your query and resolvers look ok except maybe your resolver should return context.prisma.query.favoriteLists({}, info)?
I am using graphql + prisma (in docker locally), nodejs and postgresql.
How can I make it add some fields like created/edited date?
For example . I have this type:
type Post {
id: ID! #unique
title: String!
content: String!
published: Boolean! #default(value: "false")
author: User!
}
How can i add field like date. Make it equal to date the element created/updated?
Make this change in datamodel.prisma:
type Post {
id: ID! #unique
title: String!
content: String!
published: Boolean! #default(value: "false")
author: User!
updatedAt: DateTime!
createdAt: DateTime!
}
Make this change in schema.graphql:
updatedAt: String!
createdAt: String!
There are 2 fields hidden by default on each type you create in Prisma, but they're always created and kept up to date in the background: createdAt: DateTime! and updatedAt: DateTime!. To expose them, simply add them to your type and try querying them, you should see that there's data already.
Also note that they can't be deleted, by removing them from your schema you hide them again.
Hope this helps.
I am trying to use Graphcool to design a backend for our product. We already have the domain model for the product.
The problem I am facing is with non scalar attributes where I am forced to use #relation (that too two way relation).
Help me design the following case
# projectId: cj4dsyc7ur8cc0142s3hesy1r
# version: 2
type File implements Node {
contentType: String!
createdAt: DateTime!
id: ID! #isUnique
name: String!
secret: String! #isUnique
size: Int!
updatedAt: DateTime!
url: String! #isUnique
}
type User implements Node {
createdAt: DateTime!
email: String #isUnique
id: ID! #isUnique
password: String
updatedAt: DateTime!
}
type Record implements Node {
createdAt: DateTime!
id: ID! #isUnique
name: String!
description: String!
createdBy: User!
modifiedBy: User!
}
For this I get the error
Errors
createdBy: The relation field `createdBy` must specify a `#relation` directive: `#relation(name: "MyRelation")`
modifiedBy: The relation field `modifiedBy` must specify a `#relation` directive: `#relation(name: "MyRelation")`
Then when I try to add the relation I am forced to do the following
# projectId: cj4dsyc7ur8cc0142s3hesy1r
# version: 2
type File implements Node {
contentType: String!
createdAt: DateTime!
id: ID! #isUnique
name: String!
secret: String! #isUnique
size: Int!
updatedAt: DateTime!
url: String! #isUnique
}
type User implements Node {
createdAt: DateTime!
email: String #isUnique
id: ID! #isUnique
password: String
updatedAt: DateTime!
}
type Record implements Node {
createdAt: DateTime!
id: ID! #isUnique
name: String!
description: String!
createdBy: User! #relation(name: "createdBy")
modifiedBy: User! #relation(name: "modifiedBy")
}
Which leads to following error
Errors
createdBy: A relation directive with a name must appear exactly 2 times.
modifiedBy: A relation directive with a name must appear exactly 2 times.
I understand for every non scalar outgoing graphcool expects to put up a 2 way relationship. But consider a use case for modifiedBy and createdBy (User), I can not put a relationship on User object for every entity I add.
What is a better way of doing this on GraphCool?
Also, why is this restriction put up by GraphCool, GraphQL doesn't dictate this restriction?
Cheers
Rohit
thanks for using Graphcool!
First things first. As you point out Graphcool enforces two-way relations when you want to connect types. In some cases you know up front that you only want to query the data in one direction, so it might seem unintuitive that you have to create a two-way relation.
The reason for this is that the Graphcool Permission System allows you to succinctly specify permission rules based on the structure of your data. In your case you might want to specify that only the creator of a post is allowed to delete a record.
In practice it turns out that very often you end up needing the reverse relation when you implement your permission rules. Enforcing two-way relations makes the entire programming model simpler to understand and sets you up to easily implement permission rules.
For your schema a solution would be to extend the User type as follows:
type User implements Node {
createdAt: DateTime!
email: String #isUnique
id: ID! #isUnique
password: String
updatedAt: DateTime!
createdRecords: [Record!]! #relation(name: "CreatedBy")
modifiedRecords: [Record!]! #relation(name: "UpdatedBy")
}
(please note that relation names must begin with a uppercase letter)
Hope that helps :-)