From e9167757b32e373d81b06038fb0b039bbe9e47c7 Mon Sep 17 00:00:00 2001 From: Theodor Diaconu Date: Sun, 26 Nov 2017 19:15:00 +0200 Subject: [PATCH] Fixed #182 added named query cacher --- lib/namedQuery/README.md | 57 ---------------------- lib/namedQuery/cache/MemoryResultCacher.js | 44 +++++++++++++++++ lib/namedQuery/cache/generateQueryId.js | 5 ++ lib/namedQuery/namedQuery.base.js | 6 ++- lib/namedQuery/namedQuery.server.js | 28 ++++++++++- lib/namedQuery/testing/bootstrap/server.js | 15 +++++- lib/namedQuery/testing/server.test.js | 33 +++++++++++-- main.server.js | 4 ++ 8 files changed, 129 insertions(+), 63 deletions(-) delete mode 100644 lib/namedQuery/README.md create mode 100644 lib/namedQuery/cache/MemoryResultCacher.js create mode 100644 lib/namedQuery/cache/generateQueryId.js diff --git a/lib/namedQuery/README.md b/lib/namedQuery/README.md deleted file mode 100644 index a4277fa..0000000 --- a/lib/namedQuery/README.md +++ /dev/null @@ -1,57 +0,0 @@ -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() - } -}) - -``` \ No newline at end of file diff --git a/lib/namedQuery/cache/MemoryResultCacher.js b/lib/namedQuery/cache/MemoryResultCacher.js new file mode 100644 index 0000000..7f2ed31 --- /dev/null +++ b/lib/namedQuery/cache/MemoryResultCacher.js @@ -0,0 +1,44 @@ +import {Meteor} from 'meteor/meteor'; +import cloneDeep from 'lodash.cloneDeep'; + +const DEFAULT_TTL = 60000; + +/** + * This is a very basic in-memory result caching functionality + */ +export default class MemoryResultCacher { + constructor(config = {}) { + this.store = {}; + this.config = config; + } + + get(cacheId, { + query, + countCursor, + }) { + const cacheData = this.store[cacheId]; + if (cacheData !== undefined) { + return cloneDeep(cacheData); + } + + let data; + if (query) { + data = query.fetch(); + } else { + data = countCursor.count(); + } + + this.set(cacheId, data); + + return data; + } + + set(cacheId, data) { + const ttl = this.config.ttl || DEFAULT_TTL; + this.store[cacheId] = cloneDeep(data); + + Meteor.setTimeout(() => { + delete this.store[cacheId]; + }, ttl) + } +} diff --git a/lib/namedQuery/cache/generateQueryId.js b/lib/namedQuery/cache/generateQueryId.js new file mode 100644 index 0000000..ce3f423 --- /dev/null +++ b/lib/namedQuery/cache/generateQueryId.js @@ -0,0 +1,5 @@ +import {EJSON} from 'meteor/ejson'; + +export default function(queryName, params) { + return `${queryName}::${EJSON.stringify(params)}`; +} \ No newline at end of file diff --git a/lib/namedQuery/namedQuery.base.js b/lib/namedQuery/namedQuery.base.js index df8dea3..473c37e 100644 --- a/lib/namedQuery/namedQuery.base.js +++ b/lib/namedQuery/namedQuery.base.js @@ -24,11 +24,15 @@ export default class NamedQueryBase { } clone(newParams) { - return new this.constructor( + let clone = new this.constructor( this.queryName, this.collection, deepClone(this.body), _.extend({}, deepClone(this.params), newParams) ); + + clone.cacher = this.cacher; + + return clone; } } \ No newline at end of file diff --git a/lib/namedQuery/namedQuery.server.js b/lib/namedQuery/namedQuery.server.js index 26320a4..4551b00 100644 --- a/lib/namedQuery/namedQuery.server.js +++ b/lib/namedQuery/namedQuery.server.js @@ -1,6 +1,8 @@ import prepareForProcess from '../query/lib/prepareForProcess.js'; import Base from './namedQuery.base'; import deepClone from 'lodash.cloneDeep'; +import MemoryResultCacher from './cache/MemoryResultCacher'; +import generateQueryId from './cache/generateQueryId'; export default class extends Base { /** @@ -13,6 +15,11 @@ export default class extends Base { deepClone(this.params) ); + if (this.cacher) { + const cacheId = generateQueryId(this.queryName, this.params); + return this.cacher.get(cacheId, {query}); + } + return query.fetch(); } @@ -30,7 +37,15 @@ export default class extends Base { * @returns {any} */ getCount() { - return this.getCursorForCounting().count(); + const countCursor = this.getCursorForCounting(); + + if (this.cacher) { + const cacheId = 'count::' + generateQueryId(this.queryName, this.params); + + return this.cacher.get(cacheId, {countCursor}); + } + + return countCursor.count(); } /** @@ -42,4 +57,15 @@ export default class extends Base { return this.collection.find(body.$filters || {}, {fields: {_id: 1}}); } + + /** + * @param cacher + */ + cacheResults(cacher) { + if (!cacher) { + cacher = new MemoryResultCacher(); + } + + this.cacher = cacher; + } } \ No newline at end of file diff --git a/lib/namedQuery/testing/bootstrap/server.js b/lib/namedQuery/testing/bootstrap/server.js index 73476ba..671a255 100644 --- a/lib/namedQuery/testing/bootstrap/server.js +++ b/lib/namedQuery/testing/bootstrap/server.js @@ -1,4 +1,4 @@ -import { createQuery } from 'meteor/cultofcoders:grapher'; +import { createQuery, MemoryResultCacher } from 'meteor/cultofcoders:grapher'; import postListExposure from './queries/postListExposure.js'; const postList = createQuery('postList', { @@ -16,6 +16,7 @@ const postList = createQuery('postList', { } }); + export { postList }; export { postListExposure }; @@ -28,3 +29,15 @@ postListExposure.expose({ } } }); + +const postListCached = createQuery('postListCached', { + posts: { + title: 1, + } +}); + +export {postListCached}; + +postListCached.cacheResults(new MemoryResultCacher({ + ttl: 400, +})); \ No newline at end of file diff --git a/lib/namedQuery/testing/server.test.js b/lib/namedQuery/testing/server.test.js index bda4273..c712222 100644 --- a/lib/namedQuery/testing/server.test.js +++ b/lib/namedQuery/testing/server.test.js @@ -1,6 +1,7 @@ -import { postList } from './bootstrap/server.js'; +import { postList, postListCached } from './bootstrap/server.js'; import { createQuery } from 'meteor/cultofcoders:grapher'; + describe('Named Query', function () { it('Should return the proper values', function () { const createdQuery = createQuery({ @@ -42,5 +43,31 @@ describe('Named Query', function () { assert.isObject(post.author); assert.isObject(post.group); }) - }) -}); \ No newline at end of file + }); + + it('Should properly cache the values', function (done) { + const posts = postListCached.fetch(); + const postsCount = postListCached.getCount(); + + const Posts = Mongo.Collection.get('posts'); + const postId = Posts.insert({title: 'Hello Cacher!'}); + + assert.equal(posts.length, postListCached.fetch().length); + assert.equal(postsCount, postListCached.getCount()); + + Meteor.setTimeout(function () { + const newPosts = postListCached.fetch(); + const newCount = postListCached.getCount(); + + Posts.remove(postId); + + assert.isArray(newPosts); + assert.isNumber(newCount); + + assert.equal(posts.length + 1, newPosts.length); + assert.equal(postsCount + 1, newCount); + + done(); + }, 500) + }); +}); diff --git a/main.server.js b/main.server.js index f7544fd..d489d4e 100644 --- a/main.server.js +++ b/main.server.js @@ -21,4 +21,8 @@ export { default as getDocumentationObject } from './lib/documentor/index.js'; +export { + default as MemoryResultCacher +} from './lib/namedQuery/cache/MemoryResultCacher'; + export { Types } from './lib/constants';