GraphQL - resolve JSON as array of graphql types to support batch querying - postgresql

I've got a collection of graphql services that are combined together in a gateway using graphql-tools schema stitching.
Here's some sample code for reference:
async function makeGatewaySchema() {
// define the remote executors, introspect the subschemas, etc.
return stitchSchemas({
subschemas: [
{
schema: postSubschema,
executor: postExecutor,
batch: true,
},
{
schema: userSubschema,
executor: userExecutor,
batch: true,
},
],
typeDefs: `
...
extend type User {
posts: [Post]
batchPostsJSON: JSON
batchPosts: [Post]
}
...
`,
resolvers: {
User: {
posts: {
selectionSet: "{ id }",
resolve(user, args, context, info) {
return delegateToSchema({
schema: schemaPost,
operation: "query",
fieldName: "postsByUserId",
args: { id: user.id },
context,
info,
});
},
},
batchPostsJSON: {
selectionSet: "{ id }",
resolve(user, args, context, info) {
return batchDelegateToSchema({
schema: schemaFavorite,
operation: "query",
fieldName: "postsByUserIds",
key: user.id,
argsFromKeys: (ids) => ({ ids }),
valuesFromResults: (results, keys) => {
return keys.map((id) => {
return results.filter((f) => f.user_id === id);
});
},
context,
info,
});
},
},
batchPosts: {
selectionSet: "{ id }",
resolve(user, args, context, info) {
return batchDelegateToSchema({
schema: schemaPost,
operation: "query",
fieldName: "postsByUserIds",
key: user.id,
argsFromKeys: (ids) => ({ ids }),
valuesFromResults: (results, keys) => {
// doesn't matter, we don't get here
},
context,
info,
});
},
},
},
},
});
}
In the example code above, I have three ways to get posts associated with a user:
1: User.posts
This works fine but isn't batched
2: User.batchPostsJSON
This is batched very nicely and works perfectly with one problem: the JSON return type doesn't allow me to query Post fields - I just get all of them no matter what. Worse, if Post was related to some third type, I wouldn't be able to follow that relationship.
3: User.batchPosts
This allows me to query fields of Post, but throws an exception - I've defined the return type as array of Post, but JSON is what comes back and I get an error in the gateway.
Is there a way to handle the JSON returned by the subschema and pretend that I'm really getting [Post] back instead? By the time my valueFromResults is finished, that IS what it will look like. The problem is that the return type mismatch between my typedef and the actual resolver throws an error before I have the chance to reformat the return value.

Related

graphql.GraphQLSchema: what type of argument to use to grab the query to pass to mongo db.collection.find to resolve the query

