Django Graphene writing mutations with multiple layers of nested foreign keys - python-3.x

How do you write the schema and query for nested foreign keys? I checked the docs and found no examples of how to do this. So here was my attempt based on github and stackoverflow answers lets say I have these models:
class Address(models.Model):
name = models.CharField()
class Person(models.Model):
name = models.CharField()
address = models.ForeignKey('Address', on_delete=models.CASCADE, blank=False, null=False)
class Blog(models.Model):
person = models.ForeignKey('Person', on_delete=models.CASCADE, blank=False, null=False)
text = models.TextField()
I tried writing a schema like this:
class AddressInput(graphene.InputObjectType):
name = graphene.String(required=True)
class PersonInput(graphene.InputObjectType):
name = graphene.String(required=True)
address =graphene.Field(AddressInput)
class CreateNewBlog(graphene.Mutation):
blog=graphene.Field(BlogType)
class Arguments:
address_data = AddressInput()
person_data = PersonInput()
text = graphene.String()
#staticmethod
def mutate(root, info, person_data=None, address_data=None, **input):
address = Address.objects.create(name=address_data.name)
person = Person.objects.create(address=address, name=person_data.name)
blog = Blog.objects.create(person =person, text=input['text'])
blog.save()
return CreateNewBlog(blog=blog)
and I used a query like this:
mutation {
CreateNewBlog(person: { address: {name: "aaa"},
name: "First Last" }, text: "hi hi") {
Blog {
person{
name
address{
name
}
},
text
}
}
}
I got this error message:
{
"errors": [
{
"message": "'NoneType' object has no attribute 'name'",
"locations": [
{
"line": 32,
"column": 9
}
],
"path": [
"CreateNewBlog"
]
}
],
"data": {
"CreateNewBlog": null
}
}
I think the issue is in the way I wrote the schema.py file. Where it does not work to nest InputFields inside another InputField. Is there any other ways to write a single mutation?

