Skip to content
This repository has been archived by the owner on Dec 20, 2023. It is now read-only.

Commit

Permalink
Merge pull request #145 from C2FO/naked-filter-include
Browse files Browse the repository at this point in the history
adding eager loading on dataset and model
  • Loading branch information
dustinsmith1024 authored Jun 29, 2016
2 parents 04e67a1 + 963de42 commit ae7ffda
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 8 deletions.
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ after_script:
language: node_js
node_js:
- "4"
- "5"
- "5.6"
- "5.12"
- "stable"
112 changes: 111 additions & 1 deletion lib/dataset/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,116 @@ define({
return this.mergeOptions({distinct: args});
},

/**
* Allows the loading of another query and combining them in one patio command.
* Queries can be related or completely unrelated.
* All data comes back as JSON NOT Patio models.
*
* @example
*
* DB.from('company').filter({name: 'Amazon'})
* .eager({
* // company from parent query is passed in and usable in the eager query.
* leader: (company) => DB.from('leader').filter({id: company.leaderId}).one()
* })
* // Load completely unrelated data.
* .eager({org: () => DB.from('organization').one() })
* .one()})
*
* { id: 1,
* name: 'Amazon.com',
* leader: {
* id: 1,
* name: 'Jeff'
* },
* org: {
* id: 1,
* name: 'Google Inc.'
* }
* }
*
* Can do one to many loading for every item in the parent dataset.
* Be careful doing this because it can lead to lots of extra queries.
*
* DB.from('company').filter({state: 'IA'})
* .eager({invoices: (company) => DB.from('invoices').filter({companyId: company.id}).one() })
* .all()})
*
* [
* { id: 1,
* name: 'Principal',
* invoices: [
* { id: 1, amount: 200},
* { id: 2, amount: 300},
* ]
* },
* { id: 2,
* name: 'John Deere',
* invoices: [
* { id: 3, amount: 200},
* { id: 4, amount: 300},
* ]
* }
* ]
*
*/
eager: function(includeDatasets, fromModel) {
var ds = this.mergeOptions({}),
originalRowCb = ds.rowCb;

if(!ds.__opts._eagerAssoc) {
ds.__opts._eagerAssoc = includeDatasets;
ds.rowCb = function (topLevelResults) {

function toObject(thing) {
if (!thing) {
return comb.when(thing);
}
if (Array.isArray(thing)) {
return comb.when(thing.map(function(item) {
return toObject(item);
}));
}
if ('toObject' in thing) {
return comb.when(thing.toObject());
}
return comb.when(thing);
}

var eagerResults = {},
whens = [];

if (!originalRowCb) {
// pass through for when topLevelResults is already resolved
originalRowCb = function(r){return r;};
}

return comb.when(originalRowCb(topLevelResults)).chain(function(maybeModel) {
whens = Object.keys(ds.__opts._eagerAssoc).map(function(key) {
return ds.__opts._eagerAssoc[key](maybeModel).chain(function(result) {
return toObject(result).chain(function(res) {
eagerResults[key] = res;
return result;
});
});
});

return comb.when(whens).chain(function () {
return toObject(maybeModel).chain(function(json) {
// merge associations on to main data
return Object.assign(json, eagerResults);
});
});

});
};
}

return ds.mergeOptions({
_eagerAssoc: Object.assign(ds.__opts._eagerAssoc, includeDatasets)
});
},

/**
* Adds an EXCEPT clause using a second dataset object.
* An EXCEPT compound dataset returns all rows in the current dataset
Expand Down Expand Up @@ -2327,7 +2437,7 @@ define({
/**
* Methods that return modified datasets
*/
QUERY_METHODS: ['addGraphAliases', "and", "distinct", "except", "exclude", "filter", "find", "is", "isNot",
QUERY_METHODS: ['addGraphAliases', "and", "distinct", "eager", "except", "exclude", "filter", "find", "is", "isNot",
"eq", "neq", "lt", "lte", "gt", "gte", "forUpdate", "from", "fromSelf", "graph", "grep", "group",
"groupAndCount", "groupBy", "having", "intersect", "invert", "limit", "lockStyle", "naked", "or", "order",
"orderAppend", "orderBy", "orderMore", "orderPrepend", "qualify", "reverse",
Expand Down
1 change: 0 additions & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,6 @@ var Model = define([QueryPlugin, Middleware], {
}
},


sync: function (cb) {
var ret = new Promise();
if (!this.synced) {
Expand Down
58 changes: 58 additions & 0 deletions lib/plugins/association.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,64 @@ exports.AssociationPlugin = comb.define(null, {
return this.associate(this.ONE_TO_MANY, name, options, filter);
},

/* Allows eager loading of an association. This does an extra SQL query for the association.
* It will load any association singular or plural.
*
* @example
*
* Person.eager('company').one()
* { id: 1,
* name: 'Obi-Wan',
* company: {
* id: 1,
* name: 'Jedi council'
* }
* }
*
* Person.eager(['emails', 'phones', 'company']).limit(2).all()
* [{ id: 1,
* name: 'Obi-Wan',
* emails: ['[email protected]', '[email protected]'],
* phones: ['911', '888-991-0991'],
* company: {
* id: 1,
* name: 'Jedi council'
* }
* },
* { id: 2,
* name: 'Luke',
* emails: ['[email protected]', '[email protected]'],
* phones: ['911', '888-991-0992'],
* company: {
* id: 1,
* name: 'Jedi council'
* }
* }]
*
*/
eager: function(associations) {
var model = new this(),
includes = [],
associationsObj = {};

if (Array.isArray(associations)) {
includes = includes.concat(associations);
} else if(associations) {
includes.push(associations);
}

includes.forEach(function(association) {
associationsObj[association] = function(parent) {
if (!parent[association]) {
throw new Error("Association of " + association + " not found");
}
return parent[association];
};
});

return model.dataset.eager(associationsObj);
},

/**
* Creates a MANY_TO_ONE association.
* See {@link patio.plugins.AssociationPlugin.oneToMany}.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,6 @@
"patio": "./bin/patio"
},
"engines": {
"node": ">=4.0.0"
"node": ">=4.0.0 <6.0.0"
}
}
179 changes: 179 additions & 0 deletions test/associations/staticEager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use strict";

var it = require('it'),
assert = require('assert'),
helper = require("../data/oneToOne.helper.js"),
patio = require("../../lib"),
comb = require("comb");

var gender = ["M", "F"];

it.describe("patio.Model static eager method", function (it) {
var Works, Employee, DB;
it.beforeAll(function () {
Works = patio.addModel("works");
Works.manyToOne("employee", {fetchType: Works.fetchType.LAZY});
Employee = patio.addModel("employee");
Employee.oneToMany("works", {fetchType: Employee.fetchType.LAZY});
DB = null;
return helper.createSchemaAndSync(true).chain(function(db){
DB = db;
});
});


it.should("have associations", function () {
assert.deepEqual(Employee.associations, ["works"]);
assert.deepEqual(Works.associations, ["employee"]);
var emp = new Employee();
var work = new Works();
assert.deepEqual(emp.associations, ["works"]);
assert.deepEqual(work.associations, ["employee"]);
});

it.describe("load associations", function (it) {

it.beforeEach(function () {
return comb
.when([
Employee.remove(),
Works.remove()
])
.chain(function () {
return new Employee({
lastName: "last" + 1,
firstName: "first" + 1,
midInitial: "m",
gender: gender[1 % 2],
street: "Street " + 1,
city: "City " + 1,
works: [{
companyName: "Google",
salary: 100000
},{
companyName: "Alphabet",
salary: 100000
}]
}).save();
}).chain(function () {
return new Employee({
lastName: "Skywalker",
firstName: "Luke",
midInitial: "m",
gender: gender[1 % 2],
street: "Street " + 1,
city: "City " + 1,
works: {
companyName: "C2FO",
salary: 200000
}
}).save();
});

});

it.should("when querying", function () {
return comb
.when([Employee.eager('works').one(), Works.eager('employee').one()])
.chain(function (res) {
var emp = res[0], work = res[1];
var empWorks = emp.works, worksEmp = work.employee;
assert(emp.works[0].id, work.id);
assert(work.employee.id, emp.id);
});
});

it.should("when querying with filtering", function () {
return Employee.eager('works').filter({lastName: "Skywalker"}).one()
.chain(function (emp) {
assert(emp.id, emp.works[0].employeeId);
});
});

it.should("and load other eager queries", function () {
return Employee.eager('works').eager({
who: function(emp) {
return Employee.findById(emp.id);
}
}).one().chain(function (emp) {
assert(emp.id, emp.works[0].employeeId);
assert(emp.id, emp.who.id);
}).chain(function() {
// run same queries back to back
// make sure eager is not being cached across model instances
return Employee.eager('works').eager({
you: function(emp) {
return Employee.findById(emp.id);
}
}).one()
.chain(function (emp) {
assert(emp.id, emp.works[0].employeeId);
assert.isUndefined(emp.who);
assert(emp.id, emp.you.id);
});
});
});

});

it.describe("dataset loading", function (it) {

it.beforeEach(function () {
return comb
.when([
Employee.remove(),
Works.remove()
])
.chain(function () {
return new Employee({
lastName: "last" + 1,
firstName: "first" + 1,
midInitial: "m",
gender: gender[1 % 2],
street: "Street " + 1,
city: "City " + 1,
works: [{
companyName: "Google",
salary: 100000
},{
companyName: "Alphabet",
salary: 100000
}]
}).save();
}).chain(function () {
return new Employee({
lastName: "Skywalker",
firstName: "Luke",
midInitial: "m",
gender: gender[1 % 2],
street: "Street " + 1,
city: "City " + 1,
works: {
companyName: "C2FO",
salary: 200000
}
}).save();
});

});

it.should("and load other eager queries", function () {
return DB.from('employee').filter({lastName: 'Skywalker'})
.eager({
who: function(emp) {
return DB.from('works').filter({employeeId: emp.id}).one();
}
}).one()
.chain(function (emp) {
assert(emp.lastName, 'Skywalker');
assert(emp.who.companyName, 'C2FO');
assert(emp.id, emp.who.employeeId);
});
});

});

it.afterAll(function () {
return helper.dropModels();
});
});
Loading

0 comments on commit ae7ffda

Please sign in to comment.