From eb619b2048a091f9af2e1f8ac5c3c5b47cae27ad Mon Sep 17 00:00:00 2001 From: Matthew Creager Date: Tue, 28 Jan 2014 16:24:24 -0400 Subject: [PATCH] feat($goQuery): Experimental intergration of Query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit $goQuery is a service built on `key.query()`. It accepts a key, room, filter and options. It returns a QueryModel. The Model has the standard `$sync` method which keeps the models value current with that of the query result set. ```js angular.module('ChatApp').controller('ChatCtrl', ['$scope', '$goQuery', function($scope, $query) { var filter = { “sender”: { “$eq”: “GrandMasterLivingston” }}; var sort = { “$name”: “desc”}; var limit = 20; var options = { sort: sort, limit: limit }; $scope.chat = $query('messages', filter, options); $scope.chat.$sync(); }]); ``` Related: #48 --- component.json | 5 +- index.js | 15 +++ lib/query_model.js | 173 +++++++++++++++++++++++++ lib/query_sync.js | 127 +++++++++++++++++++ lib/util/args.js | 306 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 lib/query_model.js create mode 100644 lib/query_sync.js create mode 100644 lib/util/args.js diff --git a/component.json b/component.json index af290eb..d4f7ca6 100644 --- a/component.json +++ b/component.json @@ -15,6 +15,8 @@ "main": "index.js", "scripts": [ "index.js", + "lib/query_model.js", + "lib/query_sync.js", "lib/safe_apply.js", "lib/connection.js", "lib/connection_factory.js", @@ -28,6 +30,7 @@ "lib/model_binder.js", "lib/bounce_protection.js", "lib/errors.js", - "lib/util/normalize.js" + "lib/util/normalize.js", + "lib/util/args.js" ] } diff --git a/index.js b/index.js index 4dcc8e2..c76786d 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,8 @@ var connectionFactory = require('./lib/connection_factory'); var goAngularFactory = require('./lib/go_angular_factory'); var syncFactory = require('./lib/sync_factory'); var keyFactory = require('./lib/key_factory'); +var QueryModel = require('./lib/query_model'); +var QuerySync = require('./lib/query_sync'); /** goangular module registration */ @@ -29,12 +31,25 @@ goangular.factory('$goSync', [ syncFactory ]); +goangular.factory('$goQuerySync', [ + '$parse', + '$timeout', + QuerySync +]); + goangular.factory('$goKey', [ '$goSync', '$goConnection', keyFactory ]); +goangular.factory('$goQuery', [ + '$goQuerySync', + '$goConnection', + '$goKey', + QueryModel +]); + goangular.factory('GoAngular', [ '$q', '$parse', diff --git a/lib/query_model.js b/lib/query_model.js new file mode 100644 index 0000000..91a47e2 --- /dev/null +++ b/lib/query_model.js @@ -0,0 +1,173 @@ +/* jshint browser:true */ +/* global require, module */ + +/** + * @fileOverview + * + * This file contains the Query Factory & Model, responsible for creating and + * returning instances of the GoAngular Query Model. + */ + +'use strict'; + +var _ = require('lodash'); +var Emitter = require('emitter'); +var Args = require('./util/args'); + +var LOCAL_EVENTS = ['ready', 'error']; +var $key; + +/** + * queryFactory + * @public + * @param {Object} $sync - Responsible for synchronizing query model + * @param {Object} $conn - GoInstant connection + * @param {Function} $goKey - Uses to create GoAngular key model + * @returns {Function} option validation & instance creation + */ +module.exports = function queryFactory($sync, $conn, $goKey) { + $key = $goKey; + + /** + * @public + * @param {Object} key - GoInstant key + */ + return function() { + var a = new Args([ + { key: Args.OBJECT | Args.STRING | Args.Required }, + { room: Args.STRING | Args.Optional }, + { filter: Args.OBJECT | Args.Required }, + { options: Args.OBJECT | Args.Required } + ], arguments); + + return new QueryModel($sync, $conn, a.key, a.room, a.filter, a.options); + }; +}; + +function QueryModel($querySync, $connection, key, room, filter, options) { + _.bindAll(this); + + // If a key is provided, use it, otherwise create one + key = (_.isObject(key) ? key : $connection.$key(key, room)); + + _.extend(this, { + $$querySync: $querySync, + $$connection: $connection, + $$key: key, + $$query: key.query(filter, options), + $$emitter: new Emitter(), + $$index: [] + }); +} + +/** + * Primes our model, fetching the current result set and monitoring it for + * changes. + * @public + */ +QueryModel.prototype.$sync = function() { + var self = this; + + var connected = self.$$connection.$ready(); + + connected.then(function() { + self.$$sync = self.$$querySync(self.$$query, self); + self.$$sync.$initialize(); + }); + + connected.fail(function(err) { + self.$$emitter.emit('error', err); + }); + + return self; +}; + +/** + * Add a generated id with a + + * @param {*} value - New value of key + * @returns {Object} promise + */ +QueryModel.prototype.$add = function(value, opts) { + opts = opts || {}; + + var self = this; + return this.$$connection.$ready().then(function() { + return self.$$key.add(value, opts); + }); +}; + +/** + * Give the current key a new value + * @public + * @param {*} value - New value of key + * @returns {Object} promise + */ +QueryModel.prototype.$set = function(value, opts) { + opts = opts || {}; + + var self = this; + return this.$$goConnection.$ready().then(function() { + return self.$$key.set(value, opts); + }); +}; + +/** + * Returns a new object that does not contain prefixed methods + * @public + * @returns {Object} model + */ +QueryModel.prototype.$omit = function() { + return _.omit(this, function(value, key){ + return _.first(key) === '$'; + }); +}; + +/** + * Create and return a new instance of Model, with a relative key. + * @public + * @param {String} keyName - Key name + */ +QueryModel.prototype.$key = function(keyName) { + var key = this.$$key.key(keyName); + + return $key(key); +}; + +/** + * Remove this key + * @public + * @returns {Object} promise + */ +QueryModel.prototype.$remove = function(opts) { + opts = opts || {}; + + var self = this; + return this.$$connection.$ready().then(function() { + return self.$$key.remove(opts); + }); +}; + +/** + * Bind a listener to events on this key + * @public + */ +QueryModel.prototype.$on = function(eventName, listener) { + if (!_.contains(LOCAL_EVENTS, eventName)) { + return this.$$query.on(eventName, listener); + } + + this.$$emitter.on(eventName, listener); +}; + +/** + * Remove a listener on this key + * @public + */ +QueryModel.prototype.$off = function(eventName, listener) { + if (!_.contains(LOCAL_EVENTS, eventName)) { + return this.$$query.off(eventName, listener); + } + + this.$$emitter.off(eventName, listener); +}; diff --git a/lib/query_sync.js b/lib/query_sync.js new file mode 100644 index 0000000..37ac979 --- /dev/null +++ b/lib/query_sync.js @@ -0,0 +1,127 @@ +/* jshint browser:true */ +/* global require, module */ + +/** + * @fileOverview + * + * This file contains the QuerySync class, used to create a binding between + * a query model on $scope and a GoInstant query + */ + +'use strict'; + +var _ = require('lodash'); + +module.exports = function querySync($parse, $timeout) { + + /** + * @public + * @param {Object} key - GoInstant key + */ + return function(query, qModel) { + return new QuerySync($parse, $timeout, query, qModel); + }; +}; + +/** + * The Sync class is responsible for synchronizing the state of a local model, + * with that of a GoInstant key. + * + * @constructor + * @param {Object} $parse - Angular parse object + * @param {Object} $timeout - Angular timeout object + * @param {Object} query - GoInstant query object + * @param {Object} model - local object + */ +function QuerySync($parse, $timeout, query, model) { + _.bindAll(this, [ + '$initialize', + '$$handleUpdate', + '$$handleRemove' + ]); + + _.extend(this, { + $parse: $parse, + $timeout: $timeout, + $$query: query, + $$model: model, + $$registry: {} + }); +} + +/** + * Creates an association between a local object and a query by + * fetching a result set and monitoring the query. + */ +QuerySync.prototype.$initialize = function() { + var self = this; + var index = self.$$model.$$index; + + self.$$query.execute(function(err, results) { + if (err) { + return self.$$model.$$emitter.emit('error', err); + } + + self.$$registry = { + update: self.$$handleUpdate, + add: self.$$handleUpdate, + remove: self.$$handleRemove + }; + + _.each(self.$$registry, function(fn, event) { + self.$$query.on(event, fn); + }); + + self.$timeout(function() { + _.map(results, function(result) { + index.push(result.name); + self.$$model[result.name] = result.value; + }); + + self.$$model.$$emitter.emit('ready'); + }); + }); +}; + +/** + * When the query result set changes, update our local model object + * @private + * @param {*} result - new result set + * @param {Object} context - Information related to the key being set + */ +QuerySync.prototype.$$handleUpdate = function(result, context) { + var self = this; + + var index = self.$$model.$$index; + + // Update the index array with the new position of this key + var currentIndex = index.indexOf(result.name); + + if (currentIndex !== -1) { + index.splice(currentIndex, 1); + } + + index.splice(context.position.current, 0, result.name); + + // Update the value of the model. + // The index update above will NOT trigger any changes on scope. + // Removing this will cause position to not be updated. + self.$timeout(function() { + self.$$model[result.name] = result.value; + }); +}; + +/** + * When an item is removed from the result set, update the model + * @private + * @param {*} result - new result set + * @param {Object} context - information related to the key being set + */ +QuerySync.prototype.$$handleRemove = function(result, context) { + this.$$model.$$index.splice(context.position.previous, 1); + + var self = this; + this.$timeout(function() { + delete self.$$model[result.name]; + }); +}; diff --git a/lib/util/args.js b/lib/util/args.js new file mode 100644 index 0000000..808b9c0 --- /dev/null +++ b/lib/util/args.js @@ -0,0 +1,306 @@ +/** +The MIT License (MIT) + +Copyright (c) 2013-2014, OMG Life Ltd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +module.exports = Args; + +var Args = (function() { + + "use strict"; + + var _extractSchemeEl = function(rawSchemeEl) { + var schemeEl = {}; + schemeEl.defValue = undefined; + schemeEl.typeValue = undefined; + schemeEl.customCheck = undefined; + for (var name in rawSchemeEl) { + if (!rawSchemeEl.hasOwnProperty(name)) continue; + if (name === "_default") { + schemeEl.defValue = rawSchemeEl[name]; + } else if (name === "_type") { + schemeEl.typeValue = rawSchemeEl[name]; + } else if (name === "_check") { + schemeEl.customCheck = rawSchemeEl[name]; + } else { + schemeEl.sname = name; + } + } + schemeEl.sarg = rawSchemeEl[schemeEl.sname]; + return schemeEl; + }; + + var _typeMatches = function(arg, schemeEl) { + if ((schemeEl.sarg & Args.ANY) !== 0) { + return true; + } + if ((schemeEl.sarg & Args.STRING) !== 0 && typeof arg === "string") { + return true; + } + if ((schemeEl.sarg & Args.FUNCTION) !== 0 && typeof arg === "function") { + return true; + } + if ((schemeEl.sarg & Args.INT) !== 0 && (typeof arg === "number" && Math.floor(arg) === arg)) { + return true; + } + if ((schemeEl.sarg & Args.FLOAT) !== 0 && typeof arg === "number") { + return true; + } + if ((schemeEl.sarg & Args.ARRAY) !== 0 && (arg instanceof Array)) { + return true; + } + if (((schemeEl.sarg & Args.OBJECT) !== 0 || schemeEl.typeValue !== undefined) && ( + typeof arg === "object" && + (schemeEl.typeValue === undefined || (arg instanceof schemeEl.typeValue)) + )) { + return true; + } + if ((schemeEl.sarg & Args.ARRAY_BUFFER) !== 0 && arg.toString().match(/ArrayBuffer/)) { + return true; + } + if ((schemeEl.sarg & Args.DATE) !== 0 && arg instanceof Date) { + return true; + } + if ((schemeEl.sarg & Args.BOOL) !== 0 && typeof arg === "boolean") { + return true; + } + if ((schemeEl.sarg & Args.DOM_EL) !== 0 && + ( + (arg instanceof HTMLElement) || + (window.$ !== undefined && arg instanceof window.$) + ) + ) { + return true; + } + if (schemeEl.customCheck !== undefined && typeof schemeEl.customCheck === "function") { + if (schemeEl.customCheck(arg)) { + return true; + } + } + return false; + }; + + var _isTypeSpecified = function(schemeEl) { + return (schemeEl.sarg & (Args.ANY | Args.STRING | Args.FUNCTION | Args.INT | Args.FLOAT | Args.OBJECT | Args.ARRAY_BUFFER | Args.DATE | Args.BOOL | Args.DOM_EL | Args.ARRAY)) != 0 || schemeEl.typeValue !== undefined; + }; + + var _getTypeString = function(schemeEl) { + var sarg = schemeEl.sarg; + var typeValue = schemeEl.typeValue; + var customCheck = schemeEl.customCheck; + + if ((sarg & Args.STRING) !== 0 ) { + return "String"; + } + if ((sarg & Args.FUNCTION) !== 0 ) { + return "Function"; + } + if ((sarg & Args.INT) !== 0 ) { + return "Int"; + } + if ((sarg & Args.FLOAT) !== 0 ) { + return "Float"; + } + if ((sarg & Args.ARRAY) !== 0 ) { + return "Array"; + } + if ((sarg & Args.OBJECT) !== 0) { + if (typeValue !== undefined) { + return "Object (" + typeValue.toString() + ")"; + } else { + return "Object"; + } + } + if ((sarg & Args.ARRAY_BUFFER) !== 0 ) { + return "Arry Buffer"; + } + if ((sarg & Args.DATE) !== 0 ) { + return "Date"; + } + if ((sarg & Args.BOOL) !== 0 ) { + return "Bool"; + } + if ((sarg & Args.DOM_EL) !== 0 ) { + return "DOM Element"; + } + if (customCheck !== undefined) { + return "[Custom checker]"; + } + return "unknown"; + }; + + var _checkNamedArgs = function(namedArgs, scheme, returns) { + var foundOne = false; + for (var s = 0 ; s < scheme.length ; s++) { + foundOne &= (function(schemeEl) { + var argFound = false; + for (var name in namedArgs) { + var namedArg = namedArgs[name]; + if (name === schemeEl.sname) { + if (_typeMatches(namedArg, schemeEl)) { + returns[name] = namedArg; + argFound = true; + break; + } + } + } + return argFound; + })(_extractSchemeEl(scheme[s])); + } + return foundOne; + }; + + var Args = function(scheme, args) { + if (scheme === undefined) throw new Error("The scheme has not been passed."); + if (args === undefined) throw new Error("The arguments have not been passed."); + + var returns = {}; + var err = undefined; + + var a, s; + + for (a = 0, s = 0; a < args.length, s < scheme.length ; s++) { + a = (function(a,s) { + + var arg = args[a]; + + // argument group + if (scheme[s] instanceof Array) { + if (arg === null || arg === undefined) { + err = "Argument " + a + " is null or undefined but it must be not null."; + return a; + } else { + var group = scheme[s]; + var retName = undefined; + for (var g = 0 ; g < group.length ; g++) { + var schemeEl = _extractSchemeEl(group[g]); + if (_typeMatches(arg, schemeEl)) { + retName = schemeEl.sname; + } + } + if (retName === undefined) { + err = "Argument " + a + " should be one of: "; + for (var g = 0 ; g < group.length ; g++) { + var schemeEl = _extractSchemeEl(group[g]); + err += _getTypeString(schemeEl) + ", "; + } + err += "but it was type " + (typeof arg) + " with value " + arg + "."; + return a; + } else { + returns[retName] = arg; + return a+1; + } + } + } else { + var schemeEl = _extractSchemeEl(scheme[s]); + + // optional arg + if ((schemeEl.sarg & Args.Optional) !== 0) { + // check if this arg matches the next schema slot + if ( arg === null || arg === undefined) { + if (schemeEl.defValue !== undefined) { + returns[schemeEl.sname] = schemeEl.defValue; + } else { + returns[schemeEl.sname] = arg; + } + return a+1; // if the arg is null or undefined it will fill a slot, but may be replace by the default value + } else if (_typeMatches(arg, schemeEl)) { + returns[schemeEl.sname] = arg; + return a+1; + } else if (schemeEl.defValue !== undefined) { + returns[schemeEl.sname] = schemeEl.defValue; + return a; + } + } + + // manadatory arg + else { //if ((schemeEl.sarg & Args.NotNull) !== 0) { + if (arg === null || arg === undefined) { + err = "Argument " + a + " ("+schemeEl.sname+") is null or undefined but it must be not null."; + return a; + } + else if (!_typeMatches(arg, schemeEl)) { + if (_isTypeSpecified(schemeEl)) { + err = "Argument " + a + " ("+schemeEl.sname+") should be type "+_getTypeString(schemeEl)+", but it was type " + (typeof arg) + " with value " + arg + "."; + } else if (schemeEl.customCheck !== undefined) { + var funcString = schemeEl.customCheck.toString(); + if (funcString.length > 50) { + funcString = funcString.substr(0, 40) + "..." + funcString.substr(funcString.length-10); + } + err = "Argument " + a + " ("+schemeEl.sname+") does not pass the custom check ("+funcString+")."; + } else { + err = "Argument " + a + " ("+schemeEl.sname+") has no valid type specified."; + } + return a; + } else { + returns[schemeEl.sname] = arg; + return a+1; + } + } + + } + + return a; + })(a,s); + if (err) { + break; + } + } + + // check named args for optional args, named args are last + var namedArgsToCheck = (a < args.length && (typeof args[a]) === "object"); + if (namedArgsToCheck) { + var namedArgs = args[a]; + var foundNamedArg = _checkNamedArgs(namedArgs, scheme, returns); + } + + if (err && (!namedArgsToCheck || !foundNamedArg)) { + throw new Error(err); + } + + return returns; + }; + + Args.ANY = 0x1; + Args.STRING = 0x1 << 1; + Args.FUNCTION = 0x1 << 2; + Args.INT = 0x1 << 3; + Args.FLOAT = 0x1 << 4; + Args.ARRAY_BUFFER = 0x1 << 5; + Args.OBJECT = 0x1 << 6; + Args.DATE = 0x1 << 7; + Args.BOOL = 0x1 << 8; + Args.DOM_EL = 0x1 << 9; + Args.ARRAY = 0x1 << 10; + + + Args.Optional = 0x1 << 11; + Args.NotNull = + Args.Required = 0x1 << 12; + + return Args; +})(); + + +try { + module.exports = Args; +} catch (e) {}