var cloneDeep = require('lodash.clonedeep'); var assign = require('object-assign'); var isobject = require('lodash.isobject'); var Q = require('q'); /** * OKQuery! * Takes a query spec for a resource and maps it the proper read * methods of that resource. This is simply to separate the notions * of queries and resources. */ function OKQuery(options) { if (!(this instanceof OKQuery)) return new OKQuery(options); options = options || {}; if (!options.resource) throw new Error('No resource provided to query'); var resource = options.resource; var type = resource.type; var query = options.query || '*'; // Ensure immutability if (isobject(query)) query = cloneDeep(query); // Queries are ordered by index by default var sortField = options.sortBy || '__index'; // TODO Make descending by default var descending = options.descending || false; Object.defineProperty(this, 'resource', { value: resource, writable: false, enumerable: true }); Object.defineProperty(this, 'type', { value: resource.type, writable: false, enumerable: true }); Object.defineProperty(this, 'as', { value: options.as, writable: false, enumerable: true }); Object.defineProperty(this, 'groupBy', { value: options.groupBy, writable: false, enumerable: true }) this.get = createQuery(resource, query, { default: options.default, sortField: sortField, descending : descending, groupBy: options.groupBy }); } function createQuery(resource, query, options) { options = options || {}; if (resource.bound) { query = queryBound(resource); } else if (isobject(query)) { query = queryComplex(resource, query) } else if (isDynamic(query)) { query = queryDynamic(resource); } else if (isSet(query)) { query = queryAll(resource, options.sortField, options.descending); } else { query = querySingle(resource, query); } if (options.default) { query = withDefault(query, options.default); } if (options.groupBy) { query = withGrouping(query, options.groupBy) } return query; } function queryComplex(resource, query) { var dynamicProp; // Query is an object specifying key value pairs against which // to match DB entries. Iterate through and check if any of the values // is unbound e.g. :id var notDynamic = Object.keys(query).every(function(prop) { var matcher = query[prop]; if (isDynamic(matcher)) { dynamicProp = prop; return false; } else { return true; } }); if (notDynamic) { return function() { return resource.find(query); } } else { return function(id) { // Bind the dynamic property to its value // and add the pair to the query var dynamicQuery = {}; dynamicQuery[dynamicProp] = id; var query = assign({}, query, dynamicQuery); return resource.find(query); } } } function queryDynamic(resource) { return function(id) { return resource.get(id); }; } function queryAll(resource, sortField, descending) { return function() { return resource.sortBy(sortField, descending); }; } function querySingle(resource, id) { return function() { return resource.get(id); }; } function queryBound(resource) { return function() { return resource.get(); }; } /** * Transform the query such that the results are grouped by the * given field */ function withGrouping(queryFn, groupField) { return function() { return Q.Promise(function(resolve, reject) { queryFn().then(function(data) { data = data || [] if (typeof data.length === 'undefined') { data = [data] } var result = {} result[groupField] = data.reduce(reduceToGroups, {}) resolve(result) }, reject) }) } function reduceToGroups(acc, data) { var groupName = data[groupField] if (groupName) { if (!acc[groupName]) { acc[groupName] = [] } acc[groupName].push(data) } return acc } } function withDefault(queryFn, resultDefault) { return function() { return Q.Promise(function(resolve, reject) { queryFn().then(function(data) { data = data || resultDefault; resolve(data); }, reject); }); }; } function isDynamic(query) { return query && query.charAt(0) === ':'; } function isSet(query) { return query && query === '*'; } module.exports = OKQuery;