Okay, a few things here. Firstly, you should generate your schema.graphql file, because that'll show you the actual final shape of the schema being built by Graphene, which would've made your debugging easier. Or you could use GraphiQL to test out your queries and lets its documentation and autocomplete do the heavy lifting for you.
But on to the specifics, your Graphene mutation definition is going to be generating a mutation that looks like this:
input AddressInput {
name: String!
}
input PersonInput {
name: String!
address: AddressInput
}
type CreateNewBlogOutput {
blog: Blog
}
type Mutation {
CreateNewBlog(addressData: AddressInput, personData: PersonInput, text: String): CreateNewBlogOutput!
}
Worth noting that there are two ways for you to supply an AddressInput here, one at root, and one inside PersonInput. This probably isn't what you're intending to do. Secondly, none of the root arguments are required, which is contributing to your error message being fairly unhelpful, because the problem is you're calling the mutation incorrect parameters but the query validator is letting it through because your types are very permissive.
I believe that if you were to run the mutation like the following, it'd actually work:
mutation {
CreateNewBlog(
personData: {
address: {
name: "aaa"
},
name: "First Last"
},
text: "hi hi"
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
I only made two changes here, person was changed to personData (to match your mutation definition, Graphene does the conversation from snake case to camel case automatically), and Blog to blog in the field selection.
But lets go a little further, here's how I would have made the mutation.
class AddressInput(graphene.InputObjectType):
name = graphene.String(required=True)
class PersonInput(graphene.InputObjectType):
name = graphene.String(required=True)
address = AddressInput(required=True)
class CreateNewBlogInput(graphene.InputObjectType):
person = PersonInput(required=True)
text = graphene.String(required=True)
class CreateNewBlogPayload(graphene.ObjectType):
blog = graphene.Field(BlogType, required=True)
class CreateNewBlog(graphene.Mutation):
class Arguments:
input_data = CreateNewBlogInput(required=True, name="input")
Output = CreateNewBlogPayload
#staticmethod
def mutate(root, info, input_data):
address = Address.objects.create(name=input_data.person.address.name)
person = Person.objects.create(address=address, name=input_data.person.name)
blog = Blog.objects.create(person=person, text=input_data.text)
blog.save()
return CreateNewBlogPayload(blog=blog)
I'd also change CreateNewBlog to createNewBlog when constructing Graphene's mutation object, because the GraphQL convention is to use lower camel case for mutations.
Then you'd run it like this:
mutation {
createNewBlog(
input: {
person: {
address: {
name: "aaa"
},
name: "First Last"
}
text: "hi hi"
}
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
Why wrap the entire input in a single input field? Mainly because it makes calling the mutation easier in the client when using variables, you can just provide single input arg of the correct shape rather than multiple.
// So instead of this
mutation OldCreateNewBlog($person: PersonInput, $text: String) {
createNewBlog(
personData: $person
text: $text
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
// You have this
mutation NewCreateNewBlog($input: CreateNewBlogInput!) {
createNewBlog(
input: $input
) {
blog {
person {
name
address {
name
}
}
text
}
}
}
The latter makes it easier to change the input shape over time and only have to make the change in one place in client code.

Related

How can I automatically append a property from a relation to the root object?

How can I automatically append a property from a relation to the root object, as if it were a column from the same table but actually it is coming from another table.
Supose I have an User model that hasMany Emails.
How can I only append the email from the first Email of the User model, so that everytime I query the User model I get it like a property?
Example:
What I'm doing:
(await User.query().where('id', id).with('emails').first()).toJSON()
{
"name": "Eleandro Duzentos",
"emails": [
{ "email": "eleandro#inbox.ru" },
{ "email": "eleandro#mail.ru" }
]
}
What I want:
(await User.find(id)).toJSON()
{
"name": "Eleandro Duzentos",
"email": "eleandro#inbox.ru"
}
Obs: I'm not putting the email on the same table because, there's a chance that a user may need more then one email in a long future, but for now, it has only one.
How can I do that?
For the customized JSON response i would suggest the use of serializers.
You can override the default serializers to get the desired result.
You can refer to this - https://adonisjs.com/docs/4.0/serializers
Here is my code. You could be inspired by it:
Model User:
...
const Email = use('App/Models/Email')
class User extends Model {
async getEmails() {
let list = []
let emails = await Email.query().where('user_id', this.id).fetch()
emails.rows.forEach(email => {
list.push({ name: this.username, email: email.email })
});
return list
}
emails() {
return this.hasMany('App/Models/Email')
}
}
module.exports = User
Controller :
...
let user = await User.find(1)
return await user.getEmails()
Output :
[
{"name":"CrBast","email":"test#crbast.ch"},
{"name":"CrBast","email":"test2#crbast.ch"}
]
Feel free to correct me if that's not what you want :)

How to define a manytomany relationship on the same table in objectionjs?

I have an article table, where an article can cite multiple articles. Each article can also have a single author.
Article model :
class Article extends DefaultModel {
static get tableName() {
return "articles";
}
static get relationMappings() {
return {
author: {
relation: DefaultModel.BelongsToOneRelation,
modelClass: "Author",
join: {
from: "articles.authorId",
to: "authors.id"
}
},
mentions: {
relation: DefaultModel.ManyToManyRelation,
modelClass: "Article",
join: {
from: "articles.id",
through: {
from: "mentions.citingArticle",
to: "mentions.citedArticle"
},
to: "articles.id"
}
}
};
}
}
Mention model :
class Mention extends DefaultModel {
static get tableName() {
return "mentions";
}
static get idColumn() {
return ["citingArticle", "citedArticle"];
}
}
What I'm trying to do, is insertGraph, the main article + the articles that are mentioned in it, here is what I have :
async function insertData(fullArticle) {
const articleTx = await transaction(Article.knex(), async tx => {
const references = fullArticle.references
.map(articleObj => {
return {
...articleObj.article,
author: articleObj.author
};
});
const article = await Article.query(tx).insertGraph({
...fullArticle.article,
author: fullArticle.author,
mentions: references[0]
});
return article;
});
console.log(articleTx);
}
I know this is only inserting the main row + the first row, but I ran into multiple problems :
The mention model has a composite key, when I try to insert using the code above I get a (node:5944) UnhandledPromiseRejectionWarning: error: column "citedArticle" of relation "mentions" does not exist.
I also got the same error when I try to add "#id" to the main article mentions relation, and "#ref" to the cited article, and insert them in 2 different objects.
A third problem I ran into here, was a unique constraint on both the citing and the cited article authors. What if both of them were written by the same author? I have a unique string column in the authors table that isn't the id, when I tried to insert I got duplicate key value violates unique constraint "author_isn". I'm not sure how I can update the row to reference the existing author id if that happens.

GraphQL arguments on Object in query

I want to execute a query like this:
{
houses(owner: "Thomas") {
id
color
cars(type: "Sports Car") {
name
year
}
}
}
But this returns an error:
"message": "Unknown argument \"type\" on field \"cars\" of type \"House\".",
However, I'm able to execute this properly:
cars(type: "Sports Car") {
name
year
}
Is what I'm trying to do even possible?
Thanks in advance!
Make sure that you have your resolver for cars set up as a sub query of houses. The result from the houses query should be passed as the root argument to the cars sub query.
type House {
id
color
cars( type: String! ): [ Car ]
}
type Car {
name
year
}
Your resolver might look like:
Query: {
async houses( root, args, context ) {
return { ... houses ... }
}
},
houses: {
async cars( root, args, context ) {
return { ... cars ... }
}
}
Then create a resolver for cars that is nested underneath the houses query. Here's an article on it if you are using graphql-tools from Apollo: Writing resolvers with graphql-tools
Hope this helps!

How to connect list of objects to node in Relay

In GraphQL, I have the following structure
# represents any node with GlobalIdField
node(id: "uniqueIdOfNode") {
}
# represents actually logged user
viewer {
}
In sample MVC app schema todos are connected to the viewer so when querying you can get only those todos that belong to logged user.
However, in my case, I want to display data which is not related to a user. Let's say data is type Country which is NodeInterfaceType and I want to make a query where I ask for the list of countries. So in Relay, I can make fragment on CountryList where I get a country and pass it to React component using Relay.
If what I've written is not clear enough let me know because I'm confused with it and I'm not sure if I explained it well.
I've written GraphQL server in PHP but the code or hints can be written in node.js and I'll understand it as well.
How to do it?
Update
Some code:
schema.graphql
# A country
type Country implements NodeInterface {
# The ID of an object
id: ID!
countryId: Int
phonePrefix: String
name: String
timezone: String
}
# Representation of date and time in "Y-m-d H:i:s" format
scalar DateTime
interface NodeInterface {
# The ID of an object
id: ID!
}
type Query {
# Fetches an object given its ID
node(
# The ID of an object
id: ID!
): NodeInterface
viewer: User
countries: [Country]
}
type User implements NodeInterface {
# The ID of an object
id: ID!
username: String
userId: Int
lastLoginDateTime: DateTime
}
SampleRoute.js -used as the first route when someone opens React app
import Relay from 'react-relay';
export default class extends Relay.Route {
static queries = {
countries: () => Relay.QL`
query AppHomeRoute {
viewer
}
`,
};
static routeName = 'AppHomeRoute';
}
App.js I want to have countries list in Index component, Ideally in App js I don't want to event pass countries with props but I want them to be in Index component which will be renamed to CountryList in the future
import React, { Component } from 'react';
import Relay from 'react-relay';
import Index from './page/index';
import './App.css';
class App extends Component {
render() {
console.log(this.props);
return (
<Index country={this.props.countries} />
);
}
}
export default Relay.createContainer(App, {
fragments: {
countries: () => Relay.QL`
countries {
${Index.getFragment('country')}
}
}
// fragment F1 on Country {
// ${Index.getFragment('country')}
// },
`
}
});
Now I get an error:
SampleRoute.js:5 Uncaught Error: GraphQL validation error ``Cannot query field "viewer" on type "Query"
which I completely don't understand because when I query the server with graphiql then it works okay.
Query:
{
viewer {
id
username
}
node(id:"dXNlcjo2NjY=") {
id
username
}
countries {
name
}
}
response:
{
"data": {
"viewer": {
"id": "dXNlcjo2NjY=",
"username": "Robert"
},
"node": {
"id": "dXNlcjo2NjY=",
"username": "Robert"
},
"countries": [
{
"name": "AFGHANISTAN"
},
{
"name": "ALBANIA"
}, (...)
]
}
}

graphql - use queries in mutations - create a nested object

I have a very simple model with post that embeds several comments
I wondered how I should do a mutation to add a new comment to the post
As I already have queries defined to get back a postwith a given id, I wanted to try to have the following mutation syntax working
mutation {
post(id: "57334cdcb6d7fb172d4680bb") {
addComment(data: {
text: "test comment"
})
}
}
but I can't seem to find a way to make it work. Even if I'm in a mutation, output type being a post addComment is seen as a field post should have.
Do you guys have any idea ?
Thanks
You can't embed fields into other fields like that.
You would create a new input object for your post mutation
input CommentInput {
text: String
}
type Mutation {
post(id: ID!, addComment: CommentInput): Post
}
In your resolver you look for the addComment variable and call the addComment resolver with the arguments.
Your mutation would be
mutation {
post(id: "57334cdcb6d7fb172d4680bb",
addComment: {
text: "test comment"
}) {
id
comment {
text
}
}
}
I could be wrong but you may want to just create a separate updatePost mutation that accepts the post id as an argument
type Post {
comments: [ Comment ]
}
input PostInput {
comments: [ CommentInput ]
}
type Mutation {
updatePost( id: ID!, input: PostInput ): Post
}
The updatePost mutation here takes the id of the post and the updated post object as arguments and returns a type Post.
So I would use this like so:
mutation {
updatePost( id: '1234', input: { comments: []) {
id
}
}
Hope this helps!
Maybe you could create addComment mutation that you pass post id to and then return a Post.
type Mutation {
addComment( postId: ID!, input: CommentInput ): Post
}

Resources