diff --git a/.travis.yml b/.travis.yml index f1dd20c..b455780 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,5 +27,5 @@ before_script: script: - meteor create --bare test - cd test - - meteor npm i --save selenium-webdriver@3.6.0 chromedriver@2.34.1 simpl-schema + - meteor npm i --save selenium-webdriver@3.6.0 chromedriver@2.36.0 simpl-schema - METEOR_PACKAGE_DIRS="../" TEST_BROWSER_DRIVER=chrome meteor test-packages --once --driver-package meteortesting:mocha ../ \ No newline at end of file diff --git a/lib/exposure/exposure.js b/lib/exposure/exposure.js index 8e0a5ca..6d49afd 100644 --- a/lib/exposure/exposure.js +++ b/lib/exposure/exposure.js @@ -2,14 +2,18 @@ import genCountEndpoint from '../query/counts/genEndpoint.server.js'; import createGraph from '../query/lib/createGraph.js'; import recursiveCompose from '../query/lib/recursiveCompose.js'; import hypernova from '../query/hypernova/hypernova.js'; -import {ExposureSchema, ExposureDefaults, validateBody} from './exposure.config.schema.js'; +import { + ExposureSchema, + ExposureDefaults, + validateBody, +} from './exposure.config.schema.js'; import enforceMaxDepth from './lib/enforceMaxDepth.js'; import enforceMaxLimit from './lib/enforceMaxLimit.js'; import cleanBody from './lib/cleanBody.js'; import deepClone from 'lodash.clonedeep'; import restrictFieldsFn from './lib/restrictFields.js'; import restrictLinks from './lib/restrictLinks.js'; -import {check} from 'meteor/check'; +import { check } from 'meteor/check'; let globalConfig = {}; @@ -47,7 +51,10 @@ export default class Exposure { } if (!this.config.method && !this.config.publication) { - throw new Meteor.Error('weird', 'If you want to expose your collection you need to specify at least one of ["method", "publication"] options to true') + throw new Meteor.Error( + 'weird', + 'If you want to expose your collection you need to specify at least one of ["method", "publication"] options to true' + ); } this.initCountMethod(); @@ -55,12 +62,17 @@ export default class Exposure { } _validateAndClean() { - if (typeof(this.config) === 'function') { + if (typeof this.config === 'function') { const firewall = this.config; - this.config = {firewall}; + this.config = { firewall }; } - this.config = Object.assign({}, ExposureDefaults, Exposure.getConfig(), this.config); + this.config = Object.assign( + {}, + ExposureDefaults, + Exposure.getConfig(), + this.config + ); check(this.config, ExposureSchema); if (this.config.body) { @@ -94,7 +106,10 @@ export default class Exposure { */ getBody(userId) { if (!this.config.body) { - throw new Meteor.Error('missing-body', 'Cannot get exposure body because it was not defined.'); + throw new Meteor.Error( + 'missing-body', + 'Cannot get exposure body because it was not defined.' + ); } let body; @@ -109,10 +124,7 @@ export default class Exposure { return true; } - return deepClone( - body, - userId - ); + return deepClone(body, userId); } /** @@ -123,7 +135,7 @@ export default class Exposure { const config = this.config; const getTransformedBody = this.getTransformedBody.bind(this); - Meteor.publishComposite(this.name, function (body) { + Meteor.publishComposite(this.name, function(body) { let transformedBody = getTransformedBody(body); const rootNode = createGraph(collection, transformedBody); @@ -132,7 +144,7 @@ export default class Exposure { restrictLinks(rootNode, this.userId); return recursiveCompose(rootNode, this.userId, { - bypassFirewalls: !!config.body + bypassFirewalls: !!config.body, }); }); } @@ -159,12 +171,12 @@ export default class Exposure { // if there is no exposure body defined, then we need to apply firewalls return hypernova(rootNode, this.userId, { - bypassFirewalls: !!config.body + bypassFirewalls: !!config.body, }); }; Meteor.methods({ - [this.name]: methodBody + [this.name]: methodBody, }); } @@ -179,9 +191,11 @@ export default class Exposure { [this.name + '.count'](body) { this.unblock(); - return collection.find(body.$filters || {}, {}, this.userId).count(); - } - }) + return collection + .find(body.$filters || {}, {}, this.userId) + .count(); + }, + }); } /** @@ -191,10 +205,14 @@ export default class Exposure { const collection = this.collection; genCountEndpoint(this.name, { - getCursor(session) { - return collection.find(session.filters, { - fields: {_id: 1}, - }, this.userId); + getCursor({ session }) { + return collection.find( + session.filters, + { + fields: { _id: 1 }, + }, + this.userId + ); }, getSession(body) { @@ -209,13 +227,18 @@ export default class Exposure { */ initSecurity() { const collection = this.collection; - const {firewall, maxLimit, restrictedFields} = this.config; + const { firewall, maxLimit, restrictedFields } = this.config; const find = collection.find.bind(collection); const findOne = collection.findOne.bind(collection); collection.firewall = (filters, options, userId) => { if (userId !== undefined) { - this._callFirewall({collection: collection}, filters, options, userId); + this._callFirewall( + { collection: collection }, + filters, + options, + userId + ); enforceMaxLimit(options, maxLimit); @@ -225,7 +248,7 @@ export default class Exposure { } }; - collection.find = function (filters, options = {}, userId = undefined) { + collection.find = function(filters, options = {}, userId = undefined) { if (arguments.length == 0) { filters = {}; } @@ -240,27 +263,31 @@ export default class Exposure { return find(filters, options); }; - collection.findOne = function (filters, options = {}, userId = undefined) { + collection.findOne = function( + filters, + options = {}, + userId = undefined + ) { // If filters is undefined it should return an empty item if (arguments.length > 0 && filters === undefined) { return null; } - if (typeof(filters) === 'string') { - filters = {_id: filters}; + if (typeof filters === 'string') { + filters = { _id: filters }; } collection.firewall(filters, options, userId); return findOne(filters, options); - } + }; } /** * @private */ _callFirewall(...args) { - const {firewall} = this.config; + const { firewall } = this.config; if (!firewall) { return; } @@ -268,9 +295,9 @@ export default class Exposure { if (_.isArray(firewall)) { firewall.forEach(fire => { fire.call(...args); - }) + }); } else { firewall.call(...args); } } -}; +} diff --git a/lib/namedQuery/expose/extension.js b/lib/namedQuery/expose/extension.js index be48382..a556d21 100644 --- a/lib/namedQuery/expose/extension.js +++ b/lib/namedQuery/expose/extension.js @@ -1,5 +1,5 @@ import NamedQuery from '../namedQuery.js'; -import {ExposeSchema, ExposeDefaults} from './schema.js'; +import { ExposeSchema, ExposeDefaults } from './schema.js'; import mergeDeep from './lib/mergeDeep.js'; import createGraph from '../../query/lib/createGraph.js'; import recursiveCompose from '../../query/lib/recursiveCompose.js'; @@ -7,7 +7,7 @@ import prepareForProcess from '../../query/lib/prepareForProcess.js'; import deepClone from 'lodash.clonedeep'; import intersectDeep from '../../query/lib/intersectDeep'; import genCountEndpoint from '../../query/counts/genEndpoint.server'; -import {check} from 'meteor/check'; +import { check } from 'meteor/check'; _.extend(NamedQuery.prototype, { /** @@ -15,11 +15,17 @@ _.extend(NamedQuery.prototype, { */ expose(config = {}) { if (!Meteor.isServer) { - throw new Meteor.Error('invalid-environment', `You must run this in server-side code`); + throw new Meteor.Error( + 'invalid-environment', + `You must run this in server-side code` + ); } if (this.isExposed) { - throw new Meteor.Error('query-already-exposed', `You have already exposed: "${this.name}" named query`); + throw new Meteor.Error( + 'query-already-exposed', + `You have already exposed: "${this.name}" named query` + ); } this.exposeConfig = Object.assign({}, ExposeDefaults, config); @@ -53,7 +59,10 @@ _.extend(NamedQuery.prototype, { } if (!config.method && !config.publication) { - throw new Meteor.Error('weird', 'If you want to expose your named query you need to specify at least one of ["method", "publication"] options to true') + throw new Meteor.Error( + 'weird', + 'If you want to expose your named query you need to specify at least one of ["method", "publication"] options to true' + ); } this._initCountMethod(); @@ -62,8 +71,8 @@ _.extend(NamedQuery.prototype, { /** * Returns the embodied body of the request - * @param {*} _embody - * @param {*} body + * @param {*} _embody + * @param {*} body */ doEmbodimentIfItApplies(body) { // query is not exposed yet, so it doesn't have embodiment logic @@ -71,19 +80,16 @@ _.extend(NamedQuery.prototype, { return; } - const {embody} = this.exposeConfig; + const { embody } = this.exposeConfig; if (!embody) { return; } if (_.isFunction(embody)) { - embody.call(this, body, this.params) + embody.call(this, body, this.params); } else { - mergeDeep( - body, - embody - ); + mergeDeep(body, embody); } }, @@ -98,8 +104,8 @@ _.extend(NamedQuery.prototype, { // security is done in the fetching because we provide a context return self.clone(newParams).fetch(this); - } - }) + }, + }); }, /** @@ -115,7 +121,7 @@ _.extend(NamedQuery.prototype, { // security is done in the fetching because we provide a context return self.clone(newParams).getCount(this); - } + }, }); }, @@ -127,7 +133,7 @@ _.extend(NamedQuery.prototype, { const self = this; genCountEndpoint(self.name, { - getCursor(session) { + getCursor({ session }) { const query = self.clone(session.params); return query.getCursorForCounting(); }, @@ -136,7 +142,7 @@ _.extend(NamedQuery.prototype, { self.doValidateParams(newParams); self._callFirewall(this, this.userId, params); - return { params: newParams }; + return { name: self.name, params: newParams }; }, }); }, @@ -147,7 +153,7 @@ _.extend(NamedQuery.prototype, { _initPublication() { const self = this; - Meteor.publishComposite(this.name, function (params = {}) { + Meteor.publishComposite(this.name, function(params = {}) { self._unblockIfNecessary(this); self.doValidateParams(params); self._callFirewall(this, this.userId, params); @@ -173,7 +179,7 @@ _.extend(NamedQuery.prototype, { * @private */ _callFirewall(context, userId, params) { - const {firewall} = this.exposeConfig; + const { firewall } = this.exposeConfig; if (!firewall) { return; } @@ -181,7 +187,7 @@ _.extend(NamedQuery.prototype, { if (_.isArray(firewall)) { firewall.forEach(fire => { fire.call(context, userId, params); - }) + }); } else { firewall.call(context, userId, params); } diff --git a/lib/namedQuery/testing/client.test.js b/lib/namedQuery/testing/client.test.js index 2eef850..f2aacb3 100644 --- a/lib/namedQuery/testing/client.test.js +++ b/lib/namedQuery/testing/client.test.js @@ -1,12 +1,12 @@ import postListExposure from './bootstrap/queries/postListExposure.js'; import { createQuery } from 'meteor/cultofcoders:grapher'; -describe('Named Query', function () { - it('Should return proper values', function (done) { +describe('Named Query', function() { + it('Should return proper values', function(done) { const query = createQuery({ postListExposure: { - title: 'User Post - 3' - } + title: 'User Post - 3', + }, }); query.fetch((err, res) => { @@ -20,11 +20,11 @@ describe('Named Query', function () { }); done(); - }) + }); }); - it('Should return proper values using query directly via import', function (done) { - const query = postListExposure.clone({title: 'User Post - 3'}); + it('Should return proper values using query directly via import', function(done) { + const query = postListExposure.clone({ title: 'User Post - 3' }); query.fetch((err, res) => { assert.isUndefined(err); @@ -37,20 +37,20 @@ describe('Named Query', function () { }); done(); - }) + }); }); - it('Should work with count', function (done) { - const query = postListExposure.clone({title: 'User Post - 3'}); + it('Should work with count', function(done) { + const query = postListExposure.clone({ title: 'User Post - 3' }); query.getCount((err, res) => { assert.equal(6, res); done(); - }) + }); }); - it('Should work with reactive counts', function (done) { - const query = postListExposure.clone({title: 'User Post - 3'}); + it('Should work with reactive counts', function(done) { + const query = postListExposure.clone({ title: 'User Post - 3' }); const handle = query.subscribeCount(); Tracker.autorun(c => { @@ -65,11 +65,11 @@ describe('Named Query', function () { }); }); - it('Should work with reactive queries', function (done) { + it('Should work with reactive queries', function(done) { const query = createQuery({ postListExposure: { - title: 'User Post - 3' - } + title: 'User Post - 3', + }, }); const handle = query.subscribe(); @@ -90,12 +90,12 @@ describe('Named Query', function () { done(); } - }) + }); }); - it('Should work with reactive queries via import', function (done) { + it('Should work with reactive queries via import', function(done) { const query = postListExposure.clone({ - title: 'User Post - 3' + title: 'User Post - 3', }); const handle = query.subscribe(); @@ -116,6 +116,6 @@ describe('Named Query', function () { done(); } - }) - }) -}); \ No newline at end of file + }); + }); +}); diff --git a/lib/query/counts/genEndpoint.server.js b/lib/query/counts/genEndpoint.server.js index bb22903..8de3bcc 100644 --- a/lib/query/counts/genEndpoint.server.js +++ b/lib/query/counts/genEndpoint.server.js @@ -18,7 +18,12 @@ export default (name, { getCursor, getSession }) => { Meteor.methods({ [name + '.count.subscribe'](paramsOrBody) { const session = getSession.call(this, paramsOrBody); - const existingSession = collection.findOne({ ...session, userId: this.userId }); + const sessionId = JSON.stringify(session); + + const existingSession = collection.findOne({ + session: sessionId, + userId: this.userId, + }); // Try to reuse sessions if the user subscribes multiple times with the same data if (existingSession) { @@ -26,7 +31,7 @@ export default (name, { getCursor, getSession }) => { } const token = collection.insert({ - ...session, + session: sessionId, query: name, userId: this.userId, }); @@ -41,30 +46,41 @@ export default (name, { getCursor, getSession }) => { const request = collection.findOne({ _id: token, userId: self.userId }); if (!request) { - throw new Error('no-request', `You must acquire a request token via the "${name}.count.subscribe" method first.`); + throw new Error( + 'no-request', + `You must acquire a request token via the "${name}.count.subscribe" method first.` + ); } + request.session = JSON.parse(request.session); const cursor = getCursor.call(this, request); // Start counting let count = 0; - self.added(COUNTS_COLLECTION_CLIENT, token, { count }); - const handle = cursor.observeChanges({ - added(id) { + + let isReady = false; + const handle = cursor.observe({ + added() { count++; - self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); + isReady && + self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); }, - removed(id) { + removed() { count--; - self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); + isReady && + self.changed(COUNTS_COLLECTION_CLIENT, token, { count }); }, }); + isReady = true; + self.added(COUNTS_COLLECTION_CLIENT, token, { count }); + self.onStop(() => { handle.stop(); collection.remove(token); }); + self.ready(); }); }; diff --git a/lib/query/counts/testing/bootstrap/namedQuery.test.js b/lib/query/counts/testing/bootstrap/namedQuery.test.js index 6506717..21900f0 100644 --- a/lib/query/counts/testing/bootstrap/namedQuery.test.js +++ b/lib/query/counts/testing/bootstrap/namedQuery.test.js @@ -1,10 +1,33 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; -const query = createQuery('counts_posts_query', { +export const postsQuery = createQuery('counts_posts_query', { counts_posts: { _id: 1, text: 1, }, }); -export default query; +export const postsQuery2 = createQuery('counts_posts_query2', { + counts_posts: { + $filters: { + text: 'text 1', + }, + _id: 1, + text: 1, + }, +}); + +export const postsQuery3 = createQuery('counts_posts_query3', { + counts_posts: { + $filters: { + text: { + $regex: 'text', + $options: 'i', + }, + }, + _id: 1, + text: 1, + }, +}); + +export default postsQuery; diff --git a/lib/query/counts/testing/client.test.js b/lib/query/counts/testing/client.test.js index d70d5ca..0e1cbd5 100644 --- a/lib/query/counts/testing/client.test.js +++ b/lib/query/counts/testing/client.test.js @@ -1,9 +1,16 @@ import { Tracker } from 'meteor/tracker'; import PostsCollection from './bootstrap/collection.test'; -import NamedQuery from './bootstrap/namedQuery.test'; +import NamedQuery, { + postsQuery, + postsQuery2, + postsQuery3, +} from './bootstrap/namedQuery.test'; +import callWithPromise from '../../lib/callWithPromise'; -describe('Reactive count tests', function () { - it('Should fetch the initial count', function (done) { +describe('Reactive count tests', function() { + callWithPromise('resetPosts'); + + it('Should fetch the initial count', function(done) { const query = NamedQuery.clone(); const handle = query.subscribeCount(); @@ -20,7 +27,7 @@ describe('Reactive count tests', function () { }); // TODO: Can these tests fail if assert gets called too quickly? - it('Should update when a document is added', function (done) { + it('Should update when a document is added', function(done) { const query = NamedQuery.clone(); const handle = query.subscribeCount(); @@ -42,7 +49,7 @@ describe('Reactive count tests', function () { }); }); - it('Should update when a document is removed', function (done) { + it('Should update when a document is removed', function(done) { const query = NamedQuery.clone(); const handle = query.subscribeCount(); @@ -52,7 +59,7 @@ describe('Reactive count tests', function () { const count = query.getCount(); assert.equal(count, 3); - Meteor.call('removePost', 'removeid', (error) => { + Meteor.call('removePost', 'removeid', error => { const newCount = query.getCount(); assert.equal(newCount, 2); @@ -62,4 +69,40 @@ describe('Reactive count tests', function () { } }); }); + + it('Should work with two different queries', function(done) { + const query1 = postsQuery.clone(); + const query2 = postsQuery2.clone(); + + const handle2 = query2.subscribeCount(); + const handle1 = query1.subscribeCount(); + + Tracker.autorun(c => { + if (handle1.ready() && handle2.ready()) { + const count1 = query1.getCount(); + const count2 = query2.getCount(); + + assert.equal(count1, 2); + assert.equal(count2, 1); + done(); + } + }); + }); + + it('Should work with special filter params', function(done) { + const query = postsQuery3.clone({ + $regex: 'BOMB', + }); + + const handle = query.subscribeCount(); + + Tracker.autorun(c => { + if (handle.ready()) { + const count = query.getCount(); + + assert.equal(count, 2); + done(); + } + }); + }); }); diff --git a/lib/query/counts/testing/server.test.js b/lib/query/counts/testing/server.test.js index a8cdf69..7667eb1 100644 --- a/lib/query/counts/testing/server.test.js +++ b/lib/query/counts/testing/server.test.js @@ -1,14 +1,23 @@ import { Meteor } from 'meteor/meteor'; import PostsCollection from './bootstrap/collection.test'; -import query from './bootstrap/namedQuery.test'; +import { + postsQuery, + postsQuery2, + postsQuery3, +} from './bootstrap/namedQuery.test'; -query.expose(); -PostsCollection.remove({}); -PostsCollection.insert({ text: 'text 1' }); -PostsCollection.insert({ text: 'text 2' }); -PostsCollection.insert({ _id: 'removeid', text: 'text 3' }); +postsQuery.expose(); +postsQuery2.expose(); +postsQuery3.expose(); Meteor.methods({ + resetPosts() { + PostsCollection.remove({}); + PostsCollection.insert({ text: 'text 1' }); + PostsCollection.insert({ text: 'text 2' }); + PostsCollection.insert({ _id: 'removeid', text: 'text 3' }); + }, + addPost(text) { return PostsCollection.insert({ text }); }, diff --git a/lib/query/testing/bootstrap/authors/links.js b/lib/query/testing/bootstrap/authors/links.js index a885c42..3800471 100644 --- a/lib/query/testing/bootstrap/authors/links.js +++ b/lib/query/testing/bootstrap/authors/links.js @@ -25,8 +25,12 @@ Authors.addReducers({ body: { name: 1 }, - reduce(object) { - return 'full - ' + object.name; + reduce(object, params) { + return ( + 'full - ' + + object.name + + (params && params.suffix ? params.suffix : null) + ); } }, groupNames: { @@ -66,10 +70,10 @@ Authors.addReducers({ }, paramBasedReducer: { body: { - _id: 1, + _id: 1 }, reduce(object, params) { return params.element; } } -}); \ No newline at end of file +}); diff --git a/lib/query/testing/reducers.client.test.js b/lib/query/testing/reducers.client.test.js index df5054f..0891214 100644 --- a/lib/query/testing/reducers.client.test.js +++ b/lib/query/testing/reducers.client.test.js @@ -1,12 +1,12 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; import waitForHandleToBeReady from './lib/waitForHandleToBeReady'; -describe('Client-side reducers', function () { - it('Should work with field only reducers', async function () { +describe('Client-side reducers', function() { + it('Should work with field only reducers', async function() { const query = createQuery({ authors: { - fullName: 1 - } + fullName: 1, + }, }); let handle = query.subscribe(); @@ -18,17 +18,43 @@ describe('Client-side reducers', function () { data.forEach(author => { assert.isString(author.fullName); assert.isUndefined(author.name); - assert.isTrue(author.fullName.substr(0, 7) === 'full - ') + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); }); handle.stop(); }); - it('Should work with nested fields reducers', async function () { + it('Should work with field only reducers and parameters', async function() { const query = createQuery({ authors: { - fullNameNested: 1 - } + fullName: 1, + }, + }); + + query.setParams({ + suffix: 'Bomb', + }); + + let handle = query.subscribe(); + await waitForHandleToBeReady(handle); + const data = query.fetch(); + + assert.isTrue(data.length > 0); + + data.forEach(author => { + assert.isString(author.fullName); + assert.isUndefined(author.name); + assert.isTrue(author.fullName.indexOf('Bomb') >= 0); + }); + + handle.stop(); + }); + + it('Should work with nested fields reducers', async function() { + const query = createQuery({ + authors: { + fullNameNested: 1, + }, }); let handle = query.subscribe(); @@ -47,14 +73,14 @@ describe('Client-side reducers', function () { handle.stop(); }); - it('Should work with nested fields reducers', async function () { + it('Should work with nested fields reducers', async function() { const query = createQuery({ authors: { profile: { - firstName: 1 + firstName: 1, }, fullNameNested: 1, - } + }, }); let handle = query.subscribe(); @@ -75,11 +101,11 @@ describe('Client-side reducers', function () { handle.stop(); }); - it('Should work with links reducers', async function () { + it('Should work with links reducers', async function() { const query = createQuery({ authors: { - groupNames: 1 - } + groupNames: 1, + }, }); let handle = query.subscribe(); @@ -96,11 +122,11 @@ describe('Client-side reducers', function () { handle.stop(); }); - it('Should work with links and nested reducers', async function () { + it('Should work with links and nested reducers', async function() { const query = createQuery({ authors: { - referenceReducer: 1 - } + referenceReducer: 1, + }, }); let handle = query.subscribe(); @@ -112,18 +138,18 @@ describe('Client-side reducers', function () { data.forEach(author => { assert.isString(author.referenceReducer); assert.isUndefined(author.fullName); - assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - ') + assert.isTrue(author.referenceReducer.substr(0, 9) === 'nested - '); }); handle.stop(); }); - it('Should not clean nested reducers if not specified', async function () { + it('Should not clean nested reducers if not specified', async function() { const query = createQuery({ authors: { referenceReducer: 1, fullName: 1, - } + }, }); let handle = query.subscribe(); @@ -140,16 +166,16 @@ describe('Client-side reducers', function () { handle.stop(); }); - it('Should keep previously used items - Part 1', async function () { + it('Should keep previously used items - Part 1', async function() { const query = createQuery({ authors: { fullName: 1, name: 1, groupNames: 1, groups: { - name: 1 - } - } + name: 1, + }, + }, }); let handle = query.subscribe(); @@ -163,20 +189,20 @@ describe('Client-side reducers', function () { assert.isDefined(author.groups); assert.isArray(author.groupNames); assert.isString(author.fullName); - assert.isTrue(author.fullName.substr(0, 7) === 'full - ') + assert.isTrue(author.fullName.substr(0, 7) === 'full - '); }); handle.stop(); }); - it('Should keep previously used items - Part 2', async function () { + it('Should keep previously used items - Part 2', async function() { const query = createQuery({ authors: { groupNames: 1, groups: { - _id: 1 - } - } + _id: 1, + }, + }, }); let handle = query.subscribe(); @@ -198,9 +224,9 @@ describe('Client-side reducers', function () { author.groups.forEach(group => { assert.isDefined(group._id); assert.isDefined(group.name); - }) + }); }); handle.stop(); }); -}); \ No newline at end of file +}); diff --git a/lib/query/testing/server.test.js b/lib/query/testing/server.test.js index 5d8046c..cef6da9 100644 --- a/lib/query/testing/server.test.js +++ b/lib/query/testing/server.test.js @@ -4,15 +4,15 @@ import './metaFilters.server.test'; import './reducers.server.test'; import './link-cache/server.test'; -describe('Hypernova', function () { - it('Should fetch One links correctly', function () { +describe('Hypernova', function() { + it('Should fetch One links correctly', function() { const data = createQuery({ comments: { text: 1, author: { - name: 1 - } - } + name: 1, + }, + }, }).fetch(); assert.lengthOf(data, Comments.find().count()); @@ -23,31 +23,35 @@ describe('Hypernova', function () { assert.isString(comment.author.name); assert.isString(comment.author._id); assert.isTrue(_.keys(comment.author).length == 2); - }) + }); }); - it('Should fetch One links with limit and options', function () { + it('Should fetch One links with limit and options', function() { const data = createQuery({ comments: { - $options: {limit: 5}, - text: 1 - } + $options: { limit: 5 }, + text: 1, + }, }).fetch(); assert.lengthOf(data, 5); }); - it('Should fetch One-Inversed links with limit and options', function () { - const query = createQuery({ - authors: { - $options: {limit: 5}, - comments: { - $filters: {text: 'Good'}, - $options: {limit: 2}, - text: 1 - } - } - }, {}, {debug: true}); + it('Should fetch One-Inversed links with limit and options', function() { + const query = createQuery( + { + authors: { + $options: { limit: 5 }, + comments: { + $filters: { text: 'Good' }, + $options: { limit: 2 }, + text: 1, + }, + }, + }, + {}, + { debug: true } + ); const data = query.fetch(); @@ -56,19 +60,19 @@ describe('Hypernova', function () { assert.lengthOf(author.comments, 2); _.each(author.comments, comment => { assert.equal('Good', comment.text); - }) - }) + }); + }); }); - it('Should fetch Many links correctly', function () { + it('Should fetch Many links correctly', function() { const data = createQuery({ posts: { - $options: {limit: 5}, + $options: { limit: 5 }, title: 1, tags: { - text: 1 - } - } + text: 1, + }, + }, }).fetch(); assert.lengthOf(data, 5); @@ -76,39 +80,39 @@ describe('Hypernova', function () { assert.isString(post.title); assert.isArray(post.tags); assert.isTrue(post.tags.length > 0); - }) + }); }); - it('Should fetch Many - inversed links correctly', function () { + it('Should fetch Many - inversed links correctly', function() { const data = createQuery({ tags: { name: 1, posts: { - $options: {limit: 5}, - title: 1 - } - } + $options: { limit: 5 }, + title: 1, + }, + }, }).fetch(); - + _.each(data, tag => { assert.isString(tag.name); assert.isArray(tag.posts); assert.isTrue(tag.posts.length <= 5); _.each(tag.posts, post => { assert.isString(post.title); - }) - }) + }); + }); }); - it('Should fetch One-Meta links correctly', function () { + it('Should fetch One-Meta links correctly', function() { const data = createQuery({ posts: { - $options: {limit: 5}, + $options: { limit: 5 }, title: 1, group: { - name: 1 - } - } + name: 1, + }, + }, }).fetch(); assert.lengthOf(data, 5); @@ -118,17 +122,17 @@ describe('Hypernova', function () { assert.isObject(post.group); assert.isString(post.group._id); assert.isString(post.group.name); - }) + }); }); - it('Should fetch One-Meta inversed links correctly', function () { + it('Should fetch One-Meta inversed links correctly', function() { const data = createQuery({ groups: { name: 1, posts: { - title: 1 - } - } + title: 1, + }, + }, }).fetch(); _.each(data, group => { @@ -139,19 +143,19 @@ describe('Hypernova', function () { _.each(group.posts, post => { assert.isString(post.title); assert.isString(post._id); - }) - }) + }); + }); }); - it('Should fetch Many-Meta links correctly', function () { + it('Should fetch Many-Meta links correctly', function() { const data = createQuery({ authors: { name: 1, groups: { - $options: {limit: 1}, - name: 1 - } - } + $options: { limit: 1 }, + name: 1, + }, + }, }).fetch(); _.each(data, author => { @@ -162,19 +166,19 @@ describe('Hypernova', function () { assert.isObject(group); assert.isString(group._id); assert.isString(group.name); - }) - }) + }); + }); }); - it('Should fetch Many-Meta inversed links correctly', function () { + it('Should fetch Many-Meta inversed links correctly', function() { const data = createQuery({ groups: { name: 1, authors: { - $options: {limit: 2}, - name: 1 - } - } + $options: { limit: 2 }, + name: 1, + }, + }, }).fetch(); _.each(data, group => { @@ -185,17 +189,17 @@ describe('Hypernova', function () { assert.isObject(author); assert.isString(author._id); assert.isString(author.name); - }) - }) + }); + }); }); - it('Should fetch direct One & Many Meta links with $metadata', function () { + it('Should fetch direct One & Many Meta links with $metadata', function() { let data = createQuery({ posts: { group: { - name: 1 - } - } + name: 1, + }, + }, }).fetch(); _.each(data, post => { @@ -206,10 +210,10 @@ describe('Hypernova', function () { data = createQuery({ authors: { groups: { - $options: {limit: 1}, - name: 1 - } - } + $options: { limit: 1 }, + name: 1, + }, + }, }).fetch(); _.each(data, author => { @@ -217,21 +221,21 @@ describe('Hypernova', function () { _.each(author.groups, group => { assert.isObject(group.$metadata); - }) - }) + }); + }); }); - it('Should fetch direct One Meta links with $metadata that are under a nesting level', function () { + it('Should fetch direct One Meta links with $metadata that are under a nesting level', function() { let authors = createQuery({ authors: { $options: { limit: 1 }, posts: { $options: { limit: 1 }, group: { - name: 1 - } - } - } + name: 1, + }, + }, + }, }).fetch(); let data = authors[0]; @@ -240,63 +244,62 @@ describe('Hypernova', function () { assert.isObject(post.group.$metadata); assert.isDefined(post.group.$metadata.random); }); - }); - it('Should fetch Inversed One & Many Meta links with $metadata', function () { + it('Should fetch Inversed One & Many Meta links with $metadata', function() { let data = createQuery({ groups: { posts: { group_groups_meta: 1, - title: 1 - } - } + title: 1, + }, + }, }).fetch(); _.each(data, group => { _.each(group.posts, post => { assert.isObject(post.$metadata); assert.isDefined(post.$metadata.random); - }) + }); }); data = createQuery({ groups: { authors: { - $options: {limit: 1}, - name: 1 - } - } + $options: { limit: 1 }, + name: 1, + }, + }, }).fetch(); _.each(data, group => { _.each(group.authors, author => { assert.isObject(author.$metadata); }); - }) + }); }); - it('Should fetch in depth properly at any given level.', function () { + it('Should fetch in depth properly at any given level.', function() { const data = createQuery({ authors: { - $options: {limit: 5}, + $options: { limit: 5 }, posts: { - $options: {limit: 5}, + $options: { limit: 5 }, comments: { - $options: {limit: 5}, + $options: { limit: 5 }, author: { groups: { posts: { - $options: {limit: 5}, + $options: { limit: 5 }, author: { - name: 1 - } - } - } - } - } - } - } + name: 1, + }, + }, + }, + }, + }, + }, + }, }).fetch(); assert.lengthOf(data, 5); @@ -310,62 +313,62 @@ describe('Hypernova', function () { assert.isObject(post.author); assert.isString(post.author.name); arrivedInDepth = true; - }) - }) - }) - }) + }); + }); + }); + }); }); assert.isTrue(arrivedInDepth); }); - it('Should work with filters of $and and $or on subcollections', function () { + it('Should work with filters of $and and $or on subcollections', function() { let data = createQuery({ posts: { comments: { $filters: { $and: [ { - text: 'Good' - } - ] + text: 'Good', + }, + ], }, - text: 1 - } - } + text: 1, + }, + }, }).fetch(); data.forEach(post => { if (post.comments) { post.comments.forEach(comment => { assert.equal(comment.text, 'Good'); - }) + }); } - }) + }); }); - it('Should work sorting with options that contain a dot', function () { + it('Should work sorting with options that contain a dot', function() { let data = createQuery({ posts: { author: { - $filter({options}) { + $filter({ options }) { options.sort = { - 'profile.firstName': 1 - } + 'profile.firstName': 1, + }; }, profile: 1, - } - } + }, + }, }).fetch(); assert.isArray(data); }); - it('Should properly clone and work with setParams', function () { + it('Should properly clone and work with setParams', function() { let query = createQuery({ posts: { - $options: {limit: 5} - } + $options: { limit: 5 }, + }, }); let clone = query.clone({}); @@ -376,7 +379,7 @@ describe('Hypernova', function () { assert.isFunction(clone.setParams({}).fetchOne); }); - it('Should work with $postFilters', function () { + it('Should work with $postFilters', function() { let query = createQuery({ posts: { $postFilters: { @@ -384,9 +387,9 @@ describe('Hypernova', function () { }, title: 1, comments: { - text: 1 - } - } + text: 1, + }, + }, }); const data = query.fetch(); @@ -399,43 +402,64 @@ describe('Hypernova', function () { }, title: 1, comments: { - text: 1 - } - } + text: 1, + }, + }, }); assert.isTrue(query.fetch().length > 0); - }) + }); - it('Should work with $postOptions', function () { + it('Should work with $postOptions', function() { let query = createQuery({ posts: { $postOptions: { - limit:5, - skip:5, - sort:{title:1} + limit: 5, + skip: 5, + sort: { title: 1 }, }, title: 1, comments: { - text: 1 - } - } + text: 1, + }, + }, }); const data = query.fetch(); assert.lengthOf(data, 5); }); - it('Should work with a nested field from reversedSide using aggregation framework', function () { + it('Should work with $postFilter and params', function(done) { + let query = createQuery({ + posts: { + $postFilter(results, params) { + assert.equal(params.text, 'Good'); + done(); + }, + title: 1, + comments: { + text: 1, + }, + }, + }); + + query.setParams({ + text: 'Good', + }); + + query.fetch(); + }); + + it('Should work with a nested field from reversedSide using aggregation framework', function() { let query = createQuery({ groups: { - $options: {limit: 1}, + $options: { limit: 1 }, authors: { profile: { firstName: 1, - } - } - } + }, + }, + }, }); const data = query.fetch(); @@ -453,19 +477,22 @@ describe('Hypernova', function () { assert.isUndefined(author.profile.lastName); }); - it('Should apply a default filter function to first root', function () { - let query = createQuery({ - groups: { - authors: {} + it('Should apply a default filter function to first root', function() { + let query = createQuery( + { + groups: { + authors: {}, + }, + }, + { + params: { + options: { limit: 1 }, + filters: { + name: 'JavaScript', + }, + }, } - }, { - params: { - options: {limit: 1}, - filters: { - name: 'JavaScript' - } - } - }); + ); const data = query.fetch(); assert.lengthOf(data, 1); @@ -477,34 +504,32 @@ describe('Hypernova', function () { const Users = new Mongo.Collection('__many_inversed_users'); const Restaurants = new Mongo.Collection('__many_inversed_restaurants'); - it('Should fetch Many - inversed links correctly when the field is not the first', function () { + it('Should fetch Many - inversed links correctly when the field is not the first', function() { Restaurants.addLinks({ users: { type: 'many', field: 'userIds', collection: Users, - } + }, }); Users.addLinks({ restaurants: { collection: Restaurants, - inversedBy: 'users' - } + inversedBy: 'users', + }, }); const userId1 = Users.insert({ - name: 'John' + name: 'John', }); const userId2 = Users.insert({ - name: 'John' + name: 'John', }); const restaurantId = Restaurants.insert({ name: 'Jamie Oliver', - userIds: [ - userId2, userId1 - ] + userIds: [userId2, userId1], }); const user = Users.createQuery({ @@ -513,7 +538,7 @@ describe('Hypernova', function () { }, restaurants: { name: 1, - } + }, }).fetchOne(); assert.isObject(user); diff --git a/package.js b/package.js index 18ffd9f..2628706 100644 --- a/package.js +++ b/package.js @@ -1,23 +1,23 @@ Package.describe({ name: 'cultofcoders:grapher', - version: '1.3.2', + version: '1.3.3', // Brief, one-line summary of the package. summary: 'Grapher is a data fetching layer on top of Meteor', // URL to the Git repository containing the source code for this package. git: 'https://github.com/cult-of-coders/grapher', // By default, Meteor will default to using README.md for documentation. // To avoid submitting documentation, set this field to null. - documentation: 'README.md' + documentation: 'README.md', }); Npm.depends({ - 'sift': '3.2.6', + sift: '3.2.6', 'dot-object': '1.5.4', 'lodash.clonedeep': '4.5.0', 'deep-extend': '0.5.0', }); -Package.onUse(function (api) { +Package.onUse(function(api) { api.versionsFrom('1.3'); var packages = [ @@ -39,7 +39,7 @@ Package.onUse(function (api) { api.mainModule('main.server.js', 'server'); }); -Package.onTest(function (api) { +Package.onTest(function(api) { api.use('cultofcoders:grapher'); var packages = [ @@ -49,16 +49,13 @@ Package.onTest(function (api) { 'reywood:publish-composite@1.5.2', 'dburles:mongo-collection-instances@0.3.5', 'herteby:denormalize@0.6.5', - 'mongo' + 'mongo', ]; api.use(packages); api.use('tracker'); - api.use([ - 'cultofcoders:mocha', - 'practicalmeteor:chai' - ]); + api.use(['cultofcoders:mocha', 'practicalmeteor:chai']); // LINKS api.addFiles('lib/links/tests/main.js', 'server');