diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e8ffc5..9f323e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +##### 2.1.0 - 10 July 2015 + +###### Backwards compatible API changes +- #15 - Add support for loading relations in find() +- #16 - Add support for loading relations in findAll() + ##### 2.0.0 - 02 July 2015 Stable Version 2.0.0 diff --git a/dist/js-data-firebase.js b/dist/js-data-firebase.js index c03da66..098b277 100644 --- a/dist/js-data-firebase.js +++ b/dist/js-data-firebase.js @@ -1,6 +1,6 @@ /*! * js-data-firebase - * @version 2.0.0 - Homepage + * @version 2.1.0 - Homepage * @author Jason Dobry * @copyright (c) 2014-2015 Jason Dobry * @license MIT @@ -65,19 +65,18 @@ return /******/ (function(modules) { // webpackBootstrap var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } var JSData = __webpack_require__(1); var Firebase = __webpack_require__(2); var values = __webpack_require__(3); + var map = __webpack_require__(4); + var unique = __webpack_require__(5); var emptyStore = new JSData.DS(); var DSUtils = JSData.DSUtils; - var omit = DSUtils.omit; - var deepMixIn = DSUtils.deepMixIn; - var removeCircular = DSUtils.removeCircular; - var forOwn = DSUtils.forOwn; - var filter = emptyStore.defaults.defaultFilter; var Defaults = function Defaults() { @@ -129,7 +128,7 @@ return /******/ (function(modules) { // webpackBootstrap options = options || {}; this.defaults = new Defaults(); - deepMixIn(this.defaults, options); + DSUtils.deepMixIn(this.defaults, options); this.ref = new Firebase(options.basePath || this.defaults.basePath); } @@ -144,18 +143,90 @@ return /******/ (function(modules) { // webpackBootstrap value: function find(resourceConfig, id, options) { var _this = this; - return createTask(function (resolve, reject) { - queueTask(function () { - _this.getRef(resourceConfig, options).child(id).once('value', function (dataSnapshot) { - var item = dataSnapshot.val(); - if (!item) { - reject(new Error('Not Found!')); - } else { - item[resourceConfig.idAttribute] = item[resourceConfig.idAttribute] || id; - resolve(item); - } - }, reject, _this); + var instance = undefined; + options = options || {}; + options['with'] = options['with'] || []; + return new DSUtils.Promise(function (resolve, reject) { + _this.getRef(resourceConfig, options).child(id).once('value', function (dataSnapshot) { + var item = dataSnapshot.val(); + if (!item) { + reject(new Error('Not Found!')); + } else { + item[resourceConfig.idAttribute] = item[resourceConfig.idAttribute] || id; + resolve(item); + } + }, reject, _this); + }).then(function (_instance) { + instance = _instance; + var tasks = []; + + DSUtils.forEach(resourceConfig.relationList, function (def) { + var relationName = def.relation; + var relationDef = resourceConfig.getResource(relationName); + var containedName = null; + if (DSUtils.contains(options['with'], relationName)) { + containedName = relationName; + } else if (DSUtils.contains(options['with'], def.localField)) { + containedName = def.localField; + } + if (containedName) { + (function () { + var __options = DSUtils.deepMixIn({}, options.orig ? options.orig() : options); + __options = DSUtils._(relationDef, __options); + DSUtils.remove(__options['with'], containedName); + DSUtils.forEach(__options['with'], function (relation, i) { + if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') { + __options['with'][i] = relation.substr(containedName.length + 1); + } + }); + + var task = undefined; + + if ((def.type === 'hasOne' || def.type === 'hasMany') && def.foreignKey) { + task = _this.findAll(resourceConfig.getResource(relationName), { + where: _defineProperty({}, def.foreignKey, { + '==': instance[resourceConfig.idAttribute] + }) + }, __options).then(function (relatedItems) { + if (def.type === 'hasOne' && relatedItems.length) { + DSUtils.set(instance, def.localField, relatedItems[0]); + } else { + DSUtils.set(instance, def.localField, relatedItems); + } + return relatedItems; + }); + } else if (def.type === 'hasMany' && def.localKeys) { + var localKeys = []; + var itemKeys = instance[def.localKeys] || []; + itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + localKeys = localKeys.concat(itemKeys || []); + task = _this.findAll(resourceConfig.getResource(relationName), { + where: _defineProperty({}, relationDef.idAttribute, { + 'in': DSUtils.filter(unique(localKeys), function (x) { + return x; + }) + }) + }, __options).then(function (relatedItems) { + DSUtils.set(instance, def.localField, relatedItems); + return relatedItems; + }); + } else if (def.type === 'belongsTo' || def.type === 'hasOne' && def.localKey) { + task = _this.find(resourceConfig.getResource(relationName), DSUtils.get(instance, def.localKey), __options).then(function (relatedItem) { + DSUtils.set(instance, def.localField, relatedItem); + return relatedItem; + }); + } + + if (task) { + tasks.push(task); + } + })(); + } }); + + return DSUtils.Promise.all(tasks); + }).then(function () { + return instance; }); } }, { @@ -163,18 +234,132 @@ return /******/ (function(modules) { // webpackBootstrap value: function findAll(resourceConfig, params, options) { var _this2 = this; - return createTask(function (resolve, reject) { - queueTask(function () { - _this2.getRef(resourceConfig, options).once('value', function (dataSnapshot) { - var data = dataSnapshot.val(); - forOwn(data, function (value, key) { - if (!value[resourceConfig.idAttribute]) { - value[resourceConfig.idAttribute] = '/' + key; + var items = null; + options = options || {}; + options['with'] = options['with'] || []; + return new DSUtils.Promise(function (resolve, reject) { + _this2.getRef(resourceConfig, options).once('value', function (dataSnapshot) { + var data = dataSnapshot.val(); + DSUtils.forOwn(data, function (value, key) { + if (!value[resourceConfig.idAttribute]) { + value[resourceConfig.idAttribute] = '/' + key; + } + }); + resolve(filter.call(emptyStore, values(data), resourceConfig.name, params, options)); + }, reject, _this2); + }).then(function (_items) { + items = _items; + var tasks = []; + DSUtils.forEach(resourceConfig.relationList, function (def) { + var relationName = def.relation; + var relationDef = resourceConfig.getResource(relationName); + var containedName = null; + if (DSUtils.contains(options['with'], relationName)) { + containedName = relationName; + } else if (DSUtils.contains(options['with'], def.localField)) { + containedName = def.localField; + } + if (containedName) { + (function () { + var __options = DSUtils.deepMixIn({}, options.orig ? options.orig() : options); + __options['with'] = options['with'].slice(); + __options = DSUtils._(relationDef, __options); + DSUtils.remove(__options['with'], containedName); + + DSUtils.forEach(__options['with'], function (relation, i) { + if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') { + __options['with'][i] = relation.substr(containedName.length + 1); + } else { + __options['with'][i] = ''; + } + }); + + var task = undefined; + + if ((def.type === 'hasOne' || def.type === 'hasMany') && def.foreignKey) { + task = _this2.findAll(resourceConfig.getResource(relationName), { + where: _defineProperty({}, def.foreignKey, { + 'in': DSUtils.filter(map(items, function (item) { + return DSUtils.get(item, resourceConfig.idAttribute); + }), function (x) { + return x; + }) + }) + }, __options).then(function (relatedItems) { + DSUtils.forEach(items, function (item) { + var attached = []; + DSUtils.forEach(relatedItems, function (relatedItem) { + if (DSUtils.get(relatedItem, def.foreignKey) === item[resourceConfig.idAttribute]) { + attached.push(relatedItem); + } + }); + if (def.type === 'hasOne' && attached.length) { + DSUtils.set(item, def.localField, attached[0]); + } else { + DSUtils.set(item, def.localField, attached); + } + }); + return relatedItems; + }); + } else if (def.type === 'hasMany' && def.localKeys) { + (function () { + var localKeys = []; + DSUtils.forEach(items, function (item) { + var itemKeys = item[def.localKeys] || []; + itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + localKeys = localKeys.concat(itemKeys || []); + }); + task = _this2.findAll(resourceConfig.getResource(relationName), { + where: _defineProperty({}, relationDef.idAttribute, { + 'in': DSUtils.filter(unique(localKeys), function (x) { + return x; + }) + }) + }, __options).then(function (relatedItems) { + DSUtils.forEach(items, function (item) { + var attached = []; + var itemKeys = item[def.localKeys] || []; + itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + DSUtils.forEach(relatedItems, function (relatedItem) { + if (itemKeys && DSUtils.contains(itemKeys, relatedItem[relationDef.idAttribute])) { + attached.push(relatedItem); + } + }); + DSUtils.set(item, def.localField, attached); + }); + return relatedItems; + }); + })(); + } else if (def.type === 'belongsTo' || def.type === 'hasOne' && def.localKey) { + task = _this2.findAll(resourceConfig.getResource(relationName), { + where: _defineProperty({}, relationDef.idAttribute, { + 'in': DSUtils.filter(map(items, function (item) { + return DSUtils.get(item, def.localKey); + }), function (x) { + return x; + }) + }) + }, __options).then(function (relatedItems) { + DSUtils.forEach(items, function (item) { + DSUtils.forEach(relatedItems, function (relatedItem) { + if (relatedItem[relationDef.idAttribute] === item[def.localKey]) { + DSUtils.set(item, def.localField, relatedItem); + } + }); + }); + return relatedItems; + }); } - }); - resolve(filter.call(emptyStore, values(data), resourceConfig.name, params, options)); - }, reject, _this2); + + if (task) { + tasks.push(task); + } + })(); + } }); + return DSUtils.Promise.all(tasks); + }).then(function () { + return items; }); } }, { @@ -189,7 +374,7 @@ return /******/ (function(modules) { // webpackBootstrap return createTask(function (resolve, reject) { queueTask(function () { var resourceRef = _this3.getRef(resourceConfig, options); - var itemRef = resourceRef.push(removeCircular(omit(attrs, resourceConfig.relationFields || [])), function (err) { + var itemRef = resourceRef.push(DSUtils.removeCircular(DSUtils.omit(attrs, resourceConfig.relationFields || [])), function (err) { if (err) { return reject(err); } else { @@ -220,7 +405,7 @@ return /******/ (function(modules) { // webpackBootstrap return createTask(function (resolve, reject) { queueTask(function () { - attrs = removeCircular(omit(attrs || {}, resourceConfig.relationFields || [])); + attrs = DSUtils.removeCircular(DSUtils.omit(attrs || {}, resourceConfig.relationFields || [])); var itemRef = _this4.getRef(resourceConfig, options).child(id); itemRef.once('value', function (dataSnapshot) { try { @@ -237,7 +422,7 @@ return /******/ (function(modules) { // webpackBootstrap delete attrs[fields[i]]; } } - deepMixIn(item, attrs); + DSUtils.deepMixIn(item, attrs); if (resourceConfig.relations) { fields = resourceConfig.relationFields; for (i = 0; i < fields.length; i++) { @@ -328,7 +513,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 3 */ /***/ function(module, exports, __webpack_require__) { - var forOwn = __webpack_require__(4); + var forOwn = __webpack_require__(6); /** * Get object values @@ -350,8 +535,67 @@ return /******/ (function(modules) { // webpackBootstrap /* 4 */ /***/ function(module, exports, __webpack_require__) { - var hasOwn = __webpack_require__(5); - var forIn = __webpack_require__(6); + var makeIterator = __webpack_require__(7); + + /** + * Array map + */ + function map(arr, callback, thisObj) { + callback = makeIterator(callback, thisObj); + var results = []; + if (arr == null){ + return results; + } + + var i = -1, len = arr.length; + while (++i < len) { + results[i] = callback(arr[i], i, arr); + } + + return results; + } + + module.exports = map; + + + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + var filter = __webpack_require__(8); + + /** + * @return {array} Array of unique items + */ + function unique(arr, compare){ + compare = compare || isEqual; + return filter(arr, function(item, i, arr){ + var n = arr.length; + while (++i < n) { + if ( compare(item, arr[i]) ) { + return false; + } + } + return true; + }); + } + + function isEqual(a, b){ + return a === b; + } + + module.exports = unique; + + + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + var hasOwn = __webpack_require__(9); + var forIn = __webpack_require__(10); /** * Similar to Array/forEach but works over object properties and fixes Don't @@ -372,7 +616,79 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 5 */ +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + var identity = __webpack_require__(11); + var prop = __webpack_require__(12); + var deepMatches = __webpack_require__(13); + + /** + * Converts argument into a valid iterator. + * Used internally on most array/object/collection methods that receives a + * callback/iterator providing a shortcut syntax. + */ + function makeIterator(src, thisObj){ + if (src == null) { + return identity; + } + switch(typeof src) { + case 'function': + // function is the first to improve perf (most common case) + // also avoid using `Function#call` if not needed, which boosts + // perf a lot in some cases + return (typeof thisObj !== 'undefined')? function(val, i, arr){ + return src.call(thisObj, val, i, arr); + } : src; + case 'object': + return function(val){ + return deepMatches(val, src); + }; + case 'string': + case 'number': + return prop(src); + } + } + + module.exports = makeIterator; + + + + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + var makeIterator = __webpack_require__(7); + + /** + * Array filter + */ + function filter(arr, callback, thisObj) { + callback = makeIterator(callback, thisObj); + var results = []; + if (arr == null) { + return results; + } + + var i = -1, len = arr.length, value; + while (++i < len) { + value = arr[i]; + if (callback(value, i, arr)) { + results.push(value); + } + } + + return results; + } + + module.exports = filter; + + + + +/***/ }, +/* 9 */ /***/ function(module, exports, __webpack_require__) { @@ -390,10 +706,10 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 6 */ +/* 10 */ /***/ function(module, exports, __webpack_require__) { - var hasOwn = __webpack_require__(5); + var hasOwn = __webpack_require__(9); var _hasDontEnumBug, _dontEnums; @@ -471,6 +787,160 @@ return /******/ (function(modules) { // webpackBootstrap +/***/ }, +/* 11 */ +/***/ function(module, exports, __webpack_require__) { + + + + /** + * Returns the first argument provided to it. + */ + function identity(val){ + return val; + } + + module.exports = identity; + + + + +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { + + + + /** + * Returns a function that gets a property of the passed object + */ + function prop(name){ + return function(obj){ + return obj[name]; + }; + } + + module.exports = prop; + + + + +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { + + var forOwn = __webpack_require__(6); + var isArray = __webpack_require__(14); + + function containsMatch(array, pattern) { + var i = -1, length = array.length; + while (++i < length) { + if (deepMatches(array[i], pattern)) { + return true; + } + } + + return false; + } + + function matchArray(target, pattern) { + var i = -1, patternLength = pattern.length; + while (++i < patternLength) { + if (!containsMatch(target, pattern[i])) { + return false; + } + } + + return true; + } + + function matchObject(target, pattern) { + var result = true; + forOwn(pattern, function(val, key) { + if (!deepMatches(target[key], val)) { + // Return false to break out of forOwn early + return (result = false); + } + }); + + return result; + } + + /** + * Recursively check if the objects match. + */ + function deepMatches(target, pattern){ + if (target && typeof target === 'object') { + if (isArray(target) && isArray(pattern)) { + return matchArray(target, pattern); + } else { + return matchObject(target, pattern); + } + } else { + return target === pattern; + } + } + + module.exports = deepMatches; + + + + +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { + + var isKind = __webpack_require__(15); + /** + */ + var isArray = Array.isArray || function (val) { + return isKind(val, 'Array'); + }; + module.exports = isArray; + + + +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { + + var kindOf = __webpack_require__(16); + /** + * Check if value is from a specific "kind". + */ + function isKind(val, kind){ + return kindOf(val) === kind; + } + module.exports = isKind; + + + +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { + + + + var _rKind = /^\[object (.*)\]$/, + _toString = Object.prototype.toString, + UNDEF; + + /** + * Gets the "kind" of value. (e.g. "String", "Number", etc) + */ + function kindOf(val) { + if (val === null) { + return 'Null'; + } else if (val === UNDEF) { + return 'Undefined'; + } else { + return _rKind.exec( _toString.call(val) )[1]; + } + } + module.exports = kindOf; + + + /***/ } /******/ ]) }); diff --git a/dist/js-data-firebase.min.js b/dist/js-data-firebase.min.js index d4144f3..025806b 100644 --- a/dist/js-data-firebase.min.js +++ b/dist/js-data-firebase.min.js @@ -1,6 +1,6 @@ /*! * js-data-firebase -* @version 2.0.0 - Homepage +* @version 2.1.0 - Homepage * @author Jason Dobry * @copyright (c) 2014-2015 Jason Dobry * @license MIT @@ -8,5 +8,5 @@ * @overview Firebase adapter for js-data. */ -!function(a,b){"object"==typeof exports&&"object"==typeof module?module.exports=b(require("js-data"),require("firebase")):"function"==typeof define&&define.amd?define(["js-data","firebase"],b):"object"==typeof exports?exports.DSFirebaseAdapter=b(require("js-data"),require("firebase")):a.DSFirebaseAdapter=b(a.JSData,a.Firebase)}(this,function(a,b){return function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={exports:{},id:d,loaded:!1};return a[d].call(e.exports,e,e.exports,b),e.loaded=!0,e.exports}var c={};return b.m=a,b.c=c,b.p="",b(0)}([function(a,b,c){function d(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}function e(a){u.push(a)}function f(){u.length&&!v&&(v=!0,u[0]())}function g(a){u.length?e(a):(e(a),f())}function h(a){return new n.Promise(a).then(function(a){return v=!1,u.shift(),setTimeout(f,0),a},function(a){return v=!1,u.shift(),setTimeout(f,0),n.Promise.reject(a)})}var i=function(){function a(a,b){for(var c=0;c=j.length&&"."===a[j.length]&&(k["with"][b]=a.substr(j.length+1))});var l=void 0;if("hasOne"!==b.type&&"hasMany"!==b.type||!b.foreignKey)if("hasMany"===b.type&&b.localKeys){var m=[],n=f[b.localKeys]||[];n=Array.isArray(n)?n:q.keys(n),m=m.concat(n||[]),l=e.findAll(a.getResource(h),{where:d({},i.idAttribute,{"in":q.filter(o(m),function(a){return a})})},k).then(function(a){return q.set(f,b.localField,a),a})}else("belongsTo"===b.type||"hasOne"===b.type&&b.localKey)&&(l=e.find(a.getResource(h),q.get(f,b.localKey),k).then(function(a){return q.set(f,b.localField,a),a}));else l=e.findAll(a.getResource(h),{where:d({},b.foreignKey,{"==":f[a.idAttribute]})},k).then(function(a){return"hasOne"===b.type&&a.length?q.set(f,b.localField,a[0]):q.set(f,b.localField,a),a});l&&g.push(l)}()}),q.Promise.all(g)}).then(function(){return f})}},{key:"findAll",value:function(a,b,c){var e=this,f=null;return c=c||{},c["with"]=c["with"]||[],new q.Promise(function(d,f){e.getRef(a,c).once("value",function(e){var f=e.val();q.forOwn(f,function(b,c){b[a.idAttribute]||(b[a.idAttribute]="/"+c)}),d(r.call(p,m(f),a.name,b,c))},f,e)}).then(function(b){f=b;var g=[];return q.forEach(a.relationList,function(b){var h=b.relation,i=a.getResource(h),j=null;q.contains(c["with"],h)?j=h:q.contains(c["with"],b.localField)&&(j=b.localField),j&&!function(){var k=q.deepMixIn({},c.orig?c.orig():c);k["with"]=c["with"].slice(),k=q._(i,k),q.remove(k["with"],j),q.forEach(k["with"],function(a,b){a&&0===a.indexOf(j)&&a.length>=j.length&&"."===a[j.length]?k["with"][b]=a.substr(j.length+1):k["with"][b]=""});var l=void 0;"hasOne"!==b.type&&"hasMany"!==b.type||!b.foreignKey?"hasMany"===b.type&&b.localKeys?!function(){var c=[];q.forEach(f,function(a){var d=a[b.localKeys]||[];d=Array.isArray(d)?d:q.keys(d),c=c.concat(d||[])}),l=e.findAll(a.getResource(h),{where:d({},i.idAttribute,{"in":q.filter(o(c),function(a){return a})})},k).then(function(a){return q.forEach(f,function(c){var d=[],e=c[b.localKeys]||[];e=Array.isArray(e)?e:q.keys(e),q.forEach(a,function(a){e&&q.contains(e,a[i.idAttribute])&&d.push(a)}),q.set(c,b.localField,d)}),a})}():("belongsTo"===b.type||"hasOne"===b.type&&b.localKey)&&(l=e.findAll(a.getResource(h),{where:d({},i.idAttribute,{"in":q.filter(n(f,function(a){return q.get(a,b.localKey)}),function(a){return a})})},k).then(function(a){return q.forEach(f,function(c){q.forEach(a,function(a){a[i.idAttribute]===c[b.localKey]&&q.set(c,b.localField,a)})}),a})):l=e.findAll(a.getResource(h),{where:d({},b.foreignKey,{"in":q.filter(n(f,function(b){return q.get(b,a.idAttribute)}),function(a){return a})})},k).then(function(c){return q.forEach(f,function(d){var e=[];q.forEach(c,function(c){q.get(c,b.foreignKey)===d[a.idAttribute]&&e.push(c)}),"hasOne"===b.type&&e.length?q.set(d,b.localField,e[0]):q.set(d,b.localField,e)}),c}),l&&g.push(l)}()}),q.Promise.all(g)}).then(function(){return f})}},{key:"create",value:function(a,b,c){var d=this,e=b[a.idAttribute];return q.isString(e)||q.isNumber(e)?this.update(a,e,b,c):i(function(e,f){h(function(){var g=d.getRef(a,c),h=g.push(q.removeCircular(q.omit(b,a.relationFields||[])),function(b){if(b)return f(b);var c=h.toString().replace(g.toString(),"");h.child(a.idAttribute).set(c,function(a){a?f(a):h.once("value",function(a){try{e(a.val())}catch(b){f(b)}},f,d)})})})})}},{key:"update",value:function(a,b,c,d){var e=this;return i(function(f,g){h(function(){c=q.removeCircular(q.omit(c||{},a.relationFields||[]));var h=e.getRef(a,d).child(b);h.once("value",function(b){try{!function(){var d=b.val()||{},e=void 0,i=void 0,j=void 0;if(a.relations)for(e=a.relationFields,i=[],j=0;j { - queueTask(() => { - this.getRef(resourceConfig, options).child(id).once('value', dataSnapshot => { - let item = dataSnapshot.val(); - if (!item) { - reject(new Error('Not Found!')); - } else { - item[resourceConfig.idAttribute] = item[resourceConfig.idAttribute] || id; - resolve(item); + let instance; + options = options || {}; + options.with = options.with || []; + return new DSUtils.Promise((resolve, reject) => { + this.getRef(resourceConfig, options).child(id).once('value', dataSnapshot => { + let item = dataSnapshot.val(); + if (!item) { + reject(new Error('Not Found!')); + } else { + item[resourceConfig.idAttribute] = item[resourceConfig.idAttribute] || id; + resolve(item); + } + }, reject, this); + }).then(_instance => { + instance = _instance; + let tasks = []; + + DSUtils.forEach(resourceConfig.relationList, def => { + let relationName = def.relation; + let relationDef = resourceConfig.getResource(relationName); + let containedName = null; + if (DSUtils.contains(options.with, relationName)) { + containedName = relationName; + } else if (DSUtils.contains(options.with, def.localField)) { + containedName = def.localField; } - }, reject, this); - }); - }); + if (containedName) { + let __options = DSUtils.deepMixIn({}, options.orig ? options.orig() : options); + __options = DSUtils._(relationDef, __options); + DSUtils.remove(__options.with, containedName); + DSUtils.forEach(__options.with, (relation, i) => { + if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') { + __options.with[i] = relation.substr(containedName.length + 1); + } + }); + + let task; + + if ((def.type === 'hasOne' || def.type === 'hasMany') && def.foreignKey) { + task = this.findAll(resourceConfig.getResource(relationName), { + where: { + [def.foreignKey]: { + '==': instance[resourceConfig.idAttribute] + } + } + }, __options).then(relatedItems => { + if (def.type === 'hasOne' && relatedItems.length) { + DSUtils.set(instance, def.localField, relatedItems[0]); + } else { + DSUtils.set(instance, def.localField, relatedItems); + } + return relatedItems; + }); + } else if (def.type === 'hasMany' && def.localKeys) { + let localKeys = []; + let itemKeys = instance[def.localKeys] || []; + itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + localKeys = localKeys.concat(itemKeys || []); + task = this.findAll(resourceConfig.getResource(relationName), { + where: { + [relationDef.idAttribute]: { + 'in': DSUtils.filter(unique(localKeys), x => x) + } + } + }, __options).then(relatedItems => { + DSUtils.set(instance, def.localField, relatedItems); + return relatedItems; + }); + } else if (def.type === 'belongsTo' || (def.type === 'hasOne' && def.localKey)) { + task = this.find(resourceConfig.getResource(relationName), DSUtils.get(instance, def.localKey), __options).then(relatedItem => { + DSUtils.set(instance, def.localField, relatedItem); + return relatedItem; + }); + } + + if (task) { + tasks.push(task); + } + } + }); + + return DSUtils.Promise.all(tasks); + }) + .then(() => instance); } findAll(resourceConfig, params, options) { - return createTask((resolve, reject) => { - queueTask(() => { - this.getRef(resourceConfig, options).once('value', dataSnapshot => { - let data = dataSnapshot.val(); - forOwn(data, (value, key) => { - if (!value[resourceConfig.idAttribute]) { - value[resourceConfig.idAttribute] = `/${key}`; + let items = null; + options = options || {}; + options.with = options.with || []; + return new DSUtils.Promise((resolve, reject) => { + this.getRef(resourceConfig, options).once('value', dataSnapshot => { + let data = dataSnapshot.val(); + DSUtils.forOwn(data, (value, key) => { + if (!value[resourceConfig.idAttribute]) { + value[resourceConfig.idAttribute] = `/${key}`; + } + }); + resolve(filter.call(emptyStore, values(data), resourceConfig.name, params, options)); + }, reject, this); + }).then(_items => { + items = _items; + let tasks = []; + DSUtils.forEach(resourceConfig.relationList, def => { + let relationName = def.relation; + let relationDef = resourceConfig.getResource(relationName); + let containedName = null; + if (DSUtils.contains(options.with, relationName)) { + containedName = relationName; + } else if (DSUtils.contains(options.with, def.localField)) { + containedName = def.localField; + } + if (containedName) { + let __options = DSUtils.deepMixIn({}, options.orig ? options.orig() : options); + __options.with = options.with.slice(); + __options = DSUtils._(relationDef, __options); + DSUtils.remove(__options.with, containedName); + + DSUtils.forEach(__options.with, (relation, i) => { + if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') { + __options.with[i] = relation.substr(containedName.length + 1); + } else { + __options.with[i] = ''; + } + }); + + let task; + + if ((def.type === 'hasOne' || def.type === 'hasMany') && def.foreignKey) { + task = this.findAll(resourceConfig.getResource(relationName), { + where: { + [def.foreignKey]: { + 'in': DSUtils.filter(map(items, item => DSUtils.get(item, resourceConfig.idAttribute)), x => x) + } + } + }, __options).then(relatedItems => { + DSUtils.forEach(items, item => { + let attached = []; + DSUtils.forEach(relatedItems, relatedItem => { + if (DSUtils.get(relatedItem, def.foreignKey) === item[resourceConfig.idAttribute]) { + attached.push(relatedItem); + } + }); + if (def.type === 'hasOne' && attached.length) { + DSUtils.set(item, def.localField, attached[0]); + } else { + DSUtils.set(item, def.localField, attached); + } + }); + return relatedItems; + }); + } else if (def.type === 'hasMany' && def.localKeys) { + let localKeys = []; + DSUtils.forEach(items, item => { + let itemKeys = item[def.localKeys] || []; + itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + localKeys = localKeys.concat(itemKeys || []); + }); + task = this.findAll(resourceConfig.getResource(relationName), { + where: { + [relationDef.idAttribute]: { + 'in': DSUtils.filter(unique(localKeys), x => x) + } + } + }, __options).then(relatedItems => { + DSUtils.forEach(items, item => { + let attached = []; + let itemKeys = item[def.localKeys] || []; + itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + DSUtils.forEach(relatedItems, relatedItem => { + if (itemKeys && DSUtils.contains(itemKeys, relatedItem[relationDef.idAttribute])) { + attached.push(relatedItem); + } + }); + DSUtils.set(item, def.localField, attached); + }); + return relatedItems; + }); + } else if (def.type === 'belongsTo' || (def.type === 'hasOne' && def.localKey)) { + task = this.findAll(resourceConfig.getResource(relationName), { + where: { + [relationDef.idAttribute]: { + 'in': DSUtils.filter(map(items, item => DSUtils.get(item, def.localKey)), x => x) + } + } + }, __options).then(relatedItems => { + DSUtils.forEach(items, item => { + DSUtils.forEach(relatedItems, relatedItem => { + if (relatedItem[relationDef.idAttribute] === item[def.localKey]) { + DSUtils.set(item, def.localField, relatedItem); + } + }); + }); + return relatedItems; + }); } - }); - resolve(filter.call(emptyStore, values(data), resourceConfig.name, params, options)); - }, reject, this); - }); - }); + + if (task) { + tasks.push(task); + } + } + }); + return DSUtils.Promise.all(tasks); + }).then(() => items); } create(resourceConfig, attrs, options) { @@ -103,7 +279,7 @@ class DSFirebaseAdapter { return createTask((resolve, reject) => { queueTask(() => { let resourceRef = this.getRef(resourceConfig, options); - var itemRef = resourceRef.push(removeCircular(omit(attrs, resourceConfig.relationFields || [])), err => { + var itemRef = resourceRef.push(DSUtils.removeCircular(DSUtils.omit(attrs, resourceConfig.relationFields || [])), err => { if (err) { return reject(err); } else { @@ -131,7 +307,7 @@ class DSFirebaseAdapter { update(resourceConfig, id, attrs, options) { return createTask((resolve, reject) => { queueTask(() => { - attrs = removeCircular(omit(attrs || {}, resourceConfig.relationFields || [])); + attrs = DSUtils.removeCircular(DSUtils.omit(attrs || {}, resourceConfig.relationFields || [])); let itemRef = this.getRef(resourceConfig, options).child(id); itemRef.once('value', dataSnapshot => { try { @@ -145,7 +321,7 @@ class DSFirebaseAdapter { delete attrs[fields[i]]; } } - deepMixIn(item, attrs); + DSUtils.deepMixIn(item, attrs); if (resourceConfig.relations) { fields = resourceConfig.relationFields; for (i = 0; i < fields.length; i++) { diff --git a/test/find.spec.js b/test/find.spec.js index 2c6e96b..693a423 100644 --- a/test/find.spec.js +++ b/test/find.spec.js @@ -14,4 +14,74 @@ describe('dsFirebaseAdapter#find', function () { assert.deepEqual(user, { id: id, name: 'John' }); }); }); + it('should find a user with relations', function () { + var id, id2, _user, _post, _comments; + return dsFirebaseAdapter.create(User, {name: 'John'}) + .then(function (user) { + _user = user; + id = user.id; + assert.equal(user.name, 'John'); + assert.isDefined(user.id); + return dsFirebaseAdapter.find(User, user.id); + }) + .then(function (user) { + assert.equal(user.name, 'John'); + assert.isDefined(user.id); + assert.equalObjects(user, {id: id, name: 'John'}); + return dsFirebaseAdapter.create(Post, { + content: 'test', + userId: user.id + }); + }) + .then(function (post) { + _post = post; + id2 = post.id; + assert.equal(post.content, 'test'); + assert.isDefined(post.id); + assert.isDefined(post.userId); + return Promise.all([ + dsFirebaseAdapter.create(Comment, { + content: 'test2', + postId: post.id, + userId: _user.id + }), + dsFirebaseAdapter.create(Comment, { + content: 'test3', + postId: post.id, + userId: _user.id + }) + ]); + }) + .then(function (comments) { + _comments = comments; + _comments.sort(function (a, b) { + return a.content > b.content; + }); + return dsFirebaseAdapter.find(Post, _post.id, {'with': ['user', 'comment']}); + }) + .then(function (post) { + post.comments.sort(function (a, b) { + return a.content > b.content; + }); + assert.equalObjects(post.user, _user, 'post.user should equal _user'); + assert.equalObjects(post.comments, _comments, 'post.comments should equal _comments'); + return dsFirebaseAdapter.destroyAll(Comment); + }) + .then(function () { + return dsFirebaseAdapter.destroy(Post, id2); + }) + .then(function () { + return dsFirebaseAdapter.destroy(User, id); + }) + .then(function (user) { + assert.isFalse(!!user); + return dsFirebaseAdapter.find(User, id); + }) + .then(function () { + throw new Error('Should not have reached here!'); + }) + .catch(function (err) { + assert.equal(err.message, 'Not Found!'); + }); + }); }); diff --git a/test/findAll.spec.js b/test/findAll.spec.js index 9b5d365..7d381df 100644 --- a/test/findAll.spec.js +++ b/test/findAll.spec.js @@ -21,4 +21,88 @@ describe('dsFirebaseAdapter#findAll', function () { assert.deepEqual(JSON.stringify(users, null, 2), JSON.stringify([u, u2], null, 2)); }); }); + it('should load belongsTo relations', function () { + return dsFirebaseAdapter.create(Profile, { + email: 'foo@test.com' + }).then(function (profile) { + return Promise.all([ + dsFirebaseAdapter.create(User, {name: 'John', profileId: profile.id}).then(function (user) { + return dsFirebaseAdapter.create(Post, {content: 'foo', userId: user.id}); + }), + dsFirebaseAdapter.create(User, {name: 'Sally'}).then(function (user) { + return dsFirebaseAdapter.create(Post, {content: 'bar', userId: user.id}); + }) + ]) + }) + .spread(function (post1, post2) { + return Promise.all([ + dsFirebaseAdapter.create(Comment, { + content: 'test2', + postId: post1.id, + userId: post1.userId + }), + dsFirebaseAdapter.create(Comment, { + content: 'test3', + postId: post2.id, + userId: post2.userId + }) + ]); + }) + .then(function () { + return dsFirebaseAdapter.findAll(Comment, {}, {'with': ['post', 'post.user', 'user', 'user.profile']}); + }) + .then(function (comments) { + assert.isDefined(comments[0].post); + assert.isDefined(comments[0].post.user); + assert.isDefined(comments[0].user); + assert.isDefined(comments[0].user.profile || comments[1].user.profile); + assert.isDefined(comments[1].post); + assert.isDefined(comments[1].post.user); + assert.isDefined(comments[1].user); + }) + .catch(function (err) { + console.log(err.stack); + throw err; + }); + }); + it('should load hasMany and belongsTo relations', function () { + return dsFirebaseAdapter.create(Profile, { + email: 'foo@test.com' + }).then(function (profile) { + return Promise.all([ + dsFirebaseAdapter.create(User, {name: 'John', profileId: profile.id}).then(function (user) { + return dsFirebaseAdapter.create(Post, {content: 'foo', userId: user.id}); + }), + dsFirebaseAdapter.create(User, {name: 'Sally'}).then(function (user) { + return dsFirebaseAdapter.create(Post, {content: 'bar', userId: user.id}); + }) + ]); + }) + .spread(function (post1, post2) { + return Promise.all([ + dsFirebaseAdapter.create(Comment, { + content: 'test2', + postId: post1.id, + userId: post1.userId + }), + dsFirebaseAdapter.create(Comment, { + content: 'test3', + postId: post2.id, + userId: post2.userId + }) + ]); + }) + .then(function () { + return dsFirebaseAdapter.findAll(Post, {}, {'with': ['user', 'comment', 'comment.user', 'comment.user.profile']}); + }) + .then(function (posts) { + assert.isDefined(posts[0].comments); + assert.isDefined(posts[0].comments[0].user); + assert.isDefined(posts[0].comments[0].user.profile || posts[1].comments[0].user.profile); + assert.isDefined(posts[0].user); + assert.isDefined(posts[1].comments); + assert.isDefined(posts[1].comments[0].user); + assert.isDefined(posts[1].user); + }); + }); });