0

This is the first version of the code that I have attempted. I've tried a whole lot of other things like mutual exclusion, adding catch blocks everywhere, and Promise anti-patterns, but I can't seem to get over this mental or syntactical block:

populateJoins() {
    let promises = [];
    for (let c in this.columns) {
        let transformColumn = this.columns[c];

        if (transformColumn.joins) {
            let joinPointer = this.databaseObject;
            for (let j in transformColumn.joins) {
                let join = transformColumn.joins[j];

                if (joinPointer[join.as] != null) {
                    joinPointer = joinPointer.dataValues[join.as];
                } else {
                    if (this.requestQuery[toCamelCase(join.as) + 'Id']) {
                        promises.push(
                                join.model.findOne({where: {id: this.requestQuery[toCamelCase(join.as) + 'Id']}})
                                .then((tmp) => {
                                    joinPointer.dataValues[join.as] = tmp;
                                }));

                    } else if (joinPointer[toSnakeCase(join.as) + '_id']) {

                        promises.push(
                                join.model.findOne({where: {id: joinPointer[toSnakeCase(join.as) + '_id']}})
                                .then((tmp) => {
                                    joinPointer.dataValues[join.as] = tmp;
                                }));

                    }
                }
            }
        }           
    }
    return Promises.all(promises);
}

And this is the structure of this.columns:

    child1Name: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child1'),
            as: 'Child1'
        }],
        hidden: true
    },
    child2Name: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child2'),
            as: 'Child2'
        }],
        hidden: true
    },
    child1Status1Name: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child1'),
            as: 'Child1'
        },{
            model: Database.getInstance().getModel('status1'),
            as: 'Status1'
        }],
        hidden: true
    },
    child1Status2Name: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child1'),
            as: 'Child1'
        },{
            model: Database.getInstance().getModel('status2'),
            as: 'Grandchild2'
        }],
        hidden: true
    },
    serverName: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child1'),
            as: 'Child2'
        },{
            model: Database.getInstance().getModel('grandchild'),
            as: 'Grandchild'
        },{
            model: Database.getInstance().getModel('great_grandchild'),
            as: 'GreatGrandchild'
        }],
        hidden: true
    },
    child2Status1Name: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child2'),
            as: 'Child2'
        },{
            model: Database.getInstance().getModel('status1'),
            as: 'Grandchild1'
        }],
        hidden: true
    },
    child2Status2Name: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child2'),
            as: 'Child2'
        },{
            model: Database.getInstance().getModel('status2'),
            as: 'Grandchild2'
        }],
        hidden: true
    },
    archetypeName: {
        name: 'name',
        forceSelect: true,
        joins: [{
            model: Database.getInstance().getModel('child2'),
            as: 'Child2'
        },{
            model: Database.getInstance().getModel('archetype'),
            as: 'Archetype'
        },{
            model: Database.getInstance().getModel('archetype'),
            as: 'ArchetypeLink'
        }],
        hidden: true
    },

So for things I've already learned, joinPointer[join.as] != null will never prevent duplicate Child database calls from firing because the property will not be populated until the promises finish resolving.

Similarly, none of the grandchildren will populate because they have to wait for the children to populate, and if the grandchildren fulfill first, then they will never make it into the child object. The same goes for great-grandchildren.

I read this answer, where he says, "If you already have them in an array then they are already executing." I understand that the contents of the Promise will already resolve, which is why in other code I always use numerical indices to populate objects, i.e. jsonObject['list'][i]['anotherList'][j] = ...;, but I don't see how I can do this here.

I've been working on this for a while and haven't come up with a solution, so any workable code is more than appreciated.

