grapher/docs/query_options.md

379 lines
8.8 KiB
Markdown

# Query Options
Let's learn what a query can do, we know it can fetch related links, but it has some
interesting features.
## Nested fields
Most likely you will have documents which organize data inside an object, such as `user` may have a `profile` object
that stores `firstName`, `lastName`, etc
Grapher automatically detects these fields, as long as there is no link named `profile`:
```js
const user = Meteor.users.createQuery({
$filters: {_id: userId},
profile: {
firstName: 1,
lastName: 1,
}
}).fetchOne();
```
Now `user` will look like:
```
{
_id: userId,
profile: {
firstName: 'John',
lastName: 'Smith',
}
}
```
If you want to fetch the full `profile`, use `profile: 1` inside your query body. Alternatively,
you can also use `'profile.firstName': 1` and `'profile.lastName': 1` but it's less elegant.
## Deep filtering
Lets say we have a `Post` with `comments`:
```js
import {Comments, Posts} from '/imports/db';
Comments.addLinks({
post: {
type: 'one',
field: 'postId',
collection: Posts,
},
});
Posts.addLinks({
comments: {
collection: Comments,
inversedBy: 'post',
}
})
```
If any bit of the code written above creates confusion, take another look on [Linking Collections](linking_collections.md).
We already know that we can query with `$filters`, `$options`, `$filter` and have some parameters.
The same logic applies for child collection nodes:
```js
Posts.createQuery({
title: 1,
comments: {
$filters: {
isApproved: true,
},
text: 1,
}
})
```
The query above will fetch as `comments` only the ones that have been approved and that are linked with the `post`.
The `$filter` function shares the same `params` across all collection nodes:
```js
export default Posts.createQuery({
$filter({filters, params}) {
if (params.lastWeekPosts) {
filters.createdAt = {$gt: date}
}
},
$options: {createdAt: -1},
title: 1,
comments: {
$filter({filters, params}) {
if (params.approvedCommentsOnly) {
filters.isApproved = true;
}
},
$options: {createdAt: -1},
text: 1,
}
})
```
```js
const postsWithComments = postListQuery.clone({
lastWeekPosts: true,
approvedCommentsOnly: true
}).fetch();
```
### Default $filter()
The $filter is a function that defaults to:
```js
function $filter({filters, options, params}) {
if (params.filters) {
Object.assign(filters, params.filters);
}
if (params.options) {
Object.assign(filters, params.options)
}
}
```
Which basically means you can easily configure your filters and options through params:
```js
const postsQuery = Posts.createQuery({
title: 1,
});
const posts = postQuery.clone({
filters: {isApproved: true},
options: {
sort: {createdAt: -1},
}
}).fetch();
```
If you like to disable this functionality, add your own $filter() function or use a dummy one:
```js
{
$filter: () => {},
}
```
Note the default $filter() only applies to the top collection node, otherwise we would have headed into a lot of trouble.
### Pagination
There is a special field that extends the pre-fetch filtering process, and it's called `$paginate`, that allows us
to receive `limit` and `skip` params:
```js
const postsQuery = Posts.createQuery({
$filter({filters, params}) {
filters.isApproved = params.postsApproved;
},
$paginate: true,
title: 1,
});
const page = 1;
const perPage = 10;
const posts = postsQuery.clone({
postsApproved: true,
limit: perPage,
skip: (page - 1) * perPage
}).fetch()
```
This was created for your convenience, as pagination is a common used technique and makes your code easier to read.
Note that it doesn't override the $filter() function, it just applies `limit` and `skip` to the options, before `$filter()` runs.
It only works for the top level node, not for the child collection nodes.
### Meta Filters
Let's say we have `users` that belong in `groups` and they have some roles attached in the link description:
```js
import {Groups} from '/imports/db';
Meteor.users.addLinks({
groups: {
type: 'many',
collection: Groups,
field: 'groupLinks',
metadata: true,
}
});
Groups.addLinks({
users: {
collection: Meteor.users,
inversedBy: 'groups',
}
})
```
Let's assume the `groupLinks` looks like this:
```js
[
{
_id: 'groupId',
roles: ['ADMIN']
}
]
```
And you want to query users and fetch only the groups he is admin in:
```js
const users = Meteor.users.createQuery({
name: 1,
groups: {
$filters: {
$meta: {
roles: {$in: 'ADMIN'}
}
}
}
}).fetch()
```
But what if you want to fetch the groups and all their admins? It's the same.
```js
const groups = Groups.createQuery({
name: 1,
users: {
$filters: {
$meta: {
roles: {$in: 'ADMIN'}
}
}
}
}).fetch()
```
We have gone through great efforts to support such functionality, but it makes our code so easy to read and it doesn't impact performance.
## Post Filtering
This concept allows us to filter/manipulate data after we retrived it and assembled it.
The `$postFilters` option uses the `sift` npm library (https://www.npmjs.com/package/sift) to make your filters look like MongoDB filters.
For example, what if you want to get the users that are admins in at least one group:
```js
const users = Meteor.users.createQuery({
$postFilters: {
'groups.$metadata.roles': {$in: 'ADMIN'},
},
name: 1,
groups: {
name :1,
}
}).fetch()
```
If you had a `many` relationship without metadata your `$postFilters` would look like:
```
{
'groups.roles': {$in: 'ADMIN'},
}
```
In addition to `$postFilters` we've also got `$postOptions` that allows:
- limit
- sort
- skip
They work exactly like you expect from $options, the difference is that they are applied after the data has been fetched.
```js
const users = Meteor.users.createQuery({
$postOptions: {
sort: {'groups.name': 1},
},
name: 1,
groups: {
name :1,
}
}).fetch()
```
And to offer you full flexibility, we also allow a `$postFilter` function that needs
to return the new set of results.
```js
const users = Meteor.users.createQuery({
$postFilter(results, params) {
if (params.mustHaveGroupsAsAdmin) {
return results.filter(r => {
// your filter goes here.
});
}
},
name: 1,
groups: {
name: 1,
}
}, {
params: {
mustHaveGroupsAsAdmin: true
},
}).fetch()
```
Note the fact that these special fields only work for `top level nodes`, not for child collection nodes unlike `$filters`, `$options`, and `$filter`.
This type of queries that rely on post processing, can prove to be costly in some cases, because it will still fetch the users from the database
that don't have a group in which they are admin. There are alternatives to this to this in the [Denormalization](denormalization.md) section of the documentation.
It really depends on your context, but `$postFilters`, `$postOptions` and `$postFilter` can be very useful in some cases.
## Counters
If you want just to return the number of top level documents a query has:
```js
query.getCount()
```
This will be very useful for pagination when we reach the client-side domain, or you just need a count.
Note that `getCount()` applies only the processed `filters` but not `options`.
## Mix'em up
Both functions `$filter` and `$postFilter` also allow you to provide an array of functions:
```js
function userContext({filters, params}) {
if (!params.userId) {
throw new Meteor.Error('not-allowed');
}
filters.userId = params.userId;
}
const posts = Posts.createQuery({
$filter: [userContext, ({filters, options, params}) => {
// do something
}],
}).fetch()
```
The example above is just to illustrate the possibility, in order to ensure that a `userId` param is sent you will use `validateParams`.
```js
Posts.createQuery({
$filter({filters, options, params}) {
filters.userId = params.userId;
},
title: 1,
}, {
validateParams: {
userId: String
}
})
```
Validating params will also protect you from injections such as:
```js
const query = postLists.clone({
userId: {$nin: []},
})
```
When we cross to the client-side domain we need to be very wary of these type of injections.
## Conclusion
Query is a very powerful tool, very flexible, it allows us to do very complex things that would have taken us a lot of time
to do otherwise.
## [Continue Reading](reducers.md) or [Back to Table of Contents](index.md)