Aggregation in Meteor with Apollo/Graphql

Hi, I have the following set up to obtain an aggregated query in Mongo as part of a Graphql resolver:

return collection.rawCollection().aggregate([
    { $match : { charID: args.charID } },
    { $unwind : "$enemies" },
    { $match : { "enemies.name" : args.name } }
]))

This is the error thrown in GraphiQL: Cannot return null for non-nullable field collection.charID. Take note that my data exists, the query works in Robo3T, and there isn’t an issue with my GraphQL schemas or anything of that sort. When I console log this entire object I see that it’s an Aggregation Cursor. How do I go about “converting” this back into something readable in Meteor’s context? .fetch() and .toArray have not worked thus far, and I’m not sure how to get around this.

Here are some images for context:
image
image
image
This is without aggregation:
image
This is with aggregation:

{
    "_id" : ObjectId("5fdf16c956d6a07c2880d8a3"),
    "charID" : "a",
    "enemy" : [ 
        {
            "name" : "enemyA",
            "weapon" : "weaponA",
            "type" : "metal"
        }
    ]
}

Should I just go with this package? https://github.com/meteorhacks/meteor-aggregate However, it’s not been updated in quite a while but then again it’s a very simple package.

Thanks!

The MongoDB aggregate method returns an aggregation cursor, which can be converted to a Promise to an array of documents using toArray. You’ll then need to resolve the Promise. For example:

return Promise.await(collection.rawCollection().aggregate([
    { $match : { charID: args.charID } },
    { $unwind : "$enemies" },
    { $match : { "enemies.name" : args.name } }
]).toArray())

Excuse possible typos - coding on my phone!

1 Like

This doesn’t seem to be working - here’s a full example of it. In my GraphQL schema, I have the following defined:

type UserRelation {
    date: String!
    userID: String!
}
type UserData {
    userID: String!
    description: String!
    friends_list: [UserRelation]!
}
extend type Query {
    getFriends(userID: String!, friendID: String): [UserRelation]
}

In my resolvers.js file I have:

import UserData from "./userData";
export default {
    Query: {
        getFriends(obj, args, context) {
            if (args.friendID) return Promise.resolve(UserData.rawCollection().aggregate([
                { $match: { userID: args.userID, "friends_list.userID":args.friendID}},
                { $project:
                     { friends_list : 
                         { $filter: { input: "$friends_list",
                                      as:"friends_list",
                                      cond: { $eq : ["$$friends_list.userID",args.friendID]}
                                     }
                         }, _id:0 
                     }
                }
            ]).toArray());
            return UserData.findOne({ userID: args.userID }, {fields: {_id: 0,  friends_list: 1 }}).friends_list;
        }
    }
}

This is a slightly more complex aggregation but basically for my data in this shape:

{
    "_id" : ObjectId("XXXXXXXXXXXXXX"),
    "userID" : "user1",
    "description" : "test",
    "friends_list" : [ 
        {
            "date" : "date1",
            "userID" : "friend1"
        }, 
        {
            "date" : "date2",
            "userID" : "friend2"
        }, 
        {
            "date" : "date3",
            "userID" : "friend3"
        }
    ]
}

I can extract a specific element in the friends_list array, so if I want to search for when user1 became friends with friend2 the query:

getFriends(userID:"user1",friendID:"friend2") {
    date
    userID
}

would return

{
    "friends_list" : [ 
        {
            "date" : "date2",
            "userID" : "friend2"
        }
    ]
}

But again even with the previous aggregation example, the return doesn’t work as intended. (The aggregations themselves are correct, as per how I’m doing it in Robo3T with direct MongoDB queries). The error thrown in Graphiql is: Cannot return null for non-nullable field UserRelation.userID. Isn’t a Promise supposed to catch its result in a .then() clause?

EDIT: I understand that there is a way using find() as per https://stackoverflow.com/questions/3985214/retrieve-only-the-queried-element-in-an-object-array-in-mongodb-collection to achieve what I’m trying to do. However there will be cases where I need to retrieve multiple matches and hence will need to use an aggregation. Furthermore, aggregation is extremely useful for more complex queries, so it’d be great if I learn to incorporate it properly.

I don’t know if I actually figured it out because I need to test it more, but this is working so far, in my resolvers.js I have:

import UserData from "./userData";
async function getFriend(userID, friendID) {
    return UserData.rawCollection().aggregate([
        {
            $match: {
                _id: userID    
            }
        },
        {
            $unwind: '$friends_list'
        },
        {
            $match: {
                'friends_list._id': friendID
            }
        }
    ]).toArray();
}

export default {
    Query: {
        async getFriends(obj, args, context) {
            if (args.friendID){
                const result = await getFriend(args.userID, args.friendID);
                return result[0].friends_list;
            }
            return UserData.findOne({ userID: args.userID }, {fields: {_id: 0,  friends_list: 1 }}).friends_list;
        }
    }
}

By making it async I was able to get this done. Not sure what implications this has, but so far it works. If anyone is able to review this and give some critique to improve this it would be sincerely appreciated, as any information about this has been excruciatingly difficult to find, as I have not seen any use-cases of this so far.

There was a typo in my code, which I’ve corrected. Sorry about that. If you can successfully refactor to use async/await that’s definitely preferable, as it’s more reusable than Promise.await, which is specific to Meteor.

1 Like

Awesome, thanks for the info!

1 Like