NobleUplift
  • 5,631
  • 8
  • 45
  • 87
  • Have you considered using flatMap to build the promises? Would flow cleaner than a nested loop. – Nayeem Zen Mar 14 '18 at 21:05
  • @NayeemZen I didn't know `flatMap` existed until now, thanks. I was actually considering creating buckets for each level of nested database calls, so 0 would have all the references to children, 1 all the references to grandchildren, etc., and then reduce each array until all of the database calls in the previous index are finished. – NobleUplift Mar 14 '18 at 21:11
  • The problem is I have no idea how to reduce each generational array once they're built up such that I can guarantee all of the children are guaranteed to be populated before the grandchildren. – NobleUplift Mar 14 '18 at 21:13
  • I don't think there is a flatMap natively in ES6 but you could use lodash/lazy.js or just chain a map and reduce like: x.map(i => someFunc(i)).reduce((prev, next) => prev.concat(next), []) – Nayeem Zen Mar 14 '18 at 21:15
  • It's worth noting that I'm transpiling using TypeScript, but I have the transpiler set to `esnext` because I needed to use `Object.values` or `Object.entries` (I forget which), so I should be able to use `flatMap`. – NobleUplift Mar 14 '18 at 21:20
  • Actually, couldn't I just filter out my first array for only objects with `joins` set, and then iterate over that? – NobleUplift Mar 14 '18 at 21:21
  • Yeah the pseudo-code would look something like: Promise.all(this.columns.filter(col => col.joins) .map(col => col.joins) .flatMap(join => getJoinProimse(join))) – Nayeem Zen Mar 14 '18 at 21:22
  • If you want to guarantee the order of operations, make sure you build your promises array in a sorted order and then instead of using Promise.all you could do a reduce on your promises array. – Nayeem Zen Mar 14 '18 at 21:27

1 Answers1

1

The code in the question is difficult to follow but it appears that what you are trying to do is reasonably simple, ie execute a set of asynchronous findOne queries in series and progressively construct an ever-deeper hierarchy comprising the returned results.

If so, then :

  • you can use a reduce() pattern, see "The Collection Kerfuffle" here.
  • the full .columns object is unnecessary - you just need .columns.greatGrandchildName.joins.

The code should look at least something like this :

populateJoin(joins) {
    return joins.reduce((p, join) => {
        return p.then(obj => {
            let id = this.requestQuery[toCamelCase(join.as) + 'Id'] || obj[toSnakeCase(join.as) + '_id'] || obj[toSnakeCase(join.as + 's') + '_id'] || null;
            if(id) {
                return join.model.findOne({'where': { 'id': id }}).then(tmp => {
                    if (obj.dataValues[join.as]) {
                        return obj.dataValues[join.as];
                    } else {
                        obj.dataValues[join.as] = tmp;
                        return tmp; // 'tmp' will be `obj` at next iteration of the reduction.
                    }
                    return tmp; // 'tmp' will be `obj` at next iteration of the reduction.
                });
            } else {
                return obj; // send unmodified obj to next iteration of the reduction.
            }
        });
    }, Promise.resolve(this.databaseObject)) // starter promise for the reduction
    .then(() => this.databaseObject); // useful though not essential to make the top level object available to the caller's .then() callback.
}

populateJoins() {
    var promises = [];
    for (let c in this.columns) {
        let transformColumn = this.columns[c];
        if (transformColumn.joins) {
            promises.push(this.populateJoin(transformColumn.joins));
        }
    }
    return Promise.all(promises);
}
NobleUplift
  • 5,631
  • 8
  • 45
  • 87
Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • Thank you very much for the answer @Roamer-1888, it looks great! I also have to say sorry for minimizing my example data to be too simplistic. The code is over 400 lines long and proprietary, so I just wanted to show the basic structure. – NobleUplift Mar 15 '18 at 14:40
  • To better explain, this database object is a many-to-many join between two other tables, so it needs to join each of those two objects, plus two status columns that are present on both of the child objects, plus additional joins that are unique to each of the child objects, including joining to the second object, then a join to its dependent object, and then a 1:1 reflective join on that object. – NobleUplift Mar 15 '18 at 14:40
  • I understand joins but don't pretend to follow everything you describe. Hopefully you can gain some knowledge from my answer and build on it to solve the full problem. – Roamer-1888 Mar 15 '18 at 18:00
  • I have my code working down at the level of triple-nested join objects. Yay! All yours was missing was a replacement for `joinPointer[join.as] != null` so that join objects with multiple children don't get overridden. There might be a chance of race conditions with the `Promise.all`, but I just want to move forward with development at this point. – NobleUplift Mar 16 '18 at 21:00
  • Looks like progress.If I understand correctly, the answer can be deleted from "Assuming ..." onwards, since `.populateJoins()` doesn't accept any parameters. – Roamer-1888 Mar 16 '18 at 21:56
  • I had a very busy end of March, sorry for the late update. Accepted. – NobleUplift Apr 03 '18 at 22:11