mirror of
https://github.com/vale981/grapher
synced 2025-03-05 09:31:42 -05:00
Added documentation
This commit is contained in:
parent
324e2b78fd
commit
35f297c395
19 changed files with 3048 additions and 59 deletions
|
@ -2,6 +2,8 @@
|
||||||
- Added link caching
|
- Added link caching
|
||||||
- Added named query results caching
|
- Added named query results caching
|
||||||
- Added subbody to NamedQuery
|
- Added subbody to NamedQuery
|
||||||
|
- Added named query first resolver
|
||||||
|
- Bug fixes and other small stuff
|
||||||
|
|
||||||
## 1.2.5
|
## 1.2.5
|
||||||
- Support for promises via .fetchSync and .fetchOneSync for client-side queries
|
- Support for promises via .fetchSync and .fetchOneSync for client-side queries
|
||||||
|
|
|
@ -38,3 +38,8 @@ createNamedQuery('xxx', {});
|
||||||
// working
|
// working
|
||||||
createQuery('xxx', {});
|
createQuery('xxx', {});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you used `$postFilter` in your queries, rename it to `$postFilters`
|
||||||
|
|
||||||
|
If you used resolver links, migrate to reducers.
|
||||||
|
|
||||||
|
|
145
README.md
145
README.md
|
@ -1,76 +1,105 @@
|
||||||
Welcome to Grapher
|
# Grapher 1.3
|
||||||
==================
|
|
||||||
|
|
||||||
[](https://travis-ci.org/cult-of-coders/grapher)
|
[](https://travis-ci.org/cult-of-coders/grapher)
|
||||||
|
|
||||||
Documentation
|
*Grapher* is a data retrieval layer inside Meteor and MongoDB.
|
||||||
-------------
|
|
||||||
[http://grapher.cultofcoders.com](http://grapher.cultofcoders.com/)
|
|
||||||
|
|
||||||
Long Term Support
|
Main features:
|
||||||
-----------------
|
- Innovative way to make MongoDB relational
|
||||||
Version 1.2 will be supported until 2020.
|
- Reactive data graphs for high availability
|
||||||
|
- Incredible performance
|
||||||
|
- Denormalization Modules
|
||||||
|
- Connection to external data sources
|
||||||
|
- Usable from anywhere
|
||||||
|
|
||||||
What ?
|
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.
|
||||||
*Grapher* is a high performance data fetcher and collection relationship manager for Meteor and MongoDB:
|
|
||||||
|
|
||||||
1. Makes data MongoDB denormalization easy (storing and linking data in different collections)
|
## Installation
|
||||||
2. You can link your MongoDB data with any type of database, and fetch it via Queries
|
|
||||||
3. You have the same API for data-fetching whether you want your data to be reactive or not.
|
|
||||||
4. It is compatible with simpl-schema and the older version of it.
|
|
||||||
|
|
||||||
Sample
|
|
||||||
-------------
|
|
||||||
|
|
||||||
To give you an idea how this works, you can fetch the data like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
users: {
|
|
||||||
profile: 1,
|
|
||||||
githubTickets: {},
|
|
||||||
posts: {
|
|
||||||
title: 1,
|
|
||||||
comments: {
|
|
||||||
text: 1,
|
|
||||||
date: 1,
|
|
||||||
author: {
|
|
||||||
profile: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Updates
|
|
||||||
-------
|
|
||||||
Check-out the [CHANGELOG](CHANGELOG.md) for latest updates.
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
```
|
```
|
||||||
meteor add cultofcoders:grapher
|
meteor add cultofcoders:grapher
|
||||||
```
|
```
|
||||||
|
|
||||||
Useful packages and integrations
|
## [Documentation](docs/table_of_contents.md)
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
#### Integration with React (cultofcoders:grapher-react)
|
## [API](docs/api.md)
|
||||||
|
|
||||||
Provides you with an easy to use "createQueryContainer" function.
|
## Quick Illustration
|
||||||
|
|
||||||
- [Atmosphere](https://atmospherejs.com/cultofcoders/grapher-react)
|
<table>
|
||||||
- [GitHub](https://github.com/cult-of-coders/grapher-react/)
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
<pre>
|
||||||
|
import {createQuery} from 'meteor/cultofcoders-grapher';
|
||||||
|
|
||||||
#### Live View (cultofcoders:grapher-live)
|
createQuery({
|
||||||
|
posts: {
|
||||||
|
title: 1,
|
||||||
|
author: {
|
||||||
|
fullName: 1
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
text: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
author: {
|
||||||
|
fullName: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
name: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fetch();
|
||||||
|
</pre>
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
<pre>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
_id: 'postId',
|
||||||
|
title: 'Introducing Grapher',
|
||||||
|
author: {
|
||||||
|
_id: 'authorId',
|
||||||
|
fullName: 'John Smith
|
||||||
|
},
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
_id: 'commentId',
|
||||||
|
text: 'Nice article!,
|
||||||
|
createdAt: Date,
|
||||||
|
author: {
|
||||||
|
fullName: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
categories: [ {_id: 'categoryId', name: 'JavaScript'} ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Useful packages and integrations
|
||||||
|
|
||||||
|
### Live View (cultofcoders:grapher-live)
|
||||||
|
|
||||||
Provides a playground for grapher and provides documentation of your data
|
Provides a playground for grapher and provides documentation of your data
|
||||||
|
|
||||||
- [Atmosphere](https://atmospherejs.com/cultofcoders/grapher-live)
|
https://github.com/cult-of-coders/grapher-live
|
||||||
- [GitHub](https://github.com/cult-of-coders/grapher-live)
|
|
||||||
|
|
||||||
Boiler plate Meteor + React + Grapher
|
### Integration with UI Frameworks (cultofcoders:grapher-react)
|
||||||
-------------------------------------
|
|
||||||
https://github.com/cult-of-coders/grapher-boilerplate
|
#### React
|
||||||
|
https://github.com/cult-of-coders/grapher-react
|
||||||
|
|
||||||
|
#### Vue JS
|
||||||
|
https://github.com/Herteby/grapher-vue
|
||||||
|
|
||||||
|
https://github.com/cult-of-coders/grapher-react
|
||||||
|
|
||||||
|
|
||||||
|
## Premium Support
|
||||||
|
|
||||||
|
If you are looking to integrate Grapher in your apps and want online or on-site consulting and training,
|
||||||
|
shoot us an e-mail contact@cultofcoders.com, we will be more than happy to aid you.
|
||||||
|
|
164
docs/api.md
Normal file
164
docs/api.md
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
# API
|
||||||
|
|
||||||
|
Use this as a cheatsheet after you have read the full documentation.
|
||||||
|
|
||||||
|
### Adding Links
|
||||||
|
|
||||||
|
```js
|
||||||
|
Collection.addLinks({
|
||||||
|
linkName: {
|
||||||
|
collection, // Mongo.Collection
|
||||||
|
type, // 'one' or 'many'
|
||||||
|
metadata, // Boolean
|
||||||
|
field, // String
|
||||||
|
denormalize: {
|
||||||
|
field, // String
|
||||||
|
body, // Body from related collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Collection.addLinks({
|
||||||
|
linkName: {
|
||||||
|
collection, // Mongo.Collection
|
||||||
|
inversedBy, // The link name from the other side
|
||||||
|
denormalize: {
|
||||||
|
field, // String
|
||||||
|
body, // Body from related collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Reducers
|
||||||
|
|
||||||
|
```js
|
||||||
|
Collection.addReducers({
|
||||||
|
reducerName: {
|
||||||
|
body, // Object, dependency graph
|
||||||
|
compute(object) {
|
||||||
|
// anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Named Query
|
||||||
|
|
||||||
|
```js
|
||||||
|
Collection.createQuery('queryName', {
|
||||||
|
$options, // Mongo Options {sort, limit, skip}
|
||||||
|
$filters, // Mongo Filters
|
||||||
|
$filter({filters, options, params}) {}, // Function or [Function]
|
||||||
|
$postOptions, // {limit, sort, skip}
|
||||||
|
$postFilters, // any sift() available filters
|
||||||
|
$postFilter(results, params) {}, // Function => results, or [Function] => results
|
||||||
|
body, // The query body
|
||||||
|
}, {
|
||||||
|
params, // Default parameters
|
||||||
|
validateParams, // Object or Function
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exposing Named Query
|
||||||
|
|
||||||
|
```js
|
||||||
|
query.expose({
|
||||||
|
firewall(userId, params) {}, // Function or [Function]
|
||||||
|
method, // Boolean
|
||||||
|
publication, // Boolean
|
||||||
|
unblock, // Boolean
|
||||||
|
validateParams, // Function or Object
|
||||||
|
embody // Object which extends the body server-side securely
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating and Exposting Resolvers
|
||||||
|
|
||||||
|
```js
|
||||||
|
// both
|
||||||
|
const query = createQuery('queryName', () => {});
|
||||||
|
|
||||||
|
// server
|
||||||
|
query.expose({
|
||||||
|
firewall, // Function or [Function]
|
||||||
|
});
|
||||||
|
|
||||||
|
query.resolve(function (params) {
|
||||||
|
// this.userId
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Query
|
||||||
|
|
||||||
|
```js
|
||||||
|
query.setParams({}) // extends current params
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Server-Side
|
||||||
|
```js
|
||||||
|
query.clone({params}).fetch();
|
||||||
|
query.clone({params}).fetchOne();
|
||||||
|
query.clone({params}).getCount();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Client-Side
|
||||||
|
|
||||||
|
Static:
|
||||||
|
```js
|
||||||
|
query.clone({params}).fetch((err, res) => {});
|
||||||
|
query.clone({params}).fetchOne((err, res) => {});
|
||||||
|
query.clone({params}).getCount((err, res) => {});
|
||||||
|
```
|
||||||
|
|
||||||
|
Reactive:
|
||||||
|
```js
|
||||||
|
const query = userListQuery.clone({params});
|
||||||
|
|
||||||
|
const handle = query.subscribe(); // handle.ready()
|
||||||
|
const data = query.fetch();
|
||||||
|
const oneData = query.fetchOne();
|
||||||
|
|
||||||
|
const handleCount = query.subscribeCount();
|
||||||
|
const count = query.getCount();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Caching Named Queries
|
||||||
|
```js
|
||||||
|
import {MemoryResultCacher} from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
// server-side
|
||||||
|
query.cacheResults(new MemoryResultCacher({
|
||||||
|
ttl: 60 * 1000, // 60 seconds
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Creating Global Query
|
||||||
|
```js
|
||||||
|
Collection.createQuery({
|
||||||
|
$options, // Mongo Options {sort, limit, skip}
|
||||||
|
$filters, // Mongo Filters
|
||||||
|
$filter({filters, options, params}) {}, // Function or [Function]
|
||||||
|
$postOptions, // {limit, sort, skip}
|
||||||
|
$postFilters, // any sift() available filters
|
||||||
|
$postFilter, // Function => results, or [Function] => results
|
||||||
|
body, // the rest of the object
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Exposing Global Query
|
||||||
|
|
||||||
|
```js
|
||||||
|
Collection.expose({
|
||||||
|
firewall(filters, options, userId) {}, // Function or [Function]
|
||||||
|
publication, // Boolean
|
||||||
|
method, // Boolean
|
||||||
|
blocking, // Boolean
|
||||||
|
maxLimit, // Number
|
||||||
|
maxDepth, // Number
|
||||||
|
restrictedFields, // [String]
|
||||||
|
restrictLinks, // [String] or Function,
|
||||||
|
body, // Object or Function(userId) => Object
|
||||||
|
});
|
||||||
|
```
|
139
docs/caching_results.md
Normal file
139
docs/caching_results.md
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
Caching results only works for Named Queries.
|
||||||
|
|
||||||
|
Let's say we have some very heavy queries, that do complex sorting
|
||||||
|
and complex filtering:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default Meteor.users.createQuery('myFriendsEmails', {
|
||||||
|
$filter({filters, params}) {
|
||||||
|
filters.friendIds = params.userId;
|
||||||
|
},
|
||||||
|
$options: {
|
||||||
|
sort: {createdAt: -1}
|
||||||
|
},
|
||||||
|
email: 1,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// server-side
|
||||||
|
import myFriendsQuery from '...';
|
||||||
|
import {MemoryResultCacher} from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
myFriendsQuery.expose({
|
||||||
|
firewall(userId, params) {
|
||||||
|
params.userId = userId;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacher = new MemoryResultCacher({
|
||||||
|
ttl: 60 * 1000, // 1 minute caching
|
||||||
|
});
|
||||||
|
|
||||||
|
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:
|
||||||
|
```
|
||||||
|
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:
|
||||||
|
```
|
||||||
|
fetch(cacheId, {
|
||||||
|
query: {fetch()},
|
||||||
|
countCursor: {count()}
|
||||||
|
})
|
||||||
|
generateQueryId(queryName, params)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to roll-out your own cache that stores the data outside memory, and you want to share in your cloud,
|
||||||
|
you may want to use `redis` for that.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {BaseResultCacher} from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis Cacher
|
||||||
|
*/
|
||||||
|
export default class RedisCacher extends BaseResultCacher {
|
||||||
|
// this is the default one in case you need to override it
|
||||||
|
generateQueryId(queryName, params) {
|
||||||
|
return `${queryName}::${EJSON.stringify(params)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case of a count cursor cacheId gets prefixed with 'count::'
|
||||||
|
fetch(cacheId, fetchables) {
|
||||||
|
const {client, ttl} = this.config;
|
||||||
|
|
||||||
|
const cacheData = client.get(cacheId);
|
||||||
|
if (cacheData) {
|
||||||
|
return EJSON.parse(cacheData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = BaseResultCacher.fetchData(fetchables);
|
||||||
|
client.set(cacheId, data, 'PX', ttl);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And use it:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import RedisCacher from 'somewhere';
|
||||||
|
|
||||||
|
const cacher = new RedisCacher({
|
||||||
|
client: redisClient,
|
||||||
|
ttl: 60 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
myFriendsQuery.cacheResults(cacher);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Caching Resolver Queries
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
148
docs/denormalization.md
Normal file
148
docs/denormalization.md
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
# Denormalization
|
||||||
|
|
||||||
|
Grapher allows you to denormalize your links, leveraging the power of `herteby:denormalize` package.
|
||||||
|
You can learn more about its capabilities here: https://github.com/Herteby/denormalize
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Assuming Images of schema: {_id, path, smallThumb, createdAt}
|
||||||
|
import Images from '...';
|
||||||
|
|
||||||
|
Meteor.users.addLinks({
|
||||||
|
avatar: {
|
||||||
|
collection: Images,
|
||||||
|
field: 'avatarId',
|
||||||
|
type: 'one',
|
||||||
|
denormalize: {
|
||||||
|
field: 'avatarCache', // in which field to store the cache
|
||||||
|
// what fields to cache from Images, this only works for fields and not links
|
||||||
|
body: {
|
||||||
|
path: 1,
|
||||||
|
smallThumb: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If you did not add this since the beginning of your app, you need to add this into your migration script:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {migrate} from 'meteor/herteby:denormalize'
|
||||||
|
migrate('users', 'avatarCache');
|
||||||
|
```
|
||||||
|
|
||||||
|
Now if you are doing the following query:
|
||||||
|
```js
|
||||||
|
const user = Meteor.users.createQuery({
|
||||||
|
avatar: {
|
||||||
|
smallThumbPath: 1,
|
||||||
|
}
|
||||||
|
}).fetchOne()
|
||||||
|
```
|
||||||
|
|
||||||
|
Grapher will check to see if avatar's body is a subbody of the denormalized body. If yes, it will hit the cache,
|
||||||
|
leading to a single database request.
|
||||||
|
|
||||||
|
And the result will be as you expect it to be:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
_id: 'XXX',
|
||||||
|
avatar: {
|
||||||
|
_id: 'YYY',
|
||||||
|
smallThumbPath: '/path/to/file.small.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It works very well with nested fields, so you don't have to worry about that.
|
||||||
|
|
||||||
|
However, a query like this:
|
||||||
|
```js
|
||||||
|
const user = Meteor.users.createQuery({
|
||||||
|
avatar: {
|
||||||
|
smallThumbPath: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
}
|
||||||
|
}).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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Books.addLinks({
|
||||||
|
'reviewedByUsers': {
|
||||||
|
type: 'many',
|
||||||
|
collection: Meteor.users,
|
||||||
|
field: 'reviewedByUserIds',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Meteor.users.addLinks({
|
||||||
|
'bookReviews': {
|
||||||
|
collection: Books,
|
||||||
|
inversedBy: 'reviewedByUsers',
|
||||||
|
denormalize: {
|
||||||
|
body: {
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
field: 'bookReviewsCache',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
*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.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const dramaticUsers = Meteor.users.createQuery({
|
||||||
|
$filters: {
|
||||||
|
'bookReviewsCache.type': 'Drama'
|
||||||
|
},
|
||||||
|
email: 1,
|
||||||
|
}).fetch();
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Caution
|
||||||
|
|
||||||
|
If you want to use deep filters, it will not work with denormalized cache, you can use `$postFilter()` method for that.
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
340
docs/global_exposure.md
Normal file
340
docs/global_exposure.md
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
## Global Queries
|
||||||
|
|
||||||
|
Global queries are not recommended because they are very hard to secure.
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
A `Global Query` is as almost feature rich as a `Named Query` with the exception of caching.
|
||||||
|
|
||||||
|
#### Real life usage
|
||||||
|
|
||||||
|
- You have a public database, you just want to expose it
|
||||||
|
- You have a multi-tenant system, and you want to give full database access to the tenant admin
|
||||||
|
- Other cases as well
|
||||||
|
|
||||||
|
In order to query for a collection from the client-side and fetch it or subscribe to it. You must expose it.
|
||||||
|
|
||||||
|
Exposing a collection does the following things:
|
||||||
|
|
||||||
|
- Creates a method called: exposure_{collectionName} which accepts a query
|
||||||
|
- Creates a publication called: exposure_{collectionName} which accepts a query and uses [reywood:publish-composite](https://atmospherejs.com/reywood/publish-composite) to achieve reactive relationships.
|
||||||
|
- If firewall is specified, it extends *find* method of your collection, allowing an extra parameter:
|
||||||
|
|
||||||
|
```
|
||||||
|
Collection.find(filters, options, userId);
|
||||||
|
```
|
||||||
|
|
||||||
|
If *userId* is undefined, the firewall and constraints will not be applied. If the *userId* is *null*, the firewall will be applied. This is to allows server-side fetching without any restrictions.
|
||||||
|
|
||||||
|
#### Exposing a collection to everyone
|
||||||
|
|
||||||
|
```js
|
||||||
|
// server-side
|
||||||
|
Meteor.users.expose();
|
||||||
|
```
|
||||||
|
|
||||||
|
This means that any user, from the client can do:
|
||||||
|
```js
|
||||||
|
createQuery({
|
||||||
|
users: {
|
||||||
|
services: 1, // yes, everything becomes exposed
|
||||||
|
anyLink: {
|
||||||
|
anySubLink: {
|
||||||
|
// and it can go on and on and on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Ok this is very bad. Let's secure it.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.expose({
|
||||||
|
restrictedFields: ['services'],
|
||||||
|
restrictLinks: ['anyLink'],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Phiew, that's better. But is it? You'll have to keep track of all the link restrictions.
|
||||||
|
|
||||||
|
#### Exposing a collection to logged in users
|
||||||
|
|
||||||
|
```js
|
||||||
|
// server-side
|
||||||
|
Collection.expose({
|
||||||
|
firewall(filters, options, userId) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Meteor.Error('...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Exposure Options
|
||||||
|
|
||||||
|
```js
|
||||||
|
Collection.expose({
|
||||||
|
// it can also be an array of functions
|
||||||
|
firewall(filters, options, userId) {
|
||||||
|
filters.userId = userId;
|
||||||
|
},
|
||||||
|
// Allow reactive query-ing
|
||||||
|
publication: true,
|
||||||
|
// Allow static query-in
|
||||||
|
method: true,
|
||||||
|
// Unblock() the method/publication
|
||||||
|
blocking: false,
|
||||||
|
// The publication/method will not allow data fetching for more than 100 items.
|
||||||
|
maxLimit: 100,
|
||||||
|
// The publication/method will not allow a query with more than 3 levels deep.
|
||||||
|
maxDepth: 3,
|
||||||
|
// This will clean up filters, options.sort and options.fields and remove those fields from there.
|
||||||
|
// It even removes it from deep filters with $or, $nin, etc
|
||||||
|
restrictedFields: ['services', 'secretField'],
|
||||||
|
// Array of strings or a function that has userId
|
||||||
|
restrictLinks: ['link1', 'link2']
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Exposure firewalls are linked
|
||||||
|
|
||||||
|
When querying for a data-graph like:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
users: {
|
||||||
|
comments: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is not necessary to have an exposure for *comments*, however if you do have it, and it has a firewall. The firewall rules will be applied.
|
||||||
|
The reason for this is security.
|
||||||
|
|
||||||
|
Don't worry about performance. We went great lengths to retrieve data in as few MongoDB requests as possible, in the scenario above,
|
||||||
|
if you do have a firewall for users and comments, both will be called only once, because we only make 2 MongoDB requests.
|
||||||
|
|
||||||
|
#### Setting Default Configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
import { Exposure } from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
// Make sure you do this before exposing any collections.
|
||||||
|
Exposure.setConfig({
|
||||||
|
firewall,
|
||||||
|
method,
|
||||||
|
publication,
|
||||||
|
blocking,
|
||||||
|
maxLimit,
|
||||||
|
maxDepth,
|
||||||
|
restrictedFields
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When you expose a collection, it will extend the global exposure methods.
|
||||||
|
The reason for this is you may want a global limit of 100, or you may want a maximum graph depth of 5 for all your exposed collections,
|
||||||
|
without having to specify this for each.
|
||||||
|
|
||||||
|
Important: if global exposure has a firewall and the collection exposure has a firewall defined as well,
|
||||||
|
the collection exposure firewall will be applied.
|
||||||
|
|
||||||
|
##### Taming The Firewall
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Apply filters based on userId
|
||||||
|
Collection.expose({
|
||||||
|
firewall(filters, options, userId) {
|
||||||
|
if (!isAdmin(userId)) {
|
||||||
|
filters.isVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Make certain fields invisible for certain users
|
||||||
|
import { Exposure } from 'meteor/cultofcoders:grapher'
|
||||||
|
Collection.expose({
|
||||||
|
firewall(filters, options, userId) {
|
||||||
|
if (!isAdmin(userId)) {
|
||||||
|
Exposure.restrictFields(filters, options, ['privateData']);
|
||||||
|
// it will remove all specified fields from filters, options.sort, options.fields
|
||||||
|
// this way you will not expose unwanted data.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Restrict certain links by userId
|
||||||
|
|
||||||
|
Compute restricted links when fetching the query:
|
||||||
|
```js
|
||||||
|
Collection.expose({
|
||||||
|
restrictLinks(userId) {
|
||||||
|
return ['privateLink', 'anotherPrivateLink']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exposure Body
|
||||||
|
|
||||||
|
If *body* is specified, it is first applied on the requested body and then the subsequent rules such as *restrictedFields*, *restrictLinks*
|
||||||
|
will apply still.
|
||||||
|
|
||||||
|
This is for advanced usage and it completes the security of exposure.
|
||||||
|
|
||||||
|
By using body, Grapher automatically assumes you have control over what you give,
|
||||||
|
meaning all firewalls from other exposures for linked elements in this body will be bypassed.
|
||||||
|
|
||||||
|
The firewall of the current exposure still executes ofcourse.
|
||||||
|
|
||||||
|
#### Basic Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.expose({
|
||||||
|
body: {
|
||||||
|
firstName: 1,
|
||||||
|
groups: {
|
||||||
|
name: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If you query from the *client-side* something like:
|
||||||
|
```js
|
||||||
|
createQuery({
|
||||||
|
users: {
|
||||||
|
firstName: 1,
|
||||||
|
lastName: 1,
|
||||||
|
groups: {
|
||||||
|
name: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The intersected body will look like:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
firstName: 1,
|
||||||
|
groups: {
|
||||||
|
name: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ok, but what if I want to have a different body based on the userId?
|
||||||
|
Body can also be a function that takes in an `userId`, and returns an actual body, an `Object`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Collection.expose({
|
||||||
|
body(userId) {
|
||||||
|
let body = { firstName: 1 };
|
||||||
|
|
||||||
|
if (isAdmin(userId)) {
|
||||||
|
_.extend(body, { lastName: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Deep nesting with other links not be allowed unless your *body* specifies it.
|
||||||
|
|
||||||
|
The special fields `$filters` and `$options` are allowed at any link level (including root). However, they will go through a phase of cleaning,
|
||||||
|
meaning it will only allow you to `filter` and `sort` for fields that exist in the body.
|
||||||
|
|
||||||
|
This check goes deeply to verify "$and", "$or", "$nin" and "$not" special MongoDB selectors. This way you are sure you do not expose data you don't want to.
|
||||||
|
Because, given enough requests, a hacker playing with `$filters` and `$sort` options can figure out a field that you may not want to give him access to.
|
||||||
|
|
||||||
|
If the *body* contains functions they will be computed before intersection. Each function will receive userId.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
linkName(userId) { return {test: 1} }
|
||||||
|
}
|
||||||
|
|
||||||
|
// transforms into
|
||||||
|
{
|
||||||
|
linkName: {
|
||||||
|
test: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can return *undefined* or *false* in your function if you want to disable the field/link for intersection.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
linkName(userId) {
|
||||||
|
if (isAdmin(userId)) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linking Grapher Exposure Bodies
|
||||||
|
|
||||||
|
Now things start to get crazy around here!
|
||||||
|
|
||||||
|
You can link bodies in your own way and also reference other bodies'links.
|
||||||
|
Functions are computed on-demand, meaning you can have self-referencing body functions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Comments ONE link to Users as 'user'
|
||||||
|
// Users INVERSED 'user' from Comments AS 'comments'
|
||||||
|
|
||||||
|
const commentBody = function(userId) {
|
||||||
|
return {
|
||||||
|
user: userBody,
|
||||||
|
text: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userBody = function(userId) {
|
||||||
|
if (isAdmin(userId)) {
|
||||||
|
return {
|
||||||
|
comments: commentBody
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return somethingElse;
|
||||||
|
}
|
||||||
|
|
||||||
|
Users.expose({
|
||||||
|
body: userBody
|
||||||
|
})
|
||||||
|
|
||||||
|
Comments.expose({
|
||||||
|
body: commentBody
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This will allow requests like:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
users: {
|
||||||
|
comments: {
|
||||||
|
user: {
|
||||||
|
// It doesn't make much sense for this case
|
||||||
|
// but you can :)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The global queries are a very powerful tool to expose your full database, but unlike `Named Queries` they do
|
||||||
|
not benefit of `caching`.
|
77
docs/hypernova.md
Normal file
77
docs/hypernova.md
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
## Hypernova
|
||||||
|
|
||||||
|
This is the crown jewl of Grapher. It was named like this because it felt like an explosion of data.
|
||||||
|
|
||||||
|
Grapher is very performant. To understand what we're talking about let's take this example of a query
|
||||||
|
|
||||||
|
```js
|
||||||
|
createQuery({
|
||||||
|
posts: {
|
||||||
|
categories: {
|
||||||
|
name: 1,
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
name: 1,
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
$options: {limit: 10},
|
||||||
|
author: {
|
||||||
|
name: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Queries Counting
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Assuming we have:
|
||||||
|
- 10 posts
|
||||||
|
- 2 categories per post
|
||||||
|
- 1 author per post
|
||||||
|
- 10 comments per post
|
||||||
|
- 1 author per comment
|
||||||
|
|
||||||
|
We would have blasted the database with:
|
||||||
|
- Posts: 1
|
||||||
|
- Categories: 10
|
||||||
|
- Post authors: 10
|
||||||
|
- Post comments: 10
|
||||||
|
- Post comments authors: 100
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Hypernova to the rescue
|
||||||
|
|
||||||
|
How many requests does the Hypernova?
|
||||||
|
- 1 for Posts
|
||||||
|
- 1 for all authors inside Posts
|
||||||
|
- 1 for all categories inside Posts
|
||||||
|
- 1 for all comments inside Posts
|
||||||
|
- 1 for all authors inside all comments
|
||||||
|
|
||||||
|
The number of database is predictable, because it represents the number of collection nodes inside the graph.
|
||||||
|
|
||||||
|
It does this by aggregating filters and then it reassembles data locally.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Keep in mind that Hypernova is only used for static queries. For reactive queries, we still rely on the recursive fetching.
|
||||||
|
|
||||||
|
|
||||||
|
|
220
docs/introduction.md
Normal file
220
docs/introduction.md
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
# Welcome
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
```
|
||||||
|
meteor add cultofcoders:grapher
|
||||||
|
```
|
||||||
|
|
||||||
|
## The 3 Modules
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Query
|
||||||
|
The query module is used for fetching your data in a friendly manner, such as:
|
||||||
|
```js
|
||||||
|
createQuery({
|
||||||
|
users: {
|
||||||
|
firstName: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
It abstracts your query into a graph composed of Collection Nodes, Field Nodes and Resolver 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.
|
||||||
|
|
||||||
|
### Exposure
|
||||||
|
|
||||||
|
The exposure represents the layer between your queries and the client, allowing you to securely expose your queries,
|
||||||
|
only to users that have access.
|
||||||
|
|
||||||
|
|
||||||
|
### Your first query
|
||||||
|
|
||||||
|
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({
|
||||||
|
posts() {
|
||||||
|
return Posts.find({}, {
|
||||||
|
fields: {
|
||||||
|
title: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
createdBy: 1,
|
||||||
|
}
|
||||||
|
}).fetch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Transforming this into a Grapher query would simply look like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.methods({
|
||||||
|
posts() {
|
||||||
|
const query = Posts.createQuery({
|
||||||
|
title: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
createdBy: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.fetch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
One of the advantages that Grapher has, is the fact that it forces you to specify the fields you need,
|
||||||
|
you may find this cumbersome in the beginning, but as your application grows, new fields are added,
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.methods({
|
||||||
|
posts() {
|
||||||
|
// Previously Posts.find({isApproved: true}, {sort: '...', fields: '...'});
|
||||||
|
const query = Posts.createQuery({
|
||||||
|
$filters: {
|
||||||
|
isApproved: true,
|
||||||
|
},
|
||||||
|
$options: {
|
||||||
|
sort: {createdAt: -1}
|
||||||
|
},
|
||||||
|
title: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
createdBy: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.fetch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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()`.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default Posts.createQuery({
|
||||||
|
$filter({filters, options, params}) {
|
||||||
|
filters.isApproved = params.isApproved;
|
||||||
|
},
|
||||||
|
$options: {sort: {createdAt: -1}},
|
||||||
|
title: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
createdBy: 1,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `$filter()` function receives a single object that contains 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.
|
||||||
|
|
||||||
|
The job of `$filter()` is to extend/modify `filters` and `options`, based on params.
|
||||||
|
|
||||||
|
Lets see how we can use that query:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// assuming you exported it from '...'
|
||||||
|
import postListQuery from '...';
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
posts() {
|
||||||
|
return postListQuery.clone({
|
||||||
|
isApproved: true
|
||||||
|
}).fetch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You could also use `setParams()` to configure parameters:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import postListQuery from '...';
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
posts() {
|
||||||
|
const query = postListQuery.clone();
|
||||||
|
|
||||||
|
query.setParams({
|
||||||
|
isApproved: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.fetch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {Match} from 'meteor/check';
|
||||||
|
|
||||||
|
export default Posts.createQuery({
|
||||||
|
$filter({filters, options, params}) {
|
||||||
|
filters.isApproved = params.isApproved;
|
||||||
|
if (params.authorId) {
|
||||||
|
filters.authorId = params.authorId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}, {
|
||||||
|
validateParams: {
|
||||||
|
isApproved: Boolean,
|
||||||
|
authorId: Match.Maybe(String),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
But you can craft your own validation:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
validateParams(params) {
|
||||||
|
if (somethingIsWrong) {
|
||||||
|
throw new Meteor.Error('invalid-params', 'Explain why');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: params validation is done prior to fetching the query.
|
||||||
|
|
||||||
|
And if you want to set some default parameters:
|
||||||
|
```js
|
||||||
|
export default Posts.createQuery({...}, {
|
||||||
|
params: {
|
||||||
|
isApproved: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
# Linker Engine
|
||||||
|
|
||||||
|
The linker engine is composed of 3 components:
|
||||||
|
- Definition of Links
|
||||||
|
- Linked Data Retrieval
|
||||||
|
- Setting Links
|
||||||
|
|
||||||
|
Let's explore how we can play with `Linked Data Retrieval` and `Setting Links`, assumming we already defined our links
|
||||||
|
using `addLinks()` method.
|
||||||
|
|
||||||
|
Each collection gets extended with a `getLink` function:
|
||||||
|
```js
|
||||||
|
const linker = Collection.getLink(collectionItemId, 'linkName');
|
||||||
|
```
|
||||||
|
|
||||||
|
A real life example (assuming you have a `direct` or `inversed` link with groups)
|
||||||
|
```js
|
||||||
|
const userGroupLinker = Meteor.users.getLink(userId, 'groups');
|
||||||
|
```
|
||||||
|
|
||||||
|
Linker is polymorphic and it allows you to do almost magical things. It is so flexible that it will
|
||||||
|
allow you to use it very naturally.
|
||||||
|
|
||||||
|
## Linked Data Retrieval
|
||||||
|
|
||||||
|
```js
|
||||||
|
const userGroupLinker = Meteor.users.getLink(userId, 'groups');
|
||||||
|
|
||||||
|
// fetching the groups for that user:
|
||||||
|
const groups = userGroupLinker.find().fetch();
|
||||||
|
// or for ease of use
|
||||||
|
const groups = userGroupLinker.fetch();
|
||||||
|
// or to filter the nested 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
|
||||||
|
const groups = userGroupLinker.find(filters, options).count();
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
|
||||||
|
#### "One" Relationships
|
||||||
|
|
||||||
|
Performing a `set()` will automatically execute the update or insert in the database.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||||
|
|
||||||
|
userPaymentProfileLink.set(paymentProfileId);
|
||||||
|
// but it also works if you have the object directly if it has _id, for ease of use:
|
||||||
|
userPaymentProfileLink.set(paymentProfile);
|
||||||
|
|
||||||
|
// it works from the other side as well
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||||
|
|
||||||
|
userPaymentProfileLink.set({
|
||||||
|
last4digits: '1234',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This will insert into the `PaymentProfiles` collection and link it to user and it works from both `direct` and `inversed` side as well.
|
||||||
|
|
||||||
|
To remove a link for a `one` relationship (no arguments required):
|
||||||
|
```js
|
||||||
|
userPaymentProfileLink.unset();
|
||||||
|
// or
|
||||||
|
paymentProfileUserLink.unset();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### "Many" Relationships
|
||||||
|
|
||||||
|
Same principles as above apply, with some minor changes, this time we use `add` and `remove`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const userGroupsLink = Meteor.users.getLink(userId, 'groups');
|
||||||
|
userGroupsLink.add(groupId);
|
||||||
|
userGroupsLink.add(group); // object containing an _id
|
||||||
|
userGroupsLink.add({
|
||||||
|
name: 1,
|
||||||
|
}); // will add the group to the database and link it accordingly
|
||||||
|
```
|
||||||
|
|
||||||
|
The methods `add()` and `remove()` also accept arrays
|
||||||
|
```js
|
||||||
|
userGroupsLink.add([
|
||||||
|
groupId1,
|
||||||
|
groupId2
|
||||||
|
]);
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Ofcourse, the `remove()` cannot accept objects without `_id` as it makes no sense to do so.
|
||||||
|
|
||||||
|
#### "Meta" Relationships
|
||||||
|
|
||||||
|
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
|
||||||
|
same principles described as above.
|
||||||
|
|
||||||
|
The `add()` and `set()` allow an additional parameter `metadata`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// one
|
||||||
|
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||||
|
|
||||||
|
userPaymentProfileLink.set(paymentProfileId, {
|
||||||
|
createdAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// many
|
||||||
|
const userGroupsLink = Meteor.users.getLink(userId, 'groups');
|
||||||
|
|
||||||
|
userGroupsLink.add(groupId, {
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// if you add multiple objects, they will receive the same metadata
|
||||||
|
userGroupsLink.add([groupId1, groupId2], {
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Updating existing metadata:
|
||||||
|
```js
|
||||||
|
// one
|
||||||
|
const userPaymentProfileLink = Meteor.users.getLink(userId, 'paymentProfile');
|
||||||
|
|
||||||
|
userPaymentProfileLink.metadata({
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// many
|
||||||
|
userGroupsLink.metadata(groupId, {
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
userGroupsLink.metadata([groupId1, groupId2], {
|
||||||
|
createdAt: new Date(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Updating metadata only works with strings or objects that contain `_id`, and it works from both sides.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,453 @@
|
||||||
|
# Linking Collections
|
||||||
|
|
||||||
|
Let's learn what types of links we can do between collections, and what is the best way to do them.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Something like:
|
||||||
|
```js
|
||||||
|
Meteor.methods({
|
||||||
|
getPost({postId}) {
|
||||||
|
let post = Posts.findOne(postId, {
|
||||||
|
fields: {
|
||||||
|
title: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
authorId: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!post) { throw new Meteor.Error('not-found') }
|
||||||
|
const author = Meteor.users.findOne(post.authorId, {
|
||||||
|
fields: {
|
||||||
|
firstName: 1,
|
||||||
|
lastName: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.assign(post, {author});
|
||||||
|
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
With Grapher, your code above is transformed to:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.methods({
|
||||||
|
getPost({postId}) {
|
||||||
|
let post = Posts.createQuery({
|
||||||
|
$filters: {_id: postId},
|
||||||
|
title: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
author: {
|
||||||
|
firstName: 1,
|
||||||
|
lastName: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return post.fetchOne();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Linking Types
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: /imports/db/posts/links.js
|
||||||
|
import Posts from '...';
|
||||||
|
|
||||||
|
Posts.addLinks({
|
||||||
|
'author': {
|
||||||
|
type: 'one',
|
||||||
|
collection: Meteor.users,
|
||||||
|
field: 'authorId',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: /imports/db/users/links.js
|
||||||
|
import Posts from '...';
|
||||||
|
|
||||||
|
Meteor.users.addLinks({
|
||||||
|
'posts': {
|
||||||
|
collection: Posts,
|
||||||
|
inversedBy: 'author'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`author` represents the link name that was defined inside Posts. By defining inversed links we can do:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.createQuery({
|
||||||
|
posts: {
|
||||||
|
title: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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,
|
||||||
|
field: 'categoryIds',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, `categoryIds` is an array of Strings, each String, representing `_id` from `Categories` collection.
|
||||||
|
|
||||||
|
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 '...';
|
||||||
|
import Posts from '...';
|
||||||
|
|
||||||
|
Categories.addLinks({
|
||||||
|
'posts': {
|
||||||
|
collection: Posts,
|
||||||
|
inversedBy: 'categories'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: /imports/db/users/links.js
|
||||||
|
import Groups from '...'
|
||||||
|
|
||||||
|
Meteor.users.addLinks({
|
||||||
|
group: {
|
||||||
|
type: 'one',
|
||||||
|
collection: Groups,
|
||||||
|
field: 'groupLink',
|
||||||
|
metadata: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice the new option `metadata: true` this means that `groupLink` is no longer a `String`, but an `Object` that looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
// inside a Meteor.users document
|
||||||
|
{
|
||||||
|
...
|
||||||
|
groupLink: {
|
||||||
|
_id: 'XXX',
|
||||||
|
roles: 'ADMIN',
|
||||||
|
createdAt: Date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's see how this works out in our query:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const user = Meteor.users.createQuery({
|
||||||
|
$filters: {_id: userId},
|
||||||
|
group: {
|
||||||
|
name: 1,
|
||||||
|
}
|
||||||
|
}).fetchOne()
|
||||||
|
```
|
||||||
|
|
||||||
|
`user` will look like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
_id: userId,
|
||||||
|
group: {
|
||||||
|
$metadata: {
|
||||||
|
roles: 'ADMIN',
|
||||||
|
createdAt: Date
|
||||||
|
},
|
||||||
|
name: 'My Funky Group'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We store the metadata of the link inside a special `$metadata` field. And this works from inversed side as well:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Groups.addLinks({
|
||||||
|
users: {
|
||||||
|
collection: Meteor.users,
|
||||||
|
inversedBy: 'group'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const group = Groups.createQuery({
|
||||||
|
$filters: {_id: groupId},
|
||||||
|
name: 1,
|
||||||
|
users: {
|
||||||
|
firstName: 1,
|
||||||
|
}
|
||||||
|
}).fetchOne()
|
||||||
|
```
|
||||||
|
|
||||||
|
`group` will look like:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
_id: groupId,
|
||||||
|
name: 'My Funky Group',
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
$metadata: {
|
||||||
|
roles: 'ADMIN',
|
||||||
|
createdAt: Date
|
||||||
|
},
|
||||||
|
_id: userId,
|
||||||
|
firstName: 'My Funky FirstName',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same principles apply to `meta` links that are `type: 'many'`, if we change that in the example above.
|
||||||
|
The storage field will look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
groupLinks: [
|
||||||
|
{_id: 'groupId', roles: 'ADMIN', createdAt: Date},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And they work the same as you expect, from the inversed side as well.
|
||||||
|
|
||||||
|
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 ?
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: /imports/db/groupUserLinks/links.js
|
||||||
|
import Groups from '...';
|
||||||
|
import GroupUserLinks from '...';
|
||||||
|
|
||||||
|
GroupUserLinks.addLinks({
|
||||||
|
user: {
|
||||||
|
type: 'one',
|
||||||
|
collection: Meteor.users,
|
||||||
|
field: 'userId'
|
||||||
|
},
|
||||||
|
adder: {
|
||||||
|
type: 'one',
|
||||||
|
collection: Meteor.users,
|
||||||
|
field: 'addedBy'
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
type: 'one',
|
||||||
|
collection: Meteor.users,
|
||||||
|
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:
|
||||||
|
```js
|
||||||
|
Meteor.users.createQuery({
|
||||||
|
groupLink: {
|
||||||
|
group: {
|
||||||
|
name: 1,
|
||||||
|
},
|
||||||
|
adder: {
|
||||||
|
firstName: 1
|
||||||
|
},
|
||||||
|
roles: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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: {
|
||||||
|
collection: Meteor.users,
|
||||||
|
type: 'many',
|
||||||
|
field: 'friendIds',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Say you want to get your friends, and friends of friends, and friends of friends of friends!
|
||||||
|
```js
|
||||||
|
Meteor.users.createQuery({
|
||||||
|
$filters: {_id: userId},
|
||||||
|
friends: {
|
||||||
|
nickname: 1,
|
||||||
|
friends: {
|
||||||
|
nickname: 1,
|
||||||
|
friends: {
|
||||||
|
nickname: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.addLinks({
|
||||||
|
paymentProfile: {
|
||||||
|
collection: PaymentProfiles,
|
||||||
|
inversedBy: 'user'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
PaymentProfiles.addLinks({
|
||||||
|
user: {
|
||||||
|
field: 'userId',
|
||||||
|
collection: Meteor.users,
|
||||||
|
type: 'one',
|
||||||
|
unique: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Now fetching:
|
||||||
|
```js
|
||||||
|
Meteor.users.createQuery({
|
||||||
|
paymentProfile: {
|
||||||
|
type: 1,
|
||||||
|
last4digits: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`paymentProfile` inside `user` will be an object because it knows it should be unique.
|
||||||
|
|
||||||
|
## Data Consistency
|
||||||
|
|
||||||
|
This is referring to having consistency amongst links.
|
||||||
|
|
||||||
|
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.
|
||||||
|
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`.
|
||||||
|
|
||||||
|
In conclusion, if you want to benefit of this, you have to define inversed links for every direct links.
|
||||||
|
|
||||||
|
## Autoremoval
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.addLinks({
|
||||||
|
'posts': {
|
||||||
|
collection: Posts,
|
||||||
|
inversedBy: 'author',
|
||||||
|
autoremove: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
After you deleted 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.
|
||||||
|
|
||||||
|
## Indexing
|
||||||
|
|
||||||
|
As a rule of thumb, you must index all of your links. Because that's how you achieve absolute performance.
|
||||||
|
|
||||||
|
This is not done by default, to allow the developer flexibility, but you can do it simply enough from the direct side definition of the link:
|
||||||
|
|
||||||
|
```js
|
||||||
|
PaymentProfiles.addLinks({
|
||||||
|
user: {
|
||||||
|
field: 'userId',
|
||||||
|
collection: Meteor.users,
|
||||||
|
type: 'one',
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
In the future, this limitation may change, but for now you can work around this and keep your code elegant.
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,398 @@
|
||||||
|
# Named Queries
|
||||||
|
|
||||||
|
Before we explain what they are, we need to understand an alternative way of creating the queries.
|
||||||
|
|
||||||
|
## Alternative Creation
|
||||||
|
|
||||||
|
Currently, we only saw how to create a query starting from a collection, like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.createQuery(body, options);
|
||||||
|
```
|
||||||
|
|
||||||
|
But Grapher also exposes a `createQuery` functionality:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
createQuery({
|
||||||
|
users: {
|
||||||
|
profile: 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The first key inside the object needs to represent an existing collection name:
|
||||||
|
```js
|
||||||
|
const Posts = new Mongo.Collection('posts');
|
||||||
|
|
||||||
|
// then we can do:
|
||||||
|
createQuery({
|
||||||
|
posts: {
|
||||||
|
title: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
The `Named Query` has the same API as a normal `Query`, we'll understand the difference in this documentation.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: /imports/api/users/queries/userAdminList.js
|
||||||
|
export default Meteor.users.createQuery('userAdminList', {
|
||||||
|
$filters: {
|
||||||
|
roles: {$in: 'ADMIN'}
|
||||||
|
},
|
||||||
|
name: 1,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If you would like to use this query you have two ways:
|
||||||
|
```js
|
||||||
|
import userAdminListQuery from '/imports/db/users/queries/userAdminList.js';
|
||||||
|
|
||||||
|
const admins = userAdminListQuery.fetch();
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you could use `createQuery`:
|
||||||
|
```js
|
||||||
|
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
const admins = createQuery({
|
||||||
|
userAdminList: {},
|
||||||
|
}).fetch();
|
||||||
|
// will return a clone of the named query if it is already loaded
|
||||||
|
```
|
||||||
|
|
||||||
|
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 the default `$filter()` allows as params `filters` and `options` for the top collection node,
|
||||||
|
you can do something like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const admins = createQuery({
|
||||||
|
userAdminList: {
|
||||||
|
options: {createdAt: -1}
|
||||||
|
},
|
||||||
|
}).fetch();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Always go modular
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fullPostList = Posts.createQuery('fullPostList', {
|
||||||
|
title: 1,
|
||||||
|
author: {
|
||||||
|
firstName: 1,
|
||||||
|
lastName: 1,
|
||||||
|
},
|
||||||
|
comments: {
|
||||||
|
text: 1,
|
||||||
|
createdAt: 1,
|
||||||
|
author: {
|
||||||
|
firstName: 1,
|
||||||
|
lastName: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
```js
|
||||||
|
fullPostsList.clone({
|
||||||
|
$body: {
|
||||||
|
title: 1,
|
||||||
|
author: {
|
||||||
|
firstName: 1,
|
||||||
|
services: 1, // will be removed after intersection, and not queried
|
||||||
|
},
|
||||||
|
otherLink: {} // will be removed after intersection, and not queried
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
This will only fetch the intersection, the transformed body will look like:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
title: 1,
|
||||||
|
author: {firstName: 1}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {Match} from 'meteor/check';
|
||||||
|
const fullPostList = Posts.createQuery('fullPostList', {}, {
|
||||||
|
validateParams: {
|
||||||
|
$body: Match.Maybe(Object),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exposure
|
||||||
|
|
||||||
|
We are now crossing the bridge to client-side.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: /imports/api/users/queries/userAdminList.js
|
||||||
|
export default Meteor.users.createQuery('userAdminList', {
|
||||||
|
$filters: {
|
||||||
|
roles: {$in: 'ADMIN'}
|
||||||
|
},
|
||||||
|
name: 1,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
// file: /imports/api/users/queries/userAdminList.expose.js
|
||||||
|
import userAdminListQuery from './userAdminList';
|
||||||
|
|
||||||
|
userAdminListQuery.expose({
|
||||||
|
firewall(userId, params) {
|
||||||
|
if (!Roles.userIsInRole(userId, 'ADMIN')) {
|
||||||
|
throw new Meteor.Error('not-allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// in the firewall you also have the ability to modify the parameters
|
||||||
|
// that are going to hit the $filter() function in the query
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client-side
|
||||||
|
|
||||||
|
Let's use it on the client side:
|
||||||
|
```js
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
A beautiful part of Grapher lies in it's polymorphic ability, you can have a reactive query, just by using the same API:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {Tracker} from 'meteor/tracker';
|
||||||
|
import userAdminListQuery from '/imports/api/users/queries/userAdminList.js';
|
||||||
|
|
||||||
|
const query = userAdminListQuery.clone();
|
||||||
|
const subscriptionHandle = query.subscribe();
|
||||||
|
// if we did subscribe, we no longer need to supply a callback for fetch()
|
||||||
|
// as the query morphed into a reactive query
|
||||||
|
|
||||||
|
Tracker.autorun(() => {
|
||||||
|
if (subscriptionHandle.ready()) {
|
||||||
|
console.log(query.fetch());
|
||||||
|
query.unsubscribe();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can do `meteor add tracker` if it complains it's missing `Tracker`.
|
||||||
|
|
||||||
|
|
||||||
|
You can also use `fetchOne()` on the client as well, even supply a callback:
|
||||||
|
```js
|
||||||
|
userAdminListQuery.clone().fetchOne((err, user) => {
|
||||||
|
// do something
|
||||||
|
});
|
||||||
|
```
|
||||||
|
You can fetch static queries using promises:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const users = await userAdminListQuery.clone().fetchSync();
|
||||||
|
const user = await userAdminListQuery.clone().fetchOneSync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Counters
|
||||||
|
|
||||||
|
You can ofcourse use the same paradigm to use counts:
|
||||||
|
```js
|
||||||
|
// static query
|
||||||
|
query.getCount((err, count) => {
|
||||||
|
// do something
|
||||||
|
});
|
||||||
|
|
||||||
|
const count = await query.getCountSync();
|
||||||
|
|
||||||
|
// reactive counts
|
||||||
|
const handle = query.subscribeCount();
|
||||||
|
|
||||||
|
Tracker.autorun(() => {
|
||||||
|
if (handle.ready()) {
|
||||||
|
console.log(query.getCount());
|
||||||
|
query.unsubscribeCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
We do not subscribe to counts by default because they may be too expensive, but if you need them,
|
||||||
|
feel free to use them.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Expose Options
|
||||||
|
|
||||||
|
```js
|
||||||
|
query.expose({
|
||||||
|
// Secure your query
|
||||||
|
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: {
|
||||||
|
$filter({filters, params}) {
|
||||||
|
// do something
|
||||||
|
},
|
||||||
|
aLink: {
|
||||||
|
$filter() {
|
||||||
|
// works with links as well, because it's a deep extension
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resolvers
|
||||||
|
|
||||||
|
Named Queries have the ability to morph themselves into a function that executes on the server.
|
||||||
|
There are situations where you want to retrieve some data, that is not necessarily inside a collection,
|
||||||
|
or it needs additional operations to give you the correct result.
|
||||||
|
|
||||||
|
For example, let's say you want to provide the user with some analytic results, that perform some heavy
|
||||||
|
aggregations on the database, or call some external APIs.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// shared code between client and server
|
||||||
|
const getAnalytics = createQuery('getAnalytics', () => {}, {
|
||||||
|
validateParams: {} // Object or Function
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// server code only
|
||||||
|
import getAnalyticsQuery from './getAnalytics';
|
||||||
|
|
||||||
|
getAnalyticsQuery.expose({
|
||||||
|
firewall(userId, params) {
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
validateParams: {} // Object or Function that you don't want exposed
|
||||||
|
});
|
||||||
|
|
||||||
|
getAnalyticsQuery.resolve(function(params) {
|
||||||
|
// perform your magic here
|
||||||
|
// you are in `Meteor.method` context, so you have access to `this.userId`
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Mix'em up
|
||||||
|
|
||||||
|
The `firewall` can also be an array of functions. Allowing you to easily expose a query like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function checkLoggedIn(userId, params) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Meteor.Error('not-allowed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query.expose({
|
||||||
|
firewall: [checkLoggedIn, (userId, params) => {
|
||||||
|
params.userId = userId;
|
||||||
|
// other stuff here if you want
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
100
docs/outside_grapher.md
Normal file
100
docs/outside_grapher.md
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
## Grapher as an API
|
||||||
|
|
||||||
|
If you like Grapher, 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 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.
|
||||||
|
|
||||||
|
### Exposing an HTTP API
|
||||||
|
|
||||||
|
```js
|
||||||
|
// This is how you can create a sample endpoint
|
||||||
|
|
||||||
|
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||||
|
import Picker from 'meteor/meteorhacks:picker';
|
||||||
|
import {EJSON} from 'meteor/ejson';
|
||||||
|
import {Meteor} from 'meteor/meteor';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
|
||||||
|
const grapherRoutes = Picker.filter(function () {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
grapherRoutes.middleware(bodyParser.raw({
|
||||||
|
'type': 'application/ejson',
|
||||||
|
}));
|
||||||
|
|
||||||
|
grapherRoutes.route('/grapher', function (req, res) {
|
||||||
|
const body = req.body.toString();
|
||||||
|
const data = EJSON.parse(body);
|
||||||
|
|
||||||
|
// lets say this is a named query that looks like
|
||||||
|
// {getUserList: params}
|
||||||
|
const {query} = data;
|
||||||
|
|
||||||
|
// authorize the user somehow
|
||||||
|
// it's up to you to extract an userId
|
||||||
|
// or something else that you use for authorization
|
||||||
|
|
||||||
|
const actualQuery = createQuery(query);
|
||||||
|
|
||||||
|
// if it's not a named query and the collection is not exposed, don't allow it.
|
||||||
|
if (actualQuery.isGlobalQuery && !actualQuery.collection.__isExposedForGrapher) {
|
||||||
|
throw new Meteor.Error('not-allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = actualQuery.fetch({
|
||||||
|
// the userId (User Identification) that hits the firewalls
|
||||||
|
// user id can be anything, an API key maybe, not only a 'string'
|
||||||
|
// it's up to you and your firewalls to decide
|
||||||
|
userId: 'XXX'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.end(EJSON.stringify({
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end(EJSON.stringify({
|
||||||
|
error: e.reason,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can do HTTP requests of `Content-Type: application/ejson` to http://meteor-server/grapher and retrieve data.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {createQuery} from 'meteor/cultofcoders:grapher';
|
||||||
|
|
||||||
|
Meteor.methods({
|
||||||
|
'grapher'(query) {
|
||||||
|
const actualQuery = createQuery(query);
|
||||||
|
|
||||||
|
if (actualQuery.isGlobalQuery && !actualQuery.collection.__isExposedForGrapher) {
|
||||||
|
throw new Meteor.Error('not-allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualQuery.fetch({
|
||||||
|
userId: this.userId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Nothing stops you from using Grapher outside Meteor!
|
375
docs/query_options.md
Normal file
375
docs/query_options.md
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
# 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 wanted to fetch the full `profile`, simply 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';
|
||||||
|
|
||||||
|
Posts.addLinks({
|
||||||
|
comments: {
|
||||||
|
collection: Comments,
|
||||||
|
inversedBy: 'postId',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Comments.addLinks({
|
||||||
|
post: {
|
||||||
|
type: 'one',
|
||||||
|
field: 'postId',
|
||||||
|
collection: Posts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If any bit of the code written above creates confusion, try reading again the `Linking Collections` part of the documentation.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The `$filter` function share 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`:
|
||||||
|
|
||||||
|
```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 is mostly used 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.
|
||||||
|
|
||||||
|
### 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 ? Easy.
|
||||||
|
```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 received it.
|
||||||
|
|
||||||
|
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'},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
- 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 they only work for top level nodes, not for child collection nodes unlike `$filters` and `$options`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Mix'em up
|
||||||
|
|
||||||
|
The options `$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
|
||||||
|
}],
|
||||||
|
$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`
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
196
docs/reducers.md
Normal file
196
docs/reducers.md
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
# Reducers
|
||||||
|
|
||||||
|
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:
|
||||||
|
```js
|
||||||
|
Collection.addReducer({
|
||||||
|
reducerName: {
|
||||||
|
body: graphDependencyBody,
|
||||||
|
reduce(object) {
|
||||||
|
return value; // can be anything, object, date, string, number, etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basics
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.addReducers({
|
||||||
|
fullName: {
|
||||||
|
body: {
|
||||||
|
profile: {
|
||||||
|
firstName: 1,
|
||||||
|
lastName: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reduce(object) {
|
||||||
|
const {profile} = object;
|
||||||
|
|
||||||
|
return `${profile.firstName} ${profileLastName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Query:
|
||||||
|
```js
|
||||||
|
const user = Meteor.users.createQuery({
|
||||||
|
fullName: 1,
|
||||||
|
}).fetchOne();
|
||||||
|
```
|
||||||
|
|
||||||
|
Results to:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
_id: 'XXX',
|
||||||
|
fullName: 'John Smith',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reducers and links
|
||||||
|
|
||||||
|
Easily grab the data from your links (as deep as you want them), if you want to reduce it.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Meteor.users.addReducers({
|
||||||
|
groupNames: {
|
||||||
|
body: {
|
||||||
|
// assuming you have a link called groups
|
||||||
|
groups: { name: 1 }
|
||||||
|
},
|
||||||
|
reduce(object) { // a pure function that returns the data
|
||||||
|
return object.groups.map(group => group.name).join(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Query:
|
||||||
|
```js
|
||||||
|
const user = Meteor.users.createQuery({
|
||||||
|
groupNames: 1,
|
||||||
|
}).fetchOne();
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
_id: 'XXX',
|
||||||
|
groupNames: ['Group 1', 'Group 2'],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `groups: []` is not present in your result set. This is because we detect the fact that you
|
||||||
|
did not include it in the body of your query, however if you would have done:
|
||||||
|
|
||||||
|
Query:
|
||||||
|
```js
|
||||||
|
const user = Meteor.users.createQuery({
|
||||||
|
groupNames: 1,
|
||||||
|
groups: {
|
||||||
|
createdAt: 1,
|
||||||
|
}
|
||||||
|
}).fetchOne();
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
_id: 'XXX',
|
||||||
|
groupNames: ['Group 1', 'Group 2'],
|
||||||
|
groups: [
|
||||||
|
{_id: 'groupId1', createdAt: Date},
|
||||||
|
{_id: 'groupId2', createdAt: Date},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that group `name` is not there. This is because we clean leftovers so the result is predictable.
|
||||||
|
|
||||||
|
## Reducers and reducers
|
||||||
|
|
||||||
|
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
|
||||||
|
fullName: 1,
|
||||||
|
roles: 1
|
||||||
|
},
|
||||||
|
reduce(object) { // a pure function that returns the data
|
||||||
|
return object.fullName + object.roles.join(',');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
And again, unless you specified `fullName: 1` in your query, it will not be present in the result set.
|
||||||
|
|
||||||
|
## Params-aware reducers
|
||||||
|
|
||||||
|
By default the reducer receives the parameters the query has.
|
||||||
|
|
||||||
|
This can open the path to some nice customizations:
|
||||||
|
```js
|
||||||
|
Collection.addReducers({
|
||||||
|
reducer: {
|
||||||
|
body,
|
||||||
|
reduce(user, params) {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Be aware that this reducer may be used from any queries with different types of parameters.
|
||||||
|
|
||||||
|
## Reducers can do anything!
|
||||||
|
|
||||||
|
If we want to just receive the number of posts a user has, we can use reducers for this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Meteor.users.addReducers({
|
||||||
|
postCount: {
|
||||||
|
body: {_id: 1},
|
||||||
|
reduce(user) {
|
||||||
|
const linker = Users.getLink(user, 'posts');
|
||||||
|
|
||||||
|
return linker.find().count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you want to fetch some data from an external API:
|
||||||
|
|
||||||
|
Note that these reducers need to be defined server-side only, and they can only work with static queries.
|
||||||
|
|
||||||
|
```js
|
||||||
|
Projects.addReducers({
|
||||||
|
githubStars: {
|
||||||
|
body: {
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filtering by reducers
|
||||||
|
|
||||||
|
If you want to filter reducers you can use `$postFilters` or `$postFilter` special functions.
|
||||||
|
|
||||||
|
## 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!
|
108
docs/structure_and_patterns.md
Normal file
108
docs/structure_and_patterns.md
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# Structure and Patterns
|
||||||
|
|
||||||
|
These are a set of recommended ways to handle things, they do not enforce
|
||||||
|
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.
|
||||||
|
|
||||||
|
If you respect the patterns above you will avoid having the most common pitfalls with Grapher:
|
||||||
|
|
||||||
|
**Reducer/Links not working?**
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
## Fragments
|
||||||
|
|
||||||
|
You will find yourself requiring often same fields for Users, such as email, fullName, and maybe avatar.
|
||||||
|
|
||||||
|
For that let's create some fragments:
|
||||||
|
```js
|
||||||
|
// file: /imports/db/fragments/UserPublic.js
|
||||||
|
export default {
|
||||||
|
fullName: 1,
|
||||||
|
avatar: {
|
||||||
|
path: 1,
|
||||||
|
},
|
||||||
|
email: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// file: /imports/db/fragments/index.js
|
||||||
|
export {default as UserPublicFragment} from './UserPublicFields';
|
||||||
|
```
|
||||||
|
|
||||||
|
Now use it:
|
||||||
|
```js
|
||||||
|
import {UserPublicFragment} from '/imports/db/fragments';
|
||||||
|
Invoices.createQuery({
|
||||||
|
number: 1,
|
||||||
|
total: 1,
|
||||||
|
user: {
|
||||||
|
...UserPublicFragment,
|
||||||
|
billingInfo: {
|
||||||
|
// etc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also compose certain fragments:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import compose from 'meteor/cultofcoders:grapher';
|
||||||
|
import {
|
||||||
|
UserPublicFragment,
|
||||||
|
UserBillingFragment,
|
||||||
|
} from '/imports/db/fragments';
|
||||||
|
|
||||||
|
Invoices.createQuery({
|
||||||
|
number: 1,
|
||||||
|
total: 1,
|
||||||
|
user: {
|
||||||
|
...compose(
|
||||||
|
UserPublicFragment,
|
||||||
|
UserBillingFragment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Compose uses a deep extension, so it works how you expected to work, especially if some fragments have shared bodies.
|
||||||
|
|
||||||
|
## Scaling Reactivity
|
||||||
|
|
||||||
|
If you want to have highly scalable reactive queries, 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.
|
||||||
|
|
||||||
|
Sample:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default Messages.createQuery('messagesForThread', {
|
||||||
|
$filter({filters, options, params}) {
|
||||||
|
filters.threadId = params.threadId;
|
||||||
|
options.namespace = `thread::${params.threadId}`;
|
||||||
|
},
|
||||||
|
text: 1,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This ends our journey through Grapher. We hope you enjoyed, and that you are going to use it.
|
53
docs/table_of_contents.md
Normal file
53
docs/table_of_contents.md
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# 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.
|
|
@ -17,3 +17,7 @@ export {
|
||||||
export {
|
export {
|
||||||
default as NamedQuery
|
default as NamedQuery
|
||||||
} from './lib/namedQuery/namedQuery.client';
|
} from './lib/namedQuery/namedQuery.client';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as compose
|
||||||
|
} from './lib/compose';
|
||||||
|
|
|
@ -23,3 +23,11 @@ export {
|
||||||
export {
|
export {
|
||||||
default as MemoryResultCacher
|
default as MemoryResultCacher
|
||||||
} from './lib/namedQuery/cache/MemoryResultCacher';
|
} from './lib/namedQuery/cache/MemoryResultCacher';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as BaseResultCacher
|
||||||
|
} from './lib/namedQuery/cache/BaseResultCacher';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as compose
|
||||||
|
} from './lib/compose';
|
Loading…
Add table
Reference in a new issue