I'm learning ho to develop GraphQL service with express, express-graphql, **graphql, mongoose,
db.collection.find has an optional query parameter that specifies selection filter using query operators.
I wonder if it is possible to define a schema in which to define an argument for a query field that ultimately it is passed as it is to the collection find methods.
for example I expect that the graphql query:
{ todosQuerable(query: {title: "Andare a Novellara"})
{ _id, title, completed }
}
responds with:
{
"data": {
"todos": [
{
"title": "Andare a Novellara",
"completed": false
}
]
}
}
since in mongo
> db.Todo.find({title: 'Andare a Novellara'})
{ "_id" : ObjectId("600d95d2e506988bc4430bb7"), "title" : "Andare a Novellara", "completed" : false }
I'm thinking something like:
todosQuerable: {
type: new graphql.GraphQLList(TodoType),
args: {
query: { type: <???????????????> },
},
resolve: (source, { query }) => {
return new Promise((resolve, reject) => {
TODO.find(query, (err, todos) => {
if (err) reject(err)
else resolve(todos)
})
})
}
}
I have made a few attempts but have not been able to get an idea of which type I should use in this case
ho help reproduce the problem here the source repository of my tests
Please note that this works fine:
todosByTitle: {
type: new graphql.GraphQLList(TodoType),
args: {
title: { type: graphql.GraphQLString },
},
resolve: (source, { title }) => {
return new Promise((resolve, reject) => {
TODO.find({title: {$regex: '.*' + title + '.*', $options: 'i'}}, (err, todos) => {
if (err) reject(err)
else resolve(todos)
})
})
}
}
but what I'm looking for is something more generic: I would like to grab graphql field argument named query and pass it as is to the the query parameter of the mongo collection find.
So the good news is you can do whatever you want. The bad news is that:
You have to do it yourself
You have to add every searchable field, so you'll probably end up with two copies of the Todo object here.
The type you're looking for is just a custom input object type like this:
Notice the GraphQLInputObjectType below is different from GraphQLObjectType.
var TodoQueryType = new graphql.GraphQLInputObjectType({
name: 'TodoQuery',
fields: function () {
return {
_id: {
type: graphql.GraphQLID
},
title: {
type: graphql.GraphQLString
},
completed: {
type: graphql.GraphQLBoolean
}
}
}
});
todosQuerable: {
...
type: new graphql.GraphQLList(TodoType),
...
args: {
query: { type: TodoQueryType },
},
...
}
These two queries work great!
(this is me using aliases so I can make the same query twice in one call)
{
titleSearch: todosQuerable(query:{ title:"Buy orange" }) {
_id
title
completed
}
idSearch: todosQuerable(query:{ _id:"601c3f374b6dcc601890048d" }) {
_id
title
completed
}
}
Footnote:
Just to have it said, this is generally a GraphQL anti-pattern, as this is building an API based on your database choices, rather than as a client-driven API.
Regex Edit as requested:
If you're trying to do regular expression lookups, you have to figure out how to programmatically convert your strings into regular expressions. i.e. your input is a string ("/Novellara/"), but mongoose requires passing a RegExp to do wildcards (/Novellara/, no quotes).
You can do that a number of ways, but I'll show one example. If you change your input fields to use two properties of value & isExpression, like below, you can do it, but you have to specifically craft your query, since it's no longer just a passthrough.
var ExpressableStringInput = new graphql.GraphQLInputObjectType({
name: 'ExpressableString',
fields: {
value: {
type: graphql.GraphQLString
},
isExpression:{
type: graphql.GraphQLBoolean,
defaultValue: false,
}
}
})
var TodoQueryType = new graphql.GraphQLInputObjectType({
name: 'TodoQuery',
fields: function () {
return {
_id: {
type: graphql.GraphQLID
},
title: {
type: ExpressableStringInput
},
completed: {
type: graphql.GraphQLBoolean
}
}
}
});
// resolver
todosQuerable: {
type: new graphql.GraphQLList(TodoType),
args: {
query: { type: TodoQueryType },
},
resolve: async (source, { query }) => {
const dbQuery = {};
if (query.title.isExpression) {
dbQuery.title = new RegExp(query.title.value);
} else {
dbQuery.title = query.title.value;
}
return new Promise((resolve, reject) => {
TODO.find(dbQuery, (err, todos) => {
if (err) reject(err)
else resolve(todos)
})
})
}
}
your query would then look like
query {
todosQuerable(query:{ title: { value: "Buy.*", isExpression: true }}) {
_id
title
completed
}
}
This query makes sense in my mind. If I think about the form you would show to a user, there is probably an input box and a checkbox that says "is this a regular expression?" or something, which would populate this query.
Alternatively, you could do like string matching: if the first and last characters are "/", you automagically make it into a regex before passing it into mongoose.

Why I'm I getting an error saving date using graphql > hasura > postgres

