neumino / thinky

JavaScript ORM for RethinkDB
http://justonepixel.com/thinky/
Other
1.12k stars 128 forks source link

Define multiple n-n circular relationships with different types and return the joined documents as saved #521

Open ShaggyDev opened 8 years ago

ShaggyDev commented 8 years ago

I am attempting to use the n-n relation using hasAndBelongsToMany() in thinky to define circular relations as "parents" and "children". Although the feature is working, I was wondering if there is a way that I can retrieve these circular relations as they were defined/saved, where in the below example, nodes saved in another node as a parent would only show up in the child node's parents field and not also in the children field of the same node as well as the root parent node only containing the children in the children field while the root parent node would have no objects contained in it's parents field. I hope the below test case code makes sense and illustrates my explanation.

var Reqlite = require('reqlite');
var assert = require('assert');

var thinky = require('thinky')({
    "host": "localhost",
    "port": 28016,
    "db": "test"
});

var server = new Reqlite({"driver-port": 28016});

var r = thinky.r;
var type = thinky.type;

// Models
var Account = thinky.createModel('accounts', {
    id: type.string(),
    username: type.string(),
});

var User = thinky.createModel('users', {
    id: type.string(),
    name: type.string(),
    accountId: type.string(),
    archetypeId: type.string(),
});

var Archetype = thinky.createModel('archetypes', {
    id: type.string(),
    name: type.string(),
});

var Node = thinky.createModel('nodes', {
    id: type.string(),
    skillId: type.string(),
    archetypeId: type.string(),
});

var Skill = thinky.createModel('skills', {
    id: type.string(),
    name: type.string(),
    desc: type.string(),
});

// Relationships
// Account <--> User
Account.hasMany(User, 'users', 'id', 'accountId');
User.belongsTo(Account, 'owner', 'accountId', 'id');

// User <--> Archetype
User.belongsTo(Archetype, 'archetype', 'archetypeId', 'id');

// Archetype <--> Node
Archetype.hasMany(Node, 'nodes', 'id', 'archetypeId');
Node.belongsTo(Archetype, 'archetype', 'archetypeId', 'id');

// Node <--> Skill
Node.belongsTo(Skill, 'skill', 'skillId', 'id');
Skill.hasMany(Node, 'nodes', 'id', 'skillId');

// Node <--> Node
Node.hasAndBelongsToMany(Node, 'parents', 'id', 'id', {type: 'parent'});
Node.hasAndBelongsToMany(Node, 'children', 'id', 'id', {type: 'child'});

