mirror of
https://github.com/vale981/grapher
synced 2025-03-04 09:01:40 -05:00
GraphQL Bridge initial implementation
This commit is contained in:
parent
1cf94bc8c2
commit
94a7acd0c0
22 changed files with 1167 additions and 235 deletions
44
README.md
44
README.md
|
@ -2,22 +2,27 @@
|
|||
|
||||
[](https://travis-ci.org/cult-of-coders/grapher)
|
||||
|
||||
*Grapher* is a Data Fetching Layer on top of Meteor and MongoDB. It is production ready and battle tested.
|
||||
_Grapher_ is a Data Fetching Layer on top of Meteor and MongoDB. It is production ready and battle tested.
|
||||
|
||||
Main features:
|
||||
- Innovative way to make MongoDB relational
|
||||
- Reactive data graphs for high availability
|
||||
- Incredible performance
|
||||
- Denormalization ability
|
||||
- Connection to external data sources
|
||||
- Usable from anywhere
|
||||
|
||||
* Innovative way to make MongoDB relational
|
||||
* Blends in with Apollo GraphQL making it highly performant
|
||||
* Reactive data graphs for high availability
|
||||
* Incredible performance
|
||||
* Denormalization ability
|
||||
* Connection to external data sources
|
||||
* Usable from anywhere
|
||||
|
||||
It marks a stepping stone into evolution of data, enabling developers to write complex and secure code,
|
||||
while maintaining the code base easy to understand.
|
||||
|
||||
Grapher 1.3 is LTS until 2024
|
||||
|
||||
[Read more about the GraphQL Bridge](docs/graphql.md)
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
meteor add cultofcoders:grapher
|
||||
```
|
||||
|
@ -34,42 +39,43 @@ Grapher cheatsheet, after you've learned it's powers this is the document will b
|
|||
|
||||
### Useful packages
|
||||
|
||||
- Live View: https://github.com/cult-of-coders/grapher-live
|
||||
- Graphical Grapher: https://github.com/Herteby/graphical-grapher
|
||||
- React HoC: https://github.com/cult-of-coders/grapher-react
|
||||
- VueJS: https://github.com/Herteby/grapher-vue
|
||||
* Live View: https://github.com/cult-of-coders/grapher-live
|
||||
* Graphical Grapher: https://github.com/Herteby/graphical-grapher
|
||||
* React HoC: https://github.com/cult-of-coders/grapher-react
|
||||
* VueJS: https://github.com/Herteby/grapher-vue
|
||||
|
||||
### Premium Support
|
||||
|
||||
If you are looking to integrate Grapher in your apps and want online or on-site consulting and training,
|
||||
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.
|
||||
|
||||
|
||||
### Quick Illustration
|
||||
|
||||
Query:
|
||||
|
||||
```js
|
||||
createQuery({
|
||||
posts: {
|
||||
title: 1,
|
||||
author: {
|
||||
fullName: 1
|
||||
fullName: 1,
|
||||
},
|
||||
comments: {
|
||||
text: 1,
|
||||
createdAt: 1,
|
||||
author: {
|
||||
fullName: 1
|
||||
}
|
||||
fullName: 1,
|
||||
},
|
||||
},
|
||||
categories: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
name: 1,
|
||||
},
|
||||
},
|
||||
}).fetch();
|
||||
```
|
||||
|
||||
Result:
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
|
|
165
docs/api.md
165
docs/api.md
|
@ -2,16 +2,14 @@
|
|||
|
||||
Use this as a cheatsheet after you have read the full documentation.
|
||||
|
||||
|
||||
- [Adding Links](#adding-links)
|
||||
- [Adding Reducers](#adding-reducers)
|
||||
- [Creating Named Queries](#creating-named-queries)
|
||||
- [Exposing Named Queries](#exposing-named-queries)
|
||||
- [Using Queries](#using-queries)
|
||||
- [Caching Named Queries](#caching-named-queries)
|
||||
- [Creating Global Queries](#creating-global-queries)
|
||||
- [Exposing Global Queries](#exposing-global-queries)
|
||||
|
||||
* [Adding Links](#adding-links)
|
||||
* [Adding Reducers](#adding-reducers)
|
||||
* [Creating Named Queries](#creating-named-queries)
|
||||
* [Exposing Named Queries](#exposing-named-queries)
|
||||
* [Using Queries](#using-queries)
|
||||
* [Caching Named Queries](#caching-named-queries)
|
||||
* [Creating Global Queries](#creating-global-queries)
|
||||
* [Exposing Global Queries](#exposing-global-queries)
|
||||
|
||||
### Adding Links
|
||||
|
||||
|
@ -25,20 +23,20 @@ Collection.addLinks({
|
|||
denormalize: {
|
||||
field, // String
|
||||
body, // Body from related collection
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Collection.addLinks({
|
||||
linkName: {
|
||||
collection, // Mongo.Collection
|
||||
inversedBy, // The link name from the other side
|
||||
inversedBy, // The link name from the other side
|
||||
denormalize: {
|
||||
field, // String
|
||||
body, // Body from related collection
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Reducers
|
||||
|
@ -49,26 +47,83 @@ Collection.addReducers({
|
|||
body, // Object, dependency graph
|
||||
compute(object) {
|
||||
// anything
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Named Queries
|
||||
|
||||
```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
|
||||
})
|
||||
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
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Creating GraphQL Queries
|
||||
|
||||
```js
|
||||
const Query = {
|
||||
users(_, args, context, ast) {
|
||||
const query = Users.astToQuery(ast, {
|
||||
// Manipulate the transformed body
|
||||
embody({body, getArgs}) {}
|
||||
|
||||
$filters, // Mongo Filters/Selector
|
||||
$options, // Mongo Options
|
||||
|
||||
// It will only allow you to query against this body graph
|
||||
// Meaning it won't allow fields outside, links outside, or deeper nested than the ones you specify
|
||||
intersect: Body,
|
||||
|
||||
// Useful when you don't have an intersection body, to restrict the limit of depth, to avoid a nested GraphQL attack
|
||||
maxDepth,
|
||||
|
||||
// Automatically enforces a maximum number of results
|
||||
maxLimit, // Integer
|
||||
|
||||
// Simply removes from the graph what fields it won't allow
|
||||
// Can work with deep strings like 'comments.author'
|
||||
deny, // String[]
|
||||
})
|
||||
|
||||
return query.fetch();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Setting global defaults for all `astToQuery` queries:
|
||||
|
||||
```js
|
||||
import { setAstToQueryDefaults } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
setAstToQueryDefaults({
|
||||
maxLimit: 100,
|
||||
maxDepth: 5,
|
||||
});
|
||||
```
|
||||
|
||||
Getting the db context to inject it:
|
||||
|
||||
```js
|
||||
import { db } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
// db.users
|
||||
// db.posts
|
||||
// db.${collectionName}
|
||||
```
|
||||
|
||||
### Exposing Named Queries
|
||||
|
@ -80,8 +135,8 @@ query.expose({
|
|||
publication, // Boolean
|
||||
unblock, // Boolean
|
||||
validateParams, // Function or Object
|
||||
embody // Object which extends the body server-side securely, or Function(body, params)
|
||||
})
|
||||
embody, // Object which extends the body server-side securely, or Function(body, params)
|
||||
});
|
||||
```
|
||||
|
||||
### Creating and Exposing Resolvers
|
||||
|
@ -95,7 +150,7 @@ query.expose({
|
|||
firewall, // Function or [Function]
|
||||
});
|
||||
|
||||
query.resolve(function (params) {
|
||||
query.resolve(function(params) {
|
||||
// this.userId
|
||||
return [];
|
||||
});
|
||||
|
@ -104,28 +159,31 @@ query.resolve(function (params) {
|
|||
### Using Queries
|
||||
|
||||
```js
|
||||
query.setParams({}) // extends current params
|
||||
query.setParams({}); // extends current params
|
||||
```
|
||||
|
||||
#### Server-Side
|
||||
|
||||
```js
|
||||
query.clone({params}).fetch();
|
||||
query.clone({params}).fetchOne();
|
||||
query.clone({params}).getCount();
|
||||
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) => {});
|
||||
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 query = userListQuery.clone({ params });
|
||||
|
||||
const handle = query.subscribe(); // handle.ready()
|
||||
const data = query.fetch();
|
||||
|
@ -136,13 +194,16 @@ const count = query.getCount();
|
|||
```
|
||||
|
||||
#### Caching Named Queries
|
||||
|
||||
```js
|
||||
import {MemoryResultCacher} from 'meteor/cultofcoders:grapher';
|
||||
import { MemoryResultCacher } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
// server-side
|
||||
query.cacheResults(new MemoryResultCacher({
|
||||
ttl: 60 * 1000, // 60 seconds
|
||||
}))
|
||||
query.cacheResults(
|
||||
new MemoryResultCacher({
|
||||
ttl: 60 * 1000, // 60 seconds
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
#### Creating Global Queries
|
||||
|
@ -151,26 +212,26 @@ query.cacheResults(new MemoryResultCacher({
|
|||
Collection.createQuery({
|
||||
$options, // Mongo Options {sort, limit, skip}
|
||||
$filters, // Mongo Filters
|
||||
$filter({filters, options, params}) {}, // Function or [Function]
|
||||
$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 Queries
|
||||
|
||||
```js
|
||||
Collection.expose({
|
||||
firewall(filters, options, userId) {}, // Function or [Function]
|
||||
firewall(filters, options, userId) {}, // Function or [Function]
|
||||
publication, // Boolean
|
||||
method, // Boolean
|
||||
blocking, // Boolean
|
||||
maxLimit, // Number
|
||||
maxLimit, // Number
|
||||
maxDepth, // Number
|
||||
restrictedFields, // [String]
|
||||
restrictLinks, // [String] or Function,
|
||||
body, // Object or Function(userId) => Object
|
||||
});
|
||||
```
|
||||
```
|
||||
|
|
92
docs/graphql.md
Normal file
92
docs/graphql.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
## GraphQL Bridge
|
||||
|
||||
You will start by installing [`cultofcoders:apollo`](https://github.com/cult-of-coders/apollo) package, which makes it super easy to get your barebones Meteor app up with GraphQL Services.
|
||||
|
||||
## Creating your Queries
|
||||
|
||||
The 4th argument that we receive inside the resolver, is the AST (Abstract Source Tree), which represents the query we receive. Based on that information, we can extract the Grapher's body, and we also added some very useful options, to enable security and customisation.
|
||||
|
||||
```js
|
||||
const Query = {
|
||||
users(_, args, context, ast) {
|
||||
return Users.astToQuery(ast, {
|
||||
// Manipulate the transformed body
|
||||
embody({body, getArgs}) {}
|
||||
|
||||
$filters, // Mongo Filters/Selector
|
||||
$options, // Mongo Options
|
||||
|
||||
// It will only allow you to query against this body graph
|
||||
// Meaning it won't allow fields outside, links outside, or deeper nested than the ones you specify
|
||||
intersect: Body,
|
||||
|
||||
// Useful when you don't have an intersection body, to restrict the limit of depth, to avoid a nested GraphQL attack
|
||||
maxDepth,
|
||||
|
||||
// Automatically enforces a maximum number of results
|
||||
maxLimit, // Integer
|
||||
|
||||
// Simply removes from the graph what fields it won't allow
|
||||
// Can work with deep strings like 'comments.author'
|
||||
deny, // String[]
|
||||
}).fetch();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mapping Fields
|
||||
|
||||
There may be scenarios where your database field is different from the field in the API you expose, Grapher treats that easily by exposing an `addFieldMap` function:
|
||||
|
||||
```js
|
||||
Users.addFieldMap({
|
||||
createdAt: 'created_at',
|
||||
});
|
||||
```
|
||||
|
||||
Meaning that the body received from GraphQL is going to properly handle the situation. What happens behind, basically, we create a reducer for that field.
|
||||
|
||||
## Global Config
|
||||
|
||||
Setting global defaults for all `astToQuery` queries:
|
||||
|
||||
```js
|
||||
import { setAstToQueryDefaults } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
setAstToQueryDefaults({
|
||||
maxLimit: 100,
|
||||
maxDepth: 5,
|
||||
});
|
||||
```
|
||||
|
||||
## Resolver's Context
|
||||
|
||||
```js
|
||||
import { db } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
// Inject db in your context
|
||||
db.users
|
||||
db.${collectionName}
|
||||
```
|
||||
|
||||
## GraphQL Directives
|
||||
|
||||
It would be nice if we could configure our directives directly inside GraphQL right? Something like this:
|
||||
|
||||
```js
|
||||
type User @mongo(name: "users") {
|
||||
comments: [Comment] @link(to: "user")
|
||||
}
|
||||
|
||||
type Comment @mongo(name: "comments") {
|
||||
user: User @link(field: "userId")
|
||||
post: Post @link(field: "commentId")
|
||||
createdAt: Date @map("created_at")
|
||||
}
|
||||
|
||||
type Post @mongo(name: "posts") {
|
||||
comments: [Comment] @link(to="post")
|
||||
}
|
||||
```
|
||||
|
||||
Find out more here: https://github.com/cult-of-coders/grapher-schema-directives
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
Let's learn Grapher. It's quite easy and it's time to change the way you think about your data.
|
||||
|
||||
Before we dive into Grapher you need to know to fundamentals of Meteor, but not mandatory, you can
|
||||
Before we dive into Grapher you need to know to fundamentals of Meteor, but not mandatory, you can
|
||||
learn the principles either way.
|
||||
|
||||
- http://www.meteor-tuts.com/chapters/1/intro.html
|
||||
- http://docs.meteor.com/api/collections.html
|
||||
- http://docs.meteor.com/api/methods.html
|
||||
- http://docs.meteor.com/api/pubsub.html
|
||||
* http://www.meteor-tuts.com/chapters/1/intro.html
|
||||
* http://docs.meteor.com/api/collections.html
|
||||
* http://docs.meteor.com/api/methods.html
|
||||
* http://docs.meteor.com/api/pubsub.html
|
||||
|
||||
### [Introduction](introduction.md)
|
||||
|
||||
|
@ -20,7 +20,7 @@ Learn the various ways of linking collections to each other.
|
|||
|
||||
### [Linker Engine](linker_engine.md)
|
||||
|
||||
Learn about how you can programatically set and fetch related data.
|
||||
Learn about how you can programatically set and fetch related data.
|
||||
|
||||
### [Query Options](query_options.md)
|
||||
|
||||
|
@ -39,7 +39,7 @@ Learn the right way to define and expose queries to the client in a secure manne
|
|||
Read about the tool that makes Grapher so performant.
|
||||
|
||||
### [Denormalization](denormalization.md)
|
||||
|
||||
|
||||
Learn how to denormalize your data to enable even more performance, and super advanced searching.
|
||||
|
||||
### [Caching Results](caching_results.md)
|
||||
|
@ -59,6 +59,10 @@ Learn about some good ways to structure your code some and about common pitfalls
|
|||
|
||||
If you want to use Grapher in a React Native app so from any other language as an API, it is possible
|
||||
|
||||
### [GraphQL Bridge](graphql.md)
|
||||
|
||||
If you want to use Grapher in a React Native app so from any other language as an API, it is possible
|
||||
|
||||
### [API](api.md)
|
||||
|
||||
After you've learned about Grapher, here's your cheatsheet.
|
||||
After you've learned about Grapher, here's your cheatsheet.
|
||||
|
|
11
lib/db.js
Normal file
11
lib/db.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Mongo } from 'meteor/mongo';
|
||||
|
||||
class DBProxy {
|
||||
get(key) {
|
||||
return Mongo.Collection.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new DBProxy();
|
||||
|
||||
export default db;
|
|
@ -1,11 +1,14 @@
|
|||
import Exposure from './exposure.js';
|
||||
|
||||
_.extend(Mongo.Collection.prototype, {
|
||||
Object.assign(Mongo.Collection.prototype, {
|
||||
expose(config) {
|
||||
if (!Meteor.isServer) {
|
||||
throw new Meteor.Error('not-allowed', `You can only expose a collection server side. ${this._name}`);
|
||||
throw new Meteor.Error(
|
||||
'not-allowed',
|
||||
`You can only expose a collection server side. ${this._name}`
|
||||
);
|
||||
}
|
||||
|
||||
new Exposure(this, config);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,5 +16,5 @@ _.extend(Mongo.Collection.prototype, {
|
|||
|
||||
return new Query(this, body, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
2
lib/graphql/index.js
Normal file
2
lib/graphql/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as astToBody } from './lib/astToBody';
|
||||
export { default as astToQuery } from './lib/astToQuery';
|
32
lib/graphql/lib/astToBody.js
Normal file
32
lib/graphql/lib/astToBody.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
export const Symbols = {
|
||||
ARGUMENTS: Symbol('arguments'),
|
||||
};
|
||||
|
||||
export default function astToBody(ast) {
|
||||
const fieldNodes = ast.fieldNodes;
|
||||
|
||||
const body = extractSelectionSet(ast.fieldNodes[0].selectionSet);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function extractSelectionSet(set) {
|
||||
let body = {};
|
||||
set.selections.forEach(el => {
|
||||
if (!el.selectionSet) {
|
||||
body[el.name.value] = 1;
|
||||
} else {
|
||||
body[el.name.value] = extractSelectionSet(el.selectionSet);
|
||||
if (el.arguments.length) {
|
||||
let argumentMap = {};
|
||||
el.arguments.forEach(arg => {
|
||||
argumentMap[arg.name.value] = arg.value.value;
|
||||
});
|
||||
|
||||
body[el.name.value][Symbols.ARGUMENTS] = argumentMap;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return body;
|
||||
}
|
148
lib/graphql/lib/astToQuery.js
Normal file
148
lib/graphql/lib/astToQuery.js
Normal file
|
@ -0,0 +1,148 @@
|
|||
import { check, Match } from 'meteor/check';
|
||||
import astToBody, { Symbols } from './astToBody';
|
||||
import defaults from './defaults';
|
||||
import intersectDeep from '../../query/lib/intersectDeep';
|
||||
import enforceMaxLimit from '../../exposure/lib/enforceMaxLimit';
|
||||
|
||||
const Errors = {
|
||||
MAX_DEPTH: 'The maximum depth of this request exceeds the depth allowed.',
|
||||
};
|
||||
|
||||
export default function astToQuery(ast, config = {}) {
|
||||
const collection = this;
|
||||
|
||||
check(config, {
|
||||
embody: Match.Maybe(Function),
|
||||
$filters: Match.Maybe(Object),
|
||||
$options: Match.Maybe(Object),
|
||||
maxDepth: Match.Maybe(Number),
|
||||
maxLimit: Match.Maybe(Number),
|
||||
deny: Match.Maybe([String]),
|
||||
intersect: Match.Maybe(Object),
|
||||
});
|
||||
|
||||
config = Object.assign(
|
||||
{
|
||||
$options: {},
|
||||
$filters: {},
|
||||
},
|
||||
defaults,
|
||||
config
|
||||
);
|
||||
|
||||
// get the body
|
||||
let body = astToBody(ast);
|
||||
|
||||
// first we do the intersection
|
||||
if (config.intersect) {
|
||||
body = intersectDeep(config.intersect, body);
|
||||
}
|
||||
|
||||
// enforce the maximum amount of data we allow to retrieve
|
||||
if (config.maxLimit) {
|
||||
enforceMaxLimit(config.$options, config.maxLimit);
|
||||
}
|
||||
|
||||
// figure out depth based
|
||||
if (config.maxDepth) {
|
||||
const currentMaxDepth = getMaxDepth(body);
|
||||
if (currentMaxDepth > config.maxDepth) {
|
||||
throw Errors.MAX_DEPTH;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.deny) {
|
||||
deny(body, config.deny);
|
||||
}
|
||||
|
||||
Object.assign(body, {
|
||||
$filters,
|
||||
$options,
|
||||
});
|
||||
|
||||
if (config.embody) {
|
||||
const getArgs = createGetArgs(body);
|
||||
config.embody.call(null, {
|
||||
body,
|
||||
getArgs,
|
||||
});
|
||||
}
|
||||
|
||||
// we return the query
|
||||
return this.createQuery(body);
|
||||
}
|
||||
|
||||
export function getMaxDepth(body) {
|
||||
let depths = [];
|
||||
for (key in body) {
|
||||
if (_.isObject(body[key])) {
|
||||
depths.push(getMaxDepth(body[key]));
|
||||
}
|
||||
}
|
||||
|
||||
if (depths.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(...depths) + 1;
|
||||
}
|
||||
|
||||
export function deny(body, fields) {
|
||||
fields.forEach(field => {
|
||||
let parts = field.split('.');
|
||||
let accessor = body;
|
||||
while (parts.length != 0) {
|
||||
if (parts.length === 1) {
|
||||
delete accessor[parts[0]];
|
||||
} else {
|
||||
if (!_.isObject(accessor)) {
|
||||
break;
|
||||
}
|
||||
accessor = accessor[parts[0]];
|
||||
}
|
||||
parts.shift();
|
||||
}
|
||||
});
|
||||
|
||||
return clearEmptyObjects(body);
|
||||
}
|
||||
|
||||
export function clearEmptyObjects(body) {
|
||||
// clear empty nodes then back-propagate
|
||||
for (let key in body) {
|
||||
if (_.isObject(body[key])) {
|
||||
const shouldDelete = clearEmptyObjects(body[key]);
|
||||
if (shouldDelete) {
|
||||
delete body[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(body).length === 0;
|
||||
}
|
||||
|
||||
export function createGetArgs(body) {
|
||||
return function(path) {
|
||||
const parts = path.split('.');
|
||||
let stopped = false;
|
||||
let accessor = body;
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
if (!accessor) {
|
||||
stopped = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (accessor[parts[i]]) {
|
||||
accessor = accessor[parts[i]];
|
||||
}
|
||||
}
|
||||
|
||||
if (stopped) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (accessor) {
|
||||
return accessor[Symbols.ARGUMENTS] || {};
|
||||
}
|
||||
};
|
||||
}
|
7
lib/graphql/lib/defaults.js
Normal file
7
lib/graphql/lib/defaults.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
let defaults = {};
|
||||
|
||||
export default defaults;
|
||||
|
||||
export function setAstToQueryDefaults(object) {
|
||||
Object.assign(defaults, object);
|
||||
}
|
343
lib/graphql/testing/ast.js
Normal file
343
lib/graphql/testing/ast.js
Normal file
|
@ -0,0 +1,343 @@
|
|||
export default {
|
||||
fieldName: 'users',
|
||||
fieldNodes: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: { kind: 'Name', value: 'users', loc: { start: 6, end: 11 } },
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'fullname',
|
||||
loc: { start: 12, end: 20 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
loc: { start: 12, end: 20 },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'comments',
|
||||
loc: { start: 21, end: 29 },
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'approved',
|
||||
loc: { start: 30, end: 38 },
|
||||
},
|
||||
value: {
|
||||
kind: 'BooleanValue',
|
||||
value: true,
|
||||
loc: { start: 39, end: 43 },
|
||||
},
|
||||
loc: { start: 30, end: 43 },
|
||||
},
|
||||
],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'text',
|
||||
loc: { start: 45, end: 49 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
loc: { start: 45, end: 49 },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'user',
|
||||
loc: { start: 50, end: 54 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'firstname',
|
||||
loc: { start: 55, end: 64 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
loc: { start: 55, end: 64 },
|
||||
},
|
||||
],
|
||||
loc: { start: 54, end: 65 },
|
||||
},
|
||||
loc: { start: 50, end: 65 },
|
||||
},
|
||||
],
|
||||
loc: { start: 44, end: 66 },
|
||||
},
|
||||
loc: { start: 21, end: 66 },
|
||||
},
|
||||
],
|
||||
loc: { start: 11, end: 67 },
|
||||
},
|
||||
loc: { start: 6, end: 67 },
|
||||
},
|
||||
],
|
||||
returnType: '[User]',
|
||||
parentType: 'Query',
|
||||
path: { key: 'users' },
|
||||
schema: {
|
||||
_queryType: 'Query',
|
||||
_mutationType: 'Mutation',
|
||||
_subscriptionType: 'Subscription',
|
||||
_directives: [
|
||||
{
|
||||
name: 'skip',
|
||||
description:
|
||||
'Directs the executor to skip this field or fragment when the `if` argument is true.',
|
||||
locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'],
|
||||
args: [
|
||||
{
|
||||
name: 'if',
|
||||
description: 'Skipped when true.',
|
||||
type: 'Boolean!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'include',
|
||||
description:
|
||||
'Directs the executor to include this field or fragment only when the `if` argument is true.',
|
||||
locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'],
|
||||
args: [
|
||||
{
|
||||
name: 'if',
|
||||
description: 'Included when true.',
|
||||
type: 'Boolean!',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'deprecated',
|
||||
description:
|
||||
'Marks an element of a GraphQL schema as no longer supported.',
|
||||
locations: ['FIELD_DEFINITION', 'ENUM_VALUE'],
|
||||
args: [
|
||||
{
|
||||
name: 'reason',
|
||||
description:
|
||||
'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).',
|
||||
type: 'String',
|
||||
defaultValue: 'No longer supported',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
astNode: {
|
||||
kind: 'SchemaDefinition',
|
||||
directives: [],
|
||||
operationTypes: [
|
||||
{
|
||||
kind: 'OperationTypeDefinition',
|
||||
operation: 'query',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'Query',
|
||||
loc: { start: 18, end: 23 },
|
||||
},
|
||||
loc: { start: 18, end: 23 },
|
||||
},
|
||||
loc: { start: 11, end: 23 },
|
||||
},
|
||||
{
|
||||
kind: 'OperationTypeDefinition',
|
||||
operation: 'mutation',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'Mutation',
|
||||
loc: { start: 36, end: 44 },
|
||||
},
|
||||
loc: { start: 36, end: 44 },
|
||||
},
|
||||
loc: { start: 26, end: 44 },
|
||||
},
|
||||
{
|
||||
kind: 'OperationTypeDefinition',
|
||||
operation: 'subscription',
|
||||
type: {
|
||||
kind: 'NamedType',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'Subscription',
|
||||
loc: { start: 61, end: 73 },
|
||||
},
|
||||
loc: { start: 61, end: 73 },
|
||||
},
|
||||
loc: { start: 47, end: 73 },
|
||||
},
|
||||
],
|
||||
loc: { start: 0, end: 75 },
|
||||
},
|
||||
_typeMap: {
|
||||
Query: 'Query',
|
||||
User: 'User',
|
||||
String: 'String',
|
||||
Boolean: 'Boolean',
|
||||
Comment: 'Comment',
|
||||
Mutation: 'Mutation',
|
||||
Subscription: 'Subscription',
|
||||
ReactiveEvent: 'ReactiveEvent',
|
||||
ID: 'ID',
|
||||
JSON: 'JSON',
|
||||
__Schema: '__Schema',
|
||||
__Type: '__Type',
|
||||
__TypeKind: '__TypeKind',
|
||||
__Field: '__Field',
|
||||
__InputValue: '__InputValue',
|
||||
__EnumValue: '__EnumValue',
|
||||
__Directive: '__Directive',
|
||||
__DirectiveLocation: '__DirectiveLocation',
|
||||
Date: 'Date',
|
||||
},
|
||||
_implementations: {},
|
||||
__validationErrors: [],
|
||||
},
|
||||
fragments: {},
|
||||
operation: {
|
||||
kind: 'OperationDefinition',
|
||||
operation: 'query',
|
||||
variableDefinitions: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'users',
|
||||
loc: { start: 6, end: 11 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'fullname',
|
||||
loc: { start: 12, end: 20 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
loc: { start: 12, end: 20 },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'comments',
|
||||
loc: { start: 21, end: 29 },
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
kind: 'Argument',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'approved',
|
||||
loc: { start: 30, end: 38 },
|
||||
},
|
||||
value: {
|
||||
kind: 'BooleanValue',
|
||||
value: true,
|
||||
loc: { start: 39, end: 43 },
|
||||
},
|
||||
loc: { start: 30, end: 43 },
|
||||
},
|
||||
],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'text',
|
||||
loc: { start: 45, end: 49 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
loc: { start: 45, end: 49 },
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'user',
|
||||
loc: { start: 50, end: 54 },
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'firstname',
|
||||
loc: {
|
||||
start: 55,
|
||||
end: 64,
|
||||
},
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
loc: {
|
||||
start: 55,
|
||||
end: 64,
|
||||
},
|
||||
},
|
||||
],
|
||||
loc: { start: 54, end: 65 },
|
||||
},
|
||||
loc: { start: 50, end: 65 },
|
||||
},
|
||||
],
|
||||
loc: { start: 44, end: 66 },
|
||||
},
|
||||
loc: { start: 21, end: 66 },
|
||||
},
|
||||
],
|
||||
loc: { start: 11, end: 67 },
|
||||
},
|
||||
loc: { start: 6, end: 67 },
|
||||
},
|
||||
],
|
||||
loc: { start: 5, end: 68 },
|
||||
},
|
||||
loc: { start: 0, end: 68 },
|
||||
},
|
||||
variableValues: {},
|
||||
};
|
18
lib/graphql/testing/astToBody.test.js
Normal file
18
lib/graphql/testing/astToBody.test.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import astToBody, { Symbols } from '../lib/astToBody';
|
||||
import ast from './ast.js';
|
||||
|
||||
describe('#astToBody', function() {
|
||||
it('Should properly parse the body with arguments and such', function() {
|
||||
const result = astToBody(ast);
|
||||
|
||||
assert.equal(result.fullname, 1);
|
||||
assert.isObject(result.comments);
|
||||
|
||||
const commentArgs = result.comments[Symbols.ARGUMENTS];
|
||||
assert.equal(commentArgs.approved, true);
|
||||
|
||||
assert.equal(result.comments.text, 1);
|
||||
assert.isObject(result.comments.user);
|
||||
assert.equal(result.comments.user.firstname, 1);
|
||||
});
|
||||
});
|
131
lib/graphql/testing/astToQuery.test.js
Normal file
131
lib/graphql/testing/astToQuery.test.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
import { Symbols } from '../lib/astToBody';
|
||||
|
||||
import astToQuery, {
|
||||
deny,
|
||||
createGetArgs,
|
||||
getMaxDepth,
|
||||
} from '../lib/astToQuery';
|
||||
|
||||
describe('#astToQuery', function() {
|
||||
it('#createGetArgs', function() {
|
||||
const getArgs = createGetArgs({
|
||||
a: {
|
||||
[Symbols.ARGUMENTS]: { approved: false },
|
||||
b: {
|
||||
[Symbols.ARGUMENTS]: { approved: true },
|
||||
c: {
|
||||
[Symbols.ARGUMENTS]: { approved: true },
|
||||
},
|
||||
d: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.isFalse(getArgs('a').approved);
|
||||
assert.isTrue(getArgs('a.b').approved);
|
||||
assert.isTrue(getArgs('a.b.c').approved);
|
||||
assert.isTrue(Object.keys(getArgs('a.b.d')).length === 0);
|
||||
});
|
||||
it('#deny', function() {
|
||||
let body = {
|
||||
test: 1,
|
||||
testDeny: 1,
|
||||
nested: {
|
||||
testDeny: 1,
|
||||
testAllow: 1,
|
||||
},
|
||||
nestedEmpty: {
|
||||
disallow: 1,
|
||||
},
|
||||
nestedDeny: {
|
||||
a: 1,
|
||||
b: 1,
|
||||
c: 1,
|
||||
},
|
||||
heavy: {
|
||||
nest: {
|
||||
ting: {
|
||||
wup: {
|
||||
denyThis: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
deny(body, [
|
||||
'testDeny',
|
||||
'nested.testDeny',
|
||||
'nestedEmpty.disallow',
|
||||
'nestedDeny',
|
||||
'heavy.nest.ting.wup.denyThis',
|
||||
]);
|
||||
|
||||
assert.isDefined(body.test);
|
||||
assert.isUndefined(body.testDeny);
|
||||
assert.isDefined(body.nested.testAllow);
|
||||
assert.isUndefined(body.nested.testDeny);
|
||||
assert.isUndefined(body.nestedDeny);
|
||||
assert.isUndefined(body.heavy);
|
||||
});
|
||||
|
||||
it('#getMaxDepth', function() {
|
||||
let body = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
};
|
||||
|
||||
assert.equal(getMaxDepth(body), 1);
|
||||
|
||||
body = {
|
||||
a: {
|
||||
b: 1,
|
||||
},
|
||||
b: 1,
|
||||
};
|
||||
|
||||
assert.equal(getMaxDepth(body), 2);
|
||||
|
||||
body = {
|
||||
a: {
|
||||
b: 1,
|
||||
c: {
|
||||
d: {
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
b: 1,
|
||||
c: {
|
||||
a: 1,
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(getMaxDepth(body), 4);
|
||||
|
||||
body = {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(getMaxDepth(body), 6);
|
||||
});
|
||||
});
|
2
lib/graphql/testing/index.js
Normal file
2
lib/graphql/testing/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import './astToBody.test';
|
||||
import './astToQuery.test';
|
|
@ -1,8 +1,8 @@
|
|||
import { Mongo } from 'meteor/mongo';
|
||||
import {LINK_STORAGE} from './constants.js';
|
||||
import { LINK_STORAGE } from './constants.js';
|
||||
import Linker from './linker.js';
|
||||
|
||||
_.extend(Mongo.Collection.prototype, {
|
||||
Object.assign(Mongo.Collection.prototype, {
|
||||
/**
|
||||
* The data we add should be valid for config.schema.js
|
||||
*/
|
||||
|
@ -13,17 +13,21 @@ _.extend(Mongo.Collection.prototype, {
|
|||
|
||||
_.each(data, (linkConfig, linkName) => {
|
||||
if (this[LINK_STORAGE][linkName]) {
|
||||
throw new Meteor.Error(`You cannot add the link with name: ${linkName} because it was already added to ${this._name} collection`)
|
||||
throw new Meteor.Error(
|
||||
`You cannot add the link with name: ${linkName} because it was already added to ${
|
||||
this._name
|
||||
} collection`
|
||||
);
|
||||
}
|
||||
|
||||
const linker = new Linker(this, linkName, linkConfig);
|
||||
|
||||
_.extend(this[LINK_STORAGE], {
|
||||
[linkName]: linker
|
||||
[linkName]: linker,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getLinks() {
|
||||
return this[LINK_STORAGE];
|
||||
},
|
||||
|
@ -46,32 +50,39 @@ _.extend(Mongo.Collection.prototype, {
|
|||
let linkData = this[LINK_STORAGE];
|
||||
|
||||
if (!linkData) {
|
||||
throw new Meteor.Error(`There are no links defined for collection: ${this._name}`);
|
||||
throw new Meteor.Error(
|
||||
`There are no links defined for collection: ${this._name}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!linkData[name]) {
|
||||
throw new Meteor.Error(`There is no link ${name} for collection: ${this._name}`);
|
||||
throw new Meteor.Error(
|
||||
`There is no link ${name} for collection: ${this._name}`
|
||||
);
|
||||
}
|
||||
|
||||
const linker = linkData[name];
|
||||
let object = objectOrId;
|
||||
if (typeof(objectOrId) == 'string') {
|
||||
if (typeof objectOrId == 'string') {
|
||||
if (!linker.isVirtual()) {
|
||||
object = this.findOne(objectOrId, {
|
||||
fields: {
|
||||
[linker.linkStorageField]: 1
|
||||
}
|
||||
[linker.linkStorageField]: 1,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
object = {_id: objectOrId};
|
||||
object = { _id: objectOrId };
|
||||
}
|
||||
|
||||
if (!object) {
|
||||
throw new Meteor.Error(`We could not find any object with _id: "${objectOrId}" within the collection: ${this._name}`);
|
||||
throw new Meteor.Error(
|
||||
`We could not find any object with _id: "${objectOrId}" within the collection: ${
|
||||
this._name
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return linkData[name].createLink(object);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -4,89 +4,88 @@
|
|||
// CommentCollection,
|
||||
// ResolverCollection
|
||||
//} from './collections.js';
|
||||
|
||||
let PostCollection = new Mongo.Collection('test_post');
|
||||
let CategoryCollection = new Mongo.Collection('test_category');
|
||||
let CommentCollection = new Mongo.Collection('test_comment');
|
||||
let ResolverCollection = new Mongo.Collection('test_resolver');
|
||||
|
||||
PostCollection.addLinks({
|
||||
'comments': {
|
||||
comments: {
|
||||
type: '*',
|
||||
collection: CommentCollection,
|
||||
field: 'commentIds',
|
||||
index: true
|
||||
index: true,
|
||||
},
|
||||
'autoRemoveComments': {
|
||||
autoRemoveComments: {
|
||||
type: '*',
|
||||
collection: CommentCollection,
|
||||
field: 'autoRemoveCommentIds',
|
||||
autoremove: true
|
||||
autoremove: true,
|
||||
},
|
||||
'autoRemovingSelfComments': {
|
||||
autoRemovingSelfComments: {
|
||||
type: '*',
|
||||
collection: CommentCollection,
|
||||
field: 'autoRemovingSelfCommentsIds',
|
||||
},
|
||||
'metaComments': {
|
||||
metaComments: {
|
||||
type: '*',
|
||||
collection: CommentCollection,
|
||||
metadata: true
|
||||
metadata: true,
|
||||
},
|
||||
category: {
|
||||
collection: CategoryCollection,
|
||||
type: '1'
|
||||
type: '1',
|
||||
},
|
||||
metaCategory: {
|
||||
metadata: true,
|
||||
collection: CategoryCollection,
|
||||
type: '1'
|
||||
type: '1',
|
||||
},
|
||||
inversedComment: {
|
||||
collection: CommentCollection,
|
||||
inversedBy: 'inversedPost'
|
||||
}
|
||||
inversedBy: 'inversedPost',
|
||||
},
|
||||
});
|
||||
|
||||
CommentCollection.addLinks({
|
||||
post: {
|
||||
collection: PostCollection,
|
||||
inversedBy: 'comments'
|
||||
inversedBy: 'comments',
|
||||
},
|
||||
inversedPost: {
|
||||
collection: PostCollection,
|
||||
field: 'postId'
|
||||
field: 'postId',
|
||||
},
|
||||
autoRemovePosts: {
|
||||
collection: PostCollection,
|
||||
inversedBy: 'autoRemovingSelfComments',
|
||||
autoremove: true
|
||||
autoremove: true,
|
||||
},
|
||||
metaPost: {
|
||||
collection: PostCollection,
|
||||
inversedBy: 'metaComments'
|
||||
}
|
||||
inversedBy: 'metaComments',
|
||||
},
|
||||
});
|
||||
|
||||
CategoryCollection.addLinks({
|
||||
'posts': {
|
||||
posts: {
|
||||
collection: PostCollection,
|
||||
inversedBy: 'category'
|
||||
inversedBy: 'category',
|
||||
},
|
||||
'metaPosts': {
|
||||
metaPosts: {
|
||||
collection: PostCollection,
|
||||
inversedBy: 'metaCategory'
|
||||
}
|
||||
inversedBy: 'metaCategory',
|
||||
},
|
||||
});
|
||||
|
||||
describe('Collection Links', function () {
|
||||
describe('Collection Links', function() {
|
||||
PostCollection.remove({});
|
||||
CategoryCollection.remove({});
|
||||
CommentCollection.remove({});
|
||||
|
||||
it('Test Many', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let commentId = CommentCollection.insert({'text': 'abc'});
|
||||
it('Test Many', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let commentId = CommentCollection.insert({ text: 'abc' });
|
||||
|
||||
let post = PostCollection.findOne(postId);
|
||||
const link = PostCollection.getLink(post, 'comments');
|
||||
|
@ -97,9 +96,9 @@ describe('Collection Links', function () {
|
|||
assert.lengthOf(link.find().fetch(), 0);
|
||||
});
|
||||
|
||||
it('Tests One', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let categoryId = CategoryCollection.insert({'text': 'abc'});
|
||||
it('Tests One', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let categoryId = CategoryCollection.insert({ text: 'abc' });
|
||||
|
||||
let post = PostCollection.findOne(postId);
|
||||
|
||||
|
@ -113,14 +112,14 @@ describe('Collection Links', function () {
|
|||
assert.lengthOf(link.find().fetch(), 0);
|
||||
});
|
||||
|
||||
it('Tests One Meta', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let categoryId = CategoryCollection.insert({'text': 'abc'});
|
||||
it('Tests One Meta', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let categoryId = CategoryCollection.insert({ text: 'abc' });
|
||||
|
||||
let post = PostCollection.findOne(postId);
|
||||
|
||||
let link = PostCollection.getLink(post, 'metaCategory');
|
||||
link.set(categoryId, {date: new Date()});
|
||||
link.set(categoryId, { date: new Date() });
|
||||
|
||||
assert.lengthOf(link.find().fetch(), 1);
|
||||
let metadata = link.metadata();
|
||||
|
@ -129,7 +128,7 @@ describe('Collection Links', function () {
|
|||
assert.instanceOf(metadata.date, Date);
|
||||
|
||||
link.metadata({
|
||||
updated: new Date()
|
||||
updated: new Date(),
|
||||
});
|
||||
|
||||
post = PostCollection.findOne(postId);
|
||||
|
@ -140,14 +139,14 @@ describe('Collection Links', function () {
|
|||
assert.lengthOf(link.find().fetch(), 0);
|
||||
});
|
||||
|
||||
it('Tests Many Meta', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let commentId = CommentCollection.insert({'text': 'abc'});
|
||||
it('Tests Many Meta', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let commentId = CommentCollection.insert({ text: 'abc' });
|
||||
|
||||
let post = PostCollection.findOne(postId);
|
||||
let metaCommentsLink = PostCollection.getLink(post, 'metaComments');
|
||||
|
||||
metaCommentsLink.add(commentId, {date: new Date});
|
||||
metaCommentsLink.add(commentId, { date: new Date() });
|
||||
assert.lengthOf(metaCommentsLink.find().fetch(), 1);
|
||||
|
||||
// verifying reverse search
|
||||
|
@ -160,7 +159,7 @@ describe('Collection Links', function () {
|
|||
assert.isObject(metadata);
|
||||
assert.instanceOf(metadata.date, Date);
|
||||
|
||||
metaCommentsLink.metadata(commentId, {updated: new Date});
|
||||
metaCommentsLink.metadata(commentId, { updated: new Date() });
|
||||
|
||||
post = PostCollection.findOne(postId);
|
||||
metaCommentsLink = PostCollection.getLink(post, 'metaComments');
|
||||
|
@ -172,62 +171,74 @@ describe('Collection Links', function () {
|
|||
assert.lengthOf(metaCommentsLink.find().fetch(), 0);
|
||||
});
|
||||
|
||||
it('Tests $meta filters for One & One-Virtual', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let categoryId = CategoryCollection.insert({'text': 'abc'});
|
||||
it('Tests $meta filters for One & One-Virtual', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let categoryId = CategoryCollection.insert({ text: 'abc' });
|
||||
let post = PostCollection.findOne(postId);
|
||||
let postMetaCategoryLink = PostCollection.getLink(post, 'metaCategory');
|
||||
postMetaCategoryLink.set(categoryId, {valid: true});
|
||||
postMetaCategoryLink.set(categoryId, { valid: true });
|
||||
|
||||
let result = postMetaCategoryLink.fetch({$meta: {valid: true}});
|
||||
let result = postMetaCategoryLink.fetch({ $meta: { valid: true } });
|
||||
assert.isObject(result);
|
||||
|
||||
result = postMetaCategoryLink.fetch({$meta: {valid: false}});
|
||||
result = postMetaCategoryLink.fetch({ $meta: { valid: false } });
|
||||
|
||||
assert.isUndefined(result);
|
||||
const metaCategoryPostLink = CategoryCollection.getLink(categoryId, 'metaPosts');
|
||||
const metaCategoryPostLink = CategoryCollection.getLink(
|
||||
categoryId,
|
||||
'metaPosts'
|
||||
);
|
||||
|
||||
result = metaCategoryPostLink.fetch({$meta: {valid: true}});
|
||||
result = metaCategoryPostLink.fetch({ $meta: { valid: true } });
|
||||
assert.lengthOf(result, 1);
|
||||
|
||||
result = metaCategoryPostLink.fetch({$meta: {valid: false}});
|
||||
result = metaCategoryPostLink.fetch({ $meta: { valid: false } });
|
||||
assert.lengthOf(result, 0);
|
||||
});
|
||||
|
||||
it('Tests $meta filters for Many & Many-Virtual', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let commentId1 = CommentCollection.insert({'text': 'abc'});
|
||||
let commentId2 = CommentCollection.insert({'text': 'abc'});
|
||||
it('Tests $meta filters for Many & Many-Virtual', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let commentId1 = CommentCollection.insert({ text: 'abc' });
|
||||
let commentId2 = CommentCollection.insert({ text: 'abc' });
|
||||
|
||||
let postMetaCommentsLink = PostCollection.getLink(postId, 'metaComments');
|
||||
let postMetaCommentsLink = PostCollection.getLink(
|
||||
postId,
|
||||
'metaComments'
|
||||
);
|
||||
|
||||
postMetaCommentsLink.add(commentId1, {approved: true});
|
||||
postMetaCommentsLink.add(commentId2, {approved: false});
|
||||
postMetaCommentsLink.add(commentId1, { approved: true });
|
||||
postMetaCommentsLink.add(commentId2, { approved: false });
|
||||
|
||||
let result = postMetaCommentsLink.fetch({$meta: {approved: true}});
|
||||
let result = postMetaCommentsLink.fetch({ $meta: { approved: true } });
|
||||
|
||||
assert.lengthOf(result, 1);
|
||||
|
||||
result = postMetaCommentsLink.fetch({$meta: {approved: false}});
|
||||
result = postMetaCommentsLink.fetch({ $meta: { approved: false } });
|
||||
|
||||
assert.lengthOf(result, 1);
|
||||
|
||||
const comment1MetaPostsLink = CommentCollection.getLink(commentId1, 'metaPost');
|
||||
result = comment1MetaPostsLink.fetch({$meta: {approved: true}});
|
||||
const comment1MetaPostsLink = CommentCollection.getLink(
|
||||
commentId1,
|
||||
'metaPost'
|
||||
);
|
||||
result = comment1MetaPostsLink.fetch({ $meta: { approved: true } });
|
||||
assert.lengthOf(result, 1);
|
||||
result = comment1MetaPostsLink.fetch({$meta: {approved: false}});
|
||||
result = comment1MetaPostsLink.fetch({ $meta: { approved: false } });
|
||||
assert.lengthOf(result, 0);
|
||||
|
||||
const comment2MetaPostsLink = CommentCollection.getLink(commentId2, 'metaPost');
|
||||
result = comment2MetaPostsLink.fetch({$meta: {approved: true}});
|
||||
const comment2MetaPostsLink = CommentCollection.getLink(
|
||||
commentId2,
|
||||
'metaPost'
|
||||
);
|
||||
result = comment2MetaPostsLink.fetch({ $meta: { approved: true } });
|
||||
assert.lengthOf(result, 0);
|
||||
result = comment2MetaPostsLink.fetch({$meta: {approved: false}});
|
||||
result = comment2MetaPostsLink.fetch({ $meta: { approved: false } });
|
||||
assert.lengthOf(result, 1);
|
||||
});
|
||||
|
||||
it('Tests inversedBy findings', function () {
|
||||
let postId = PostCollection.insert({'text': 'abc'});
|
||||
let commentId = CommentCollection.insert({'text': 'abc'});
|
||||
it('Tests inversedBy findings', function() {
|
||||
let postId = PostCollection.insert({ text: 'abc' });
|
||||
let commentId = CommentCollection.insert({ text: 'abc' });
|
||||
|
||||
let post = PostCollection.findOne(postId);
|
||||
let comment = CommentCollection.findOne(commentId);
|
||||
|
@ -246,17 +257,19 @@ describe('Collection Links', function () {
|
|||
assert.notInclude(post.commentIds, comment._id);
|
||||
});
|
||||
|
||||
it ('Should auto-save object', function () {
|
||||
let comment = {text: 'abc'};
|
||||
it('Should auto-save object', function() {
|
||||
let comment = { text: 'abc' };
|
||||
|
||||
let postId = PostCollection.insert({text: 'hello'});
|
||||
const postLink = PostCollection.getLink(postId, 'comments').add(comment);
|
||||
let postId = PostCollection.insert({ text: 'hello' });
|
||||
const postLink = PostCollection.getLink(postId, 'comments').add(
|
||||
comment
|
||||
);
|
||||
|
||||
assert.isDefined(comment._id);
|
||||
assert.lengthOf(postLink.fetch(), 1);
|
||||
});
|
||||
|
||||
it ('Should have indexes set up', function () {
|
||||
it('Should have indexes set up', function() {
|
||||
const raw = PostCollection.rawCollection();
|
||||
const indexes = Meteor.wrapAsync(raw.indexes, raw)();
|
||||
|
||||
|
@ -267,58 +280,77 @@ describe('Collection Links', function () {
|
|||
assert.isObject(found);
|
||||
});
|
||||
|
||||
it ('Should auto-remove some objects', function () {
|
||||
let comment = {text: 'abc'};
|
||||
it('Should auto-remove some objects', function() {
|
||||
let comment = { text: 'abc' };
|
||||
|
||||
let postId = PostCollection.insert({text: 'hello'});
|
||||
let postId = PostCollection.insert({ text: 'hello' });
|
||||
let postLink = PostCollection.getLink(postId, 'comments').add(comment);
|
||||
|
||||
assert.isNotNull(comment._id);
|
||||
PostCollection.remove(postId);
|
||||
assert.isNotNull(CommentCollection.findOne(comment._id));
|
||||
|
||||
comment = {text: 'abc'};
|
||||
postId = PostCollection.insert({text: 'hello'});
|
||||
postLink = PostCollection.getLink(postId, 'autoRemoveComments').add(comment);
|
||||
comment = { text: 'abc' };
|
||||
postId = PostCollection.insert({ text: 'hello' });
|
||||
postLink = PostCollection.getLink(postId, 'autoRemoveComments').add(
|
||||
comment
|
||||
);
|
||||
|
||||
assert.isDefined(comment._id);
|
||||
PostCollection.remove(postId);
|
||||
assert.isUndefined(CommentCollection.findOne(comment._id));
|
||||
});
|
||||
|
||||
it('Should allow actions from inversed links', function () {
|
||||
let comment = {text: 'abc'};
|
||||
it('Should allow actions from inversed links', function() {
|
||||
let comment = { text: 'abc' };
|
||||
|
||||
let postId = PostCollection.insert({text: 'hello'});
|
||||
let postId = PostCollection.insert({ text: 'hello' });
|
||||
const commentId = CommentCollection.insert(comment);
|
||||
|
||||
CommentCollection.getLink(commentId, 'post').set(postId);
|
||||
|
||||
assert.lengthOf(PostCollection.getLink(postId, 'comments').fetch(), 1);
|
||||
|
||||
CommentCollection.getLink(commentId, 'post').add({text: 'hi there'});
|
||||
CommentCollection.getLink(commentId, 'post').add({ text: 'hi there' });
|
||||
|
||||
let insertedPostViaVirtual = PostCollection.findOne({text: 'hi there'});
|
||||
let insertedPostViaVirtual = PostCollection.findOne({
|
||||
text: 'hi there',
|
||||
});
|
||||
assert.isObject(insertedPostViaVirtual);
|
||||
|
||||
assert.lengthOf(PostCollection.getLink(insertedPostViaVirtual, 'comments').fetch(), 1);
|
||||
assert.lengthOf(
|
||||
PostCollection.getLink(insertedPostViaVirtual, 'comments').fetch(),
|
||||
1
|
||||
);
|
||||
|
||||
const category = CategoryCollection.findOne();
|
||||
let postsCategoryLink = CategoryCollection.getLink(category, 'posts');
|
||||
postsCategoryLink.add(insertedPostViaVirtual);
|
||||
|
||||
assert.equal(category._id, PostCollection.getLink(insertedPostViaVirtual, 'category').fetch()._id);
|
||||
assert.equal(
|
||||
category._id,
|
||||
PostCollection.getLink(insertedPostViaVirtual, 'category').fetch()
|
||||
._id
|
||||
);
|
||||
|
||||
// TESTING META
|
||||
let categoryMetaPostLink = CategoryCollection.getLink(category, 'metaPosts');
|
||||
categoryMetaPostLink.add(insertedPostViaVirtual, {testValue: 'boom!'});
|
||||
let categoryMetaPostLink = CategoryCollection.getLink(
|
||||
category,
|
||||
'metaPosts'
|
||||
);
|
||||
categoryMetaPostLink.add(insertedPostViaVirtual, {
|
||||
testValue: 'boom!',
|
||||
});
|
||||
|
||||
let postMetaCategoryLink = PostCollection.getLink(insertedPostViaVirtual, 'metaCategory');
|
||||
let postMetaCategoryLink = PostCollection.getLink(
|
||||
insertedPostViaVirtual,
|
||||
'metaCategory'
|
||||
);
|
||||
assert.equal('boom!', postMetaCategoryLink.metadata().testValue);
|
||||
});
|
||||
|
||||
it('Should fail when you try to add a non-existing link', function (done) {
|
||||
let postId = PostCollection.insert({text: 'hello'});
|
||||
it('Should fail when you try to add a non-existing link', function(done) {
|
||||
let postId = PostCollection.insert({ text: 'hello' });
|
||||
|
||||
try {
|
||||
PostCollection.getLink(postId, 'comments').add('XXXXXXX');
|
||||
|
@ -328,12 +360,15 @@ describe('Collection Links', function () {
|
|||
}
|
||||
});
|
||||
|
||||
it('Should work with autoremoval from inversed and direct link', function () {
|
||||
it('Should work with autoremoval from inversed and direct link', function() {
|
||||
// autoremoval from direct side
|
||||
let postId = PostCollection.insert({text: 'autoremove'});
|
||||
const postAutoRemoveCommentsLink = PostCollection.getLink(postId, 'autoRemoveComments');
|
||||
let postId = PostCollection.insert({ text: 'autoremove' });
|
||||
const postAutoRemoveCommentsLink = PostCollection.getLink(
|
||||
postId,
|
||||
'autoRemoveComments'
|
||||
);
|
||||
|
||||
postAutoRemoveCommentsLink.add({text: 'hello'});
|
||||
postAutoRemoveCommentsLink.add({ text: 'hello' });
|
||||
|
||||
assert.lengthOf(postAutoRemoveCommentsLink.find().fetch(), 1);
|
||||
let commentId = postAutoRemoveCommentsLink.find().fetch()[0]._id;
|
||||
|
@ -342,12 +377,14 @@ describe('Collection Links', function () {
|
|||
PostCollection.remove(postId);
|
||||
assert.isUndefined(CommentCollection.findOne(commentId));
|
||||
|
||||
|
||||
// now from inversed side
|
||||
commentId = CommentCollection.insert({text: 'autoremove'});
|
||||
commentId = CommentCollection.insert({ text: 'autoremove' });
|
||||
|
||||
const commentAutoRemovePostsLink = CommentCollection.getLink(commentId, 'autoRemovePosts');
|
||||
commentAutoRemovePostsLink.add({text: 'Hello'});
|
||||
const commentAutoRemovePostsLink = CommentCollection.getLink(
|
||||
commentId,
|
||||
'autoRemovePosts'
|
||||
);
|
||||
commentAutoRemovePostsLink.add({ text: 'Hello' });
|
||||
|
||||
assert.lengthOf(commentAutoRemovePostsLink.find().fetch(), 1);
|
||||
postId = commentAutoRemovePostsLink.find().fetch()[0]._id;
|
||||
|
@ -356,4 +393,4 @@ describe('Collection Links', function () {
|
|||
CommentCollection.remove(commentId);
|
||||
assert.isUndefined(PostCollection.findOne(postId));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {check} from 'meteor/check';
|
||||
import { check } from 'meteor/check';
|
||||
import addFieldMap from './lib/addFieldMap';
|
||||
|
||||
const storage = '__reducers';
|
||||
Object.assign(Mongo.Collection.prototype, {
|
||||
|
@ -16,20 +17,28 @@ Object.assign(Mongo.Collection.prototype, {
|
|||
}
|
||||
|
||||
if (this.getLinker(reducerName)) {
|
||||
throw new Meteor.Error(`You cannot add the reducer with name: ${reducerName} because it is already defined as a link in ${this._name} collection`)
|
||||
throw new Meteor.Error(
|
||||
`You cannot add the reducer with name: ${reducerName} because it is already defined as a link in ${
|
||||
this._name
|
||||
} collection`
|
||||
);
|
||||
}
|
||||
|
||||
if (this[reducerConfig][reducerName]) {
|
||||
throw new Meteor.Error(`You cannot add the reducer with name: ${reducerName} because it was already added to ${this._name} collection`)
|
||||
throw new Meteor.Error(
|
||||
`You cannot add the reducer with name: ${reducerName} because it was already added to ${
|
||||
this._name
|
||||
} collection`
|
||||
);
|
||||
}
|
||||
|
||||
check(reducerConfig, {
|
||||
body: Object,
|
||||
reduce: Function
|
||||
reduce: Function,
|
||||
});
|
||||
|
||||
_.extend(this[storage], {
|
||||
[reducerName]: reducerConfig
|
||||
[reducerName]: reducerConfig,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -42,5 +51,10 @@ Object.assign(Mongo.Collection.prototype, {
|
|||
if (this[storage]) {
|
||||
return this[storage][name];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* This creates reducers that makes sort of aliases for the database fields we use
|
||||
*/
|
||||
addFieldMap,
|
||||
});
|
||||
|
|
20
lib/query/reducers/lib/addFieldMap.js
Normal file
20
lib/query/reducers/lib/addFieldMap.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @param {[niceField: string]: dbField} map
|
||||
*/
|
||||
export default function addFieldMap(map) {
|
||||
const collection = this;
|
||||
let reducers = {};
|
||||
for (let key in map) {
|
||||
const dbField = map[key];
|
||||
reducers[key] = {
|
||||
body: {
|
||||
[dbField]: 1,
|
||||
},
|
||||
reduce(obj) {
|
||||
return obj[dbField];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
collection.addReducers(reducers);
|
||||
}
|
|
@ -2,22 +2,14 @@ import './lib/extension.js';
|
|||
import './lib/links/extension.js';
|
||||
import './lib/query/reducers/extension.js';
|
||||
|
||||
export {
|
||||
default as createQuery
|
||||
} from './lib/createQuery.js';
|
||||
export { default as createQuery } from './lib/createQuery.js';
|
||||
|
||||
export {
|
||||
default as prepareForProcess
|
||||
default as prepareForProcess,
|
||||
} from './lib/query/lib/prepareForProcess';
|
||||
|
||||
export {
|
||||
default as Query
|
||||
} from './lib/query/query.client';
|
||||
export { default as Query } from './lib/query/query.client';
|
||||
|
||||
export {
|
||||
default as NamedQuery
|
||||
} from './lib/namedQuery/namedQuery.client';
|
||||
export { default as NamedQuery } from './lib/namedQuery/namedQuery.client';
|
||||
|
||||
export {
|
||||
default as compose
|
||||
} from './lib/compose';
|
||||
export { default as compose } from './lib/compose';
|
||||
|
|
|
@ -7,27 +7,21 @@ import './lib/namedQuery/expose/extension.js';
|
|||
import NamedQueryStore from './lib/namedQuery/store';
|
||||
import LinkConstants from './lib/links/constants';
|
||||
|
||||
export {
|
||||
NamedQueryStore,
|
||||
LinkConstants
|
||||
}
|
||||
export { NamedQueryStore, LinkConstants };
|
||||
|
||||
export { default as createQuery } from './lib/createQuery.js';
|
||||
|
||||
export { default as Exposure } from './lib/exposure/exposure.js';
|
||||
|
||||
export {
|
||||
default as createQuery
|
||||
} from './lib/createQuery.js';
|
||||
|
||||
export {
|
||||
default as Exposure
|
||||
} from './lib/exposure/exposure.js';
|
||||
|
||||
export {
|
||||
default as MemoryResultCacher
|
||||
default as MemoryResultCacher,
|
||||
} from './lib/namedQuery/cache/MemoryResultCacher';
|
||||
|
||||
export {
|
||||
default as BaseResultCacher
|
||||
default as BaseResultCacher,
|
||||
} from './lib/namedQuery/cache/BaseResultCacher';
|
||||
|
||||
export {
|
||||
default as compose
|
||||
} from './lib/compose';
|
||||
export { default as compose } from './lib/compose';
|
||||
|
||||
export * from './lib/graphql';
|
||||
export { default as db } from './lib/db';
|
||||
|
|
|
@ -68,7 +68,7 @@ Package.onTest(function(api) {
|
|||
api.addFiles('lib/query/testing/bootstrap/index.js');
|
||||
|
||||
// When you play with tests you should comment this to make tests go faster.
|
||||
api.addFiles('lib/query/testing/bootstrap/fixtures.js', 'server');
|
||||
// api.addFiles('lib/query/testing/bootstrap/fixtures.js', 'server');
|
||||
|
||||
api.addFiles('lib/query/testing/server.test.js', 'server');
|
||||
api.addFiles('lib/query/testing/client.test.js', 'client');
|
||||
|
@ -82,6 +82,10 @@ Package.onTest(function(api) {
|
|||
api.addFiles('lib/query/counts/testing/server.test.js', 'server');
|
||||
api.addFiles('lib/query/counts/testing/client.test.js', 'client');
|
||||
|
||||
// NAMED QUERIES
|
||||
api.addFiles('lib/namedQuery/testing/server.test.js', 'server');
|
||||
api.addFiles('lib/namedQuery/testing/client.test.js', 'client');
|
||||
|
||||
// GRAPHQL
|
||||
api.addFiles('lib/graphql/testing/index.js', 'server');
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue