graphql-compose-mongoose icon indicating copy to clipboard operation
graphql-compose-mongoose copied to clipboard

update Mutation: push to Array instead of replacing it.

Open riggedCoinflip opened this issue 3 years ago • 2 comments

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

  • Query the document to get foo
  • Create a new array
  • Mutate the document with the new array

which would cost many recources

or

  • Create a custom Mutation that pushes instead of updates

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

  • update Array1
  • replace Array2

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.

riggedCoinflip avatar Jun 04 '21 13:06 riggedCoinflip

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.

nodkz avatar Jun 06 '21 11:06 nodkz

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

riggedCoinflip avatar Jun 17 '21 15:06 riggedCoinflip