describe('Test Circular Relations', function() {

    before(function(done) {

        var skillDebugging = Skill({
            id: '1000',
            name: 'Debugging',
            desc: 'Increase knowledge of the inner working of things',
        });
        var skillBuilding = Skill({
            id: '1010',
            name: 'Building',
            desc: 'Survive the Apocalypse with this',
        });

        skillDebugging.save().then(function() {
            return skillBuilding.save();
        }).then(function() {

            Skill.then(function(skills) {

                var skillMap = {};
                skills.forEach(function(skill) {
                    skillMap[skill.id] = skill;
                });

                var skillDebugging = skillMap['1000'];
                var skillBuilding = skillMap['1010'];

                // Define some nodes with some differing skills
                var node1 = Node({
                    id: '2001',
                    skill: skillDebugging,
                });
                var node2 = Node({
                    id: '2002',
                    skill: skillBuilding,
                });
                var node3 = Node({
                    id: '2003',
                    skill: skillDebugging,
                });
                var node4 = Node({
                    id: '2004',
                    skill: skillBuilding,
                });
                var node5 = Node({
                    id: '2005',
                    skill: skillBuilding,
                });
                var node6 = Node({
                    id: '2006',
                    skill: skillDebugging,
                });

                // Define the 'parents' and 'children' circular relationships
                // [n1] [n2] [n3]
                //   |    | \  |
                //   |    |  \ |
                // [n4] [n5] [n6]
                node1.children = [node4];
                node2.children = [node5, node6];
                node3.children = [node6];
                node4.parents = [node1];
                node5.parents = [node2];
                node6.parents = [node2, node3];

                var archetype = Archetype({
                    id: '3000',
                    name: 'Debugger',
                    nodes: [node1, node2, node3, node4, node5, node6],
                });

                const saveAllParams = {
                    nodes: {
                        skill: true,
                        parents: {
                            nodes: true,
                        },
                        children: {
                            nodes: true,
                        }
                    }
                };

                return archetype.saveAll(saveAllParams);
            }).then(function() {
                done();
            }).error(done).catch(done);
        });
    });

    it('should return the archetype saved with circular relations defined as expected', function(done) {
        Archetype.get('3000').getJoin({nodes: {skill: true, parents: true, children: true}}).then(function(retArchetype) {
            console.log('\n\nThe retrieved archetype with relations: ', JSON.stringify(retArchetype));

            // start assertions for expected data
            assert.ok(retArchetype);
            assert.ok(retArchetype.nodes);
            assert.ok(retArchetype.nodes.length === 6);

            // For the sake of reducing assumptions map nodes by their id to make asserts on specific nodes easier
            var mapNodes = {};
            retArchetype.nodes.forEach(function(node) {
                mapNodes[node.id] = node;
            });

            Object.keys(mapNodes).forEach(function(key) {
                var node = mapNodes[key];

                // assert expectations common to all nodes
                assert.ok(node);
                assert.ok(node.skillId);
                assert.ok(node.skill);
                assert.ok(node.archetypeId);

                // here we will make assumptions on expected node ids since we saved them with specific ids
                // and run node specific assertions
                //
                // Asserting that root nodes without parents should be an empty array and base children without children
                // should be an empty array. It would also be acceptable if the field did not exist at all I believe
                // but I think the nature of getJoin() is that if a relation does not exist in the field, it will be empty?
                switch(key) {
                    case '2001':
                        assert.ok(node.parents.length === 0);
                        assert.ok(node.children.length === 1);
                        break;
                    case '2002':
                        assert.ok(node.parents.length === 0);
                        assert.ok(node.children.length === 2);
                        break;
                    case '2003':
                        assert.ok(node.parents.length === 0);
                        assert.ok(node.children.length === 1);
                        break;
                    case '2004':
                        assert.ok(node.parents.length === 1);
                        assert.ok(node.children.length === 0);
                        break;
                    case '2005':
                        assert.ok(node.parents.length === 1);
                        assert.ok(node.children.length === 0);
                        break;
                    case '2006':
                        assert.ok(node.parents.length === 2);
                        assert.ok(node.children.length === 0);
                        break;
                    default:
                        break;
                }
            });

            done();
        }).error(done).catch(done);
    });
});
neumino commented 8 years ago

Hum, I think what you're trying to do is the same as in https://github.com/neumino/thinky/issues/421#issuecomment-171334263. Thinky doesn't support non bi-directional relations at the moment.

ShaggyDev commented 8 years ago

Ahh I see, I figured that might be the case but thought I would try it out anyway ;D. From the issue you linked to, it looks like I am setting up the two hasAndBelongsToMany relations in exactly the same way. The only problem with the implementation is that there isn't an explicit way of deriving that any given node is definitely a child of another node and not the parent and vise-versa. I am unsure of how the OP from issue #421 is using that architecture and still able to determine if a given user is only following the other user, or if he/she is only being followed by the other user, or both following and being followed by the other user. I took a look at the data that is generated within the tables that are created by these relationships and as they are fairly simplistic in nature, it still takes a lot of assumptions about the data to derive more information about the relationship. As for now, utilizing these n-n tables, for now, is going to be the way to continue using this method of relating models to themselves and understanding their non bi-directional relations then? For what I will be using this for, I might be able to get away with another implementation since these relations will not be so dynamic as user friendships are ;D