diff --git a/CHANGELOG.md b/CHANGELOG.md index 35609fd..e29c951 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.3 +- Added link caching +- Added named query results caching +- Added subbody to NamedQuery + ## 1.2.5 - Support for promises via .fetchSync and .fetchOneSync for client-side queries - Support for autoremove from inverse side as well diff --git a/LICENSE b/LICENSE index 27cdba6..7041e9b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016 Theodor Diaconu +Copyright (c) 2016-2018 Theodor Diaconu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/lib/namedQuery/expose/extension.js b/lib/namedQuery/expose/extension.js index b512e1c..7af0863 100644 --- a/lib/namedQuery/expose/extension.js +++ b/lib/namedQuery/expose/extension.js @@ -8,6 +8,9 @@ import recursiveCompose from '../../query/lib/recursiveCompose.js'; import prepareForProcess from '../../query/lib/prepareForProcess.js'; import deepClone from 'lodash.cloneDeep'; import genCountEndpoint from '../../query/counts/genEndpoint.server'; +import {SimpleSchema} from 'meteor/aldeed:simple-schema'; + +const specialParameters = ['$body']; _.extend(NamedQuery.prototype, { expose(config = {}) { @@ -114,7 +117,7 @@ _.extend(NamedQuery.prototype, { } let params = _.extend({}, self.params, newParams); - let body = prepareForProcess(self.body, params); + const body = prepareForProcess(self.body, params); const rootNode = createGraph(self.collection, body); @@ -123,16 +126,18 @@ _.extend(NamedQuery.prototype, { }, _validateParams(params) { - if (params && this.exposeConfig.schema) { + if (this.exposeConfig.schema) { + const paramsToValidate = _.omit(params, ...specialParameters); + if (process.env.NODE_ENV !== 'production') { try { - this._paramSchema.validate(params); + this._paramSchema.validate(paramsToValidate); } catch (validationError) { console.error(`Invalid parameters supplied to query ${this.queryName}`, validationError); throw validationError; // rethrow } } else { - this._paramSchema.validate(params); + this._paramSchema.validate(paramsToValidate); } } } diff --git a/lib/namedQuery/testing/bootstrap/server.js b/lib/namedQuery/testing/bootstrap/server.js index 671a255..a2b15d6 100644 --- a/lib/namedQuery/testing/bootstrap/server.js +++ b/lib/namedQuery/testing/bootstrap/server.js @@ -3,8 +3,14 @@ import postListExposure from './queries/postListExposure.js'; const postList = createQuery('postList', { posts: { - $filter({filters, params}) { - filters.title = params.title + $filter({filters, options, params}) { + if (params.title) { + filters.title = params.title; + } + + if (params.limit) { + options.limit = params.limit; + } }, title: 1, author: { diff --git a/lib/namedQuery/testing/server.test.js b/lib/namedQuery/testing/server.test.js index c712222..3562935 100644 --- a/lib/namedQuery/testing/server.test.js +++ b/lib/namedQuery/testing/server.test.js @@ -70,4 +70,31 @@ describe('Named Query', function () { done(); }, 500) }); + + it('Should allow to securely fetch a subbody of a namedQuery', function () { + const query = createQuery({ + postListExposure: { + limit: 5, + $body: { + title: 1, + createdAt: 1, // should fail + group: { + name: 1, + createdAt: 1, // should fail + } + } + } + }); + + const data = query.fetch(); + + assert.isTrue(data.length > 1); + + _.each(data, post => { + assert.isUndefined(post.createdAt); + assert.isUndefined(post.author); + assert.isObject(post.group); + assert.isUndefined(post.group.createdAt); + }) + }) }); diff --git a/lib/query/lib/createGraph.js b/lib/query/lib/createGraph.js index 377027c..426ece3 100644 --- a/lib/query/lib/createGraph.js +++ b/lib/query/lib/createGraph.js @@ -4,7 +4,13 @@ import ReducerNode from '../nodes/reducerNode.js'; import dotize from './dotize.js'; import createReducers from '../reducers/lib/createReducers'; -const specialFields = ['$filters', '$options', '$postFilters', '$postOptions', '$postFilter']; //keep $postFilter for legacy support +const specialFields = [ + '$filters', + '$options', + '$postFilters', + '$postOptions', + '$postProcessing' +]; /** * Creates node objects from the body diff --git a/lib/query/lib/intersectDeep.js b/lib/query/lib/intersectDeep.js new file mode 100644 index 0000000..e03cbd1 --- /dev/null +++ b/lib/query/lib/intersectDeep.js @@ -0,0 +1,23 @@ +import dot from 'dot-object'; +import {_} from 'meteor/underscore'; + +/** + * Given a named query that has a specific body, you can query its subbody + * This performs an intersection of the bodies allowed in each + * + * @param allowedBody + * @param clientBody + */ +export default function (allowedBody, clientBody) { + const allowedBodyDot = _.keys(dot.dot(allowedBody)); + const clientBodyDot = _.keys(dot.dot(clientBody)); + + const intersection = _.intersection(allowedBodyDot, clientBodyDot); + + const build = {}; + intersection.forEach(intersectedField => { + build[intersectedField] = 1; + }); + + return dot.object(build); +} \ No newline at end of file diff --git a/lib/query/lib/prepareForDelivery.js b/lib/query/lib/prepareForDelivery.js index a5333dd..0725e83 100644 --- a/lib/query/lib/prepareForDelivery.js +++ b/lib/query/lib/prepareForDelivery.js @@ -37,6 +37,7 @@ export function applyPostFilters(node) { applyPostFilters(collectionNode); }) } + export function applyPostOptions(node) { const options = node.props.$postOptions; if (options) { diff --git a/lib/query/lib/prepareForProcess.js b/lib/query/lib/prepareForProcess.js index 3b8cbbb..e8e0176 100644 --- a/lib/query/lib/prepareForProcess.js +++ b/lib/query/lib/prepareForProcess.js @@ -1,4 +1,5 @@ import deepClone from 'lodash.cloneDeep'; +import intersectDeep from './intersectDeep'; function defaultFilterFunction({ filters, @@ -62,6 +63,10 @@ function applyPagination(body, _params) { } export default (_body, _params = {}) => { + if (_params.$body) { + _body = intersectDeep(_body, _params.$body); + } + let body = deepClone(_body); let params = deepClone(_params); diff --git a/lib/query/testing/bootstrap/fixtures.js b/lib/query/testing/bootstrap/fixtures.js index 62ca8de..1a0b3bb 100644 --- a/lib/query/testing/bootstrap/fixtures.js +++ b/lib/query/testing/bootstrap/fixtures.js @@ -26,7 +26,10 @@ const COMMENT_TEXT_SAMPLES = [ console.log('[testing] Loading test fixtures ...'); let tags = TAGS.map(name => Tags.insert({name})); -let groups = GROUPS.map(name => Groups.insert({name})); +let groups = GROUPS.map(name => Groups.insert({ + name, + createdAt: new Date(), +})); let authors = _.range(AUTHORS).map(idx => { return Authors.insert({ name: 'Author - ' + idx, @@ -47,7 +50,8 @@ _.each(authors, (author) => { _.each(_.range(POST_PER_USER), (idx) => { let post = { - title: `User Post - ${idx}` + title: `User Post - ${idx}`, + createdAt: new Date(), }; authorPostLink.add(post);