mirror of
https://github.com/vale981/grapher
synced 2025-03-06 01:51:38 -05:00
379 lines
8.8 KiB
Markdown
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)
|
|
|
|
|
|
|