I am trying out Relay and GraphQL. When I am doing the schema I am doing this:
let articleQLO = new GraphQLObjectType({
name: 'Article',
description: 'An article',
fields: () => ({
_id: globalIdField('Article'),
title: {
type: GraphQLString,
description: 'The title of the article',
resolve: (article) => article.getTitle(),
},
author: {
type: userConnection,
description: 'The author of the article',
resolve: (article) => article.getAuthor(),
},
}),
interfaces: [nodeInterface],
})
So, when I ask for an article like this:
{
article(id: 1) {
id,
title,
author
}
}
Will it do 3 queries to the database? I mean, each field has a resolve method (getTitle, getAuthor, etc.) which does a request to the database. Am I doing this wrong?
This is an example of getAuthor (I use mongoose):
articleSchema.methods.getAuthor = function(id){
let article = this.model('Article').findOne({_id: id})
return article.author
}
If the resolve method is passed the article, can't you just access the property?
let articleQLO = new GraphQLObjectType({
name: 'Article',
description: 'An article',
fields: () => ({
_id: globalIdField('Article'),
title: {
type: GraphQLString,
description: 'The title of the article',
resolve: (article) => article.title,
},
author: {
type: userConnection,
description: 'The author of the article',
resolve: (article) => article.author,
},
}),
interfaces: [nodeInterface],
})
Since Schema.methods in Mongoose defines methods on the model, it wouldn't take an ID for the article (because you call it on an article instance). So, if you wanted to keep the method, you would just do:
articleSchema.methods.getAuthor = function() {
return article.author;
}
If it was something you need to look up e.g. in another collection, then you'd need to do a separate query (assuming you're not using refs):
articleSchema.methods.getAuthor = function(callback) {
return this.model('Author').find({ _id: this.author_id }, cb);
}
Related
When I run the following code, I get the object along with the populated fields logged on the console.
Screenshot
But, the fields have not been populated in the books collection. Can someone please help me figure this out?
const bookSchema = new Schema({
title: String,
genre: { type: Schema.Types.ObjectId, ref: "genre" },
author: { type: Schema.Types.ObjectId, ref: "author" },
numberInStock: { type: Number, default: 0 },
rating: Number,
yearPublished: Number,
dateAdded: { type: Date, default: Date.now },
liked: { type: Boolean, default: false },
});
const genreSchema = new Schema({ name: String });
const authorSchema = new Schema({ name: String });
const Book = model("book", bookSchema);
const Genre = model("genre", genreSchema);
const Author = model("author", authorSchema);
const books = [
{
title: "Sapiens",
genre: "632873144b0bbfc10ae1942d",
author: "632873e706fe265eaee77de3",
numberInStock: 6,
rating: 4.4,
yearPublished: 2011,
},
];
async function saveBook(b) {
let book = new Book(b);
book
.save()
.then((result) => {
populateBook(result._id);
})
.catch((err) => console.log("Error: ", err));
}
function populateBook(id) {
Book.findById(id)
.populate("genre")
.populate("author")
.exec((err, book) => {
if (err) {
console.log("Error: ", err);
return;
}
console.log(book);
});
}
books.forEach((b) => {
saveBook(b);
});
That's how population works, it only stores references to other documents in the database. At query time, and if you ask for it (using .populate()), Mongoose will retrieve the referenced documents and insert them into the "parent" document.
If you want the referenced documents to be stored in the database, you can't use population but have to use subdocuments.
However, this will limit the flexibility of your database, because if for example an author name needs to be changed, you need to change all the Book documents in your database to update the author's name. With population, you only need to change the Author document.
I'm trying to create delete operation. I have a user and tasks to do. Each user has a list of tasks assigned. Here is my code of delete operation:
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
deleteUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) }
},
resolve(parent, args){
var id = (args.id).toString()
let todos = Todo.find({ user_id: id });
let user = User.findById(args.id);
todos.remove();
user.findOneAndRemove();
return todos;
}
}
}
})
And here are my defined types:
const ToDoType = new GraphQLObjectType({
name: 'todoItem',
fields: () => ({
id: {type: GraphQLID},
title: {type: GraphQLString},
completed: {type: GraphQLBoolean},
user: {
type: UserType,
resolve(parent, args){
return User.findById(parent.user_id);
}
}
})
});
const UserType = new GraphQLObjectType({
name: 'user',
fields: () => ({
id: {type: GraphQLID},
name: {type: GraphQLString},
email: {type: GraphQLString},
login: {type: GraphQLString},
todos: {
type: new GraphQLList(ToDoType),
resolve(parent, args){
return Todo.find({ user_id: parent.id })
}
}
})
});
The problem is that while deleting user I want to delete all his "to do's" but according to what I put in return I can delete only one thing.. So now I have "return todos" and it deletes only todo assigned to user, but not the user. When I put there "return user" it will delete only user without his tasks. How can I delete user and his tasks at once?
The code as shown appears to actually mutate the data to delete both the user, and all their todos. I believe the confusion you're having is that the mutation operation has the "type" (i.e. what it will return) of User. That is therefore what you'd need to return from the mutation function.
More conceptually, it might be a good idea to return from that both the user and all their data, so that if the API user wanted to capture some or all of that, they could, though given privacy implications that might not be ideal. An alternative approach would be to have the mutation just return a success-or-failure status.
I have a problem in my express project that I can't resolve since a day. I can't push some data into array element. Let me demonstrate my code and data.
Here is my result data which coming from mongodb:
result = {
name: 'Workflow',
steps:[
{ name: 'First Step',
assignee: '2cb56eadab3fbdc46dcb896e2ec68f33'
},
{
name: 'Second Step',
assignee: '1h374jab3fbdc46wer896e2ec687as'
}
],
__v: 0
}
Here is my code block:
var save = function(data, next) {
return new Promise(function(resolve) {
if (_.isEmpty(data._id)) {
Workflow.create(data, function (err, result) {
if (err) return next(err);
result.steps.forEach(function(step) {
step.detail = {
fullName: 'blablabla',
avatar: 'blablabla'
}
});
resolve(result);
});
}
}
}
After running code block my expectation is:
result = {
name: 'Workflow',
steps:[
{ name: 'First Step',
assignee: '2cb56eadab3fbdc46dcb896e2ec68f33',
detail: {
fullname: 'blablabla',
avatar: 'blablabla'
}
},
{
name: 'Second Step',
assignee: '1h374jab3fbdc46wer896e2ec687as',
detail: {
fullname: 'blablabla',
avatar: 'blablabla'
}
}
],
__v: 0
}
I can't get my expectation from result data and can't understand why detail is not assign steps array elements?
You can't directly modify the objects that MongoDB gives you, they're frozen.
You can copy the object and assign to the copy:
const copy = {...result, steps: result.steps.map(step => {
const stepCopy = {...step};
stepCopy.detail =
fullName: 'blablabla',
avatar: 'blablabla'
};
return stepCopy;
})};
resolve(copy);
That can actually be written more concisely, but it starts getting hard to read:
const copy = {...result, steps: result.steps.map(step => (
{...step, detail: {
fullName: 'blablabla',
avatar: 'blablabla'
}}
)};
resolve(copy);
Or, since I notice you're using ES5 syntax (but presumably with polyfills):
var copy = Object.assign({}, result);
copy.steps = copy.steps.map(function(step) {
var stepCopy = Object.assing({}, step);
stepCopy.detail = {
fullName: 'blablabla',
avatar: 'blablabla'
};
return stepCopy
)};
resolve(copy);
You'll need a polyfill for Object.assign (or I see Underscore/Lodash in your code, you can use _.extend instead, you literally just replace Object.assign with _.extend in the above).
You can do it in another way. Add detail object in the model itself. Set default value in the model definition.
I'm trying to return generic type in resolveType function if none of the provided types is matched. The example below shows this problem: API worked like a charm supporting UserType and MovieType until in database was added BookType (not supported by GraphQL schema).
const {
graphql,
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLNonNull,
GraphQLList,
GraphQLInterfaceType
} = require("graphql");
const DATA = [
{
// UserType
name: "catherine woolf",
nick: "catherine"
},
{
// MovieType
name: "cat woman",
director: "Jack Wolfgang"
},
{
// --- missing type --- (BookType)
name: "cats secrets",
author: "Nicky Glace"
}
];
const resolveType = data => {
if (data.nick) {
return UserType;
}
if (data.director) {
return MovieType;
}
};
const SearchableType = new GraphQLInterfaceType({
name: "Searchable",
fields: {
name: { type: GraphQLString }
},
resolveType: resolveType
});
const UserType = new GraphQLObjectType({
name: "User",
interfaces: [SearchableType],
fields: {
name: { type: GraphQLString },
nick: { type: GraphQLString }
}
});
const MovieType = new GraphQLObjectType({
name: "Movie",
interfaces: [SearchableType],
fields: {
name: { type: GraphQLString },
director: { type: GraphQLString }
}
});
const schema = new GraphQLSchema({
types: [MovieType, UserType, SearchableType],
query: new GraphQLObjectType({
name: "RootQueryType",
fields: {
search: {
type: new GraphQLList(SearchableType),
args: {
text: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(_, { text }) {
return DATA.filter(d => d.name.indexOf(text) !== -1);
}
}
}
})
});
const query = `
{
search(text: "cat") {
name
... on User {
nick
}
... on Movie {
director
}
}
}
`;
graphql(schema, query).then(result => {
console.log(JSON.stringify(result, null, 2));
});
So now this code ends with error:
"Abstract type Searchable must resolve to an Object type at runtime for field RootQueryType.search with value \"[object Object]\", received \"undefined\". Either the Searchable type should provide a \"resolveType\" function or each possible types should provide an \"isTypeOf\" function."
This is nothing surprising since currently resolveType may not return any type.
Workaround
Crate type containing the same field like interface SearchableType (1 to 1 implementation):
const _SearchableType = new GraphQLObjectType({
name: '_Searchable',
interfaces: [SearchableType],
fields: {
name: { type: GraphQLString },
}
});
Use it as a fallback type:
const resolveType = data => {
if (data.nick) {
return UserType;
}
if (data.director) {
return MovieType;
}
return _SearchableType;
};
And add it to types in schema definition:
types: [MovieType, UserType, SearchableType, _SearchableType],
But the problem with this solution is presence of this dummy _SearchableType in documentation like this:
Question
Is there any way to return interface SearchableType or equivalent of it in resolveType? The key point for me is hiding of this "fallback type" in a documentation.
GraphQL is strongly typed and doesn't support generics or some kind of "fallback" mechanism when resolving unions and interfaces. At the end of the day, if your underlying data layer is returning some type that you have not yet implemented in your schema, the simplest solution is to simply add that type to your schema. Migrations to your database and changes to your schema should go hand-in-hand.
If you want to derive your schema from your storage layer, I would suggest looking into something like PostGraphile (formerly PostGraphQL).
That said, if you're bent on using a workaround, you could just fallback to one of the existing types:
const resolveType = data => {
if (data.nick) {
return UserType
}
return MovieType
}
Now a book's name will still be accessible, provided you query it on the interface and not one of the types. The only downside to this approach is that movie-specific fields will be returned for a book and will resolve to null, but that won't cause any issues unless they're specifically defined as non-null in your schema.
Assume I have the following code as graphql schema. A userType including id and name for users, and there's two kind of queries: allUsers: [userType] and user(id: Int!): userType.
let db = [{
id: 1,
name: 'Amir'
}, {
id: 2,
name: 'John'
}];
const userType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
});
const queryType = new GraphQLObjectType({
name: 'Query',
fields: {
allUsers: {
type: new GraphQLList(userType),
resolve: () => db
},
user: {
type: userType,
args: {
id: { type: new GraphQLNonNull(GraphQLInt) }
},
resolve: (_, { id }) => db.find(user => user.id == id);
}
}
})
let schema = new GraphQLSchema({ query: queryType });
I need to modify this structure at boot time. I mean before actually executing the last line.
To add more kind of queries, I deferred the schema creation (new GraphQLSchema(...)) to the end, after all the modifications are done. So I can add more fields to the query itself or perhaps modify existing ones.
But how can I modify the types that are already defined? Basically, I need to add other fields to userType, like permissions, which itself is a GraphQLObjectType and has its own resolve function.
Something like this:
let queryFields = {};
const userType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
});
queryFields['allUsers'] = {
type: new GraphQLList(userType),
// ...
}
queryFields['user'] = {
type: userType,
//...
}
/* HERE <---------------------------------------- */
userType.fields.permission = {
type: GraphQLString,
resolve: user => getPermissionsFor(user);
}
const queryType = new GraphQLObjectType({
name: 'Query',
fields: queryFields
})
var schema = new GraphQLSchema({ query: queryType });
Thanks!
What I have done at the end is to add another layer between the logic of my app and GraphQL. So I have created another library that holds the information about the schema and the types, and it has an API to modify the existing types in the schema. Once all the modifications are in place, we can extract a GraphQL schema from the library.
That's the whole idea. For implementation detail, I have wrote a post here: Distributed Schema Creation in GraphQL