Modify GraphQLObjectType fields at runtime - javascript

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

Related

Graphql Delete operation using mutation

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.

How do I update an Object inside an array inside another object with mongoose

I have the following structure
{
"serverId": serverId,
"name": serverName,
"members":[
{
"birthday":{
"$date":{
"$numberLong":birthDate
}
},
"user": userName,
},
{
"birthday":{
"$date":{
"$numberLong":birthDate2
}
},
"user": userName2,
}
]
}
}
I'm not sure how to use Mongoose to update the birthday of one of the members.
I have two models for the Objects that i'm storing:
const { Schema, model } = require('mongoose')
const memberSchema = Schema({
birthday: {
default: '',
type: Date,
},
user: {
default: '',
type: String,
},
})
module.exports = model('Member', memberSchema, 'memberTest')
const mongoose = require('mongoose')
const serverSchema = mongoose.Schema({
serverId: {
default: '1',
type: String,
},
name: {
default: 'emptyServer',
type: String,
},
members: [],
})
module.exports = mongoose.model('Server', serverSchema, 'servers')
I've tried doing this:
const newBirthday = new Member({
user: message.author.tag,
birthday: date
})
let dbMember = dbServer.members.filter(member => member.user = message.author.tag)
if(dbMember){
dbServer.update({'members.user': message.author.tag}, {'$set': {
'members.$.user': message.author.tag,
'members.$.birthday': date
}})
}else{
dbServer.members.push(newBirthday)
}
dbServer.save()
but it doesn't update the members on the DB.
After I tested this, I could see the update on the object (by logging it) but when I look into de DB the changes are not there. I'm not entirely sure if what I'm doing is really off tracks

How to push an object into an array with GraphQL?

I would like to change the way my resolver is creating the payment cards in my DB. So now I create the wallet in one collection then a payment in another collection and use the wallet_id to in my payment to link them. But now I want to push the payments into a cards[] that is defined in wallet. Any idea how to do that in resolver?
This is my Wallet schema
const WalletSchema = new Schema({
tokens: {
type: Number,
default: 0
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
unique: true
},
cards: [
{
type: Schema.Types.ObjectId,
ref: 'Payment'
}
]
}, { timestamps: true });
and this is my createPayment resolver
createPayment: async (_, { wallet_id, ...args }, { user }) => {
try {
await requireAuth(user);
const payment = await Payment.create({ ...args, wallet: wallet_id });
pubsub.publish(PAYMENT_ADDED, { [PAYMENT_ADDED]: payment });
return payment;
} catch (error) {
throw error;
}
},
Any idea?
You need to create another Schema for card payments;
const WalletSchema = new Schema({
tokens: {
type: Number,
default: 0
},
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
unique: true
},
cards: [ PaymentSchema ] <-- here
}, { timestamps: true });
// new schema
const PaymentSchema = new Schema({
type: Schema.Types.ObjectId,
ref: 'Payment'
});
Based on Wallet schema you need to write expose graphql schema

GraphQL.js - use Interface as a default (fallback) type in resolveType function

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.

How does Relay / GraphQL 'resolve' works?

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);
}

Categories