mirror of
https://github.com/vale981/grapher
synced 2025-03-04 17:11:38 -05:00
Scoped query docs & some improvements
This commit is contained in:
parent
70fd03a743
commit
e72b206ae1
6 changed files with 114 additions and 5 deletions
89
docs/named_queries.md
Normal file → Executable file
89
docs/named_queries.md
Normal file → Executable file
|
@ -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_<subscriptionId>` 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_<id>` 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_<namespace>` 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
|
## Conclusion
|
||||||
|
|
||||||
We can now safely expose our queries to the client, and the client can use it in a simple and uniform way.
|
We can now safely expose our queries to the client, and the client can use it in a simple and uniform way.
|
||||||
|
|
|
@ -5,6 +5,9 @@ const userListScoped = createQuery('userListScoped', {
|
||||||
name: 1,
|
name: 1,
|
||||||
friends: {
|
friends: {
|
||||||
name: 1
|
name: 1
|
||||||
|
},
|
||||||
|
subordinates: {
|
||||||
|
name: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -157,7 +157,7 @@ describe('Named Query', function() {
|
||||||
handle.stop();
|
handle.stop();
|
||||||
|
|
||||||
assert.equal(data.length, 1);
|
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;
|
const [user3] = data;
|
||||||
assert.equal(user3.friends.length, 3);
|
assert.equal(user3.friends.length, 3);
|
||||||
|
|
||||||
|
@ -167,17 +167,26 @@ describe('Named Query', function() {
|
||||||
|
|
||||||
const scopeField = `_sub_${handle.subscriptionId}`;
|
const scopeField = `_sub_${handle.subscriptionId}`;
|
||||||
const rootQueryPathField = '_query_path_users';
|
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]) => {
|
Object.entries(docMap).forEach(([userId, userDoc]) => {
|
||||||
const isRoot = userId === user3._id;
|
const isRoot = userId === user3._id;
|
||||||
assert.equal(userDoc[scopeField], 1);
|
assert.equal(userDoc[scopeField], 1);
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
assert.equal(userDoc[rootQueryPathField], 1);
|
assert.equal(userDoc[rootQueryPathField], 1);
|
||||||
assert.isTrue(!(nestedQueryPathField in userDoc));
|
assert.isTrue(!(friendsQueryPathField in userDoc));
|
||||||
|
assert.isTrue(!(adversaryQueryPathField in userDoc));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
assert.equal(userDoc[nestedQueryPathField], 1);
|
assert.equal(userDoc[friendsQueryPathField], 1);
|
||||||
assert.isTrue(!(rootQueryPathField in userDoc));
|
assert.isTrue(!(rootQueryPathField in userDoc));
|
||||||
|
|
||||||
|
if (userDoc.name === 'User - 2') {
|
||||||
|
assert.equal(userDoc[adversaryQueryPathField], 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assert.isTrue(!(adversaryQueryPathField in userDoc));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,9 @@ export function getNodeNamespace(node) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
let n = node;
|
let n = node;
|
||||||
while (n) {
|
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;
|
n = n.parent;
|
||||||
}
|
}
|
||||||
return parts.reverse().join('_');
|
return parts.reverse().join('_');
|
||||||
|
|
|
@ -90,6 +90,7 @@ _.range(USERS).forEach(idx => {
|
||||||
const id = Users.insert({
|
const id = Users.insert({
|
||||||
name: `User - ${idx}`,
|
name: `User - ${idx}`,
|
||||||
friendIds,
|
friendIds,
|
||||||
|
subordinateIds: idx === 3 ? [friendIds[2]] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
friendIds.push(id);
|
friendIds.push(id);
|
||||||
|
|
|
@ -6,4 +6,9 @@ Users.addLinks({
|
||||||
field: 'friendIds',
|
field: 'friendIds',
|
||||||
type: 'many'
|
type: 'many'
|
||||||
},
|
},
|
||||||
|
subordinates: {
|
||||||
|
collection: Users,
|
||||||
|
field: 'subordinateIds',
|
||||||
|
type: 'many'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue