Updates to scoped query handling

This commit is contained in:
Berislav 2018-10-24 08:09:39 -07:00
parent a04edaf59f
commit 70fd03a743
13 changed files with 121 additions and 14 deletions

View file

@ -174,7 +174,7 @@ _.extend(NamedQuery.prototype, {
const rootNode = createGraph(self.collection, body);
return recursiveCompose(rootNode);
return recursiveCompose(rootNode, undefined, {scoped: isScoped});
});
},

View file

@ -18,6 +18,5 @@ export const ExposeSchema = {
),
validateParams: Match.Maybe(
Match.OneOf(Object, Function)
),
scoped: Match.Maybe(Boolean),
)
};

View file

@ -184,8 +184,7 @@ export default class extends Base {
createGraph(this.collection, body),
undefined, {
scoped: this.options.scoped,
queryScoped: true,
subscriptionHandle: this.subscriptionHandle,
subscriptionHandle: this.subscriptionHandle
});
}
}

View file

@ -6,6 +6,7 @@ import postListParamsCheck from './postListParamsCheck';
import postListParamsCheckServer from './postListParamsCheckServer';
import postListResolver from './postListResolver';
import postListResolverCached from './postListResolverCached';
import userListScoped from './userListScoped';
export {
postList,

View file

@ -0,0 +1,26 @@
import { createQuery } from 'meteor/cultofcoders:grapher';
const userListScoped = createQuery('userListScoped', {
users: {
name: 1,
friends: {
name: 1
}
}
}, {
scoped: true
});
if (Meteor.isServer) {
userListScoped.expose({
firewall() {
},
embody: {
$filter({filters, params}) {
filters.name = params.name;
}
}
})
}
export default userListScoped;

View file

@ -1,7 +1,9 @@
import postListExposure from './bootstrap/queries/postListExposure.js';
import postListExposureScoped from './bootstrap/queries/postListExposureScoped';
import userListScoped from './bootstrap/queries/userListScoped';
import { createQuery } from 'meteor/cultofcoders:grapher';
import Posts from '../../query/testing/bootstrap/posts/collection';
import Users from '../../query/testing/bootstrap/users/collection';
describe('Named Query', function() {
it('Should return proper values', function(done) {
@ -130,11 +132,53 @@ describe('Named Query', function() {
const docMap = Posts._collection._docs._map;
const scopeField = `_sub_${handle.subscriptionId}`;
const queryPathField = '_query_path_posts';
data.forEach(post => {
// no scope field returned from find
assert.isUndefined(post[scopeField]);
assert.isObject(docMap[post._id]);
assert.equal(docMap[post._id][scopeField], 1);
assert.equal(docMap[post._id][queryPathField], 1);
});
done();
}
});
});
it('Should work with reactive recursive scoped queries', function (done) {
const query = userListScoped.clone({name: 'User - 3'});
const handle = query.subscribe();
Tracker.autorun(c => {
if (handle.ready()) {
c.stop();
const data = query.fetch();
handle.stop();
assert.equal(data.length, 1);
// User 3 has users 0,1,2 as friends
const [user3] = data;
assert.equal(user3.friends.length, 3);
const docMap = Users._collection._docs._map;
// users collection on the client should have 4 items (user 3 and friends - user 0,1,2)
assert.equal(_.keys(docMap).length, 4);
const scopeField = `_sub_${handle.subscriptionId}`;
const rootQueryPathField = '_query_path_users';
const nestedQueryPathField = '_query_path_users_users';
Object.entries(docMap).forEach(([userId, userDoc]) => {
const isRoot = userId === user3._id;
assert.equal(userDoc[scopeField], 1);
if (isRoot) {
assert.equal(userDoc[rootQueryPathField], 1);
assert.isTrue(!(nestedQueryPathField in userDoc));
}
else {
assert.equal(userDoc[nestedQueryPathField], 1);
assert.isTrue(!(rootQueryPathField in userDoc));
}
});
done();

0
lib/namedQuery/testing/server.test.js Normal file → Executable file
View file

View file

@ -1,13 +1,20 @@
import applyProps from './applyProps.js';
import {getNodeNamespace} from './createGraph';
/**
* Adds _query_path fields to the cursor docs which are used for scoped query filtering on the client.
*
* @param cursor
* @param ns
*/
function patchCursor(cursor, ns) {
const originalObserve = cursor.observe;
cursor.observe = function (callbacks) {
const newCallbacks = Object.assign({}, callbacks);
if (callbacks.added) {
newCallbacks.added = doc => {
doc[`__query_path_${ns}`] = 1;
doc = _.clone(doc);
doc[`_query_path_${ns}`] = 1;
callbacks.added(doc);
};
}
@ -15,7 +22,7 @@ function patchCursor(cursor, ns) {
};
}
function compose(node, userId) {
function compose(node, userId, config) {
return {
find(parent) {
if (parent) {
@ -34,7 +41,9 @@ function compose(node, userId) {
}
const cursor = accessor.find(filters, options, userId);
if (config.scoped) {
patchCursor(cursor, getNodeNamespace(node));
}
return cursor;
}
},
@ -43,20 +52,22 @@ function compose(node, userId) {
}
}
export default (node, userId, config = {bypassFirewalls: false}) => {
export default (node, userId, config = {bypassFirewalls: false, scoped: false}) => {
return {
find() {
let {filters, options} = applyProps(node);
const cursor = node.collection.find(filters, options, userId);
if (config.scoped) {
patchCursor(cursor, getNodeNamespace(node));
}
return cursor;
},
children: _.map(node.collectionNodes, n => {
const userIdToPass = (config.bypassFirewalls) ? undefined : userId;
return compose(n, userIdToPass);
return compose(n, userIdToPass, config);
})
}
}

View file

@ -17,9 +17,9 @@ function fetch(node, parentObject, fetchOptions = {}) {
if (fetchOptions.scoped && fetchOptions.subscriptionHandle) {
_.extend(filters, fetchOptions.subscriptionHandle.scopeQuery());
}
// does not make sense without scoped parameter
if (fetchOptions.scoped && fetchOptions.queryScoped) {
_.extend(filters, {[`__query_path_${getNodeNamespace(node)}`]: {$exists: true}});
// add query path filter
if (fetchOptions.scoped) {
_.extend(filters, {[`_query_path_${getNodeNamespace(node)}`]: {$exists: true}});
}
let results = [];

View file

@ -7,16 +7,19 @@ import Comments from './comments/collection';
import Posts from './posts/collection';
import Tags from './tags/collection';
import Groups from './groups/collection';
import Users from './users/collection';
Authors.remove({});
Comments.remove({});
Posts.remove({});
Tags.remove({});
Groups.remove({});
Users.remove({});
const AUTHORS = 6;
const POST_PER_USER = 6;
const COMMENTS_PER_POST = 6;
const USERS = 4;
const TAGS = ['JavaScript', 'Meteor', 'React', 'Other'];
const GROUPS = ['JavaScript', 'Meteor', 'React', 'Other'];
const COMMENT_TEXT_SAMPLES = [
@ -81,4 +84,15 @@ _.each(authors, (author) => {
})
});
const friendIds = [];
// each user is created so his friends are previously added users
_.range(USERS).forEach(idx => {
const id = Users.insert({
name: `User - ${idx}`,
friendIds,
});
friendIds.push(id);
});
console.log('[ok] fixtures have been loaded.');

2
lib/query/testing/bootstrap/index.js Normal file → Executable file
View file

@ -4,10 +4,12 @@ import './authors/links';
import './tags/links';
import './groups/links';
import './security/links';
import './users/links';
import Posts from './posts/collection';
import Groups from './groups/collection';
import Authors from './authors/collection';
import Users from './users/collection';
if (Meteor.isServer) {
Posts.expose();

View file

@ -0,0 +1,2 @@
const Users = new Mongo.Collection('users');
export default Users;

View file

@ -0,0 +1,9 @@
import Users from './collection';
Users.addLinks({
friends: {
collection: Users,
field: 'friendIds',
type: 'many'
},
});