Skip to content

Commit 200d2fb

Browse files
Fixed #181 - deep caching
1 parent 64fc26d commit 200d2fb

File tree

10 files changed

+712
-4
lines changed

10 files changed

+712
-4
lines changed

lib/links/config.schema.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
export const CacheSchema = new SimpleSchema({
2+
field: {type: String},
3+
body: {
4+
type: Object,
5+
blackbox: true,
6+
},
7+
bypassSchema: {
8+
type: Boolean,
9+
defaultValue: false,
10+
optional: true,
11+
}
12+
});
13+
114
export default new SimpleSchema({
215
type: {
316
type: String,
@@ -39,6 +52,10 @@ export default new SimpleSchema({
3952
type: Boolean,
4053
defaultValue: false,
4154
optional: true
55+
},
56+
cache: {
57+
type: CacheSchema,
58+
optional: true,
4259
}
4360
});
4461

lib/links/linker.js

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import LinkManyMeta from './linkTypes/linkManyMeta.js';
33
import LinkOne from './linkTypes/linkOne.js';
44
import LinkOneMeta from './linkTypes/linkOneMeta.js';
55
import LinkResolve from './linkTypes/linkResolve.js';
6-
import ConfigSchema from './config.schema.js';
6+
import ConfigSchema, {CacheSchema} from './config.schema.js';
77
import smartArguments from './linkTypes/lib/smartArguments';
8+
import dot from 'dot-object';
9+
import {_} from 'meteor/underscore';
810

911
export default class Linker {
1012
/**
@@ -22,9 +24,8 @@ export default class Linker {
2224
this._extendSchema();
2325

2426
// initialize cascade removal hooks.
25-
if (linkConfig.autoremove) {
26-
this._initAutoremove();
27-
}
27+
this._initAutoremove();
28+
this._initCache();
2829

2930
if (this.isVirtual()) {
3031
// if it's a virtual field make sure that when this is deleted, it will be removed from the references
@@ -355,6 +356,10 @@ export default class Linker {
355356
}
356357

357358
_initAutoremove() {
359+
if (!this.linkConfig.autoremove) {
360+
return;
361+
}
362+
358363
if (!this.isVirtual()) {
359364
this.mainCollection.after.remove((userId, doc) => {
360365
this.getLinkedCollection().remove({
@@ -374,4 +379,77 @@ export default class Linker {
374379
})
375380
}
376381
}
382+
383+
_initCache() {
384+
if (!this.linkConfig.cache || !Meteor.isServer) {
385+
return;
386+
}
387+
388+
CacheSchema.validate(this.linkConfig.cache);
389+
390+
const packageExists = !!Package['herteby:denormalize'];
391+
if (!packageExists) {
392+
throw new Meteor.Error('missing-package', `Please add the herteby:denormalize package to your Meteor application in order to make caching work`)
393+
}
394+
395+
const {field, body, bypassSchema} = this.linkConfig.cache;
396+
let cacheConfig;
397+
398+
let referenceFieldSuffix = '';
399+
if (this.isMeta()) {
400+
referenceFieldSuffix = (this.isSingle() ? '._id' : ':_id');
401+
}
402+
403+
if (this.isVirtual()) {
404+
let inversedLink = this.linkConfig.relatedLinker.linkConfig;
405+
406+
let type = inversedLink.type == 'many' ? 'many-inverse' : 'inversed';
407+
408+
cacheConfig = {
409+
type: type,
410+
collection: this.linkConfig.collection,
411+
fields: body,
412+
referenceField: inversedLink.field + referenceFieldSuffix,
413+
cacheField: field,
414+
bypassSchema: !!bypassSchema
415+
};
416+
} else {
417+
cacheConfig = {
418+
type: this.linkConfig.type,
419+
collection: this.linkConfig.collection,
420+
fields: body,
421+
referenceField: this.linkConfig.field + referenceFieldSuffix,
422+
cacheField: field,
423+
bypassSchema: !!bypassSchema
424+
};
425+
}
426+
427+
this.mainCollection.cache(cacheConfig);
428+
}
429+
430+
/**
431+
* Verifies if this linker is cached. It can be cached from the inverse side as well.
432+
*
433+
* @returns {boolean}
434+
* @private
435+
*/
436+
isCached() {
437+
return !!this.linkConfig.cache;
438+
}
439+
440+
/**
441+
* Verifies if the body of the linked element does not contain fields outside the cache body
442+
*
443+
* @param body
444+
* @returns {boolean}
445+
* @private
446+
*/
447+
isSubBodyCache(body) {
448+
const cacheBody = this.linkConfig.cache.body;
449+
450+
const cacheBodyFields = _.keys(dot.dot(cacheBody));
451+
const bodyFields = _.keys(dot.dot(body));
452+
453+
return _.difference(bodyFields, cacheBodyFields).length === 0;
454+
}
377455
}

lib/query/lib/createGraph.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ export function createNodes(root) {
4040
let linker = root.collection.getLinker(fieldName);
4141

4242
if (linker) {
43+
// check if it is a cached link
44+
// if yes, then we need to explicitly define this at collection level
45+
// so when we transform the data for delivery, we move it to the link name
46+
if (linker.isCached()) {
47+
if (linker.isSubBodyCache(body)) {
48+
const cacheField = linker.linkConfig.cache.field;
49+
50+
root.snapCache(cacheField, fieldName);
51+
addFieldNode(body, cacheField, root);
52+
53+
return;
54+
}
55+
}
56+
4357
let subroot = new CollectionNode(linker.getLinkedCollection(), body, fieldName);
4458
root.add(subroot, linker);
4559

lib/query/lib/prepareForDelivery.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import sift from 'sift';
88
import {Minimongo} from 'meteor/minimongo';
99

1010
export default (node) => {
11+
snapBackCaches(node);
1112
applyReducers(node);
1213
cleanReducerLeftovers(node);
1314
applyPostFilters(node);
@@ -169,6 +170,30 @@ function storeMetadata(element, parentElement, storage, isVirtual) {
169170
}
170171
}
171172

173+
function snapBackCaches(node) {
174+
node.collectionNodes.forEach(collectionNode => {
175+
snapBackCaches(collectionNode);
176+
});
177+
178+
if (!_.isEmpty(node.snapCaches)) {
179+
// process stuff
180+
_.each(node.snapCaches, (linkName, cacheField) => {
181+
const isSingle = _.contains(node.snapCachesSingles, cacheField);
182+
node.results.forEach(result => {
183+
if (result[cacheField]) {
184+
if (isSingle && _.isArray(result[cacheField])) {
185+
result[linkName] = _.first(result[cacheField]);
186+
} else {
187+
result[linkName] = result[cacheField];
188+
}
189+
190+
delete result[cacheField];
191+
}
192+
})
193+
})
194+
}
195+
}
196+
172197
// /**
173198
// * @param elements
174199
// * @param storage

lib/query/nodes/collectionNode.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export default class CollectionNode {
2020
this.scheduledForDeletion = false;
2121
this.reducers = [];
2222
this.results = [];
23+
this.snapCaches = {}; // {cacheField: linkName}
24+
this.snapCachesSingles = []; // [cacheField1, cacheField2]
2325
}
2426

2527
get collectionNodes() {
@@ -168,6 +170,20 @@ export default class CollectionNode {
168170
: (this.collection ? this.collection._name : 'N/A');
169171
}
170172

173+
/**
174+
* This is used for caching links
175+
*
176+
* @param cacheField
177+
* @param subLinkName
178+
*/
179+
snapCache(cacheField, subLinkName) {
180+
this.snapCaches[cacheField] = subLinkName;
181+
182+
if (this.collection.getLinker(subLinkName).isOneResult()) {
183+
this.snapCachesSingles.push(cacheField);
184+
}
185+
}
186+
171187
/**
172188
* This method verifies whether to remove the linkStorageField form the results
173189
* unless you specify it in your query.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {Mongo} from 'meteor/mongo';
2+
3+
export const Authors = new Mongo.Collection('cache_authors');
4+
export const AuthorProfiles = new Mongo.Collection('cache_author_profiles');
5+
export const Posts = new Mongo.Collection('cache_posts');
6+
export const Groups = new Mongo.Collection('cache_groups');
7+
export const Categories = new Mongo.Collection('cache_categories');
8+
9+
Authors.remove({});
10+
AuthorProfiles.remove({});
11+
Posts.remove({});
12+
Groups.remove({});
13+
Categories.remove({});
14+
15+
Posts.addLinks({
16+
author: {
17+
type: 'one',
18+
collection: Authors,
19+
field: 'authorId',
20+
cache: {
21+
field: 'authorCache',
22+
body: {
23+
name: 1,
24+
address: 1,
25+
}
26+
}
27+
},
28+
categories: {
29+
type: 'many',
30+
metadata: true,
31+
collection: Categories,
32+
field: 'categoryIds',
33+
cache: {
34+
field: 'categoriesCache',
35+
body: {
36+
name: 1,
37+
}
38+
}
39+
}
40+
});
41+
42+
Authors.addLinks({
43+
posts: {
44+
collection: Posts,
45+
inversedBy: 'author',
46+
cache: {
47+
field: 'postCache',
48+
body: {
49+
title: 1,
50+
}
51+
}
52+
},
53+
groups: {
54+
type: 'many',
55+
collection: Groups,
56+
field: 'groupIds',
57+
cache: {
58+
field: 'groupsCache',
59+
body: {
60+
name: 1,
61+
}
62+
}
63+
},
64+
profile: {
65+
type: 'one',
66+
metadata: true,
67+
collection: AuthorProfiles,
68+
field: 'profileId',
69+
unique: true,
70+
cache: {
71+
field: 'profileCache',
72+
body: {
73+
name: 1,
74+
}
75+
}
76+
}
77+
});
78+
79+
AuthorProfiles.addLinks({
80+
author: {
81+
collection: Authors,
82+
inversedBy: 'profile',
83+
unique: true,
84+
cache: {
85+
field: 'authorCache',
86+
body: {
87+
name: 1,
88+
}
89+
}
90+
}
91+
});
92+
93+
Groups.addLinks({
94+
authors: {
95+
collection: Authors,
96+
inversedBy: 'groups',
97+
cache: {
98+
field: 'authorsCache',
99+
body: {
100+
name: 1,
101+
}
102+
}
103+
}
104+
});
105+
106+
Categories.addLinks({
107+
posts: {
108+
collection: Posts,
109+
inversedBy: 'categories',
110+
cache: {
111+
field: 'postsCache',
112+
body: {
113+
title: 1,
114+
}
115+
}
116+
}
117+
});

0 commit comments

Comments
 (0)