Merge pull request #232 from cult-of-coders/release/1.3.3

[RFC] Release/1.3.3
This commit is contained in:
Theodor Diaconu 2018-03-29 12:18:03 +03:00 committed by GitHub
commit 1cf94bc8c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 486 additions and 310 deletions

View file

@ -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 ../

View file

@ -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);
}
}
};
}

View file

@ -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);
}

View file

@ -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();
}
})
})
});
});
});
});

View file

@ -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();
});
};

View file

@ -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;

View file

@ -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();
}
});
});
});

View file

@ -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 });
},

View file

@ -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;
}
}
});
});

View file

@ -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();
});
});
});

View file

@ -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);

View file

@ -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');