mirror of
https://github.com/vale981/grapher
synced 2025-03-04 17:11:38 -05:00
added namedQuery + cloning ability to query + fixes to exposure
This commit is contained in:
parent
d0dde92cee
commit
b0e1098d1e
23 changed files with 659 additions and 27 deletions
|
@ -23,8 +23,8 @@ export default new SimpleSchema({
|
|||
optional: true
|
||||
},
|
||||
|
||||
restrictedLinks: {
|
||||
type: [String],
|
||||
restrictLinks: {
|
||||
type: null, // Can be function that accepts userId and returns array or array
|
||||
optional: true
|
||||
},
|
||||
|
||||
|
|
|
@ -44,6 +44,12 @@ export default class Exposure {
|
|||
if (config.method) {
|
||||
this.initMethod();
|
||||
}
|
||||
|
||||
if (!config.method && !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')
|
||||
}
|
||||
|
||||
this.initCountMethod();
|
||||
}
|
||||
|
||||
_validateAndClean() {
|
||||
|
@ -78,6 +84,8 @@ export default class Exposure {
|
|||
|
||||
Meteor.methods({
|
||||
[this.name](body) {
|
||||
this.unblock();
|
||||
|
||||
const rootNode = createGraph(collection, body);
|
||||
enforceMaxDepth(rootNode, config.maxDepth);
|
||||
|
||||
|
@ -86,9 +94,16 @@ export default class Exposure {
|
|||
return hypernova(rootNode, this.userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initCountMethod() {
|
||||
const collection = this.collection;
|
||||
const config = this.config;
|
||||
|
||||
Meteor.methods({
|
||||
[this.name + '.count'](body) {
|
||||
this.unblock();
|
||||
|
||||
return collection.find(body.$filters || {}, {}, this.userId).count();
|
||||
}
|
||||
})
|
||||
|
|
57
lib/namedQuery/README.md
Normal file
57
lib/namedQuery/README.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
A secure query is a query in which the form of it is locked on the server.
|
||||
Frozen queries are regarded as trusted code, the exposure from other collections will not affect them.
|
||||
Only the firewall.
|
||||
|
||||
The reason behind this concept:
|
||||
- You may have an Order for a Customer and to that order is an employee assigned
|
||||
- You want to expose all employees to admin via user exposure
|
||||
- Now, because exposures are linked you may need to add extra logic to user exposure, and it will eventually turn into a mess
|
||||
- It gets hard to validate/invalidate fields links.
|
||||
|
||||
This is the reason why you should construct your secure query and offer control over it via params. That can be used and manipulated in $filter function.
|
||||
|
||||
|
||||
```
|
||||
|
||||
const query = createNamedQuery('testList', {
|
||||
tests: {
|
||||
$filter({
|
||||
filters,
|
||||
options,
|
||||
params
|
||||
}) {
|
||||
|
||||
},
|
||||
title: 1,
|
||||
endcustomer: {
|
||||
profile: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
// In the same file or in a server-side file only:
|
||||
query.expose({
|
||||
firewall(userId, params) {
|
||||
// throw exception if not allowed
|
||||
},
|
||||
body: { // merges deeply with your current body, so you can filter without showing the client how you do it to avoid exposing precious data
|
||||
tests: {
|
||||
$filter({filters, options, params})
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```
|
||||
// You must have your collections and queries imported already.
|
||||
// Client side
|
||||
createQuery({
|
||||
testListQuery: {
|
||||
endcustomer: Meteor.userId()
|
||||
}
|
||||
})
|
||||
|
||||
```
|
26
lib/namedQuery/createNamedQuery.js
Normal file
26
lib/namedQuery/createNamedQuery.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import NamedQuery from './namedQuery.js';
|
||||
import NamedQueryStore from './store';
|
||||
|
||||
/**
|
||||
* @param name
|
||||
* @param data
|
||||
* @param args
|
||||
*
|
||||
* @returns NamedQuery
|
||||
*/
|
||||
export default (name, data, ...args) => {
|
||||
if (_.keys(data).length > 1) {
|
||||
throw new Meteor.Error('invalid-query', 'When using createNamedQuery you should only have one main root point.')
|
||||
}
|
||||
|
||||
const entryPointName = _.first(_.keys(data));
|
||||
const collection = Mongo.Collection.get(entryPointName);
|
||||
if (!collection) {
|
||||
throw new Meteor.Error('invalid-name', `We could not find any collection with the name ${entryPointName}`)
|
||||
}
|
||||
|
||||
const namedQuery = new NamedQuery(name, collection, data[entryPointName], ...args);
|
||||
NamedQueryStore.add(name, namedQuery);
|
||||
|
||||
return namedQuery;
|
||||
}
|
85
lib/namedQuery/expose/extension.js
Normal file
85
lib/namedQuery/expose/extension.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
import NamedQuery from '../namedQuery.js';
|
||||
import ExposeSchema from './schema.js';
|
||||
import mergeDeep from './lib/mergeDeep.js';
|
||||
import createGraph from '../../query/lib/createGraph.js';
|
||||
import recursiveCompose from '../../query/lib/recursiveCompose.js';
|
||||
import applyFilterFunction from '../../query/lib/applyFilterFunction.js';
|
||||
|
||||
_.extend(NamedQuery.prototype, {
|
||||
expose(config = {}) {
|
||||
if (!Meteor.isServer) {
|
||||
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`);
|
||||
}
|
||||
|
||||
ExposeSchema.clean(config);
|
||||
this.exposeConfig = config;
|
||||
|
||||
if (config.method) {
|
||||
this._initMethod();
|
||||
}
|
||||
|
||||
if (config.publication) {
|
||||
this._initPublication();
|
||||
}
|
||||
|
||||
if (!config.method && !config.publication) {
|
||||
throw new Meteor.Error('weird', 'If you want to expose your frozen query you need to specify at least one of ["method", "publication"] options to true')
|
||||
}
|
||||
|
||||
this._initCountMethod();
|
||||
|
||||
if (config.embody) {
|
||||
this.body = mergeDeep(this.body, config.embody);
|
||||
}
|
||||
|
||||
this.isExposed = true;
|
||||
},
|
||||
|
||||
_initMethod() {
|
||||
const self = this;
|
||||
Meteor.methods({
|
||||
[this.name](newParams) {
|
||||
this.unblock();
|
||||
|
||||
if (self.exposeConfig.firewall) {
|
||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
||||
}
|
||||
|
||||
return self.clone(newParams).fetch();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
_initCountMethod() {
|
||||
const self = this;
|
||||
|
||||
Meteor.methods({
|
||||
[this.name + '.count'](newParams) {
|
||||
this.unblock();
|
||||
|
||||
return self.clone(newParams).getCount();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
_initPublication() {
|
||||
const self = this;
|
||||
|
||||
Meteor.publishComposite(this.name, function (newParams) {
|
||||
if (self.exposeConfig.firewall) {
|
||||
self.exposeConfig.firewall.call(this, this.userId, newParams);
|
||||
}
|
||||
|
||||
let params = _.extend({}, self.params, newParams);
|
||||
let body = applyFilterFunction(self.body, params);
|
||||
|
||||
const rootNode = createGraph(self.collection, body);
|
||||
|
||||
return recursiveCompose(rootNode);
|
||||
});
|
||||
}
|
||||
});
|
21
lib/namedQuery/expose/lib/mergeDeep.js
Normal file
21
lib/namedQuery/expose/lib/mergeDeep.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param source
|
||||
*/
|
||||
export default function mergeDeep(target, source) {
|
||||
if (_.isObject(target) && _.isObject(source)) {
|
||||
_.each(source, (value, key) => {
|
||||
if (_.isFunction(source[key])) {
|
||||
target[key] = source[key];
|
||||
} else if (_.isObject(source[key])) {
|
||||
if (!target[key]) Object.assign(target, { [key]: {} });
|
||||
mergeDeep(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(target, { [key]: source[key] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
22
lib/namedQuery/expose/schema.js
Normal file
22
lib/namedQuery/expose/schema.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
export default new SimpleSchema({
|
||||
firewall: {
|
||||
type: Function,
|
||||
optional: true
|
||||
},
|
||||
|
||||
publication: {
|
||||
type: Boolean,
|
||||
defaultValue: true
|
||||
},
|
||||
|
||||
method: {
|
||||
type: Boolean,
|
||||
defaultValue: true
|
||||
},
|
||||
|
||||
embody: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true
|
||||
}
|
||||
})
|
154
lib/namedQuery/namedQuery.js
Normal file
154
lib/namedQuery/namedQuery.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
import createGraph from '../query/lib/createGraph.js';
|
||||
import recursiveCompose from '../query/lib/recursiveCompose.js';
|
||||
import recursiveFetch from '../query/lib/recursiveFetch.js';
|
||||
import applyFilterFunction from '../query/lib/applyFilterFunction.js';
|
||||
|
||||
export default class NamedQuery {
|
||||
constructor(name, collection, body, params = {}) {
|
||||
this.queryName = name;
|
||||
this.body = body;
|
||||
this.subscriptionHandle = null;
|
||||
this.params = params;
|
||||
this.collection = collection;
|
||||
this.isExposed = false;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return `named_query_${this.queryName}`;
|
||||
}
|
||||
|
||||
clone(newParams) {
|
||||
return new NamedQuery(
|
||||
this.queryName,
|
||||
this.collection,
|
||||
_.clone(this.body),
|
||||
_.extend({}, this.params, newParams)
|
||||
);
|
||||
}
|
||||
|
||||
setParams(params) {
|
||||
this.params = _.extend({}, this.params, params);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe
|
||||
*
|
||||
* @param callback
|
||||
* @returns {null|any|*}
|
||||
*/
|
||||
subscribe(callback) {
|
||||
if (!Meteor.isClient) {
|
||||
throw new Meteor.Error('not-allowed', 'You cannot subscribe from server');
|
||||
}
|
||||
|
||||
this.subscriptionHandle = Meteor.subscribe(
|
||||
this.name,
|
||||
this.params,
|
||||
callback
|
||||
);
|
||||
|
||||
return this.subscriptionHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe if an existing subscription exists
|
||||
*/
|
||||
unsubscribe() {
|
||||
if (!Meteor.isClient) {
|
||||
throw new Meteor.Error('not-allowed', 'You cannot subscribe/unsubscribe from server');
|
||||
}
|
||||
|
||||
if (this.subscriptionHandle) {
|
||||
this.subscriptionHandle.stop();
|
||||
}
|
||||
|
||||
this.subscriptionHandle = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the data.
|
||||
* @param callbackOrOptions
|
||||
* @returns {*}
|
||||
*/
|
||||
fetch(callbackOrOptions) {
|
||||
if (Meteor.isClient) {
|
||||
if (!this.subscriptionHandle) {
|
||||
return this._fetchAsClientMethod(callbackOrOptions)
|
||||
} else {
|
||||
return this._fetchAsClientReactive(callbackOrOptions);
|
||||
}
|
||||
} else {
|
||||
return this._fetchAsServer(callbackOrOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param args
|
||||
* @returns {*}
|
||||
*/
|
||||
fetchOne(...args) {
|
||||
return _.first(this.fetch(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of matching elements.
|
||||
* @param callback
|
||||
* @returns {any}
|
||||
*/
|
||||
getCount(callback) {
|
||||
if (Meteor.isClient) {
|
||||
if (!callback) {
|
||||
throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the count.');
|
||||
}
|
||||
|
||||
return Meteor.call(this.name + '.count', this.params, callback);
|
||||
}
|
||||
|
||||
let body = applyFilterFunction(this.body, this.params);
|
||||
return this.collection.find(body.$filters || {}, {}).count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetching non-reactive queries
|
||||
* @param callback
|
||||
* @private
|
||||
*/
|
||||
_fetchAsClientMethod(callback) {
|
||||
if (!callback) {
|
||||
throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the data or subscribe first.');
|
||||
}
|
||||
|
||||
Meteor.call(this.name, this.params, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetching when we've got an active publication
|
||||
*
|
||||
* @param options
|
||||
* @returns {*}
|
||||
* @private
|
||||
*/
|
||||
_fetchAsClientReactive(options = {}) {
|
||||
let body = applyFilterFunction(this.body, this.params);
|
||||
if (!options.allowSkip && body.$options && body.$options.skip) {
|
||||
delete body.$options.skip;
|
||||
}
|
||||
|
||||
return recursiveFetch(
|
||||
createGraph(this.collection, body)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetching from the server-side
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_fetchAsServer() {
|
||||
const query = this.collection.createQuery(this.body, this.params);
|
||||
|
||||
return query.fetch();
|
||||
}
|
||||
}
|
21
lib/namedQuery/store.js
Normal file
21
lib/namedQuery/store.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
export default new class {
|
||||
constructor() {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
add(key, value) {
|
||||
if (this.storage[key]) {
|
||||
throw new Meteor.Error(`Frozen query with key ${key} is already defined`);
|
||||
}
|
||||
|
||||
this.storage[key] = value;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.storage[key];
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.storage;
|
||||
}
|
||||
}
|
0
lib/namedQuery/testing/bootstrap/both.js
Normal file
0
lib/namedQuery/testing/bootstrap/both.js
Normal file
0
lib/namedQuery/testing/bootstrap/client.js
Normal file
0
lib/namedQuery/testing/bootstrap/client.js
Normal file
13
lib/namedQuery/testing/bootstrap/queries/postListExposure.js
Normal file
13
lib/namedQuery/testing/bootstrap/queries/postListExposure.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { createNamedQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
export default createNamedQuery('postListExposure', {
|
||||
posts: {
|
||||
title: 1,
|
||||
author: {
|
||||
name: 1
|
||||
},
|
||||
group: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
});
|
30
lib/namedQuery/testing/bootstrap/server.js
Normal file
30
lib/namedQuery/testing/bootstrap/server.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { createNamedQuery } from 'meteor/cultofcoders:grapher';
|
||||
import postListExposure from './queries/postListExposure.js';
|
||||
|
||||
const postList = createNamedQuery('postList', {
|
||||
posts: {
|
||||
$filter({filters, params}) {
|
||||
filters.title = params.title
|
||||
},
|
||||
title: 1,
|
||||
author: {
|
||||
name: 1
|
||||
},
|
||||
group: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { postList };
|
||||
export { postListExposure };
|
||||
|
||||
postListExposure.expose({
|
||||
firewall(userId, params) {
|
||||
},
|
||||
embody: {
|
||||
$filter({filters, params}) {
|
||||
filters.title = params.title
|
||||
}
|
||||
}
|
||||
});
|
105
lib/namedQuery/testing/client.test.js
Normal file
105
lib/namedQuery/testing/client.test.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
import postListExposure from './bootstrap/queries/postListExposure.js';
|
||||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
describe('Named Query', function () {
|
||||
it('Should return proper values', function (done) {
|
||||
const query = createQuery({
|
||||
postListExposure: {
|
||||
title: 'User Post - 3'
|
||||
}
|
||||
});
|
||||
|
||||
query.fetch((err, res) => {
|
||||
assert.isUndefined(err);
|
||||
assert.isTrue(res.length > 0);
|
||||
|
||||
_.each(res, post => {
|
||||
assert.equal(post.title, 'User Post - 3');
|
||||
assert.isObject(post.author);
|
||||
assert.isObject(post.group);
|
||||
});
|
||||
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
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);
|
||||
assert.isTrue(res.length > 0);
|
||||
|
||||
_.each(res, post => {
|
||||
assert.equal(post.title, 'User Post - 3');
|
||||
assert.isObject(post.author);
|
||||
assert.isObject(post.group);
|
||||
});
|
||||
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
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 queries', function (done) {
|
||||
const query = createQuery({
|
||||
postListExposure: {
|
||||
title: 'User Post - 3'
|
||||
}
|
||||
});
|
||||
|
||||
const handle = query.subscribe();
|
||||
|
||||
Tracker.autorun(c => {
|
||||
if (handle.ready()) {
|
||||
c.stop();
|
||||
const res = query.fetch();
|
||||
|
||||
assert.isTrue(res.length > 0);
|
||||
|
||||
_.each(res, post => {
|
||||
assert.equal(post.title, 'User Post - 3');
|
||||
assert.isObject(post.author);
|
||||
assert.isObject(post.group);
|
||||
});
|
||||
|
||||
handle.stop();
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('Should work with reactive queries via import', function (done) {
|
||||
const query = postListExposure.clone({
|
||||
title: 'User Post - 3'
|
||||
});
|
||||
|
||||
const handle = query.subscribe();
|
||||
|
||||
Tracker.autorun(c => {
|
||||
if (handle.ready()) {
|
||||
c.stop();
|
||||
const res = query.fetch();
|
||||
|
||||
assert.isTrue(res.length > 0);
|
||||
|
||||
_.each(res, post => {
|
||||
assert.equal(post.title, 'User Post - 3');
|
||||
assert.isObject(post.author);
|
||||
assert.isObject(post.group);
|
||||
});
|
||||
|
||||
handle.stop();
|
||||
done();
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
46
lib/namedQuery/testing/server.test.js
Normal file
46
lib/namedQuery/testing/server.test.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { postList } from './bootstrap/server.js';
|
||||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
describe('Named Query', function () {
|
||||
it('Should return the proper values', function () {
|
||||
const createdQuery = createQuery({
|
||||
postList: {
|
||||
title: 'User Post - 3'
|
||||
}
|
||||
});
|
||||
|
||||
const directQuery = postList.clone({
|
||||
title: 'User Post - 3'
|
||||
});
|
||||
|
||||
_.each([createdQuery, directQuery], (query) => {
|
||||
const data = query.fetch();
|
||||
|
||||
assert.isTrue(data.length > 1);
|
||||
|
||||
_.each(data, post => {
|
||||
assert.equal(post.title, 'User Post - 3');
|
||||
assert.isObject(post.author);
|
||||
assert.isObject(post.group);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Exposure embodyment should work properly', function () {
|
||||
const query = createQuery({
|
||||
postListExposure: {
|
||||
title: 'User Post - 3'
|
||||
}
|
||||
});
|
||||
|
||||
const data = query.fetch();
|
||||
|
||||
assert.isTrue(data.length > 1);
|
||||
|
||||
_.each(data, post => {
|
||||
assert.equal(post.title, 'User Post - 3');
|
||||
assert.isObject(post.author);
|
||||
assert.isObject(post.group);
|
||||
})
|
||||
})
|
||||
});
|
|
@ -1,17 +1,24 @@
|
|||
import Query from './query.js';
|
||||
import NamedQueryStore from '../namedQuery/store.js';
|
||||
|
||||
export default (data, ...args) => {
|
||||
if (_.keys(data).length > 1) {
|
||||
throw new Meteor.Error('invalid-query', 'When using createQuery you should only have one main root point.')
|
||||
}
|
||||
|
||||
const collectionName = _.first(_.keys(data));
|
||||
const entryPointName = _.first(_.keys(data));
|
||||
|
||||
const collection = Mongo.Collection.get(collectionName);
|
||||
const collection = Mongo.Collection.get(entryPointName);
|
||||
|
||||
if (!collection) {
|
||||
throw new Meteor.Error('collection-not-found', `We could not find any collection with the name ${collectionName}`)
|
||||
const namedQuery = NamedQueryStore.get(entryPointName);
|
||||
|
||||
if (!namedQuery) {
|
||||
throw new Meteor.Error('entry-point-not-found', `We could not find any collection or frozen-query with the name ${entryPointName}`)
|
||||
} else {
|
||||
return namedQuery.clone(data[entryPointName], ...args);
|
||||
}
|
||||
}
|
||||
|
||||
return new Query(collection, data[collectionName], ...args);
|
||||
return new Query(collection, data[entryPointName], ...args);
|
||||
}
|
|
@ -3,12 +3,5 @@ import Query from './query.js';
|
|||
_.extend(Mongo.Collection.prototype, {
|
||||
createQuery(body, params = {}) {
|
||||
return new Query(this, body, params);
|
||||
},
|
||||
|
||||
createQueryFactory(body, params) {
|
||||
const collection = this;
|
||||
return () => {
|
||||
return new Query(collection, body, params);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
function applyFilterRecursive(data, params) {
|
||||
if (data.$filter) {
|
||||
function applyFilterRecursive(data, params = {}) {
|
||||
if (_.isFunction(data.$filter)) {
|
||||
data.$filters = data.$filters || {};
|
||||
data.$options = data.$options || {};
|
||||
|
||||
|
|
|
@ -4,12 +4,19 @@ import applyFilterFunction from './lib/applyFilterFunction.js';
|
|||
import hypernova from './hypernova/hypernova.js';
|
||||
|
||||
export default class Query {
|
||||
constructor(collection, body, params = {}, options = {}) {
|
||||
constructor(collection, body, params = {}) {
|
||||
this.collection = collection;
|
||||
this.body = body;
|
||||
this.subscriptionHandle = null;
|
||||
this._params = params;
|
||||
this.debug = options.debug;
|
||||
}
|
||||
|
||||
clone(newParams) {
|
||||
return new Query(
|
||||
this.collection,
|
||||
_.clone(this.body),
|
||||
_.extend({}, this.params, newParams)
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
|
@ -80,17 +87,29 @@ export default class Query {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param args
|
||||
* @returns {*}
|
||||
*/
|
||||
fetchOne(...args) {
|
||||
return _.first(this.fetch(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of matching elements.
|
||||
* @param callback
|
||||
* @returns {any}
|
||||
*/
|
||||
getCount(callback) {
|
||||
if (Meteor.isClient && !callback) {
|
||||
throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the count.');
|
||||
if (Meteor.isClient) {
|
||||
if (!callback) {
|
||||
throw new Meteor.Error('not-allowed', 'You are on client so you must either provide a callback to get the count.');
|
||||
}
|
||||
|
||||
return Meteor.call(this.name + '.count', applyFilterFunction(this.body, this.params), callback);
|
||||
}
|
||||
|
||||
return Meteor.call(this.name + '.count', applyFilterFunction(this.body, this.params), callback);
|
||||
return this.collection.find(this.body.$filters || {}, {}).count();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -148,6 +167,6 @@ export default class Query {
|
|||
applyFilterFunction(this.body, this.params)
|
||||
);
|
||||
|
||||
return hypernova(node, options.userId, !!this.debug);
|
||||
return hypernova(node, options.userId);
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ describe('Query Client Tests', function () {
|
|||
})
|
||||
});
|
||||
|
||||
handle.stop();
|
||||
done();
|
||||
}
|
||||
})
|
||||
|
|
|
@ -4,3 +4,7 @@ import './lib/query/extension.js';
|
|||
export {
|
||||
default as createQuery
|
||||
} from './lib/query/createQuery.js';
|
||||
|
||||
export {
|
||||
default as createNamedQuery
|
||||
} from './lib/namedQuery/createNamedQuery.js';
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import './lib/exposure/extension.js';
|
||||
import './lib/links/extension.js';
|
||||
import './lib/query/extension.js';
|
||||
import './lib/namedQuery/expose/extension.js';
|
||||
|
||||
import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';
|
||||
|
||||
checkNpmVersions({
|
||||
|
@ -11,6 +13,10 @@ export {
|
|||
default as createQuery
|
||||
} from './lib/query/createQuery.js';
|
||||
|
||||
export {
|
||||
default as createNamedQuery
|
||||
} from './lib/namedQuery/createNamedQuery.js';
|
||||
|
||||
export {
|
||||
default as Exposure
|
||||
} from './lib/exposure/exposure.js';
|
||||
|
|
19
package.js
19
package.js
|
@ -47,18 +47,25 @@ Package.onTest(function (api) {
|
|||
api.use('practicalmeteor:chai');
|
||||
|
||||
// LINKS
|
||||
api.mainModule('lib/links/tests/main.js', 'server');
|
||||
api.addFiles('lib/links/tests/main.js', 'server');
|
||||
|
||||
// EXPOSURE
|
||||
api.mainModule('lib/exposure/testing/server.js', 'server');
|
||||
api.mainModule('lib/exposure/testing/client.js', 'client');
|
||||
api.addFiles('lib/exposure/testing/server.js', 'server');
|
||||
api.addFiles('lib/exposure/testing/client.js', 'client');
|
||||
|
||||
// QUERY
|
||||
api.addFiles('lib/query/testing/bootstrap/index.js');
|
||||
|
||||
// When you play with tests you should comment this to make tests go faster.
|
||||
api.addFiles('lib/query/testing/bootstrap/fixtures.js', 'server');
|
||||
api.addFiles('lib/query/testing/server.test.js', 'server');
|
||||
api.addFiles('lib/query/testing/client.test.js', 'client');
|
||||
|
||||
// NAMED QUERY
|
||||
api.addFiles('lib/namedQuery/testing/bootstrap/both.js');
|
||||
api.addFiles('lib/namedQuery/testing/bootstrap/client.js', 'client');
|
||||
api.addFiles('lib/namedQuery/testing/bootstrap/server.js', 'server');
|
||||
|
||||
api.addFiles('lib/namedQuery/testing/server.test.js', 'server');
|
||||
api.addFiles('lib/namedQuery/testing/client.test.js', 'client');
|
||||
|
||||
api.mainModule('lib/query/testing/server.test.js', 'server');
|
||||
api.mainModule('lib/query/testing/client.test.js', 'client');
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue