diff --git a/docs/named_queries.md b/docs/named_queries.md old mode 100644 new mode 100755 index 318e7d8..d76563a --- a/docs/named_queries.md +++ b/docs/named_queries.md @@ -426,6 +426,95 @@ query.expose({ }) ``` +## Scoped publications + +Scoped publications add a scope (context) to their published documents with the goal of client being able to distinguish between documents of different publications or different grapher paths in the same publication. + +Problems often arise when using only server-side filtering. + +Consider the following situation: + +```js +// the query +const usersQuery = Users.createQuery('getUsers', { + name: 1, + friends: { + name: 1, + }, +}, { + scoped: true, +}); + +// server-side exposure +usersQuery.expose({ + embody: { + $filter({filters, params}) { + filters.name = params.name; + } + } +}); + +// links +Users.addLinks({ + friends: { + collection: Users, + field: 'friendIds', + type: 'many' + }, +}); +``` + +Notice that `friends` is a link from Users to Users collection. Also, we have server-side filtering (see exposure). +On the client, we want to fetch reactively one user by name, but we are going to get all of his friends, too, and that is because of the `friends` link. +```js +// querying for user John +withQuery(props => { + return usersQuery.clone({ + name: 'John', + }); +}, { + reactive: true, +})(SomeComponent); +``` + +Client receives queried user (John) and all of his friends into the local Users collection. +By passing `{scoped: true}` query parameter to the `createQuery()`, client-side recursive fetching is now able to distinguish between queried user and his friends. + +### Technical details +Continuing on the example above, there are two pieces on how server and client achieve this functionality. + +#### Subscription scope +Each subscription adds `_sub_` field to its documents. For example, a User document could look like this: +``` +{ + name: 'John', + _sub_1: 1, + _sub_2: 1 +} +``` +This way we ensure that there is no mixup between the subscriptions (i.e. between two reactive queries on the client). + +#### Query path scope +Now suppose Alice is John's friend. Both Alice and John would have the same `_sub_` field for our example query and we would get both instead of only John. +This part is solved by adding "query path" field to the docs, in format `_query_path_` where namespace is path constructed from collection name and link names, for example: +``` +{ + name: 'John', + _sub_1: 1, + _query_path_users: 1, +}, +{ + name: 'Alice', + _sub_1: 1, + // deeper nesting than John's + _query_path_users_friends: 1 +} +``` + +where Alice has namespace equal to `users_friends` and client-side recursive fetching can now distinguish between the documents that should be returned as a query result (John) and as a `friends` link results for John (which is Alice). + +By adding query path field into the documents, we ensure that there is no mixup between the documents in the same reactive query (i.e. subscription). + ## Conclusion We can now safely expose our queries to the client, and the client can use it in a simple and uniform way. diff --git a/lib/namedQuery/testing/bootstrap/queries/userListScoped.js b/lib/namedQuery/testing/bootstrap/queries/userListScoped.js index 23b4f2d..5ef54f2 100755 --- a/lib/namedQuery/testing/bootstrap/queries/userListScoped.js +++ b/lib/namedQuery/testing/bootstrap/queries/userListScoped.js @@ -5,6 +5,9 @@ const userListScoped = createQuery('userListScoped', { name: 1, friends: { name: 1 + }, + subordinates: { + name: 1, } } }, { diff --git a/lib/namedQuery/testing/client.test.js b/lib/namedQuery/testing/client.test.js index 371783f..d0aa1dd 100755 --- a/lib/namedQuery/testing/client.test.js +++ b/lib/namedQuery/testing/client.test.js @@ -157,7 +157,7 @@ describe('Named Query', function() { handle.stop(); assert.equal(data.length, 1); - // User 3 has users 0,1,2 as friends + // User 3 has users 0,1,2 as friends and user 2 as subordinate const [user3] = data; assert.equal(user3.friends.length, 3); @@ -167,17 +167,26 @@ describe('Named Query', function() { const scopeField = `_sub_${handle.subscriptionId}`; const rootQueryPathField = '_query_path_users'; - const nestedQueryPathField = '_query_path_users_users'; + const friendsQueryPathField = '_query_path_users_friends'; + const adversaryQueryPathField = '_query_path_users_subordinates'; 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)); + assert.isTrue(!(friendsQueryPathField in userDoc)); + assert.isTrue(!(adversaryQueryPathField in userDoc)); } else { - assert.equal(userDoc[nestedQueryPathField], 1); + assert.equal(userDoc[friendsQueryPathField], 1); assert.isTrue(!(rootQueryPathField in userDoc)); + + if (userDoc.name === 'User - 2') { + assert.equal(userDoc[adversaryQueryPathField], 1); + } + else { + assert.isTrue(!(adversaryQueryPathField in userDoc)); + } } }); diff --git a/lib/query/lib/createGraph.js b/lib/query/lib/createGraph.js index dc2d660..e88ece9 100755 --- a/lib/query/lib/createGraph.js +++ b/lib/query/lib/createGraph.js @@ -109,7 +109,9 @@ export function getNodeNamespace(node) { const parts = []; let n = node; while (n) { - parts.push(n.collection._name); + const name = n.linker ? n.linker.linkName : n.collection._name; + parts.push(name); + // console.log('linker', node.linker ? node.linker.linkName : node.collection._name); n = n.parent; } return parts.reverse().join('_'); diff --git a/lib/query/testing/bootstrap/fixtures.js b/lib/query/testing/bootstrap/fixtures.js index af97455..d70b579 100755 --- a/lib/query/testing/bootstrap/fixtures.js +++ b/lib/query/testing/bootstrap/fixtures.js @@ -90,6 +90,7 @@ _.range(USERS).forEach(idx => { const id = Users.insert({ name: `User - ${idx}`, friendIds, + subordinateIds: idx === 3 ? [friendIds[2]] : [], }); friendIds.push(id); diff --git a/lib/query/testing/bootstrap/users/links.js b/lib/query/testing/bootstrap/users/links.js index 1c8a47f..9533da2 100755 --- a/lib/query/testing/bootstrap/users/links.js +++ b/lib/query/testing/bootstrap/users/links.js @@ -6,4 +6,9 @@ Users.addLinks({ field: 'friendIds', type: 'many' }, + subordinates: { + collection: Users, + field: 'subordinateIds', + type: 'many' + } });