I'm using react, apollo, graphql, hasura, postgres as my stack to interact with the database. I think my issue is something small, so I'll just focus on the part that's not working rather than posting the whole code.
Thanks.
Error: GraphQL error: unexpected variables in variableValues: birthday
at new ApolloError (bundle.esm.js:63)
at Object.next (bundle.esm.js:1004)
at notifySubscription (Observable.js:135)
at onNotify (Observable.js:179)
at SubscriptionObserver.next (Observable.js:235)
at bundle.esm.js:866
at Set.forEach (<anonymous>)
at Object.next (bundle.esm.js:866)
at notifySubscription (Observable.js:135)
at onNotify (Observable.js:179)
at SubscriptionObserver.next (Observable.js:235)
at bundle.esm.js:76
variables{ id: 2324324, name: "Fred", birthday: "1991-01-11" }
If i remove birthday the query works.
Here is the function
const onUpdateUser = (options) => {
updateUser({
variables: Object.assign({ id: userId }, options),
optimisticResponse: {
__typename: "mutation_root",
update_users: {
__typename: "users_mutation_response",
affected_rows: 1,
returning: [
{
__typename: "users",
id: userId,
...options,
},
],
},
},
});
};
input {birthday: '1991-01-11'}
So without looking at your graphql query, I think you may be thinking of it a little bit off.
You can't dynamically add non-existent variables to a graphql query. The error is telling you that you are trying to add a variable that doesn't exist in your query
i.e. this with NOT work because you haven't defined birthday.
mutation updateUser(
$userId: Int!
$birthday (UNDEFINED)
) {
rest of query...
}
If you need to add a dynamic amount of variables, you could do something like this.
React Code
const onUpdateUser = (options) => {
updateUser({
variables: {
userId,
userVariables: options
},
optimisticResponse: {
__typename: "mutation_root",
update_users: {
__typename: "users_mutation_response",
affected_rows: 1,
returning: [
{
__typename: "users",
id: userId,
...options,
},
],
},
},
});
};
GraphQL mutation
mutation updateUser(
$userId: Int!
$userVariables: user_set_input!
) {
update_user(
where: { id: { _eq: $userId} }
_set: $userVariables
) {
affected_rows
}
}
https://hasura.io/docs/1.0/graphql/manual/mutations/update.html

Pulling an object from user Model using $pull , having issues with multiple object items

