Fixed #182 added named query cacher

This commit is contained in:
Theodor Diaconu 2017-11-26 19:15:00 +02:00
parent 200d2fb2cc
commit e9167757b3
8 changed files with 129 additions and 63 deletions

View file

@ -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()
}
})
```

View file

@ -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)
}
}

View file

@ -0,0 +1,5 @@
import {EJSON} from 'meteor/ejson';
export default function(queryName, params) {
return `${queryName}::${EJSON.stringify(params)}`;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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,
}));

View file

@ -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);
})
})
});
});
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)
});
});

View file

@ -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';