Search code examples
mongodbmongoosegraphqlexpress-graphql

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.


Solution

  • So the good news is you can do whatever you want. The bad news is that:

    1. You have to do it yourself
    2. 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.