I am trying to delete a post object from a user model, I hold these refrences to the post they have created, this is how I am trying to currently pull the post
userModel.findOneAndUpdate(
{ email: req.query.email, posts: req.query.postid },
// { $pull: { posts: req.query.postid } },
{ $pull: { posts : { number: mongoose.Types.ObjectId(req.query.postid) } }},
{ new: true },
function (error, user) {
if (error) {
res.json("error in /testing backend ===",error)
}
console.log(`Post id ===== ${req.query.postid}`);
console.log(`Email===== ${req.query.email}`);
console.log(`returning user====${user}`)
res.json('Successfully updated user');
}
);
this is how I have created the post
userModel.findOne({ email: req.body.author }, function(error, user) {
const locationURL = req.files.map((item) => item.location);
postModel.create({ ...req.body, image: locationURL }, (error, returnedDocuments) => {
if (error) {
throw new Error(error);
}
user.posts.push({ number: returnedDocuments._id, title: req.body.title, image: locationURL });
user.save((err) => {
console.log(err);
});
});
I originally had only 1 item pushed into the user model, but added a few more items, then I was having issues pulling the object, thanks for your help.
this is from my DB as to my posts array
For an array of objects, you can pull your desired document using the positional operator { "<array>.$" : value }.
{ $pull: { posts.$.number : req.query.postid }}
You can check out the docs on positional operators to learn more.

graphql query return object with null id

Graphql return Oject with null id.
with mongodb.
It looks strange to me.
If I delete new GraphQLNonNull() on MailType id,
It works with id: null, another fields working fine.
const MailType = new GraphQLObjectType({
name: 'Mail',
fields: () => ({
id: { type: new GraphQLNonNull(GraphQLID), },
...
})
const Query = {
mails: {
type: new GraphQLList(MailType),
args: {
senderId: { type: GraphQLID },
isOffline: { type: GraphQLBoolean },
},
async resolve(root, args, req, ctx) {
if (args.isOffline === false) {
let a = await model.aggregate([
{ $match: { isOffline: false } },
]);
let b = await model.find({ isOffline: false });
console.log(JSON.stringify(a) == JSON.Stringify(b)) /// return true
return a // error
return b // working
}
return model.find({senderId: args.senderId});
}
}
}
// with a
"errors": [
{
"message": "Cannot return null for non-nullable field Mail.id."
}]
I am in trouble for 2 hours but I do not get the answer.
Can anybody help me?
You probably have a mistake in your mongodb schema, not in graphQl.
make sure you did not define you id by id key, it should be _id.
for example if you are using mongoose it can be something like this:
const MailSchema = new Schema({
_id: {
type: String,
unique: true,
},
....
....
});

The method findOneAndUpdate change the id of my element in a array

I'm working with mongoDB, mongoose and graphQL. I'm trying to make an update in my DB.
I'm doing an update in an array called phones, the changes work perfectly, the only problem is that when the update ends, the value of the objectId changes.
// Models -> Schema Organization
const organizationSchema = new mongoose.Schema({
name: String,
address: String,
phones: [
{
number: Number,
prefix: Number
}
],
email: String
})
// Types -> Organization
type Response {
success: Boolean!
token: String
errors: [Error]
}
type Error {
path: String!
message: String!
}
input iOrganization {
_id: ID
arrID: ID
address: String
email: String
number: Int
prefix: Int
name: String
}
type Mutation {
updateOrgGeneric(iOrg: iOrganization): Response!
}
// Resolvers -> Organization (1st way)
Mutation: {
updateOrgGeneric: (parent, args, {models}) => {
return models.Organization.findOneAndUpdate(
{ "_id": args.iOrg._id, "phones._id": args.iOrg.arrID },
{ $set: { "phones.$": { number: args.iOrg.number, prefix: args.iOrg.prefix }} },
{new: true}
)
.then((resp) => {
console.log(resp);
return {
success: true,
errors: []
}
})
.catch((error) => {
return {
success: false,
errors: error
};
})
},
}
// Resolvers -> Organization (2nd way)
Mutation: {
updateOrgGeneric: (parent, args, {models}) => {
return models.Organization.findOneAndUpdate(
{ "_id": args.iOrg._id },
{ $set: { "phones.$[arr]": { number: args.iOrg.number, prefix: args.iOrg.prefix }} },
{new: true}
{ arrayFilters:[{ "arr._id": mongoose.Types.ObjectId(args.iOrg.arrID) }], new: true}
)
.then((resp) => {
console.log(resp);
return {
success: true,
errors: []
}
})
.catch((error) => {
return {
success: false,
errors: error
};
})
}
}
// Playground (http://localhost:5000/graphql)
mutation {
updateOrgGeneric(
iOrg: {
_id: "5bdbee1b794b972bc8562aeb"
arrID: "5bdcea7cae88be098c020b19"
number: 85239,
prefix: 862
}
){
success
errors {
path
message
}
}
}
Both _id, as arrID, exist in the BD.
In the playground example the initial arrID was: _id:ObjectId("5bdcea7cae88be098c020b19"), but after the update is another, example: _id:ObjectId("5bdcec0a2ab78533b4bd1d98"). What am I doing wrong?
Thank you!
Mongodb is a nosql database which means that every object in the database should consist of an Id and revision values. Once an update occurs the revision value changes as part of the update process to implement the changes made to the data object. Since your data object don't have the revision value then the id value changes. Because it is unique. Now I'm no expert with mongo but you should check the docs on how to persist data objects and change accordingly
In case anyone lands here (despite this being old post), the problem probably lies in trying to update the entire phones object, of which the overwritten _id is a part. Since there's a model defined for phonesin mongoose, it will try to create a new _id any time an entire new phones object is created.
Someone who wanted to keep the same id would need to $set only the fields they want to change, rather than the entire object. So
{ $set: { "phones.$[arr]": { number: args.iOrg.number, prefix: args.iOrg.prefix }} }
could be changed to
{ $set: { "phones.$[arr].number": args.iOrg.number, "phones.$[arr].prefix": args.iOrg.prefix } }