DEV Community

Jonathan Gamble
Jonathan Gamble

Posted on • Edited on

DGraph Advanced Data Modeling: Part 3 - Deep Updates

So I finished the coding for the Deep Updates. I have been out of town, so it took me a while. I had to re-write the code several times, and re-think it. There is a lot more to it than meets the eye.

That being said, I added two different versions:

  1. Remove and Set -- add-update / remove items, not just links. This basically works like a merge in noSQL.
  2. Arrays -- Overwrite entire array... delete all items, add new ones

I had to re-write the cascadeDelete from the previous post, adding _add and _delete to run the function from the deepUpdate function.

async function cascadeDelete({ 
  event, 
  dql, 
  nodes, 
  _delete = false, 
  _add = false
}) {

    const _nodes: string[] = nodes;

    const op = event.operation;

    if (op === 'delete' || op === 'add' || _delete || _add) {

        const uid = event[event.operation].rootUIDs[0];
        const invType = (event.__typename as string).toLowerCase();
        const type: string = event.__typename;

        const titleCase = (t: string) =>
            t.charAt(0).toUpperCase() + t.substring(1).toLowerCase();

        let args: any;

        if (op === 'delete' || _delete) {

            // get inverse relationships, delete them
            args = `upsert { query { `;
            for (let i = 0; i < _nodes.length; ++i) {
                const child = titleCase(_nodes[i]);
                // get all child.parent
                args += `t${i} as var(func: type(${child})) 
                @filter(uid_in(${child}.${invType}, ${uid})) `;
                // get all parent.child
                args += `q${i}(func: uid(${uid})) { b${i} as 
                ${titleCase(type)}.${_nodes[i].toLowerCase()} } `;
            }
            args += `} mutation { delete { `;
            for (let i = 0; i < _nodes.length; ++i) {
                // delete all child.parent
                args += `uid(t${i}) * * . \n`;
                // delete all parent.child
                args += `<${uid}> 
                <${titleCase(type)}.${_nodes[i].toLowerCase()}> 
                uid(b${i}) . `;
            }
            args += `} } }`;

        } else if (op === 'add' || _add) {

            // creates inverse relationships
            args = `upsert { query { q(func: uid(${uid})) { `;
            for (let i = 0; i < _nodes.length; ++i) {
                // get all 
                args += `t${i} as 
                ${type}.${_nodes[i].toLowerCase()} `;
            }
            args += `} } mutation { set { `;
            for (let i = 0; i < _nodes.length; ++i) {
                args += `uid(t${i}) 
                <${titleCase(_nodes[i])}.${invType}> <${uid}> . `
            }
            args += `} } }`;
        }
        console.log(args);
        const r = await dql.mutate(args);
        console.log(r);
    }
}
Enter fullscreen mode Exit fullscreen mode

And of course, you call the functions the same way:

async function featurePostHook({ event, dql }) {

    // update timestamps
    await updateTimestamps({ event, dql });

    // cascade delete
    await cascadeDelete({ event, dql, nodes: ['private'] });

    // deep update
    await deepUpdate({ event, dql, nodes: ['private'], merge: false });

}
Enter fullscreen mode Exit fullscreen mode

Add merge: false here if you want a deep array, or leave it the default true if you just want to update the values manually.

NOTE: Right now I only support ID types for deleting (remove). I may add the code later for @id types, but you can see below in the code where you would add it. I hate writing backend code, so I got burnt out trying to get everything going. If you use an array type (merge = false), this won't matter.

My goal here is to show people how things can be done, and help out the DGraph community.

async function deepUpdate({ event, dql, nodes, merge = true }) {

    const op = event.operation;

    if (op === 'update') {

        const uid = event[event.operation].rootUIDs[0];
        const removePatch: any = event.update.removePatch;
        const setPatch: any = event.update.setPatch;
        const type: string = event.__typename;

        // get updated keys
        let toRemove: string[];
        let toAdd: string[];

        if (removePatch) {
            toRemove = nodes.filter((v: string) => Object.keys(removePatch).includes(v));
        }
        if (setPatch) {
            toAdd = nodes.filter((v: string) => Object.keys(setPatch).includes(v));
        }

        const titleCase = (t: string) =>
            t.charAt(0).toUpperCase() + t.substring(1).toLowerCase();
        let args: any;

        if (merge) {

            if (setPatch) {
                // add inverse relationship
                await cascadeDelete({ dql, nodes, event, _add: true });
            }
            // remove objects in 'remove'
            if (toRemove) {

                // todo - upsert for xids or uids

                // get inverse relationships, delete them
                args = `{ delete { `;
                for (let i = 0; i < toRemove.length; ++i) {
                    // key input array
                    const patch: any[] = removePatch[toRemove[i]];
                    const ids = patch.map(v => Object.values(v)[0]);
                    // delete all child.parent
                    for (let j = 0; j < ids.length; ++j) {
                        args += `<${ids[j]}> * * . \n`;
                    }
                }
                args += `} }`;
            }
        } else {

            // delete all records in 'array'
            await cascadeDelete({ dql, nodes, event, _delete: true });

            // re-add everything
            for (let i = 0; i < toAdd.length; ++i) {
                args = `{ set { `;
                const child = setPatch[toAdd[i]];
                for (let j = 0; j < child.length; ++j) {
                    args += `<${uid}> <${type}.${toAdd[i].toLowerCase()}> _:new${j} . 
                    _:new${j} <${titleCase(toAdd[i])}.${type.toLowerCase()}> <${uid}> . 
                    _:new${j} <${titleCase(toAdd[i])}.${Object.keys(child[j])[0]}> "${Object.values(child[j])[0]}" . 
                    _:new${j} <dgraph.type> "${titleCase(toAdd[i])}" . \n`;
                }
                args += `} }`;
            }
        }
        // mutate
        console.log(args);
        const r = await dql.mutate(args);
        console.log(r);
    }
}
Enter fullscreen mode Exit fullscreen mode

And that's it! You can see where you would add the @id code.

Just add your code in an update mutation like so:

mutation {
    updatePost(input: { 
      filter: { id: "0x" },
      set: { 
        name: "bob", 
        url: "https:///something here", 
        nested: [{ text: "summer5" }, { text: "summer4" }] } 
}) {
        post {
            ....
            nested {
              text
            }
        }
        numUids
    }
}
Enter fullscreen mode Exit fullscreen mode

Or you can just set and remove what you want as usual. Remember, the remove must be ID types:

remove { postID: '0x2' }

Hope this helps!

Next up... counting likes, bookmarks, votes, etc!

J

Top comments (3)

Collapse
 
lveillard profile image
lveillard

just created an account to say that if somebody has the issue:
"Invalid end of input"

just remove the extra line here:
args += uid(t${i})
<${titleCase(_nodes[i])}.${invType}> <${uid}> .

to fix it :)

Collapse
 
jdgamble555 profile image
Jonathan Gamble

Actually, just add a ā€˜\nā€™ to fix that. DQL requires newlines in some cases to process, unlike graphql.

Collapse
 
jurijurka profile image
Juri Jurka

thank you very much!!!! you are a gold boi for that awesome help and support!!!