graphql-compose / graphql-compose-mongoose

Mongoose model converter to GraphQL types with resolvers for graphql-compose https://github.com/nodkz/graphql-compose
MIT License
708 stars 94 forks source link

update Mutation: push to Array instead of replacing it. #344

Open riggedCoinflip opened 3 years ago

riggedCoinflip commented 3 years ago

Let's say I have a Schema like this:

const mySchema = new Schema({
    foo: [{ type: String }]
}]

And I want to update an existing Document - from what I know currently we only have the option to replace our fields with new ones. Are there any options to

  1. Push certain elements to a field
  2. Pop certain elements from a field

Example push: I have a document

{
    foo: [ "a", "b", "c"]
}

and want to add "d" to the array.

Currently I would need to

which would cost many recources

or

which I see problematic if we have Schemas with multiple arrays and we want to allow mutations like

Same Problems exist with popping elements.

I think it would be great if we had some kind of options in the Mutation itself. It would be super neat if we had a syntax that allows stuff like this:

{
    _id: "6099cb18f294f83dbcbf8936",
    foo: ["blue"],
    bar: ["dolores"],
    xyz: ["deleteThis", "butNotThat"],
}
mutation {
  somethingUpdateById(
    _id: "6099cb18f294f83dbcbf8936"
    record: { 
        foo: ["green", "yellow"]
        bar: ["Lorem", "Ipsum"]
        xyz: ["deleteThis"]
    }
    options: {
        arrays: {
            foo: replace
            bar: push
            xyz: pop
            }
        }
    }) {
    record {
            foo
            bar
            xyz
        }
    }
}
#response
{
    foo: ["green", "yellow"]
    bar: ["dolores", "Lorem", "Ipsum"]
    xyz: ["butNotThat"]
}

I know this is asking for extremely much, but perhaps I inspire someone to implement something like it. My skills unfortunately aren't high enough to do it myself.

nodkz commented 3 years ago

Interesting idea 👍 But the current suggested implementation looks now like a hack. And it can bring a lot of problems in the future when we want to migrate on InputUnions or refactor the suggested solution without breaking changes. Now it just brings complexity to the current resolver implementation. But right now record & options will not cover all user cases – what if we need to unset some fields?! So I think that we need provide some solution that will cover 95% of different uses cases with mongodb operators (push, pop, set, unset, ...) https://docs.mongodb.com/manual/reference/operator/update/

I think we need to await @oneOf implementation https://github.com/graphql/graphql-spec/pull/825 which unlocks InputUnions, and we can use it for the current or similar issues.

ANYWAY I'm glad to see any suggestion because BEST PRACTICES arise only in the "battle" (practice, implementation, design, discussions). But we should avoid any modifications until we do not become sure that it's a good solution for most developers. Better not to do something rather than provide a temporary thing.

@riggedCoinflip, as a workaround I suggest you write a custom resolver with any desirable logic.

riggedCoinflip commented 3 years ago

I went for the custom resolver.

This snippet allows to push and pop MongoIDs from a block list and allows certain other fields to be updated.

schemaComposer.createInputTC({
    name: "UserPrivateBlockedMutation",
    fields: {
        toPush: ["MongoID"],
        toPop: ["MongoID"],
    }
})

UserTCPrivate.addResolver({
    kind: "mutation",
    name: "userUpdateSelf",
    description: "Update currently logged in user",
    args: {
        name: "String",
        gender: "EnumUserPrivateGender",
        blocked: "UserPrivateBlockedMutation",
    },
    type: UserTCPrivate,
    resolve: async ({args, context}) => {
        /**
         * @param {Array} arr - array to filter
         * @param {Array} values - values to filter out
         * @returns {Array} filtered
         */
        function filterByValues(arr, values) {
            return arr.filter(
                itemArray => { !values.some(itemValues => itemArray.equals(itemValues)})
        }

        const userSelf = await User.findOne({_id: context.req.user._id})

        if (args.name) userSelf.name = args.name
        if (args.gender) userSelf.gender = args.gender
        if (args.blocked?.toPush) userSelf.blocked.push(...args.blocked.toPush)
        if (args.blocked?.toPop) userSelf.blocked = filterByValues(userSelf.blocked, args.blocked.toPop)

        await userSelf.save()
        return User.findOne({_id: context.req.user._id})
    }
})

Query:

#Template Query
mutation userUpdateSelf(
    $name: String
    $gender: EnumUserPrivateGender
    $blocked: UserPrivateBlockedMutation
) {
    userUpdateSelf(
        name: $name
        gender: $gender
        blocked: $blocked
    ) {
        name
        gender
        blocked
    }
}
#Example
mutation {
    userUpdateSelf(
        name: "MyNewName"
        gender: female
        blocked: {
            toPush: ["MongoIDsOfUsersYouWishToBlock"]
            toPop: ["MongoIDsOfUsersInYourBlocklist"]
        }
    ) {
        name
        gender
        blocked
    }
}