mirror of
https://github.com/vale981/grapher
synced 2025-03-05 09:31:42 -05:00
documentation rc
This commit is contained in:
parent
7cc67a13f7
commit
0933b8c0b1
15 changed files with 450 additions and 365 deletions
10
README.md
10
README.md
|
@ -2,23 +2,27 @@
|
|||
|
||||
[](https://travis-ci.org/cult-of-coders/grapher)
|
||||
|
||||
*Grapher* is a data retrieval layer inside Meteor and MongoDB. It's used in many production apps since 2016.
|
||||
*Grapher* is a Data Fetching Layer on top of Meteor and MongoDB. It is production ready and battle tested.
|
||||
|
||||
Main features:
|
||||
- Innovative way to make MongoDB relational
|
||||
- Reactive data graphs for high availability
|
||||
- Incredible performance
|
||||
- Denormalization Modules
|
||||
- Denormalization Ability
|
||||
- Connection to external data sources
|
||||
- Usable from anywhere
|
||||
|
||||
It marks a stepping stone into evolution of data, enabling developers to write complex and secure code,
|
||||
while maintaining the code base easy to understand.
|
||||
|
||||
### [Documentation](docs/table_of_contents.md)
|
||||
### [Documentation](docs/index.md)
|
||||
|
||||
This provides a learning curve for Grapher, explaining all the features.
|
||||
|
||||
### [API](docs/api.md)
|
||||
|
||||
Grapher cheatsheet, after you've learned it's powers this is the document will be very useful.
|
||||
|
||||
### Installation
|
||||
```
|
||||
meteor add cultofcoders:grapher
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# Caching Results
|
||||
|
||||
Grapher is already performant enough to remove this necessity, but what if we want
|
||||
to squeeze even more performance? What can we do? We cache'em up.
|
||||
Ok, it's time for Grapher to take it to the next level. Let's cache our very heavy queries.
|
||||
|
||||
Caching results only works for Named Queries.
|
||||
|
||||
Let's say we have some very heavy queries, that do complex sorting
|
||||
and complex filtering:
|
||||
|
||||
Let's take an example
|
||||
|
||||
```js
|
||||
export default Meteor.users.createQuery('myFriendsEmails', {
|
||||
|
@ -21,7 +20,7 @@ export default Meteor.users.createQuery('myFriendsEmails', {
|
|||
```
|
||||
|
||||
Ok maybe it's not that complex or heavy, but use your imagination, imagine we have 100,000,000 users
|
||||
inside the database. And you just want to look for your friends in this big world.
|
||||
inside the database. And you just want to look for your friends in this big nasty world.
|
||||
|
||||
```js
|
||||
// server-side
|
||||
|
@ -35,7 +34,7 @@ myFriendsQuery.expose({
|
|||
});
|
||||
|
||||
const cacher = new MemoryResultCacher({
|
||||
ttl: 60 * 1000, // 1 minute caching
|
||||
ttl: 60 * 1000, // 60 seconds caching
|
||||
});
|
||||
|
||||
myFriendsQuery.cacheResults(cacher);
|
||||
|
@ -44,18 +43,19 @@ myFriendsQuery.cacheResults(cacher);
|
|||
That's it. If there is no cache, it fetches the database and stores the result in memory, and after 60s it clears the cache.
|
||||
Any other calls done in between this time, will hit the cache.
|
||||
|
||||
The caching is done by serializing the params and prefixed by query name, in our case, the cache id looks like:
|
||||
The cacher will be used regardless if you are on server or on the client.
|
||||
The cacher also caches counts by default (eg: when you use `getCount()` from client or server)
|
||||
|
||||
**Cacher** is parameter bound, if you get the same parameters it will hit the cache (if it was already cached),
|
||||
the cache id is generated like this:
|
||||
```
|
||||
myFriendsEmails{userId:"XXX"}
|
||||
count::myFriendsEmails{userId:"XXX"}
|
||||
```
|
||||
|
||||
The cacher will be used regardless if you are on server or on the client.
|
||||
The cacher also works with counts. When you use `query.getCount()`
|
||||
|
||||
## Implementing your own
|
||||
|
||||
The cacher that you provide exposes:
|
||||
The cacher needs to be an object which exposes:
|
||||
```
|
||||
fetch(cacheId, {
|
||||
query: {fetch()},
|
||||
|
@ -74,13 +74,17 @@ import {BaseResultCacher} from 'meteor/cultofcoders:grapher';
|
|||
* Redis Cacher
|
||||
*/
|
||||
export default class RedisCacher extends BaseResultCacher {
|
||||
// the constructor accepts a config object, that stores it in this.config
|
||||
|
||||
// this is the default one in case you need to override it
|
||||
// if don't specify if it, it will use this one from BaseResultCacher
|
||||
generateQueryId(queryName, params) {
|
||||
return `${queryName}::${EJSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
// in case of a count cursor cacheId gets prefixed with 'count::'
|
||||
fetch(cacheId, fetchables) {
|
||||
// client and ttl are the configs passed when we instantiate it
|
||||
const {client, ttl} = this.config;
|
||||
|
||||
const cacheData = client.get(cacheId);
|
||||
|
@ -114,22 +118,21 @@ myFriendsQuery.cacheResults(cacher);
|
|||
As you may have guessed, this works with resolver queries as well, in their case, instead of the actual query,
|
||||
we pass as `query` parameter an interface containing `fetch()`.
|
||||
|
||||
And your cacher, will never receive `cursorCount` inside the `fetchables` object.
|
||||
|
||||
Therefore you can use the same paradigms for your cache for resolver queries as well.
|
||||
And your cacher, will never receive `cursorCount` inside the `fetchables` object in this case.
|
||||
|
||||
Therefore you can use the same paradigms for your cache for resolver queries as well, without any change.
|
||||
|
||||
## Invalidation
|
||||
|
||||
Unfortunately, if you want to invalidate your cache you can do it yourself manually, as this is not implemented,
|
||||
but since you can hook in your own cacher, you can do whatever you want.
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
If you have very heavy and frequent requests to the database, or any type of requests (resolver queries) you can think
|
||||
about using a cache.
|
||||
|
||||
|
||||
about using a cache, with very few lines of code.
|
||||
|
||||
#### [Continue Reading](global_exposure.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@ You may need denormalization in such cases where:
|
|||
1. You want to avoid another database fetch
|
||||
2. You need ability to perform complex filtering
|
||||
|
||||
This has some caveats that we'll explore, let's check out a simple example first:
|
||||
|
||||
A simple example, a user has an avatar that is stored in the image collection:
|
||||
|
||||
```js
|
||||
// Assuming Images of schema: {_id, path, smallThumb, createdAt}
|
||||
|
@ -46,10 +47,10 @@ const user = Meteor.users.createQuery({
|
|||
}).fetchOne()
|
||||
```
|
||||
|
||||
Grapher will check to see if avatar's body is a subbody of the denormalized body. If yes, it will hit the cache,
|
||||
Grapher will check to see if avatar's body is a subbody of the denormalized body. If yes, it will hit the `avatarCache` field,
|
||||
leading to a single database request.
|
||||
|
||||
And the result will be as you expect it to be:
|
||||
And the result will in the form as you expect:
|
||||
```
|
||||
{
|
||||
_id: 'XXX',
|
||||
|
@ -72,46 +73,43 @@ const user = Meteor.users.createQuery({
|
|||
}).fetchOne()
|
||||
```
|
||||
|
||||
Will result in a subsequent database request, because `createdAt` is not in the denormalized body. But if you swap it with `path` then it will hit the cache.
|
||||
Will result in a subsequent database request, because `createdAt` is not in the denormalized body. But if you replace `createdAt` with `path` then it will hit the cache.
|
||||
|
||||
When the user sets a new Avatar, or the `Image` object of that Avatar gets updated. The cache gets automatically updated,
|
||||
so you don't have to worry about anything. It's magical.
|
||||
|
||||
Denormalization works with any type of links `one`, `many`, `meta` whether they are `direct` or `inversed`.
|
||||
|
||||
We previously tackled the case where we needed `$postFilters` or `$postFilter` to retrieve some special kind of data:
|
||||
We previously tackled the case where we needed `$postFilters` or `$postFilter` to retrieve filtered data.
|
||||
|
||||
Let's take a case: we want to retrieve only the users that have reviewed a book of a certain type, and inside `books` collection,
|
||||
we have `reviewedByUserIds`.
|
||||
For example, let's say we want to retrieve only the users that have reviewed a book of a certain type,
|
||||
and inside `users` collection we have a `reviewedBoos` link.
|
||||
|
||||
```js
|
||||
Books.addLinks({
|
||||
'reviewedByUsers': {
|
||||
Meteor.users.addLinks({
|
||||
'reviewedBooks': {
|
||||
type: 'many',
|
||||
collection: Meteor.users,
|
||||
field: 'reviewedByUserIds',
|
||||
collection: Books,
|
||||
field: 'reviewedBooksIds',
|
||||
denormalize: {
|
||||
body: {
|
||||
type: 1,
|
||||
},
|
||||
field: 'reviewedBooksCache',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Meteor.users.addLinks({
|
||||
'bookReviews': {
|
||||
collection: Books,
|
||||
inversedBy: 'reviewedByUsers',
|
||||
denormalize: {
|
||||
body: {
|
||||
type: 1,
|
||||
},
|
||||
field: 'bookReviewsCache',
|
||||
}
|
||||
Books.addLinks({
|
||||
'reviewers': {
|
||||
collection: Meteor.users,
|
||||
inversedBy: 'reviewedBooks',
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
*Note that this case may not necessarily be the best example, this is just for illustration,
|
||||
if you have a better example in mind, let us know so we can update this documentation.*
|
||||
|
||||
And now, I want to get all the users that have reviewed books of type `Drama`, because I want
|
||||
to send them an email about a new book.
|
||||
to send them an email about a new book, or a soap opera.
|
||||
|
||||
```js
|
||||
const dramaticUsers = Meteor.users.createQuery({
|
||||
|
@ -122,23 +120,34 @@ const dramaticUsers = Meteor.users.createQuery({
|
|||
}).fetch();
|
||||
```
|
||||
|
||||
Denormalization comes with a price:
|
||||
That was it, but denormalization comes with a price:
|
||||
1. It adds hooks to the database so it can properly update it, therefore a change somewhere can result
|
||||
into additional computation.
|
||||
2. If you are not careful it can lead to very big caches, which is not what you want unless you favor performance over storage.
|
||||
|
||||
The package also supports caching fields and caching counts. You can define those caches outside Grapher without a problem, and specify those fields in your query.
|
||||
The `herteby:denormalize` package also supports caching fields and caching counts.
|
||||
You can define those caches outside Grapher without a problem, and specify those fields in your query.
|
||||
|
||||
## Caution
|
||||
|
||||
If you want to use deep filters, it will not work with denormalized cache, you can use `$postFilter()` method for that.
|
||||
|
||||
```js
|
||||
{
|
||||
users: {
|
||||
avatar: {
|
||||
$filters: {} // will not hit the cache
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because if you put `$filters: {}` inside the body of the cache, it will regard it as a foreign field, and it will fetch the linked Collection for it.
|
||||
|
||||
A current limitation for denormalized meta links, is that we will no longer be able to store the `$metadata` inside the nested object, because that
|
||||
would require additional fetching of the link storage,
|
||||
would require additional fetching of the link storage if we are querying the graph from the inversed side.
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
Using denormalization can enable you to do wonderful things inside NoSQL, but also be careful because they come with a cost,
|
||||
that may not be very noticeable in the beginning. But can also dramatically improve performance at the same time.
|
||||
|
@ -146,3 +155,4 @@ that may not be very noticeable in the beginning. But can also dramatically impr
|
|||
I suggest that they should be used to cache things that rarely change such as an user's avatar, or when you need to do
|
||||
powerful and frequent searches, that otherwise would have consumed more resources.
|
||||
|
||||
#### [Continue Reading](caching_results.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
## Global Queries
|
||||
|
||||
Global queries are not recommended because they are very hard to secure.
|
||||
Global queries are not recommended because they are very hard to secure. If you are not interested
|
||||
in exposing an API for your clients or expose a public database, [continue reading next part](structure_and_patterns.md)
|
||||
|
||||
But they are very interesting in what they offer and they can prove to be very useful.
|
||||
You can expose an API that has access to all or certain parts of your database, without
|
||||
|
@ -8,7 +9,7 @@ defining a named query for each.
|
|||
|
||||
The difference between a `Named Query` and a `Global Query` is that the later
|
||||
does not have their form defined on the server, the client can query for anything that he wishes
|
||||
as long as the query is exposed and respects the security options.
|
||||
as long as the query is exposed and respects the security restrictions.
|
||||
|
||||
A `Global Query` is as almost feature rich as a `Named Query` with the exception of caching.
|
||||
|
||||
|
@ -334,7 +335,9 @@ This will allow requests like:
|
|||
}
|
||||
```
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
The global queries are a very powerful tool to expose your full database, but unlike `Named Queries` they do
|
||||
not benefit of `caching`.
|
||||
|
||||
#### [Continue Reading](structure_and_patterns.md) or [Back to Table of Contents](table_of_contents.md)
|
|
@ -1,8 +1,10 @@
|
|||
## Hypernova
|
||||
|
||||
This is the crown jewl of Grapher. It was named like this because it felt like an explosion of data.
|
||||
This is the crown jewl of Grapher. It has been innovated in the laboratories of Cult of Coders,
|
||||
and engineered for absolute performance. We had to name this whole process somehow, and we had
|
||||
to give it a bombastic name. Hypernova is the one that stuck.
|
||||
|
||||
Grapher is very performant. To understand what we're talking about let's take this example of a query
|
||||
To understand what we're talking about let's take this example of a query:
|
||||
|
||||
```js
|
||||
createQuery({
|
||||
|
@ -27,10 +29,10 @@ createQuery({
|
|||
|
||||
In a normal scenario, to retrieve this data graph we need to:
|
||||
1. Fetch the posts
|
||||
2. posts.length * Fetch categories for each post
|
||||
3. posts.length * Fetch author for each post
|
||||
4. posts.length * Fetch comments for each post
|
||||
5. posts.length * 10 * Fetch author for each comment
|
||||
2. length(posts) x Fetch categories for each post
|
||||
3. length(posts) x Fetch author for each post
|
||||
4. length(posts) x Fetch comments for each post
|
||||
5. length(posts) * length(comments) * Fetch author for each comment
|
||||
|
||||
Assuming we have:
|
||||
- 10 posts
|
||||
|
@ -44,16 +46,16 @@ We would have blasted the database with:
|
|||
- Categories: 10
|
||||
- Post authors: 10
|
||||
- Post comments: 10
|
||||
- Post comments authors: 100
|
||||
- Post comments authors: 10*10
|
||||
|
||||
This means `131` database requests.
|
||||
|
||||
This means 131 database requests.
|
||||
Ok, you can cache some stuff, maybe some authors collide, but in order to write a performant code,
|
||||
you would have to write a bunch of non-reusable code.
|
||||
|
||||
But this is just a simple query, imagine something deeper nested. Grapher simply destroys any other
|
||||
MongoDB "relational" ORM.
|
||||
But this is just a simple query, imagine something deeper nested. For Grapher, it's a breeze.
|
||||
|
||||
### Hypernova to the rescue
|
||||
### Hypernova in Action
|
||||
|
||||
How many requests does the Hypernova?
|
||||
- 1 for Posts
|
||||
|
@ -63,15 +65,36 @@ How many requests does the Hypernova?
|
|||
- 1 for all authors inside all comments
|
||||
|
||||
The number of database is predictable, because it represents the number of collection nodes inside the graph.
|
||||
(If you use reducers that make use of links, take those into consideration as well)
|
||||
|
||||
It does this by aggregating filters and then it reassembles data locally.
|
||||
It does this by aggregating filters at each level, fetching the data, and then it reassembles data to their
|
||||
propper objects.
|
||||
|
||||
Not only it makes 5 requests instead of 131, but it smartly re-uses categories and authors at each collection node,
|
||||
meaning you will have less bandwidth consumed.
|
||||
|
||||
Now you understand why this is a revolution for MongoDB.
|
||||
Making it more efficient in terms of bandwidth than SQL. Yes, you read that correct:
|
||||
|
||||
```js
|
||||
{
|
||||
posts: {
|
||||
categories: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Let's assume we have 100 posts, and the total number of categories is like 4. Hypernova does 2 requests to the database,
|
||||
and fetches 100 posts, and 4 categories. If you would have used `JOIN` functionality in SQL, you would have received
|
||||
categories for each post.
|
||||
|
||||
Now you understand why this is a revolution for MongoDB and Meteor.
|
||||
|
||||
Keep in mind that Hypernova is only used for static queries. For reactive queries, we still rely on the recursive fetching.
|
||||
|
||||
#### [Continue Reading](denormalization.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
57
docs/index.md
Normal file
57
docs/index.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Table of Contents
|
||||
|
||||
Let's learn Grapher. It's easy, we made a lot of effort to make it this way. It's time to change
|
||||
the way you think about your data.
|
||||
|
||||
### [Introduction](introduction.md)
|
||||
|
||||
This introduces you to the way we use Grapher in the most basic form, without relationships.
|
||||
|
||||
### [Linking Collections](linking_collections.md)
|
||||
|
||||
Learn the various ways of linking collections to each other.
|
||||
|
||||
### [Linker Engine](linker_engine.md)
|
||||
|
||||
Learn about how you can programatically set and fetch related data.
|
||||
|
||||
### [Query Options](query_options.md)
|
||||
|
||||
Learn more advanced ways to use your queries.
|
||||
|
||||
### [Reducers](reducers.md)
|
||||
|
||||
Remove complexity from your queries, make them a breeze by using reducers.
|
||||
|
||||
### [Named Queries](named_queries.md)
|
||||
|
||||
Learn the right way to define and expose queries to the client in a secure manner.
|
||||
|
||||
### [Hypernova](hypernova.md)
|
||||
|
||||
Read about the tool that makes Grapher so performant.
|
||||
|
||||
### [Denormalization](denormalization.md)
|
||||
|
||||
Learn how to denormalize your data to enable even more performance, and super advanced searching.
|
||||
|
||||
### [Caching Results](caching_results.md)
|
||||
|
||||
Leverage the power of Grapher to cache the most heavy or frequently used queries.
|
||||
|
||||
### [Global Exposure](global_exposure.md)
|
||||
|
||||
Learn about a neat way to expose your collections. This was the initial approach of Grapher, and it is very useful
|
||||
for when you want to expose an API accessing most parts of the database to your clients.
|
||||
|
||||
### [Structure & Patterns](structure_and_patterns.md)
|
||||
|
||||
Learn about some good ways to structure your code some and about common pitfalls.
|
||||
|
||||
### [Outside Meteor](outside_meteor.md)
|
||||
|
||||
If you want to use Grapher in a React Native app so from any other language as an API, it is possible
|
||||
|
||||
### [API](api.md)
|
||||
|
||||
After you've learned about Grapher, here's your cheatsheet.
|
|
@ -10,7 +10,7 @@ meteor add cultofcoders:grapher
|
|||
Grapher is composed of 3 main modules, that work together:
|
||||
|
||||
### Link Manager
|
||||
This module allows you to configure relationships between collections and allows you to create denormalized links and resolver links.
|
||||
This module allows you to configure relationships between collections and allows you to create denormalized links.
|
||||
|
||||
### Query
|
||||
The query module is used for fetching your data in a friendly manner, such as:
|
||||
|
@ -22,7 +22,7 @@ createQuery({
|
|||
})
|
||||
```
|
||||
|
||||
It abstracts your query into a graph composed of Collection Nodes, Field Nodes and Resolver Nodes,
|
||||
It abstracts your query into a graph composed of Collection Nodes and Field Nodes.
|
||||
it uses the **Link Manager** to construct this graph and if the fetching is done server-side (non-reactive queries),
|
||||
it uses the **Hypernova Module** the crown jewl of Grapher, which heavily minimizes requests to database.
|
||||
|
||||
|
@ -32,9 +32,9 @@ The exposure represents the layer between your queries and the client, allowing
|
|||
only to users that have access.
|
||||
|
||||
|
||||
### Your first query
|
||||
### Let's begin!
|
||||
|
||||
You can use Grapher, without defining any links, for example, let's say you have a method which returns a list of posts.
|
||||
You can use Grapher without defining any links, for example, let's say you have a method which returns a list of posts.
|
||||
|
||||
```js
|
||||
Meteor.methods({
|
||||
|
@ -50,7 +50,7 @@ Meteor.methods({
|
|||
})
|
||||
```
|
||||
|
||||
Transforming this into a Grapher query would simply look like this:
|
||||
Transforming this into a Grapher query looks like this:
|
||||
|
||||
```js
|
||||
Meteor.methods({
|
||||
|
@ -71,8 +71,7 @@ you may find this cumbersome in the beginning, but as your application grows, ne
|
|||
fields that need to be protected, you'll find yourself refactoring parts of your code-base which exposed
|
||||
all the fields.
|
||||
|
||||
|
||||
If, for example, you want to filter or sort your query, we have some special query variables to do so:
|
||||
If, for example, you want to filter or sort your query, we introduce the `$filters` and `$options` fields:
|
||||
|
||||
```js
|
||||
Meteor.methods({
|
||||
|
@ -98,12 +97,16 @@ Meteor.methods({
|
|||
If for example you are searching an element by `_id`, you may have `$filters: {_id: 'XXX'}`, then instead of `fetch()` you
|
||||
can call `.fetchOne()` so it will return the first element found.
|
||||
|
||||
As you may have noticed, the $filters and $options are the ones you pass to `find()`.
|
||||
`$filters` and `$options` are the ones supported by `Mongo.Collection.find()`
|
||||
|
||||
The nature of a Query is to be re-usable. For this we introduce a special type of field called `$filter`.
|
||||
And we allow the query to receive parameters before it executes:
|
||||
### Dynamic $filter()
|
||||
|
||||
The nature of a query is to be re-usable. For this we introduce a special type of field called `$filter`,
|
||||
which allows the query to receive parameters and adapt before it executes:
|
||||
|
||||
```js
|
||||
// We export the query, notice there is no .fetch()
|
||||
|
||||
export default Posts.createQuery({
|
||||
$filter({filters, options, params}) {
|
||||
filters.isApproved = params.isApproved;
|
||||
|
@ -115,16 +118,15 @@ export default Posts.createQuery({
|
|||
});
|
||||
```
|
||||
|
||||
The `$filter()` function receives a single object that contains 3 objects: `filters`, `options`, `params`.
|
||||
The `$filter()` function receives a single object composed of 3 objects: `filters`, `options`, `params`.
|
||||
The `filters` and `options` are initially what you provided in `$filters` and `$options` query, they will be empty
|
||||
if they haven't been specified.
|
||||
objects if they haven't been specified.
|
||||
|
||||
The job of `$filter()` is to extend/modify `filters` and `options`, based on params.
|
||||
|
||||
Lets see how we can use that query:
|
||||
Lets see how we can re-use the query defined above:
|
||||
|
||||
```js
|
||||
// assuming you exported it from '...'
|
||||
import postListQuery from '...';
|
||||
|
||||
Meteor.methods({
|
||||
|
@ -136,11 +138,10 @@ Meteor.methods({
|
|||
})
|
||||
```
|
||||
|
||||
Whenever we want to use a modular query, we have to `clone()` it so it creates a new standalone instance,
|
||||
that does not affect the exported one. The `clone()` accepts `params` as argument.
|
||||
Those `params` will be passed to the `$filter` function.
|
||||
Whenever we want to use a modular query, we have to `clone()` it so it creates a new instance of it.
|
||||
The `clone()` accepts `params` as argument. Those `params` will be passed to the `$filter` function.
|
||||
|
||||
You could also use `setParams()` to configure parameters:
|
||||
You can also use `setParams()` to configure parameters, which extends the current query parameters:
|
||||
|
||||
```js
|
||||
import postListQuery from '...';
|
||||
|
@ -149,6 +150,8 @@ Meteor.methods({
|
|||
posts() {
|
||||
const query = postListQuery.clone();
|
||||
|
||||
// Warning, if you don't use .clone() and you just .setParams(),
|
||||
// those params will remain stored in your query
|
||||
query.setParams({
|
||||
isApproved: true,
|
||||
});
|
||||
|
@ -158,6 +161,8 @@ Meteor.methods({
|
|||
})
|
||||
```
|
||||
|
||||
### Validating Params
|
||||
|
||||
A query can be smart enough to know what parameters it needs, for this we can use the awesome `check` library from Meteor:
|
||||
http://docs.meteor.com/api/check.html
|
||||
|
||||
|
@ -180,7 +185,8 @@ export default Posts.createQuery({
|
|||
});
|
||||
```
|
||||
|
||||
But you can craft your own validation:
|
||||
If you want to craft your own validation, it also accepts a function that takes params:
|
||||
|
||||
```js
|
||||
{
|
||||
validateParams(params) {
|
||||
|
@ -191,9 +197,9 @@ But you can craft your own validation:
|
|||
}
|
||||
```
|
||||
|
||||
Note: params validation is done prior to fetching the query.
|
||||
Note: params validation is done prior to fetching the query, not when you do `setParams()` or `clone()`
|
||||
|
||||
And if you want to set some default parameters:
|
||||
If you want to store some default parameters, you can use the `params` option:
|
||||
```js
|
||||
export default Posts.createQuery({...}, {
|
||||
params: {
|
||||
|
@ -202,19 +208,9 @@ export default Posts.createQuery({...}, {
|
|||
});
|
||||
```
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
This is the end of our introduction. As we can see, we can make queries modular and this already gives us
|
||||
a big benefit. By abstracting them into their own modules we can keep our methods neat and clean,
|
||||
and we haven't even arrived to the good parts.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
a big benefit. By abstracting them into their own modules we can keep our methods neat and clean.
|
||||
|
||||
#### [Continue Reading](linking_collections.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Linker Engine
|
||||
|
||||
The linker engine is composed of 3 components:
|
||||
- Definition of Links
|
||||
- Linked Data Retrieval
|
||||
- Setting Links
|
||||
- **Definition of Links** (the `addLinks()` thingie, which we already covered)
|
||||
- **Linked Data Retrieval** (fetching related linked data)
|
||||
- **Setting Links** (actually updating the database)
|
||||
|
||||
Let's explore how we can play with `Linked Data Retrieval` and `Setting Links`, assumming we already defined our links
|
||||
Let's explore how we can play with `Linked Data Retrieval` and `Setting Links`, after we already defined our links
|
||||
using `addLinks()` method.
|
||||
|
||||
Each collection gets extended with a `getLink` function:
|
||||
|
@ -30,13 +30,12 @@ const userGroupLinker = Meteor.users.getLink(userId, 'groups');
|
|||
const groups = userGroupLinker.find().fetch();
|
||||
// or for ease of use
|
||||
const groups = userGroupLinker.fetch();
|
||||
// or to filter the nested elements
|
||||
// or to filter the linked elements
|
||||
const groups = userGroupLinker.find(filters, options).fetch();
|
||||
// and again simpler
|
||||
const groups = userGroupLinker.fetch(filters, options);
|
||||
|
||||
// and if you want to performantly get a count()
|
||||
// because find() returns a good ol' Mongo.Cursor
|
||||
// and if you want to get a count()
|
||||
const groups = userGroupLinker.find(filters, options).count();
|
||||
```
|
||||
|
||||
|
@ -45,12 +44,10 @@ This works with any kind of links from any side.
|
|||
## Setting Links
|
||||
|
||||
This allows you to very easily link collections to each other, without relying on knowing the fields and how are they stored.
|
||||
It also allows you to set links from any place `direct` and `inversed`, of any type `one` or `many` and `meta` links as well:
|
||||
It also allows you to set links from any place `direct` and `inversed`, of any type `one` or `many` and `meta` links as well,
|
||||
enabling doing this in a natural way:
|
||||
|
||||
|
||||
#### "One" Relationships
|
||||
|
||||
Performing a `set()` will automatically execute the update or insert in the database.
|
||||
### One Links
|
||||
|
||||
```js
|
||||
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||
|
@ -64,7 +61,9 @@ const paymentProfileUserLink = PaymentProfiles.getLink(paymentProfileId, 'user')
|
|||
paymentProfileUserLink.set(userId); // or a user object that contains `_id`
|
||||
```
|
||||
|
||||
You can also `set` objects that aren't in the database yet:
|
||||
|
||||
You can also `set()` objects that aren't in the database yet.
|
||||
Performing a `set()` will automatically execute the update or insert in the database.
|
||||
|
||||
```js
|
||||
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||
|
@ -78,12 +77,12 @@ This will insert into the `PaymentProfiles` collection and link it to user and i
|
|||
|
||||
To remove a link for a `one` relationship (no arguments required):
|
||||
```js
|
||||
// from direct or inversed side
|
||||
userPaymentProfileLink.unset();
|
||||
// or
|
||||
paymentProfileUserLink.unset();
|
||||
```
|
||||
|
||||
#### "Many" Relationships
|
||||
### Many Links
|
||||
|
||||
Same principles as above apply, with some minor changes, this time we use `add` and `remove`
|
||||
|
||||
|
@ -106,19 +105,23 @@ userGroupsLink.add([
|
|||
userGroupsLink.remove(groupIds)
|
||||
```
|
||||
|
||||
The same logic applies, you can add array of objects that contain `_id` or array of objects without `_id`, or a mixture of them.
|
||||
The same logic applies, you can:
|
||||
- Single string *OR* Object with _id *OR* Object without _id
|
||||
- Array of any mixture of the first ^
|
||||
|
||||
Ofcourse, the `remove()` cannot accept objects without `_id` as it makes no sense to do so.
|
||||
The `remove()` cannot accept objects without `_id` as it makes no sense to do so.
|
||||
|
||||
#### "Meta" Relationships
|
||||
### Meta Links
|
||||
|
||||
Now things get very interesting, because `metadata` allows us to store additional information about the link,
|
||||
it lets us **describe** the relationship. And again, this works from `direct` and `inversed` side as well, with the
|
||||
it lets us **describe** the relationship. This works from `direct` and `inversed` side as well, with the
|
||||
same principles described as above.
|
||||
|
||||
The `add()` and `set()` allow an additional parameter `metadata`:
|
||||
|
||||
```js
|
||||
// assumming our link now have {metadata: true} in their definition
|
||||
|
||||
// one
|
||||
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||
|
||||
|
@ -159,12 +162,12 @@ userGroupsLink.metadata([groupId1, groupId2], {
|
|||
|
||||
Updating metadata only works with strings or objects that contain `_id`, and it works from both sides.
|
||||
|
||||
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
By using this Programatic API to set your links instead of relying on updates, it makes your code much simpler to read,
|
||||
much easier to migrate in the future to a new database relational model.
|
||||
and makes schema migration easier in the future.
|
||||
|
||||
#### [Continue Reading](query_options.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
# Linking Collections
|
||||
|
||||
Let's learn what types of links we can do between collections, and what is the best way to do them.
|
||||
Let's learn what type of links we can define between collections, and what is the best way to do them.
|
||||
|
||||
First, we begin with an illustration of the power of Grapher:
|
||||
First, we begin with an illustration of the power of Grapher.
|
||||
|
||||
Let's assume our posts, contain a field, called `authorId` which represents an actual `_id` from `Meteor.users`,
|
||||
if you wanted to get the post and the author's name, you had to first fetch the post,
|
||||
and then get the author's name based on his `_id`.
|
||||
Let's assume our `posts` collection, contains a field, called `authorId` which represents an actual `_id` from `Meteor.users`,
|
||||
if you wanted to get the post and the author's name, you had to first fetch the post, and then get the author's name based on his `_id`.
|
||||
|
||||
Something like:
|
||||
```js
|
||||
Meteor.methods({
|
||||
getPost({postId}) {
|
||||
|
@ -57,13 +55,14 @@ Meteor.methods({
|
|||
|
||||
This is just a simple illustration, imagine the scenario, in which you had comments,
|
||||
and the comments had authors, and you needed their avatar. Your code can easily
|
||||
grow to many lines of code, and it will be much less performant because of the many round-trips you do to
|
||||
the database.
|
||||
grow very large, and it's going to be hard to make it performant.
|
||||
|
||||
## Linking Types
|
||||
## A basic link
|
||||
|
||||
To make the link illustrated in the example above, we create a separate `links.js` file that is
|
||||
imported separately outside the collection module. We'll understand later why.
|
||||
To define the link illustrated in the example above, we create a separate `links.js` file that is
|
||||
imported separately outside the collection module. We need to define the links in their own module,
|
||||
after all collections have been defined, because there will be situations where 2 collections import each other,
|
||||
leading to some strange behaviors.
|
||||
|
||||
```js
|
||||
// file: /imports/db/posts/links.js
|
||||
|
@ -78,13 +77,13 @@ Posts.addLinks({
|
|||
})
|
||||
```
|
||||
|
||||
That was it. You created the link, and now you can use the query illustrated above.
|
||||
We decided to choose `author` as a name for our link and `authorId` the field to store it in, but any string will do.
|
||||
You created the link, and now you can use the query illustrated above.
|
||||
We decided to choose `author` as a name for our link and `authorId` the field to store it in, but it's up to you to decide this.
|
||||
|
||||
### Inversed Links
|
||||
## Inversed links
|
||||
|
||||
Because we linked `Posts` with `Meteor.users` it means that we can also get `posts` if we are in a User.
|
||||
But because the link is stored in `Posts` we need a new type of linking, and we call it `Inversed Link`
|
||||
Because we linked `Posts` with `Meteor.users` it means that we can also get all `posts` of an user.
|
||||
Because in a way `Meteor.users` is also linked with `Posts` but an `inversed` way. We refer to it as an `Inversed Link`.
|
||||
|
||||
```js
|
||||
// file: /imports/db/users/links.js
|
||||
|
@ -98,7 +97,7 @@ Meteor.users.addLinks({
|
|||
})
|
||||
```
|
||||
|
||||
`author` represents the link name that was defined inside Posts. By defining inversed links we can do:
|
||||
`author` represents the link name that was defined inside Posts. Defining inversed links allows us to do:
|
||||
|
||||
```js
|
||||
Meteor.users.createQuery({
|
||||
|
@ -108,19 +107,17 @@ Meteor.users.createQuery({
|
|||
})
|
||||
```
|
||||
|
||||
### One and Many
|
||||
## One and Many
|
||||
|
||||
Above you've noticed a `type: 'one'` in the link definition, but let's say we have a `Post` that belongs to many `Categories`,
|
||||
which have their own collection into the database. This means that we need to relate with more than a single element.
|
||||
|
||||
|
||||
```js
|
||||
// file: /imports/db/posts/links.js
|
||||
import Posts from '...';
|
||||
import Categories from '...';
|
||||
|
||||
Posts.addLinks({
|
||||
'author': { ... },
|
||||
'categories': {
|
||||
type: 'many',
|
||||
collection: Categories,
|
||||
|
@ -129,9 +126,10 @@ Posts.addLinks({
|
|||
})
|
||||
```
|
||||
|
||||
In this case, `categoryIds` is an array of Strings, each String, representing `_id` from `Categories` collection.
|
||||
In this case, `categoryIds` is an array of strings (`[categoryId1, categoryId2, ...]`), each string, representing `_id` from `Categories` collection.
|
||||
|
||||
Let's also create an inversed link from `Categories`, so you can use it inside the `query`
|
||||
|
||||
And, ofcourse, you can also create an inversed link from `Categories`, so you can use it inside the `query`
|
||||
```js
|
||||
// file: /imports/db/posts/links.js
|
||||
import Categories from '...';
|
||||
|
@ -145,10 +143,12 @@ Categories.addLinks({
|
|||
})
|
||||
```
|
||||
|
||||
By defining this, I can query for a category, and get all the posts it has.
|
||||
|
||||
## Meta Links
|
||||
|
||||
We use a `meta` link when we want to add additional data about the relationship. For example,
|
||||
a user can belong in a single `Group`, but we need to know when he joined that group and what roles he has in it.
|
||||
a user can belong in a single `Group`, but we need to also store when he joined that group and what roles he has in it.
|
||||
|
||||
```js
|
||||
// file: /imports/db/users/links.js
|
||||
|
@ -171,7 +171,7 @@ Notice the new option `metadata: true` this means that `groupLink` is no longer
|
|||
{
|
||||
...
|
||||
groupLink: {
|
||||
_id: 'XXX',
|
||||
_id: 'XXX', // This is the _id of the group
|
||||
roles: 'ADMIN',
|
||||
createdAt: Date,
|
||||
}
|
||||
|
@ -204,7 +204,7 @@ const user = Meteor.users.createQuery({
|
|||
}
|
||||
```
|
||||
|
||||
We store the metadata of the link inside a special `$metadata` field. And this works from inversed side as well:
|
||||
We store the metadata of the link inside a special `$metadata` field. And it works from inversed side as well:
|
||||
|
||||
```js
|
||||
Groups.addLinks({
|
||||
|
@ -213,7 +213,9 @@ Groups.addLinks({
|
|||
inversedBy: 'group'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
const group = Groups.createQuery({
|
||||
$filters: {_id: groupId},
|
||||
name: 1,
|
||||
|
@ -236,7 +238,8 @@ const group = Groups.createQuery({
|
|||
},
|
||||
_id: userId,
|
||||
firstName: 'My Funky FirstName',
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
@ -253,24 +256,34 @@ The storage field will look like:
|
|||
}
|
||||
```
|
||||
|
||||
And they work the same as you expect, from the inversed side as well.
|
||||
The same principles above apply, we still store `$metadata` field.
|
||||
|
||||
I know what question comes to your mind right now, what if I want to put a field inside the metadata,
|
||||
I want to store who added that user (`addedBy`) to that link and fetch it, how do we do ?
|
||||
which represents an id from other collection, like I want to store who added that user (`addedBy`) to the group.
|
||||
|
||||
Currently, this is not possible, because it has deep implications with how the Hypernova Module works,
|
||||
but you can achieve this in a very performant way, by abstracting it into a separate collection:
|
||||
No-one stops you from storing it as a string, but if this is one of Grapher's current limitations, it can't fetch
|
||||
links that are inside the metadata.
|
||||
|
||||
But Grapher makes you think relational again, and you can abstract it to another collection:
|
||||
|
||||
```js
|
||||
// file: /imports/db/groupUserLinks/links.js
|
||||
import Groups from '...';
|
||||
import GroupUserLinks from '...';
|
||||
|
||||
// file: /imports/db/users/links.js
|
||||
Meteor.users.addLinks({
|
||||
groupLink: {
|
||||
collection: GroupUserLinks,
|
||||
type: 'one',
|
||||
field: 'groupLinkId',
|
||||
}
|
||||
})
|
||||
|
||||
GroupUserLinks.addLinks({
|
||||
user: {
|
||||
type: 'one',
|
||||
collection: Meteor.users,
|
||||
field: 'userId'
|
||||
inversedBy: 'groupLink',
|
||||
},
|
||||
adder: {
|
||||
type: 'one',
|
||||
|
@ -283,15 +296,6 @@ GroupUserLinks.addLinks({
|
|||
field: 'groupId'
|
||||
}
|
||||
})
|
||||
|
||||
// file: /imports/db/users/links.js
|
||||
Meteor.users.addLinks({
|
||||
groupLink: {
|
||||
collection: GroupUserLinks,
|
||||
type: 'one',
|
||||
field: 'groupLinkId',
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
And the query will look like this:
|
||||
|
@ -313,6 +317,7 @@ Meteor.users.createQuery({
|
|||
## Link Loopback
|
||||
|
||||
No one stops you from linking a collection to itself, say you have a list of friends which are also users:
|
||||
|
||||
```js
|
||||
Meteor.users.addLinks({
|
||||
friends: {
|
||||
|
@ -342,11 +347,14 @@ Meteor.users.createQuery({
|
|||
## Uniqueness
|
||||
|
||||
The `type: 'one'` doesn't necessarily guarantee uniqueness from the inversed side.
|
||||
For example, if we have inside `Comments` a link with `Posts` with `type: 'one'` inside postId,
|
||||
it does not mean that when I fetch the posts with comments, comments will be an array.
|
||||
|
||||
But if you want to have a one to one relationship, and you want grapher to give you an object,
|
||||
instead of an array you can do so:
|
||||
For example, we have `Comments` and `Posts` linked, by defining a `one` link from Comments to Posts,
|
||||
and an inversed link from Posts to Comments.
|
||||
|
||||
When you fetch comments, from posts, the inversed side, they will return an array.
|
||||
|
||||
But if you want to have a `OneToOne` relationship, and you want Grapher to give you a single object in return,
|
||||
you can do:
|
||||
|
||||
```js
|
||||
Meteor.users.addLinks({
|
||||
|
@ -380,14 +388,16 @@ Meteor.users.createQuery({
|
|||
|
||||
## Data Consistency
|
||||
|
||||
This is referring to having consistency amongst links.
|
||||
We clean out leftover links from deleted collection items.
|
||||
|
||||
Let's say I have a `thread` with multiple `members` from `Meteor.users`. If a `user` is deleted from the database, we don't want to keep unexisting references.
|
||||
Let's say I have a `Threads` collection with some `memberIds` linked to `Meteor.users`.
|
||||
|
||||
If a `user` is deleted from the database, we don't want to keep unexisting references.
|
||||
So after we delete a user, all threads containing that users should be cleaned.
|
||||
|
||||
This is done automatically by Grapher so you don't have to deal with it.
|
||||
The only rule is that `Meteor.users` collection needs to have an inversed link to `Threads`.
|
||||
|
||||
The only rule is that `Meteor.users` collection needs to have an inversed link to `Threads`.
|
||||
In conclusion, if you want to benefit of this, you have to define inversed links for every direct links.
|
||||
|
||||
## Autoremoval
|
||||
|
@ -402,10 +412,13 @@ Meteor.users.addLinks({
|
|||
});
|
||||
```
|
||||
|
||||
After you deleted a user, all the links that have `autoremove: true` will be deleted.
|
||||
After you delete a user, all the links that have `autoremove: true` will be deleted.
|
||||
|
||||
This works from the `direct` side as well, not only from `inversed` side.
|
||||
|
||||
Please use with caution, sometimes it's better to explicitly delete it, but there will be situations,
|
||||
where you don't care and this makes your code cleaner.
|
||||
|
||||
## Indexing
|
||||
|
||||
As a rule of thumb, you must index all of your links. Because that's how you achieve absolute performance.
|
||||
|
@ -424,24 +437,25 @@ PaymentProfiles.addLinks({
|
|||
})
|
||||
```
|
||||
|
||||
The index is applied only on the `_id`, meaning that if you have `meta` links, other fields present in that object will not be indexed.
|
||||
The index is applied only on the `_id`, meaning that if you have `meta` links, other fields present in that object will not be indexed,
|
||||
but you can run a `Collection._ensureIndex` separately.
|
||||
|
||||
If you have `unique: true` set, the index will also apply a unique constraint to it.
|
||||
|
||||
## Top Level Fields
|
||||
|
||||
Grapher currently supports only top level fields for storing data. One of the reasons it doesn't allow nested fields
|
||||
is to enforce the developer to think relational and eliminate large and complex documents by abstracting them into collections.
|
||||
Grapher currently supports only top level fields for storing linking data.
|
||||
One of the reasons it doesn't allow nested fields is to enforce the developer to think relational and
|
||||
eliminate large and complex documents by abstracting them into collections.
|
||||
|
||||
In the future, this limitation may change, but for now you can work around this and keep your code elegant.
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
Using these simple techniques, you can create a beautiful database schemas inside MongoDB that are relational and very simple to fetch,
|
||||
you will eliminate almost all your boilerplate code around this and allows you to focus on more important things.
|
||||
|
||||
On top of a cleaner code you benefit from the `Hypernova Module` which minimizes the database requests to a
|
||||
predictable number (no. of collection nodes).
|
||||
#### [Continue Reading](linker_engine.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ Before we explain what they are, we need to understand an alternative way of cre
|
|||
|
||||
## Alternative Creation
|
||||
|
||||
Currently, we only saw how to create a query starting from a collection, like:
|
||||
Currently, we only know how to create a query starting from a collection, like:
|
||||
|
||||
```js
|
||||
Meteor.users.createQuery(body, options);
|
||||
```
|
||||
|
||||
But Grapher also exposes a `createQuery` functionality:
|
||||
But Grapher also exposes a `createQuery` function:
|
||||
|
||||
```js
|
||||
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||
|
@ -22,7 +22,7 @@ createQuery({
|
|||
})
|
||||
```
|
||||
|
||||
The first key inside the object needs to represent an existing collection name:
|
||||
The first key inside the object represents an existing collection name:
|
||||
```js
|
||||
const Posts = new Mongo.Collection('posts');
|
||||
|
||||
|
@ -36,10 +36,10 @@ createQuery({
|
|||
|
||||
## What are Named Queries ?
|
||||
|
||||
As the name implies, they are a query that are identified by a `name` (No shit Sherlock).
|
||||
The difference is that they accept a `string` as the first argument.
|
||||
As the name implies, they are a query that are identified by a `name`.
|
||||
The difference is that they accept a `string` as the first argument, instead of a body.
|
||||
|
||||
The `Named Query` has the same API as a normal `Query`, we'll understand the difference in this documentation.
|
||||
The `Named Query` has the same API and options as a normal `Query`, we'll understand the difference in this documentation.
|
||||
|
||||
```js
|
||||
// file: /imports/api/users/queries/userAdminList.js
|
||||
|
@ -63,13 +63,13 @@ Or you could use `createQuery`:
|
|||
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||
|
||||
const admins = createQuery({
|
||||
userAdminList: {},
|
||||
userAdminList: params, // or {} if no params
|
||||
}).fetch();
|
||||
// will return a clone of the named query if it is already loaded
|
||||
// will return a clone of the named query, with the params specified
|
||||
```
|
||||
|
||||
Now notice the {}. Because `Named Queries` have their form well defined, they don't allow you
|
||||
to specify fields directly, however they allow you to specify parameters when using `createQuery`
|
||||
Because `Named Queries` have their form well defined, they don't allow you to specify fields directly,
|
||||
however they allow you to specify parameters when using `createQuery`
|
||||
|
||||
Because the default `$filter()` allows as params `filters` and `options` for the top collection node,
|
||||
you can do something like:
|
||||
|
@ -82,27 +82,17 @@ const admins = createQuery({
|
|||
}).fetch();
|
||||
```
|
||||
|
||||
## Always go modular
|
||||
## Go Modular, Always.
|
||||
|
||||
Ofcourse the recommended approach is to always use `modular`. Creating queries in files, and importing them accordingly.
|
||||
The reason we expose such functionality is because if you want to use Grapher as an HTTP API, you do not have access
|
||||
to the collections, so this becomes very handy, as you can transform a JSON request into a query.
|
||||
|
||||
Therefore, in such a case, a modular approach will not work.
|
||||
The reason we expose such functionality is because if you want to use [Grapher as an HTTP API](outside_grapher.md),
|
||||
you do not have access to the collections, so this becomes very handy, as you can transform a JSON request into a query.
|
||||
|
||||
In the client-side of Meteor it is recommended to always extract your queries into their own module, and import and clone them for use.
|
||||
|
||||
## But why ?
|
||||
|
||||
Why do we need another string? What is the purpose?
|
||||
|
||||
It's because we are slowly beginning to cross to the client-side domain, which
|
||||
is a dangerous and nasty place and we need to expose our queries in a very cautious and secure manner.
|
||||
|
||||
|
||||
## Special $body
|
||||
|
||||
`Named Queries` allow a special `$body` parameter. Let's see an advanced query:
|
||||
`Named Queries` treat specially the `$body` parameter. Let's see an advanced query:
|
||||
|
||||
```js
|
||||
const fullPostList = Posts.createQuery('fullPostList', {
|
||||
|
@ -125,7 +115,7 @@ const fullPostList = Posts.createQuery('fullPostList', {
|
|||
There will be situations where you initially want to show less data for this, maybe just the `title`, but if the user,
|
||||
clicks on that `title` let's say we want it to expand and then we need to fetch additional data.
|
||||
|
||||
The way to do it is to use `$body` which intersects with the allowed data graph:
|
||||
Instead of creating an additional `Named Query` with the same firewalls and such, you can use `$body` which intersects with the allowed data graph:
|
||||
```js
|
||||
fullPostsList.clone({
|
||||
$body: {
|
||||
|
@ -147,9 +137,7 @@ This will only fetch the intersection, the transformed body will look like:
|
|||
}
|
||||
```
|
||||
|
||||
When we learn about exposure you will fully understand why the `$body` special parameter is useful.
|
||||
|
||||
Be careful, if you use validation for params (and you should) to also add `$body` inside it:
|
||||
Be careful, if you use validation for params (and you should) to also add `$body` inside it.
|
||||
|
||||
```js
|
||||
import {Match} from 'meteor/check';
|
||||
|
@ -162,15 +150,17 @@ const fullPostList = Posts.createQuery('fullPostList', {}, {
|
|||
|
||||
## Exposure
|
||||
|
||||
We are now crossing the bridge to client-side.
|
||||
We are now crossing the bridge to client-side. It's a nasty place, filled with hacker, haxors, crackers,
|
||||
we cannot trust them, we need to make the code unbreakable.
|
||||
|
||||
We want our application users to be able of using these queries we define stand-alone. We initially said that Grapher
|
||||
will become the `Data Fetching Layer` inside Meteor. Therefore, we will no longer rely on methods to give us the data from
|
||||
our queries, even if it's still possible and up to you to decide, but this gives you many advantages, such as caching and performance monitoring.
|
||||
We initially said that Grapher will become the `Data Fetching Layer` inside Meteor.
|
||||
Therefore, we will no longer rely on methods to give us the data from our queries, even if it's still possible
|
||||
and up to you to decide, but this gives you many advantages, such as [Caching Results](caching_results.md) and performance monitoring
|
||||
though Kadira/Meteor APM (because every named query exposed is a method and/or a publication)
|
||||
|
||||
As you may have guessed, the exposure needs to happen on the server, but the query can be imported on the client.
|
||||
|
||||
Let's say we want only Admins to be able to query our `userAdminList` query:
|
||||
Let's say we want only users that are admins to be able to query our `userAdminList` query:
|
||||
|
||||
```js
|
||||
// file: /imports/api/users/queries/userAdminList.js
|
||||
|
@ -182,21 +172,9 @@ export default Meteor.users.createQuery('userAdminList', {
|
|||
})
|
||||
```
|
||||
|
||||
Alternatively, you could have done:
|
||||
```js
|
||||
// file: /imports/api/users/queries/userAdminList.js
|
||||
export default createQuery('userAdminList', {
|
||||
users: {
|
||||
$filters: {
|
||||
roles: {$in: 'ADMIN'}
|
||||
},
|
||||
name: 1,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```js
|
||||
// server-side (make sure it's imported from somewhere on the server only)
|
||||
// server-side
|
||||
// make sure it's imported from somewhere on the server only
|
||||
// file: /imports/api/users/queries/userAdminList.expose.js
|
||||
import userAdminListQuery from './userAdminList';
|
||||
|
||||
|
@ -208,6 +186,9 @@ userAdminListQuery.expose({
|
|||
|
||||
// in the firewall you also have the ability to modify the parameters
|
||||
// that are going to hit the $filter() function in the query
|
||||
|
||||
// the firewall runs in the Meteor.methods or Meteor.publish context
|
||||
// Meaning you can have access to this.userId and others.
|
||||
}
|
||||
})
|
||||
```
|
||||
|
@ -219,9 +200,8 @@ Let's use it on the client side:
|
|||
// client side
|
||||
import userAdminListQuery from '/imports/api/users/queries/userAdminList.js';
|
||||
|
||||
userAdminListQuery.clone().fetch((err, res) => {
|
||||
// do something with err, res
|
||||
// it will be an error if the current user is not an admin
|
||||
userAdminListQuery.clone().fetch((err, users) => {
|
||||
// it will be an error if the current user is not an admin, thrown by the firewall
|
||||
});
|
||||
```
|
||||
|
||||
|
@ -253,7 +233,8 @@ userAdminListQuery.clone().fetchOne((err, user) => {
|
|||
// do something
|
||||
});
|
||||
```
|
||||
You can fetch static queries using promises:
|
||||
|
||||
You can use `fetchSync()` or `fetchOneSync()` to work with promises.
|
||||
|
||||
```js
|
||||
const users = await userAdminListQuery.clone().fetchSync();
|
||||
|
@ -269,6 +250,7 @@ query.getCount((err, count) => {
|
|||
// do something
|
||||
});
|
||||
|
||||
// promises
|
||||
const count = await query.getCountSync();
|
||||
|
||||
// reactive counts
|
||||
|
@ -282,25 +264,34 @@ Tracker.autorun(() => {
|
|||
});
|
||||
```
|
||||
|
||||
We do not subscribe to counts by default because they may be too expensive, but if you need them,
|
||||
feel free to use them.
|
||||
We do not subscribe to counts by default because they may be too expensive, especially for large collections,
|
||||
but if you need them, feel free to use them, we worked hard at making them as performant as possible.
|
||||
|
||||
## Behind the scenes
|
||||
|
||||
When we are dealing with a static query (non-reactive), we make a call to the method that has been created when
|
||||
we did `query.expose()`, the firewall gets applied and returns the result from the query.
|
||||
we did `query.expose()`, params get checked, the firewall gets applied and returns the result from the query.
|
||||
|
||||
If we have a reactive query, `query.expose()` creates a publication end-point, and inside it, uses the `reywood:publish-composite` package
|
||||
to automatically create the proper publication.
|
||||
|
||||
The method and publication names we create are: `named_query_${queryName}`, and as argument they accept the `params` object.
|
||||
|
||||
Meaning you can do:
|
||||
- `Meteor.call('named_query_userAdminList', params, callback)`
|
||||
- `Meteor.subscribe('named_query_userAdminList', params)`
|
||||
|
||||
But don't. Grapher takes care of this.
|
||||
|
||||
When we do `query.fetch()` on a reactive query, it gives you a complete data graph with the items in the same form as you
|
||||
would have received from a static query. When an element inside your data graph changes, the `fetch()` function is re-run
|
||||
if you are inside a `Tracker.autorun()` or a reactive context, because in the back, we do `find().fetch()` on client-side collections.
|
||||
if you are inside a `Tracker.autorun()` or a reactive context, because in the back, it does `find().fetch()` on client-side collections,
|
||||
and afterwards it assembles your data.
|
||||
|
||||
## Warning
|
||||
|
||||
If you want your client-side **reactive** queries to work as expected you need to make sure that the `addLinks` and `addReducers` are loaded on the client as well.
|
||||
Because the data assembly is now done on the client.
|
||||
If you want your client-side **reactive** queries to work as expected you need to make sure that the
|
||||
`addLinks` and `addReducers` are loaded on the client as well, otherwise Grapher won't know how to assemble your data.
|
||||
|
||||
## Expose Options
|
||||
|
||||
|
@ -310,17 +301,22 @@ query.expose({
|
|||
firewall(userId, params) {
|
||||
// you can modify the parameters here
|
||||
},
|
||||
|
||||
// Allow the query to be fetched statically
|
||||
method: true, // default
|
||||
|
||||
// Allow the query to be fetched reactively (for heavy queries, you may want to set it to false)
|
||||
publication: true, // default
|
||||
|
||||
// Unblocks your method (and if you have .unblock() in publication context it also unblocks it)
|
||||
unblock: true, // default
|
||||
|
||||
// This can be an object or a function(params) that you don't want to expose it on the client via
|
||||
// The query options as it may hold some secret business data
|
||||
// If you don't specify it the default validateParams from the query applies, but not both!
|
||||
// If you allow subbody requests, don't forget to add {$body: Match.Maybe(Boolean)}
|
||||
validateParams: {},
|
||||
|
||||
// This deep extends your graph's body before processing it.
|
||||
// For example, you want a hidden $filter() functionality, or anything else.
|
||||
embody: {
|
||||
|
@ -352,13 +348,15 @@ const getAnalytics = createQuery('getAnalytics', () => {}, {
|
|||
});
|
||||
```
|
||||
|
||||
Not the dummy `() => {}`. That's how we tell Grapher to make a resolver query.
|
||||
|
||||
```js
|
||||
// server code only
|
||||
import getAnalyticsQuery from './getAnalytics';
|
||||
|
||||
getAnalyticsQuery.expose({
|
||||
firewall(userId, params) {
|
||||
// ...
|
||||
params.userId = userId;
|
||||
},
|
||||
validateParams: {} // Object or Function that you don't want exposed
|
||||
});
|
||||
|
@ -370,7 +368,9 @@ getAnalyticsQuery.resolve(function(params) {
|
|||
```
|
||||
|
||||
The advantage of doing things like this, instead of relying on a method, is that you are now
|
||||
using a uniform layer for fetching results, and on top of that, you can easily cache them.
|
||||
using a uniform layer for fetching data, and on top of that, you can [easily cache them](caching_results.md).
|
||||
|
||||
If you try to `subscribe()` to a resolver query on the client it will throw an exception.
|
||||
|
||||
## Mix'em up
|
||||
|
||||
|
@ -391,8 +391,10 @@ query.expose({
|
|||
})
|
||||
```
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
We can now safely expose our queries to the client, and the client can use it in a simple and uniform way.
|
||||
By this stage we already understand how powerful Grapher really is, but it still has some tricks.
|
||||
By this stage we already understand how powerful Grapher really is, but it still has some tricks up it's sleeve.
|
||||
|
||||
#### [Continue Reading](hypernova.md) or [Back to Table of Contents](table_of_contents.md)
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
## Grapher as an API
|
||||
|
||||
If you like Grapher, you can use it in any other language/medium.
|
||||
If you like Grapher, or if `like` is an understatement, you can use it in any other language/medium.
|
||||
|
||||
You can expose it as an HTTP API, or a DDP API (as a Meteor method) for example, because
|
||||
in React Native you have ways to connect to Meteor with:
|
||||
|
||||
You can expose it as an HTTP API, or a DDP API (as a Meteor.method()) for example,
|
||||
because in ReactNative you have ways to connect to Meteor with:
|
||||
https://www.npmjs.com/package/react-native-meteor#meteor-collections
|
||||
|
||||
Basically what Grapher needs to properly execute is the query,
|
||||
and if you have firewalls, you need to manually handle authorization yourself.
|
||||
Basically what Grapher needs to properly execute is the query, and you can make use of the firewalls easily,
|
||||
but you need to manually handle authorization yourself.
|
||||
|
||||
### Exposing an HTTP API
|
||||
|
||||
|
@ -33,8 +34,7 @@ grapherRoutes.route('/grapher', function (req, res) {
|
|||
const data = EJSON.parse(body);
|
||||
|
||||
// lets say this is a named query that looks like
|
||||
// {getUserList: params}
|
||||
const {query} = data;
|
||||
const {query} = data; // query = {userList: params}
|
||||
|
||||
// authorize the user somehow
|
||||
// it's up to you to extract an userId
|
||||
|
@ -43,6 +43,8 @@ grapherRoutes.route('/grapher', function (req, res) {
|
|||
const actualQuery = createQuery(query);
|
||||
|
||||
// if it's not a named query and the collection is not exposed, don't allow it.
|
||||
// if you don't put this snippet of code, people will be able to do { users: { services: 1 } } types of queries.
|
||||
// this is related to global queries.
|
||||
if (actualQuery.isGlobalQuery && !actualQuery.collection.__isExposedForGrapher) {
|
||||
throw new Meteor.Error('not-allowed');
|
||||
}
|
||||
|
@ -68,15 +70,25 @@ grapherRoutes.route('/grapher', function (req, res) {
|
|||
})
|
||||
```
|
||||
|
||||
Now you can do HTTP requests of `Content-Type: application/ejson` to http://meteor-server/grapher and retrieve data.
|
||||
Now you can do HTTP requests of `Content-Type: application/ejson` to http://meteor-server/grapher and retrieve data,
|
||||
in EJSON format. If `EJSON` is not available in your language, you can parse it with `JSON` as well.
|
||||
|
||||
If you want to use Meteor's Methods as an HTTP API to also handle method calls, take a look here:
|
||||
- https://github.com/cult-of-coders/fusion
|
||||
If you are in the JavaScript world: https://www.npmjs.com/package/ejson
|
||||
|
||||
If you want to use Meteor's Methods as an HTTP API to also handle method calls, take a look here: https://github.com/cult-of-coders/fusion
|
||||
|
||||
And more closely: https://github.com/cult-of-coders/fusion/blob/7ec5cd50c3a471c0bdd65c9fa482124c149dc243/fusion/server/route.js
|
||||
|
||||
### Exposing a DDP Method
|
||||
|
||||
If you are connected to Meteor by DDP, using any DDP Client:
|
||||
|
||||
- React Native: https://www.npmjs.com/package/react-native-meteor
|
||||
- JS: https://www.npmjs.com/package/ddp-client
|
||||
- PHP: https://github.com/zyzo/meteor-ddp-php
|
||||
- Python: https://github.com/hharnisc/python-ddp
|
||||
- Ruby: https://github.com/tmeasday/ruby-ddp-client
|
||||
|
||||
```js
|
||||
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||
|
||||
|
@ -95,6 +107,8 @@ Meteor.methods({
|
|||
})
|
||||
```
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
Nothing stops you from using Grapher outside Meteor!
|
||||
|
||||
#### [Continue Reading](api.md) or [Back to Table of Contents](table_of_contents.md)
|
|
@ -31,7 +31,7 @@ Now `user` will look like:
|
|||
}
|
||||
```
|
||||
|
||||
If you wanted to fetch the full `profile`, simply use `profile: 1` inside your query body. Alternatively,
|
||||
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
|
||||
|
@ -41,13 +41,6 @@ Lets say we have a `Post` with `comments`:
|
|||
```js
|
||||
import {Comments, Posts} from '/imports/db';
|
||||
|
||||
Posts.addLinks({
|
||||
comments: {
|
||||
collection: Comments,
|
||||
inversedBy: 'postId',
|
||||
}
|
||||
})
|
||||
|
||||
Comments.addLinks({
|
||||
post: {
|
||||
type: 'one',
|
||||
|
@ -55,9 +48,16 @@ Comments.addLinks({
|
|||
collection: Posts,
|
||||
},
|
||||
});
|
||||
|
||||
Posts.addLinks({
|
||||
comments: {
|
||||
collection: Comments,
|
||||
inversedBy: 'postId',
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
If any bit of the code written above creates confusion, try reading again the `Linking Collections` part of the documentation.
|
||||
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:
|
||||
|
@ -74,9 +74,9 @@ Posts.createQuery({
|
|||
})
|
||||
```
|
||||
|
||||
The query above will fetch as `comments` only the ones that have been approved.
|
||||
The query above will fetch as `comments` only the ones that have been approved and that are linekd with the `post`.
|
||||
|
||||
The `$filter` function share the same `params` across all collection nodes:
|
||||
The `$filter` function shares the same `params` across all collection nodes:
|
||||
|
||||
```js
|
||||
export default Posts.createQuery({
|
||||
|
@ -145,7 +145,8 @@ Note the default $filter() only applies to the top collection node, otherwise we
|
|||
|
||||
### Pagination
|
||||
|
||||
There is a special field that extends the pre-fetch filtering process, and it's called `$paginate`:
|
||||
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({
|
||||
|
@ -166,13 +167,14 @@ const posts = postsQuery.clone({
|
|||
}).fetch()
|
||||
```
|
||||
|
||||
This is mostly used for your convenience, as pagination is a common used technique and makes your code easier to read.
|
||||
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.
|
||||
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:
|
||||
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';
|
||||
|
@ -194,7 +196,7 @@ Groups.addLinks({
|
|||
})
|
||||
```
|
||||
|
||||
Let's assume the groupLinks looks like this:
|
||||
Let's assume the `groupLinks` looks like this:
|
||||
```js
|
||||
[
|
||||
{
|
||||
|
@ -218,7 +220,8 @@ const users = Meteor.users.createQuery({
|
|||
}).fetch()
|
||||
```
|
||||
|
||||
But what if you want to fetch the groups and all their admins ? Easy.
|
||||
But what if you want to fetch the groups and all their admins? It's the same.
|
||||
|
||||
```js
|
||||
const groups = Groups.createQuery({
|
||||
name: 1,
|
||||
|
@ -236,7 +239,9 @@ We have gone through great efforts to support such functionality, but it makes o
|
|||
|
||||
## Post Filtering
|
||||
|
||||
This concept allows us to filter/manipulate data after we received it.
|
||||
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:
|
||||
|
||||
|
@ -252,15 +257,13 @@ const users = Meteor.users.createQuery({
|
|||
}).fetch()
|
||||
```
|
||||
|
||||
If you had a `many` relationship without `metadata` your $postFilters would look like:
|
||||
If you had a `many` relationship without metadata your `$postFilters` would look like:
|
||||
```
|
||||
{
|
||||
'groups.roles': {$in: 'ADMIN'},
|
||||
}
|
||||
```
|
||||
|
||||
The `$postFilters` option uses the `sift` npm library (https://www.npmjs.com/package/sift) to make your filters look like mongo filters.
|
||||
|
||||
In addition to `$postFilters` we've also got `$postOptions` that allows:
|
||||
- limit
|
||||
- sort
|
||||
|
@ -302,10 +305,10 @@ const users = Meteor.users.createQuery({
|
|||
}).fetch()
|
||||
```
|
||||
|
||||
Note the fact that they only work for top level nodes, not for child collection nodes unlike `$filters` and `$options`.
|
||||
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 `Denormalize` section of the documentation.
|
||||
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.
|
||||
|
||||
|
@ -317,11 +320,12 @@ If you want just to return the number of top level documents a query has:
|
|||
query.getCount()
|
||||
```
|
||||
|
||||
This will be very useful for pagination when we reach the client-side domain.
|
||||
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
|
||||
|
||||
The options `$filter` and `$postFilter` also allow you to provide an array of functions:
|
||||
Both functions `$filter` and `$postFilter` also allow you to provide an array of functions:
|
||||
|
||||
```js
|
||||
function userContext({filters, params}) {
|
||||
|
@ -336,11 +340,10 @@ const posts = Posts.createQuery({
|
|||
$filter: [userContext, ({filters, options, params}) => {
|
||||
// do something
|
||||
}],
|
||||
$postFilter: [someOtherFunction],
|
||||
}).fetch()
|
||||
```
|
||||
|
||||
The example above is just to illustrate the possibility, in order to ensure that a `userId` param is sent you will use `validateParams`
|
||||
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({
|
||||
|
@ -365,11 +368,12 @@ const query = postLists.clone({
|
|||
|
||||
When we cross to the client-side domain we need to be very wary of these type of injections.
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [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](table_of_contents.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
The reducers are a sort of "smart fields" which allow you to compose different results
|
||||
from your query.
|
||||
|
||||
To achieve this we extend `Mongo.Collection` with an `addReducer()` method:
|
||||
To achieve this we use inside `Mongo.Collection` the `addReducers()` method:
|
||||
```js
|
||||
Collection.addReducer({
|
||||
reducerName: {
|
||||
|
@ -50,7 +50,7 @@ Results to:
|
|||
}
|
||||
```
|
||||
|
||||
## Reducers and links
|
||||
## Reducers that use links
|
||||
|
||||
Easily grab the data from your links (as deep as you want them), if you want to reduce it.
|
||||
|
||||
|
@ -61,7 +61,7 @@ Meteor.users.addReducers({
|
|||
// assuming you have a link called groups
|
||||
groups: { name: 1 }
|
||||
},
|
||||
reduce(object) { // a pure function that returns the data
|
||||
reduce(object) {
|
||||
return object.groups.map(group => group.name).join(',')
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ Result:
|
|||
|
||||
Notice that group `name` is not there. This is because we clean leftovers so the result is predictable.
|
||||
|
||||
## Reducers and reducers
|
||||
## Reducers can be composed
|
||||
|
||||
You can also use other reducers inside your reducers.
|
||||
|
||||
|
@ -118,13 +118,13 @@ You can also use other reducers inside your reducers.
|
|||
// setting up
|
||||
Users.addReducers({
|
||||
fullName: {...}
|
||||
fullNameWithRoles: { // the name of how you want to request it
|
||||
body: { // the dependency, what info it needs to be able to reduce
|
||||
fullNameWithRoles: {
|
||||
body: {
|
||||
fullName: 1,
|
||||
roles: 1
|
||||
},
|
||||
reduce(object) { // a pure function that returns the data
|
||||
return object.fullName + object.roles.join(',');
|
||||
reduce(object) {
|
||||
return object.fullName + ' ' + object.roles.join(',');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -148,9 +148,9 @@ Collection.addReducers({
|
|||
|
||||
Be aware that this reducer may be used from any queries with different types of parameters.
|
||||
|
||||
## Reducers can do anything!
|
||||
## Reducers can be impure
|
||||
|
||||
If we want to just receive the number of posts a user has, we can use reducers for this:
|
||||
If we want to just receive the number of posts a user posted, we can use reducers for this:
|
||||
|
||||
```
|
||||
Meteor.users.addReducers({
|
||||
|
@ -176,11 +176,11 @@ Projects.addReducers({
|
|||
repository: 1,
|
||||
},
|
||||
reduce(collectionItem) {
|
||||
const {repository} = collectionItem;
|
||||
const call = Meteor.wrapAsync(API.doSomething, API);
|
||||
// you can use anything that is in sync
|
||||
// don't return the result inside a callback because it won't work.
|
||||
call();
|
||||
const {repository} = collectionItem;
|
||||
const call = Meteor.wrapAsync(API.doSomething, API);
|
||||
return call();
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -190,7 +190,9 @@ Projects.addReducers({
|
|||
|
||||
If you want to filter reducers you can use `$postFilters` or `$postFilter` special functions.
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
## [Conclusion]
|
||||
|
||||
Reducers are a neat way to remove boilerplate from your code, especially for our infamous `emails[0].address`,
|
||||
inside `Meteor.users` collection, check if you can figure out how to reduce it!
|
||||
|
||||
#### [Continue Reading](named_queries.md) or [Back to Table of Contents](table_of_contents.md)
|
|
@ -5,17 +5,18 @@ anything.
|
|||
|
||||
We suggest you read: http://www.meteor-tuts.com/chapters/3/persistence-layer.html first.
|
||||
|
||||
1. Store queries inside `/imports/api` under their own module and proper path.
|
||||
2. Store `links` inside `/imports/db` along their collections definitions.
|
||||
3. Create a an `/imports/db/index.js` that imports and exports all your collections
|
||||
4. In `/imports/db/links.js` import all links from collections (`/imports/db/posts/links.js`)
|
||||
5. In that `/imports/db/index.js` also `imports './links'` after you imported all collections.
|
||||
6. Make sure you import `/imports/db/index.js` in both client and server environments.
|
||||
7. For Named Queries, keep `query.js` and `query.expose.js` separated.
|
||||
8. Create an `/imports/api/exposures.js` that imports all `.expose.js` files, and import that server-side.
|
||||
8. When you import your queries suffix their with `Query`
|
||||
9. Always `.clone()` queries before you use them client and server-side
|
||||
10. Store reducers inside `links.js`, if the file becomes too large (> 100 lines), separate them.
|
||||
- Store queries inside `/imports/api` under their own module and proper path (eg: `/imports/api/users/queries/getAllUsers.js`)
|
||||
- Store `links.js` files inside `/imports/db` close to their collections definitions. (eg: `/imports/db/users/links.js`)
|
||||
- Create a an `/imports/db/index.js` that imports and exports all your collections
|
||||
- In `/imports/db/links.js` which imports all links from collections (eg: `import ./posts/links.js`)
|
||||
- In that `/imports/db/index.js` also `imports './links'` after you imported all collections.
|
||||
- Make sure you import `/imports/db/index.js` in both client and server environments.
|
||||
- For Named Queries, keep `query.js` and `query.expose.js` separated.
|
||||
- Create an `/imports/api/exposures.js` that imports all `.expose.js` files, and import that server-side.
|
||||
- When you import your queries suffix their with `Query`
|
||||
- Always `.clone()` modular queries before you use them client and server-side
|
||||
- Store reducers inside `links.js`, if the file becomes too large (> 100 lines), separate them.
|
||||
- Store server-side reducers inside `/imports/api` - as they may contain business logic
|
||||
|
||||
If you respect the patterns above you will avoid having the most common pitfalls with Grapher:
|
||||
|
||||
|
@ -23,8 +24,7 @@ If you respect the patterns above you will avoid having the most common pitfalls
|
|||
Make sure they are imported in the environment you use them client/server.
|
||||
|
||||
**My link is not saved in the database**
|
||||
Make sure you added it correctly to your SimpleSchema
|
||||
|
||||
Make sure you added it correctly to your SimpleSchema, if you have that.
|
||||
|
||||
## Fragments
|
||||
|
||||
|
@ -83,9 +83,11 @@ Invoices.createQuery({
|
|||
|
||||
Compose uses a deep extension, so it works how you expected to work, especially if some fragments have shared bodies.
|
||||
|
||||
Do not use special properties inside Fragments, such as `$filters`, `$options`, etc.
|
||||
|
||||
## Scaling Reactivity
|
||||
|
||||
If you want to have highly scalable reactive queries, think about moving from tailing MongoDB oplog to RedisOplog:
|
||||
If you want to have highly scalable reactive data graphs, think about moving from tailing MongoDB oplog to RedisOplog:
|
||||
https://github.com/cult-of-coders/redis-oplog
|
||||
|
||||
Grapher is fully compatible with it. You can configure $options, inside the $filter() on to allow namespaced watchers.
|
||||
|
@ -102,7 +104,8 @@ export default Messages.createQuery('messagesForThread', {
|
|||
})
|
||||
```
|
||||
|
||||
## [Conclusion]
|
||||
|
||||
## [Conclusion](table_of_contents.md)
|
||||
Using some simple techniques we can make our code much easier to read, and we can make use of a scalable data graph using `redis-oplog`
|
||||
|
||||
This ends our journey through Grapher. We hope you enjoyed, and that you are going to use it.
|
||||
#### [Continue Reading](outside_meteor.md) or [Back to Table of Contents](table_of_contents.md)
|
|
@ -1,53 +0,0 @@
|
|||
# Table of Contents
|
||||
|
||||
### [Introduction](introduction.md)
|
||||
|
||||
You are new to Grapher and you want to learn the basics of what it can offer.
|
||||
|
||||
### [Linking Collections](linking_collections.md)
|
||||
|
||||
Learn how to link collections to each other.
|
||||
|
||||
### [Linker Engine](linker_engine.md)
|
||||
|
||||
Find out what powers Grapher behind the scenes
|
||||
|
||||
### [Query Options](query_options.md)
|
||||
|
||||
Learn more advanced ways to use your queries.
|
||||
|
||||
### [Reducers](reducers.md)
|
||||
|
||||
Remove complexity from your queries, make them a breeze.
|
||||
|
||||
### [Named Queries](named_queries.md)
|
||||
|
||||
Learn how to define and expose queries to the client.
|
||||
|
||||
### [Hypernova](hypernova.md)
|
||||
|
||||
Read about another tool that powers Grapher behind the scenes.
|
||||
|
||||
### [Denormalization](denormalization.md)
|
||||
|
||||
Learn how to denormalize your data to enable even more performance, and super advanced searching.
|
||||
|
||||
### [Caching Results](caching_results.md)
|
||||
|
||||
Leverage the power of Grapher to cache the most heavy or frequently used queries.
|
||||
|
||||
### [Global Exposure](global_exposure.md)
|
||||
|
||||
Learn about a neat way to expose your collections, careful, it may also be dangerous.
|
||||
|
||||
### [Structure & Patterns](structure_and_patterns.md)
|
||||
|
||||
Learn about some good ways to structure your code some and about common pitfalls.
|
||||
|
||||
### [Outside Grapher](outside_grapher.md)
|
||||
|
||||
Use Grapher outside Meteor with ease.
|
||||
|
||||
### [API](api.md)
|
||||
|
||||
Grapher Cheatsheet.
|
Loading…
Add table
Reference in a new issue