2017-11-25 14:45:31 +02:00
|
|
|
import genCountEndpoint from '../query/counts/genEndpoint.server.js';
|
2016-09-14 16:04:08 +03:00
|
|
|
import createGraph from '../query/lib/createGraph.js';
|
|
|
|
import recursiveCompose from '../query/lib/recursiveCompose.js';
|
2016-09-24 08:05:26 +03:00
|
|
|
import hypernova from '../query/hypernova/hypernova.js';
|
|
|
|
import ExposureConfigSchema from './exposure.config.schema.js';
|
|
|
|
import enforceMaxDepth from './lib/enforceMaxDepth.js';
|
|
|
|
import enforceMaxLimit from './lib/enforceMaxLimit.js';
|
2016-10-23 20:02:44 +03:00
|
|
|
import cleanBody from './lib/cleanBody.js';
|
2017-11-26 23:02:26 +02:00
|
|
|
import deepClone from 'lodash.clonedeep';
|
2016-09-24 08:05:26 +03:00
|
|
|
import restrictFieldsFn from './lib/restrictFields.js';
|
2016-09-28 18:30:12 +03:00
|
|
|
import restrictLinks from './lib/restrictLinks.js';
|
2016-09-24 08:05:26 +03:00
|
|
|
|
|
|
|
let globalConfig = {};
|
2016-09-14 16:04:08 +03:00
|
|
|
|
|
|
|
export default class Exposure {
|
2016-09-24 08:05:26 +03:00
|
|
|
static setConfig(config) {
|
|
|
|
ExposureConfigSchema.clean(config);
|
2016-09-28 18:30:12 +03:00
|
|
|
ExposureConfigSchema.validate(config);
|
2016-09-24 08:05:26 +03:00
|
|
|
|
|
|
|
_.extend(globalConfig, config);
|
|
|
|
}
|
|
|
|
|
|
|
|
static getConfig() {
|
|
|
|
return globalConfig;
|
|
|
|
}
|
|
|
|
|
|
|
|
static restrictFields(...args) {
|
|
|
|
return restrictFieldsFn(...args);
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(collection, config = {}) {
|
|
|
|
collection.__isExposedForGrapher = true;
|
2016-09-28 18:30:12 +03:00
|
|
|
collection.__exposure = this;
|
2016-09-24 08:05:26 +03:00
|
|
|
|
2016-09-14 16:04:08 +03:00
|
|
|
this.collection = collection;
|
|
|
|
this.name = `exposure_${collection._name}`;
|
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
this.config = config;
|
|
|
|
this._validateAndClean();
|
2016-09-24 08:05:26 +03:00
|
|
|
|
2016-09-14 16:04:08 +03:00
|
|
|
this.initSecurity();
|
2016-09-28 18:30:12 +03:00
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
if (config.publication) {
|
2016-09-28 18:30:12 +03:00
|
|
|
this.initPublication();
|
|
|
|
}
|
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
if (config.method) {
|
2016-09-28 18:30:12 +03:00
|
|
|
this.initMethod();
|
|
|
|
}
|
2016-10-07 10:31:58 +03:00
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
if (!config.method && !config.publication) {
|
2016-10-07 10:31:58 +03:00
|
|
|
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();
|
2017-11-25 14:45:31 +02:00
|
|
|
this.initCountPublication();
|
2016-09-14 16:04:08 +03:00
|
|
|
}
|
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
_validateAndClean() {
|
|
|
|
if (typeof(this.config) === 'function') {
|
|
|
|
const firewall = this.config;
|
|
|
|
this.config = {firewall};
|
2016-09-24 08:05:26 +03:00
|
|
|
}
|
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
ExposureConfigSchema.clean(this.config);
|
|
|
|
ExposureConfigSchema.validate(this.config);
|
|
|
|
|
|
|
|
if (this.config.body) {
|
|
|
|
ExposureConfigSchema.validateBody(this.collection, this.config.body);
|
2016-10-19 15:22:50 +03:00
|
|
|
}
|
|
|
|
|
2017-03-02 10:43:47 +02:00
|
|
|
this.config = _.extend({}, Exposure.getConfig(), this.config);
|
2016-09-24 08:05:26 +03:00
|
|
|
}
|
|
|
|
|
2016-10-19 15:22:50 +03:00
|
|
|
/**
|
|
|
|
* Takes the body and intersects it with the exposure body, if it exists.
|
|
|
|
*
|
|
|
|
* @param body
|
|
|
|
* @param userId
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
|
|
|
getTransformedBody(body, userId) {
|
|
|
|
if (!this.config.body) {
|
|
|
|
return body;
|
|
|
|
}
|
|
|
|
|
2017-02-17 15:29:54 +02:00
|
|
|
const processedBody = this.getBody(userId);
|
|
|
|
|
|
|
|
if (processedBody === true) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return cleanBody(processedBody, body);
|
2016-10-19 15:22:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the exposure body
|
|
|
|
*/
|
|
|
|
getBody(userId) {
|
|
|
|
if (!this.config.body) {
|
2016-10-25 10:51:21 +03:00
|
|
|
throw new Meteor.Error('missing-body', 'Cannot get exposure body because it was not defined.');
|
2016-10-19 15:22:50 +03:00
|
|
|
}
|
|
|
|
|
2017-02-17 15:29:54 +02:00
|
|
|
let body;
|
2016-10-19 15:22:50 +03:00
|
|
|
if (_.isFunction(this.config.body)) {
|
2017-02-17 15:29:54 +02:00
|
|
|
body = this.config.body.call(this, userId);
|
2016-10-19 15:22:50 +03:00
|
|
|
} else {
|
2017-02-17 15:29:54 +02:00
|
|
|
body = this.config.body;
|
2016-10-19 15:22:50 +03:00
|
|
|
}
|
2017-02-17 15:29:54 +02:00
|
|
|
|
|
|
|
// it means we allow everything, no need for cloning.
|
|
|
|
if (body === true) {
|
|
|
|
return true;
|
2016-10-19 15:22:50 +03:00
|
|
|
}
|
2017-02-17 15:29:54 +02:00
|
|
|
|
|
|
|
return deepClone(
|
|
|
|
body,
|
|
|
|
userId
|
|
|
|
);
|
2016-10-19 15:22:50 +03:00
|
|
|
}
|
|
|
|
|
2016-11-18 18:33:21 +02:00
|
|
|
/**
|
|
|
|
* Initializing the publication for reactive query fetching
|
|
|
|
*/
|
2016-09-14 16:04:08 +03:00
|
|
|
initPublication() {
|
|
|
|
const collection = this.collection;
|
2016-09-24 08:05:26 +03:00
|
|
|
const config = this.config;
|
2016-10-19 15:22:50 +03:00
|
|
|
const getTransformedBody = this.getTransformedBody.bind(this);
|
2016-09-14 16:04:08 +03:00
|
|
|
|
|
|
|
Meteor.publishComposite(this.name, function (body) {
|
2016-10-19 15:22:50 +03:00
|
|
|
let transformedBody = getTransformedBody(body);
|
2016-10-23 20:02:44 +03:00
|
|
|
|
2016-10-19 15:22:50 +03:00
|
|
|
const rootNode = createGraph(collection, transformedBody);
|
2016-09-28 18:30:12 +03:00
|
|
|
|
|
|
|
enforceMaxDepth(rootNode, config.maxDepth);
|
|
|
|
restrictLinks(rootNode, this.userId);
|
|
|
|
|
2016-10-19 15:22:50 +03:00
|
|
|
return recursiveCompose(rootNode, this.userId, {
|
|
|
|
bypassFirewalls: !!config.body
|
|
|
|
});
|
2016-09-14 16:04:08 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-18 18:33:21 +02:00
|
|
|
/**
|
|
|
|
* Initializez the method to retrieve the data via Meteor.call
|
|
|
|
*/
|
2016-09-14 16:04:08 +03:00
|
|
|
initMethod() {
|
|
|
|
const collection = this.collection;
|
2016-09-24 08:05:26 +03:00
|
|
|
const config = this.config;
|
2016-10-19 15:22:50 +03:00
|
|
|
const getTransformedBody = this.getTransformedBody.bind(this);
|
2016-09-14 16:04:08 +03:00
|
|
|
|
2016-11-29 10:52:42 +02:00
|
|
|
const methodBody = function(body) {
|
|
|
|
if (!config.blocking) {
|
|
|
|
this.unblock();
|
|
|
|
}
|
2016-11-17 13:11:55 +02:00
|
|
|
|
2016-11-29 10:52:42 +02:00
|
|
|
let transformedBody = getTransformedBody(body);
|
2016-10-07 10:31:58 +03:00
|
|
|
|
2016-11-29 10:52:42 +02:00
|
|
|
const rootNode = createGraph(collection, transformedBody);
|
2016-09-28 18:30:12 +03:00
|
|
|
|
2016-11-29 10:52:42 +02:00
|
|
|
enforceMaxDepth(rootNode, config.maxDepth);
|
|
|
|
restrictLinks(rootNode, this.userId);
|
2016-09-28 18:30:12 +03:00
|
|
|
|
2016-11-29 10:52:42 +02:00
|
|
|
// if there is no exposure body defined, then we need to apply firewalls
|
|
|
|
return hypernova(rootNode, this.userId, {
|
|
|
|
bypassFirewalls: !!config.body
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Meteor.methods({
|
2016-11-30 14:01:38 +02:00
|
|
|
[this.name]: methodBody
|
2016-09-26 10:01:29 +03:00
|
|
|
});
|
2016-10-07 10:31:58 +03:00
|
|
|
}
|
|
|
|
|
2016-11-18 18:33:21 +02:00
|
|
|
/**
|
2017-11-25 14:45:31 +02:00
|
|
|
* Initializes the method to retrieve the count of the data via Meteor.call
|
2016-11-18 18:33:21 +02:00
|
|
|
* @returns {*}
|
|
|
|
*/
|
2016-10-07 10:31:58 +03:00
|
|
|
initCountMethod() {
|
|
|
|
const collection = this.collection;
|
2016-09-26 10:01:29 +03:00
|
|
|
|
|
|
|
Meteor.methods({
|
|
|
|
[this.name + '.count'](body) {
|
2016-10-07 10:31:58 +03:00
|
|
|
this.unblock();
|
|
|
|
|
2016-09-26 10:01:29 +03:00
|
|
|
return collection.find(body.$filters || {}, {}, this.userId).count();
|
|
|
|
}
|
2016-09-14 16:04:08 +03:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-25 14:45:31 +02:00
|
|
|
/**
|
|
|
|
* Initializes the reactive endpoint to retrieve the count of the data.
|
|
|
|
*/
|
|
|
|
initCountPublication() {
|
|
|
|
const collection = this.collection;
|
|
|
|
|
|
|
|
genCountEndpoint(this.name, {
|
|
|
|
getCursor(session) {
|
|
|
|
return collection.find(session.filters, {
|
|
|
|
fields: {_id: 1},
|
|
|
|
}, this.userId);
|
|
|
|
},
|
|
|
|
|
|
|
|
getSession(body) {
|
|
|
|
return { filters: body.$filters || {} };
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-18 18:33:21 +02:00
|
|
|
/**
|
|
|
|
* Initializes security enforcement
|
|
|
|
* THINK: Maybe instead of overriding .find, I could store this data of security inside the collection object.
|
|
|
|
*/
|
2016-09-14 16:04:08 +03:00
|
|
|
initSecurity() {
|
|
|
|
const collection = this.collection;
|
2016-09-24 08:05:26 +03:00
|
|
|
const {firewall, maxLimit, restrictedFields} = this.config;
|
2016-09-26 09:04:08 +03:00
|
|
|
const find = collection.find.bind(collection);
|
|
|
|
const findOne = collection.findOne.bind(collection);
|
2016-09-22 17:06:05 +03:00
|
|
|
|
2016-09-24 08:05:26 +03:00
|
|
|
collection.firewall = (filters, options, userId) => {
|
|
|
|
if (userId !== undefined) {
|
|
|
|
if (firewall) {
|
|
|
|
firewall.call({collection: collection}, filters, options, userId);
|
2016-09-14 16:04:08 +03:00
|
|
|
}
|
2016-09-22 13:46:27 +03:00
|
|
|
|
2016-09-24 08:05:26 +03:00
|
|
|
enforceMaxLimit(options, maxLimit);
|
2016-09-14 16:04:08 +03:00
|
|
|
|
2016-09-24 08:05:26 +03:00
|
|
|
if (restrictedFields) {
|
|
|
|
Exposure.restrictFields(filters, options, restrictedFields);
|
|
|
|
}
|
2016-09-14 16:04:08 +03:00
|
|
|
}
|
2016-09-24 08:05:26 +03:00
|
|
|
};
|
|
|
|
|
2017-04-13 17:19:15 +03:00
|
|
|
collection.find = function (filters, options = {}, userId = undefined) {
|
2017-04-13 16:55:33 +03:00
|
|
|
if (arguments.length == 0) {
|
|
|
|
filters = {};
|
|
|
|
}
|
|
|
|
|
2017-03-15 16:11:14 +02:00
|
|
|
// If filters is undefined it should return an empty item
|
|
|
|
if (arguments.length > 0 && filters === undefined) {
|
|
|
|
return find(undefined, options);
|
2017-02-17 10:02:04 +02:00
|
|
|
}
|
|
|
|
|
2016-09-24 08:05:26 +03:00
|
|
|
collection.firewall(filters, options, userId);
|
|
|
|
|
2016-09-26 09:04:08 +03:00
|
|
|
return find(filters, options);
|
2016-09-24 08:05:26 +03:00
|
|
|
};
|
|
|
|
|
2017-04-13 17:19:15 +03:00
|
|
|
collection.findOne = function (filters, options = {}, userId = undefined) {
|
2017-03-15 16:11:14 +02:00
|
|
|
// If filters is undefined it should return an empty item
|
|
|
|
if (arguments.length > 0 && filters === undefined) {
|
2017-02-17 10:02:04 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-04-13 17:20:17 +03:00
|
|
|
if (typeof(filters) === 'string') {
|
|
|
|
filters = {_id: filters};
|
|
|
|
}
|
|
|
|
|
2016-09-24 08:05:26 +03:00
|
|
|
collection.firewall(filters, options, userId);
|
|
|
|
|
2016-09-26 09:04:08 +03:00
|
|
|
return findOne(filters, options);
|
2016-09-14 16:04:08 +03:00
|
|
|
}
|
|
|
|
}
|
2017-02-07 11:20:10 +01:00
|
|
|
};
|