grapher/lib/exposure/exposure.js

304 lines
8.1 KiB
JavaScript
Raw Normal View History

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';
import hypernova from '../query/hypernova/hypernova.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';
2016-09-28 18:30:12 +03:00
import restrictLinks from './lib/restrictLinks.js';
import { check } from 'meteor/check';
let globalConfig = {};
2016-09-14 16:04:08 +03:00
export default class Exposure {
static setConfig(config) {
2017-11-26 23:42:27 +02:00
Object.assign(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-14 16:04:08 +03:00
this.collection = collection;
this.name = `exposure_${collection._name}`;
this.config = config;
this._validateAndClean();
2016-09-14 16:04:08 +03:00
this.initSecurity();
2016-09-28 18:30:12 +03:00
2017-11-26 23:42:27 +02:00
if (this.config.publication) {
2016-09-28 18:30:12 +03:00
this.initPublication();
}
2017-11-26 23:42:27 +02:00
if (this.config.method) {
2016-09-28 18:30:12 +03:00
this.initMethod();
}
2017-11-26 23:42:27 +02:00
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'
);
}
this.initCountMethod();
2017-11-25 14:45:31 +02:00
this.initCountPublication();
2016-09-14 16:04:08 +03:00
}
_validateAndClean() {
if (typeof this.config === 'function') {
const firewall = this.config;
this.config = { firewall };
}
this.config = Object.assign(
{},
ExposureDefaults,
Exposure.getConfig(),
this.config
);
2017-11-26 23:42:27 +02:00
check(this.config, ExposureSchema);
if (this.config.body) {
2017-11-26 23:42:27 +02:00
validateBody(this.collection, this.config.body);
}
}
/**
* 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;
}
const processedBody = this.getBody(userId);
if (processedBody === true) {
return;
}
return cleanBody(processedBody, body);
}
/**
* Gets the exposure body
*/
getBody(userId) {
if (!this.config.body) {
throw new Meteor.Error(
'missing-body',
'Cannot get exposure body because it was not defined.'
);
}
let body;
if (_.isFunction(this.config.body)) {
body = this.config.body.call(this, userId);
} else {
body = this.config.body;
}
// it means we allow everything, no need for cloning.
if (body === true) {
return true;
}
return deepClone(body, userId);
}
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;
const config = this.config;
const getTransformedBody = this.getTransformedBody.bind(this);
2016-09-14 16:04:08 +03:00
Meteor.publishComposite(this.name, function(body) {
let transformedBody = getTransformedBody(body);
const rootNode = createGraph(collection, transformedBody);
2016-09-28 18:30:12 +03:00
enforceMaxDepth(rootNode, config.maxDepth);
restrictLinks(rootNode, this.userId);
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;
const config = this.config;
const getTransformedBody = this.getTransformedBody.bind(this);
2016-09-14 16:04:08 +03:00
const methodBody = function(body) {
if (!config.blocking) {
this.unblock();
}
let transformedBody = getTransformedBody(body);
const rootNode = createGraph(collection, transformedBody);
2016-09-28 18:30:12 +03:00
enforceMaxDepth(rootNode, config.maxDepth);
restrictLinks(rootNode, this.userId);
2016-09-28 18:30:12 +03:00
// if there is no exposure body defined, then we need to apply firewalls
return hypernova(rootNode, this.userId, {
bypassFirewalls: !!config.body,
});
};
Meteor.methods({
[this.name]: methodBody,
});
}
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 {*}
*/
initCountMethod() {
const collection = this.collection;
Meteor.methods({
[this.name + '.count'](body) {
this.unblock();
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
);
2017-11-25 14:45:31 +02:00
},
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;
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
);
2016-09-22 13:46:27 +03:00
enforceMaxLimit(options, maxLimit);
2016-09-14 16:04:08 +03:00
if (restrictedFields) {
Exposure.restrictFields(filters, options, restrictedFields);
}
2016-09-14 16:04:08 +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);
}
collection.firewall(filters, options, userId);
return find(filters, options);
};
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) {
return null;
}
if (typeof filters === 'string') {
filters = { _id: filters };
}
collection.firewall(filters, options, userId);
return findOne(filters, options);
};
2016-09-14 16:04:08 +03:00
}
/**
* @private
*/
_callFirewall(...args) {
const { firewall } = this.config;
if (!firewall) {
return;
}
if (_.isArray(firewall)) {
firewall.forEach(fire => {
fire.call(...args);
});
} else {
firewall.call(...args);
}
}
}