mirror of
https://github.com/vale981/grapher
synced 2025-03-06 01:51:38 -05:00
Merge pull request #5 from cult-of-coders/feature/hypernova
[RFC] Feature/hypernova
This commit is contained in:
commit
0d5fbcaa1c
53 changed files with 1143 additions and 368 deletions
1
.npm/package/.gitignore
vendored
Normal file
1
.npm/package/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
node_modules
|
7
.npm/package/README
Normal file
7
.npm/package/README
Normal file
|
@ -0,0 +1,7 @@
|
|||
This directory and the files immediately inside it are automatically generated
|
||||
when you change this package's NPM dependencies. Commit the files in this
|
||||
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
|
||||
so that others run the same versions of sub-dependencies.
|
||||
|
||||
You should NOT check in the node_modules directory that Meteor automatically
|
||||
creates; if you are using git, the .gitignore file tells git to ignore it.
|
9
.npm/package/npm-shrinkwrap.json
generated
Normal file
9
.npm/package/npm-shrinkwrap.json
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"sift": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-3.2.6.tgz",
|
||||
"from": "sift@3.2.6"
|
||||
}
|
||||
}
|
||||
}
|
17
README.md
17
README.md
|
@ -4,12 +4,19 @@ Welcome to Grapher
|
|||
[](https://travis-ci.org/cult-of-coders/grapher)
|
||||
[](https://raw.githubusercontent.com/hyperium/hyper/master/LICENSE)
|
||||
|
||||
General
|
||||
-------
|
||||
*Grapher* is a Meteor package that will:
|
||||
What ?
|
||||
------
|
||||
*Grapher* is a high performance data grapher will:
|
||||
|
||||
1. Allow you to work with Joins (also known as: Relationships or Links) between Collections;
|
||||
2. Expose reactive/non-reactive fetching abilities using a GraphQL like syntax.
|
||||
1. Makes data MongoDB denormalization easy (storing and linking data in different collections)
|
||||
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.
|
||||
|
||||
|
||||
How does this compare to [ApolloStack](http://www.apollostack.com/) ?
|
||||
- You can plug it in your Meteor app directly. It will just work.
|
||||
- It is built for performance and high data load.
|
||||
- Apollo tries to satisfy everybody, we are limited to Meteor only.
|
||||
|
||||
Updates
|
||||
-------
|
||||
|
|
|
@ -109,7 +109,6 @@ OR from the collection directly:
|
|||
|
||||
```
|
||||
const query = Posts.createQuery({
|
||||
// $all: 1, // use this only when you want all fields without specifying them (NOT RECOMMENDED)
|
||||
$filter({filters, options, params}) {
|
||||
filters.isApproved = true;
|
||||
options.limit = params.limit;
|
||||
|
|
|
@ -99,7 +99,6 @@ Notes:
|
|||
- Use {} to specify a link, and 1 for a field.
|
||||
- "_id" will always be fetched
|
||||
- You must always specify the fields you need, otherwise it will only fetch _id
|
||||
- If you want all fields, pass in {$all: 1}
|
||||
|
||||
```
|
||||
const query = Posts.createQuery({
|
||||
|
@ -116,7 +115,6 @@ const query = Posts.createQuery({
|
|||
comments: {
|
||||
text: 1,
|
||||
// if you don't specify any local fields for the author, only "_id" field will be fetched
|
||||
// use $all: 1, to get all fields
|
||||
// this will enforce the use of query and retrieve only the data you need.
|
||||
author: {
|
||||
groups: {
|
||||
|
@ -283,23 +281,6 @@ createQuery({
|
|||
|
||||
*posts* is the name of the collection. (when you create new Mongo.Collection("xxx"), "xxx" is the name of your collection)
|
||||
|
||||
Getting all the fields
|
||||
======================
|
||||
|
||||
Though this is not recommended, sometimes especially when you are just testing around, you want to see all the fields
|
||||
```
|
||||
createQuery({
|
||||
posts: {
|
||||
$all: 1,
|
||||
comments: {
|
||||
$all: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The query above will fetch all the fields from every posts and every comment of every post.
|
||||
|
||||
#### React Integration
|
||||
For integration with React try out [cultofcoders:grapher-react](https://github.com/cult-of-coders/grapher-react) package
|
||||
|
||||
|
|
|
@ -42,11 +42,17 @@ export default class Exposure {
|
|||
const collection = this.collection;
|
||||
const firewall = this.firewall;
|
||||
|
||||
collection.__isExposedForGrapher = true;
|
||||
|
||||
if (firewall) {
|
||||
collection.findSecure = (filters, options, userId) => {
|
||||
collection.firewall = (filters, options, userId) => {
|
||||
if (userId !== undefined) {
|
||||
firewall(filters, options, userId);
|
||||
}
|
||||
};
|
||||
|
||||
collection.findSecure = (filters, options, userId) => {
|
||||
collection.firewall(filters, options, userId);
|
||||
|
||||
return collection.find(filters, options);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ export default new SimpleSchema({
|
|||
type: Boolean,
|
||||
defaultValue: false,
|
||||
optional: true
|
||||
},
|
||||
unique: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
optional: true
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -12,14 +12,14 @@ export default (isMany, metadata) => {
|
|||
const schema = constructMetadataSchema(metadata);
|
||||
fieldSchema = (isMany) ? {type: [schema]} : {type: schema};
|
||||
} else {
|
||||
fieldSchema = (isMany)
|
||||
? {type: null, blackbox: true}
|
||||
: {type: Object, blackbox: true};
|
||||
if (isMany) {
|
||||
fieldSchema = {type: [Object], blackbox: true};
|
||||
} else {
|
||||
fieldSchema = {type: Object, blackbox: true}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fieldSchema = (isMany)
|
||||
? {type: [String]}
|
||||
: {type: String};
|
||||
fieldSchema = (isMany) ? {type: [String]} : {type: String};
|
||||
}
|
||||
|
||||
fieldSchema.optional = true;
|
||||
|
|
75
lib/links/lib/createSearchFilters.js
Normal file
75
lib/links/lib/createSearchFilters.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
export default function createSearchFilters(object, fieldStorage, strategy, isVirtual) {
|
||||
if (!isVirtual) {
|
||||
switch (strategy) {
|
||||
case 'one': return createOne(object, fieldStorage);
|
||||
case 'one-meta': return createOneMeta(object, fieldStorage);
|
||||
case 'many': return createMany(object, fieldStorage);
|
||||
case 'many-meta': return createManyMeta(object, fieldStorage);
|
||||
default:
|
||||
throw new Meteor.Error(`Invalid linking strategy: ${strategy}`)
|
||||
}
|
||||
} else {
|
||||
switch (strategy) {
|
||||
case 'one': return createOneVirtual(object, fieldStorage);
|
||||
case 'one-meta': return createOneMetaVirtual(object, fieldStorage);
|
||||
case 'many': return createManyVirtual(object, fieldStorage);
|
||||
case 'many-meta': return createManyMetaVirtual(object, fieldStorage);
|
||||
default:
|
||||
throw new Meteor.Error(`Invalid linking strategy: ${strategy}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createOne(object, fieldStorage) {
|
||||
return {
|
||||
_id: object[fieldStorage]
|
||||
};
|
||||
}
|
||||
|
||||
export function createOneVirtual(object, fieldStorage) {
|
||||
return {
|
||||
[fieldStorage]: object._id
|
||||
};
|
||||
}
|
||||
|
||||
export function createOneMeta(object, fieldStorage) {
|
||||
const value = object[fieldStorage];
|
||||
|
||||
return {
|
||||
_id: value ? value._id : value
|
||||
};
|
||||
}
|
||||
|
||||
export function createOneMetaVirtual(object, fieldStorage) {
|
||||
return {
|
||||
[fieldStorage + '._id']: object._id
|
||||
};
|
||||
}
|
||||
|
||||
export function createMany(object, fieldStorage) {
|
||||
return {
|
||||
_id: {
|
||||
$in: object[fieldStorage] || []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createManyVirtual(object, fieldStorage) {
|
||||
return {
|
||||
[fieldStorage]: object._id
|
||||
};
|
||||
}
|
||||
|
||||
export function createManyMeta(object, fieldStorage) {
|
||||
const value = object[fieldStorage];
|
||||
|
||||
return {
|
||||
_id: {$in: _.pluck(value, '_id') || []}
|
||||
};
|
||||
}
|
||||
|
||||
export function createManyMetaVirtual(object, fieldStorage) {
|
||||
return {
|
||||
[fieldStorage + '._id']: object._id
|
||||
};
|
||||
}
|
|
@ -5,9 +5,10 @@ export default class Link {
|
|||
|
||||
get isVirtual() { return this.linker.isVirtual() }
|
||||
|
||||
constructor(linker, object) {
|
||||
constructor(linker, object, collection) {
|
||||
this.linker = linker;
|
||||
this.object = object;
|
||||
this.linkedCollection = (collection) ? collection : linker.getLinkedCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,7 +47,7 @@ export default class Link {
|
|||
find(filters = {}, options = {}, userId = null) {
|
||||
let linker = this.linker;
|
||||
|
||||
const linkedCollection = linker.getLinkedCollection();
|
||||
const linkedCollection = this.linkedCollection;
|
||||
|
||||
if (!linker.isVirtual()) {
|
||||
this.clean();
|
||||
|
@ -100,7 +101,7 @@ export default class Link {
|
|||
identifyId(what, saveToDatabase) {
|
||||
return SmartArgs.getId(what, {
|
||||
saveToDatabase,
|
||||
collection: this.linker.getLinkedCollection()
|
||||
collection: this.linkedCollection
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,7 +111,7 @@ export default class Link {
|
|||
identifyIds(what, saveToDatabase) {
|
||||
return SmartArgs.getIds(what, {
|
||||
saveToDatabase,
|
||||
collection: this.linker.getLinkedCollection()
|
||||
collection: this.linkedCollection
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,10 @@ export default class Linker {
|
|||
*/
|
||||
get linkStorageField()
|
||||
{
|
||||
if (this.isVirtual()) {
|
||||
return this.linkConfig.relatedLinker.linkStorageField;
|
||||
}
|
||||
|
||||
return this.linkConfig.field;
|
||||
}
|
||||
|
||||
|
@ -91,11 +95,27 @@ export default class Linker {
|
|||
return !this.isSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the relationship for this link contains metadata
|
||||
*/
|
||||
isMeta()
|
||||
{
|
||||
if (this.isVirtual()) {
|
||||
return this.linkConfig.relatedLinker.isMeta();
|
||||
}
|
||||
|
||||
return !!this.linkConfig.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSingle()
|
||||
{
|
||||
if (this.isVirtual()) {
|
||||
return this.linkConfig.relatedLinker.isSingle();
|
||||
}
|
||||
|
||||
return _.contains(this.oneTypes, this.linkConfig.type);
|
||||
}
|
||||
|
||||
|
@ -119,11 +139,11 @@ export default class Linker {
|
|||
* @param object
|
||||
* @returns {*}
|
||||
*/
|
||||
createLink(object)
|
||||
createLink(object, collection)
|
||||
{
|
||||
let helperClass = this._getHelperClass();
|
||||
|
||||
return new helperClass(this, object);
|
||||
return new helperClass(this, object, collection);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,6 +156,16 @@ export default class Linker {
|
|||
if (!this.linkConfig.collection) {
|
||||
throw new Meteor.Error('invalid-config', `For the link ${this.linkName} you did not provide a collection. Collection is mandatory for non-resolver links.`)
|
||||
}
|
||||
|
||||
if (typeof(this.linkConfig.collection) === 'string') {
|
||||
const collectionName = this.linkConfig.collection;
|
||||
this.linkConfig.collection = Mongo.Collection.get(collectionName);
|
||||
|
||||
if (!this.linkConfig.collection) {
|
||||
throw new MeteorError('invalid-collection', `Could not find a collection with the name: ${collectionName}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isVirtual()) {
|
||||
return this._prepareVirtual();
|
||||
} else {
|
||||
|
@ -256,15 +286,42 @@ export default class Linker {
|
|||
}
|
||||
|
||||
_initIndex() {
|
||||
if (Meteor.isServer && this.linkConfig.index) {
|
||||
if (Meteor.isServer) {
|
||||
let field = this.linkConfig.field;
|
||||
if (this.linkConfig.metadata) {
|
||||
field = field + '._id';
|
||||
}
|
||||
|
||||
if (this.linkConfig.index) {
|
||||
if (this.isVirtual()) {
|
||||
throw new Meteor.Error('You cannot set index on an inversed link.');
|
||||
}
|
||||
|
||||
let options;
|
||||
if (this.linkConfig.unique) {
|
||||
if (this.isMany()) {
|
||||
throw new Meteor.Error('You cannot set unique property on a multi field.');
|
||||
}
|
||||
|
||||
options = {unique: true}
|
||||
}
|
||||
|
||||
this.mainCollection._ensureIndex({[field]: 1}, options);
|
||||
} else {
|
||||
if (this.linkConfig.unique) {
|
||||
if (this.isVirtual()) {
|
||||
throw new Meteor.Error('You cannot set unique property on an inversed link.');
|
||||
}
|
||||
|
||||
if (this.isMany()) {
|
||||
throw new Meteor.Error('You cannot set unique property on a multi field.');
|
||||
}
|
||||
|
||||
this.mainCollection._ensureIndex({
|
||||
[field]: 1
|
||||
})
|
||||
}, {unique: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
8
lib/query/hypernova/README.md
Normal file
8
lib/query/hypernova/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
Hypernova
|
||||
=========
|
||||
|
||||
Is a grapher module that allows us to use aggregate from mongodb to dramatically reduce the number of fetching requests.
|
||||
|
||||
This module does 2 things:
|
||||
- It merges the data at every node
|
||||
- It assembles it to correct parents upon receiving it.
|
109
lib/query/hypernova/aggregateSearchFilters.js
Normal file
109
lib/query/hypernova/aggregateSearchFilters.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Its purpose is to create filters to get the related data in one request.
|
||||
*/
|
||||
export default class AggregateFilters {
|
||||
constructor(collectionNode) {
|
||||
this.collectionNode = collectionNode;
|
||||
this.linker = collectionNode.linker;
|
||||
|
||||
this.isVirtual = this.linker.isVirtual();
|
||||
|
||||
this.linkStorageField = this.linker.linkStorageField;
|
||||
}
|
||||
|
||||
get parentObjects() {
|
||||
return this.collectionNode.parent.results;
|
||||
}
|
||||
|
||||
create() {
|
||||
switch (this.linker.strategy) {
|
||||
case 'one':
|
||||
return this.createOne();
|
||||
case 'one-meta':
|
||||
return this.createOneMeta();
|
||||
case 'many':
|
||||
return this.createMany();
|
||||
case 'many-meta':
|
||||
return this.createManyMeta();
|
||||
default:
|
||||
throw new Meteor.Error(`Invalid linker type: ${this.linker.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
createOne() {
|
||||
if (!this.isVirtual) {
|
||||
return {
|
||||
_id: {
|
||||
$in: _.pluck(this.parentObjects, this.linkStorageField)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
[this.linkStorageField]: {
|
||||
$in: _.pluck(this.parentObjects, '_id')
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
createOneMeta() {
|
||||
if (!this.isVirtual) {
|
||||
return {
|
||||
_id: {
|
||||
$in: _.pluck(
|
||||
_.pluck(this.parentObjects, this.linkStorageField),
|
||||
'_id'
|
||||
)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const field = this.linkStorageField + '._id';
|
||||
return {
|
||||
[field]: {
|
||||
$in: _.pluck(this.parentObjects, '_id')
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
createMany() {
|
||||
if (!this.isVirtual) {
|
||||
const arrayOfIds = _.pluck(this.parentObjects, this.linkStorageField);
|
||||
return {
|
||||
_id: {
|
||||
$in: _.union(...arrayOfIds)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const arrayOfIds = _.pluck(this.parentObjects, '_id');
|
||||
return {
|
||||
[this.linkStorageField]: {
|
||||
$in: _.union(...arrayOfIds)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
createManyMeta() {
|
||||
if (!this.isVirtual) {
|
||||
let ids = [];
|
||||
_.each(this.parentObjects, object => {
|
||||
if (object[this.linkStorageField]) {
|
||||
let localIds = _.pluck(object[this.linkStorageField], '_id');
|
||||
ids = _.union(ids, ...localIds);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
_id: {$in: ids}
|
||||
};
|
||||
} else {
|
||||
const field = this.linkStorageField + '._id';
|
||||
return {
|
||||
[field]: {
|
||||
$in: _.pluck(this.parentObjects, '_id')
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
48
lib/query/hypernova/assembleAggregateResults.js
Normal file
48
lib/query/hypernova/assembleAggregateResults.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* This only applies to inversed links. It will assemble the data in a correct manner.
|
||||
*/
|
||||
export default function (childCollectionNode, aggregateResults) {
|
||||
const linker = childCollectionNode.linker;
|
||||
const linkName = childCollectionNode.linkName;
|
||||
|
||||
let allResults = [];
|
||||
|
||||
if (linker.isMeta() && linker.isMany()) {
|
||||
_.each(childCollectionNode.parent.results, parentResult => {
|
||||
parentResult[linkName] = parentResult[linkName] || [];
|
||||
|
||||
const eligibleAggregateResults = _.filter(aggregateResults, aggregateResult => {
|
||||
return _.contains(aggregateResult._id, parentResult._id)
|
||||
});
|
||||
|
||||
if (eligibleAggregateResults.length) {
|
||||
const datas = _.pluck(eligibleAggregateResults, 'data'); /// [ [x1, x2], [x2, x3] ]
|
||||
|
||||
_.each(datas, data => {
|
||||
_.each(data, item => {
|
||||
parentResult[linkName].push(item)
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_.each(aggregateResults, aggregateResult => {
|
||||
_.each(aggregateResult.data, item => allResults.push(item))
|
||||
});
|
||||
} else {
|
||||
_.each(aggregateResults, aggregateResult => {
|
||||
let parentResult = _.find(childCollectionNode.parent.results, (result) => {
|
||||
return result._id == aggregateResult._id;
|
||||
});
|
||||
|
||||
|
||||
if (parentResult) {
|
||||
parentResult[childCollectionNode.linkName] = aggregateResult.data;
|
||||
}
|
||||
|
||||
_.each(aggregateResult.data, item => allResults.push(item))
|
||||
});
|
||||
}
|
||||
|
||||
childCollectionNode.results = allResults;
|
||||
}
|
45
lib/query/hypernova/assembler.js
Normal file
45
lib/query/hypernova/assembler.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import createSearchFilters from '../../links/lib/createSearchFilters';
|
||||
import sift from 'sift';
|
||||
|
||||
export default (childCollectionNode, {limit, skip}) => {
|
||||
const parent = childCollectionNode.parent;
|
||||
const linker = childCollectionNode.linker;
|
||||
|
||||
const strategy = linker.strategy;
|
||||
const isVirtual = linker.isVirtual();
|
||||
const isSingle = linker.isSingle();
|
||||
const removeStorageField = !childCollectionNode.parentHasMyLinkStorageFieldSpecified();
|
||||
const oneResult = (isVirtual && linker.linkConfig.relatedLinker.linkConfig.unique) || (!isVirtual) && isSingle;
|
||||
|
||||
const fieldStorage = linker.linkStorageField;
|
||||
|
||||
_.each(parent.results, result => {
|
||||
const data = assembleData(childCollectionNode, result, {
|
||||
fieldStorage, strategy, isVirtual, isSingle
|
||||
});
|
||||
|
||||
result[childCollectionNode.linkName] = filterAssembledData(data, {limit, skip, oneResult})
|
||||
});
|
||||
|
||||
if (removeStorageField) {
|
||||
_.each(parent.results, result => delete result[fieldStorage]);
|
||||
}
|
||||
}
|
||||
|
||||
function filterAssembledData(data, {limit, skip, oneResult}) {
|
||||
if (limit) {
|
||||
return data.slice(skip, limit);
|
||||
}
|
||||
|
||||
if (oneResult) {
|
||||
return _.first(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function assembleData(childCollectionNode, result, {fieldStorage, strategy, isVirtual}) {
|
||||
const filters = createSearchFilters(result, fieldStorage, strategy, isVirtual);
|
||||
|
||||
return sift(filters, childCollectionNode.results);
|
||||
}
|
55
lib/query/hypernova/buildAggregatePipeline.js
Normal file
55
lib/query/hypernova/buildAggregatePipeline.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
export default function (childCollectionNode, filters, options, userId) {
|
||||
const linker = childCollectionNode.linker;
|
||||
const linkStorageField = linker.linkStorageField;
|
||||
const collection = childCollectionNode.collection;
|
||||
|
||||
let pipeline = [];
|
||||
|
||||
if (collection.firewall) {
|
||||
collection.firewall(filters, options, userId);
|
||||
}
|
||||
|
||||
pipeline.push({$match: filters});
|
||||
|
||||
if (options.sort) {
|
||||
pipeline.push({$sort: options.sort})
|
||||
}
|
||||
|
||||
let _id = linkStorageField;
|
||||
if (linker.isMeta()) {
|
||||
_id += '._id';
|
||||
}
|
||||
|
||||
let dataPush = {};
|
||||
_.each(options.fields, (value, field) => {
|
||||
dataPush[field] = '$' + field
|
||||
});
|
||||
|
||||
if (!dataPush._id) {
|
||||
dataPush['_id'] = '$_id';
|
||||
}
|
||||
|
||||
pipeline.push({
|
||||
$group: {
|
||||
_id: "$" + _id,
|
||||
data: {
|
||||
$push: dataPush
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (options.limit || options.skip) {
|
||||
let $slice = ["$data"];
|
||||
if (options.skip) $slice.push(options.skip);
|
||||
if (options.limit) $slice.push(options.limit);
|
||||
|
||||
pipeline.push({
|
||||
$project: {
|
||||
_id: 1,
|
||||
data: {$slice}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
}
|
38
lib/query/hypernova/hypernova.js
Normal file
38
lib/query/hypernova/hypernova.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import applyProps from '../lib/applyProps.js';
|
||||
import LinkResolve from '../../links/linkTypes/linkResolve.js';
|
||||
import storeHypernovaResults from './storeHypernovaResults.js';
|
||||
import assembler from './assembler.js';
|
||||
|
||||
function hypernova(collectionNode, userId) {
|
||||
_.each(collectionNode.collectionNodes, childCollectionNode => {
|
||||
let {filters, options} = applyProps(childCollectionNode);
|
||||
|
||||
if (childCollectionNode.linker.isResolver()) {
|
||||
_.each(collectionNode.results, result => {
|
||||
const accessor = childCollectionNode.linker.createLink(result);
|
||||
|
||||
result[childCollectionNode.linkName] = accessor.find(filters, options);
|
||||
});
|
||||
} else {
|
||||
storeHypernovaResults(childCollectionNode, userId);
|
||||
|
||||
hypernova(childCollectionNode, userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function hypernovaInit(collectionNode, userId) {
|
||||
let {filters, options} = applyProps(collectionNode);
|
||||
|
||||
const collection = collectionNode.collection;
|
||||
if (collection.findSecure) {
|
||||
collectionNode.results = collection.findSecure(filters, options, userId).fetch();
|
||||
} else {
|
||||
collectionNode.results = collection.find(filters, options).fetch();
|
||||
}
|
||||
|
||||
hypernova(collectionNode, userId);
|
||||
|
||||
return collectionNode.results;
|
||||
}
|
||||
|
40
lib/query/hypernova/storeHypernovaResults.js
Normal file
40
lib/query/hypernova/storeHypernovaResults.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import applyProps from '../lib/applyProps.js';
|
||||
import AggregateFilters from './aggregateSearchFilters.js';
|
||||
import assemble from './assembler.js';
|
||||
import assembleAggregateResults from './assembleAggregateResults.js';
|
||||
import buildAggregatePipeline from './buildAggregatePipeline.js';
|
||||
|
||||
export default function storeHypernovaResults(childCollectionNode, userId) {
|
||||
if (childCollectionNode.parent.results.length === 0) {
|
||||
return childCollectionNode.results = [];
|
||||
}
|
||||
|
||||
let {filters, options} = applyProps(childCollectionNode);
|
||||
|
||||
const aggregateFilters = new AggregateFilters(childCollectionNode);
|
||||
const linker = childCollectionNode.linker;
|
||||
const isVirtual = linker.isVirtual();
|
||||
const collection = childCollectionNode.collection;
|
||||
|
||||
_.extend(filters, aggregateFilters.create());
|
||||
|
||||
// if it's not virtual then we retrieve them and assemble them here.
|
||||
if (!isVirtual) {
|
||||
const filteredOptions = _.omit(options, 'limit');
|
||||
|
||||
if (collection.findSecure) {
|
||||
childCollectionNode.results = collection.findSecure(filters, filteredOptions, userId).fetch();
|
||||
} else {
|
||||
childCollectionNode.results = collection.find(filters, filteredOptions, userId).fetch();
|
||||
}
|
||||
|
||||
assemble(childCollectionNode, options);
|
||||
} else {
|
||||
// virtuals arrive here
|
||||
let pipeline = buildAggregatePipeline(childCollectionNode, filters, options, userId);
|
||||
|
||||
const aggregateResults = collection.aggregate(pipeline, {explains: true});
|
||||
|
||||
assembleAggregateResults(childCollectionNode, aggregateResults);
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ function createNodes(root) {
|
|||
|
||||
if (linker) {
|
||||
let subroot = new CollectionNode(linker.getLinkedCollection(), body, fieldName);
|
||||
root.add(subroot);
|
||||
root.add(subroot, linker);
|
||||
|
||||
return createNodes(subroot);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function compose(node, userId) {
|
|||
if (linker.isVirtual()) {
|
||||
options.fields = options.fields || {};
|
||||
_.extend(options.fields, {
|
||||
[linker.linkConfig.relatedLinker.linkStorageField]: 1
|
||||
[linker.linkStorageField]: 1
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import applyProps from './applyProps.js';
|
||||
import createSearchFilters from '../../links/lib/createSearchFilters.js';
|
||||
import LinkResolve from '../../links/linkTypes/linkResolve.js';
|
||||
import sift from 'sift';
|
||||
|
||||
export default function fetch(node, parentObject, userId) {
|
||||
let {filters, options} = applyProps(node);
|
||||
|
@ -7,17 +9,24 @@ export default function fetch(node, parentObject, userId) {
|
|||
let results = [];
|
||||
|
||||
if (parentObject) {
|
||||
// composition
|
||||
let accessor = node.linker.createLink(parentObject);
|
||||
|
||||
// because resolvers are run server side, we may need all the variables of the object in order
|
||||
// to provide a proper fetch.
|
||||
if (accessor instanceof LinkResolve) {
|
||||
if (node.linker.isResolver()) {
|
||||
let accessor = node.linker.createLink(parentObject, node.collection);
|
||||
accessor.object = node.parent.collection.findOne(parentObject._id);
|
||||
}
|
||||
|
||||
|
||||
results = accessor.fetch(filters, options, userId);
|
||||
} else {
|
||||
const strategy = node.linker.strategy;
|
||||
const isVirtual = node.linker.isVirtual();
|
||||
const fieldStorage = node.linker.linkStorageField;
|
||||
|
||||
_.extend(filters, createSearchFilters(parentObject, fieldStorage, strategy, isVirtual));
|
||||
|
||||
if (node.collection.findSecure) {
|
||||
results = node.collection.findSecure(filters, options, userId).fetch();
|
||||
} else {
|
||||
results = node.collection.find(filters, options).fetch();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (node.collection.findSecure) {
|
||||
results = node.collection.findSecure(filters, options, userId).fetch();
|
||||
|
|
|
@ -8,6 +8,7 @@ export default class CollectionNode {
|
|||
this.body = body;
|
||||
this.props = {};
|
||||
this.parent = null;
|
||||
this.linker = null;
|
||||
}
|
||||
|
||||
get collectionNodes() {
|
||||
|
@ -18,21 +19,14 @@ export default class CollectionNode {
|
|||
return _.filter(this.nodes, n => n instanceof FieldNode);
|
||||
}
|
||||
|
||||
get linker() {
|
||||
if (this.parent) {
|
||||
return this.parent.collection.getLinker(this.linkName)
|
||||
}
|
||||
}
|
||||
|
||||
hasGlobalFieldNode() {
|
||||
return !!_.find(this.fieldNodes, n => n.isGlobal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param node
|
||||
* @param linker
|
||||
*/
|
||||
add(node) {
|
||||
add(node, linker) {
|
||||
node.parent = this;
|
||||
node.linker = linker;
|
||||
|
||||
this.nodes.push(node);
|
||||
}
|
||||
|
||||
|
@ -43,10 +37,6 @@ export default class CollectionNode {
|
|||
applyFields(filters, options) {
|
||||
let hasAddedAnyField = false;
|
||||
|
||||
if (this.hasGlobalFieldNode()) {
|
||||
return options.fields = undefined;
|
||||
}
|
||||
|
||||
_.each(this.fieldNodes, n => {
|
||||
hasAddedAnyField = true;
|
||||
n.applyFields(options.fields)
|
||||
|
@ -56,9 +46,9 @@ export default class CollectionNode {
|
|||
_.each(this.collectionNodes, (collectionNode) => {
|
||||
let linker = collectionNode.linker;
|
||||
|
||||
if (!linker.isVirtual()) {
|
||||
hasAddedAnyField = true;
|
||||
if (linker && !linker.isVirtual()) {
|
||||
options.fields[linker.linkStorageField] = 1;
|
||||
hasAddedAnyField = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -72,4 +62,16 @@ export default class CollectionNode {
|
|||
options.fields = {_id: 1};
|
||||
}
|
||||
}
|
||||
|
||||
parentHasMyLinkStorageFieldSpecified() {
|
||||
const storageField = this.linker.linkStorageField;
|
||||
|
||||
if (this.parent) {
|
||||
return !!_.find(this.parent.fieldNodes, fieldNode => {
|
||||
return fieldNode.name == storageField
|
||||
})
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
export default class FieldNode {
|
||||
constructor(name, body) {
|
||||
this.name = name;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
isGlobal() {
|
||||
return this.name === '$all';
|
||||
this.body = _.isObject(body) ? 1 : body;
|
||||
}
|
||||
|
||||
applyFields(fields) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import createGraph from './lib/createGraph.js';
|
||||
import recursiveFetch from './lib/recursiveFetch.js';
|
||||
import applyFilterFunction from './lib/applyFilterFunction.js';
|
||||
import hypernova from './hypernova/hypernova.js';
|
||||
|
||||
export default class Query {
|
||||
constructor(collection, body, params = {}) {
|
||||
|
@ -80,7 +81,7 @@ export default class Query {
|
|||
applyFilterFunction(this.body, this.params)
|
||||
);
|
||||
|
||||
return recursiveFetch(node, null, options.userId);
|
||||
return hypernova(node, options.userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
6
lib/query/testing/bootstrap/authors/collection.js
Normal file
6
lib/query/testing/bootstrap/authors/collection.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import AuthorSchema from './schema.js';
|
||||
|
||||
const Authors = new Mongo.Collection('authors');
|
||||
export default Authors;
|
||||
|
||||
Authors.attachSchema(AuthorSchema);
|
21
lib/query/testing/bootstrap/authors/links.js
Normal file
21
lib/query/testing/bootstrap/authors/links.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Authors from './collection.js';
|
||||
import Posts from '../posts/collection.js';
|
||||
import Comments from '../comments/collection.js';
|
||||
import Groups from '../groups/collection.js';
|
||||
|
||||
Authors.addLinks({
|
||||
comments: {
|
||||
collection: Comments,
|
||||
inversedBy: 'author'
|
||||
},
|
||||
posts: {
|
||||
collection: Posts,
|
||||
inversedBy: 'author'
|
||||
},
|
||||
groups: {
|
||||
type: 'many',
|
||||
metadata: {},
|
||||
collection: Groups,
|
||||
field: 'groupIds'
|
||||
}
|
||||
});
|
5
lib/query/testing/bootstrap/authors/schema.js
Normal file
5
lib/query/testing/bootstrap/authors/schema.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default new SimpleSchema({
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
});
|
6
lib/query/testing/bootstrap/comments/collection.js
Normal file
6
lib/query/testing/bootstrap/comments/collection.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import CommentSchema from './schema.js';
|
||||
|
||||
const Comments = new Mongo.Collection('comments');
|
||||
export default Comments;
|
||||
|
||||
Comments.attachSchema(CommentSchema);
|
19
lib/query/testing/bootstrap/comments/links.js
Normal file
19
lib/query/testing/bootstrap/comments/links.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import Comments from './collection.js';
|
||||
import Authors from '../authors/collection.js';
|
||||
import Posts from '../posts/collection.js';
|
||||
|
||||
Comments.addLinks({
|
||||
author: {
|
||||
type: 'one',
|
||||
collection: Authors,
|
||||
field: 'authorId',
|
||||
index: true
|
||||
},
|
||||
|
||||
post: {
|
||||
type: 'one',
|
||||
collection: Posts,
|
||||
field: 'postId',
|
||||
index: true
|
||||
}
|
||||
});
|
5
lib/query/testing/bootstrap/comments/schema.js
Normal file
5
lib/query/testing/bootstrap/comments/schema.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default new SimpleSchema({
|
||||
text: {
|
||||
type: String
|
||||
}
|
||||
});
|
67
lib/query/testing/bootstrap/fixtures.js
Normal file
67
lib/query/testing/bootstrap/fixtures.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Random } from 'meteor/random';
|
||||
import { _ } from 'meteor/underscore';
|
||||
|
||||
import Authors from './authors/collection';
|
||||
import Comments from './comments/collection';
|
||||
import Posts from './posts/collection';
|
||||
import Tags from './tags/collection';
|
||||
import Groups from './groups/collection';
|
||||
|
||||
Authors.remove({});
|
||||
Comments.remove({});
|
||||
Posts.remove({});
|
||||
Tags.remove({});
|
||||
Groups.remove({});
|
||||
|
||||
const AUTHORS = 10;
|
||||
const POST_PER_USER = 20;
|
||||
const COMMENTS_PER_POST = 10;
|
||||
const TAGS = ['JavaScript', 'Meteor', 'React', 'Other'];
|
||||
const GROUPS = ['JavaScript', 'Meteor', 'React', 'Other'];
|
||||
const COMMENT_TEXT_SAMPLES = [
|
||||
'Good', 'Bad', 'Neutral'
|
||||
];
|
||||
|
||||
console.log('[testing] Loading test fixtures ...');
|
||||
|
||||
let tags = TAGS.map(name => Tags.insert({name}));
|
||||
let groups = GROUPS.map(name => Groups.insert({name}));
|
||||
let authors = _.range(AUTHORS).map(idx => {
|
||||
return Authors.insert({
|
||||
name: 'Author - ' + idx
|
||||
});
|
||||
});
|
||||
|
||||
_.each(authors, (author) => {
|
||||
const authorPostLink = Authors.getLink(author, 'posts');
|
||||
const authorGroupLink = Authors.getLink(author, 'groups');
|
||||
|
||||
authorGroupLink.add(_.sample(groups), {
|
||||
isAdmin: _.sample([true, false])
|
||||
});
|
||||
|
||||
_.each(_.range(POST_PER_USER), (idx) => {
|
||||
let post = {
|
||||
title: `User Post - ${idx}`
|
||||
};
|
||||
|
||||
authorPostLink.add(post);
|
||||
const postCommentsLink = Posts.getLink(post, 'comments');
|
||||
const postTagsLink = Posts.getLink(post, 'tags');
|
||||
const postGroupLink = Posts.getLink(post, 'group');
|
||||
postGroupLink.set(_.sample(groups), {random: Random.id()});
|
||||
|
||||
postTagsLink.add(_.sample(tags));
|
||||
|
||||
_.each(_.range(COMMENTS_PER_POST), (idx) => {
|
||||
let comment = {
|
||||
text: _.sample(COMMENT_TEXT_SAMPLES)
|
||||
};
|
||||
|
||||
postCommentsLink.add(comment);
|
||||
Comments.getLink(comment, 'author').set(_.sample(authors));
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
console.log('[ok] fixtures have been loaded.');
|
6
lib/query/testing/bootstrap/groups/collection.js
Normal file
6
lib/query/testing/bootstrap/groups/collection.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import GroupSchema from './schema.js';
|
||||
|
||||
const Groups = new Mongo.Collection('groups');
|
||||
export default Groups;
|
||||
|
||||
Groups.attachSchema(GroupSchema);
|
3
lib/query/testing/bootstrap/groups/expose.js
Normal file
3
lib/query/testing/bootstrap/groups/expose.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Groups from './collection.js';
|
||||
|
||||
Groups.expose();
|
14
lib/query/testing/bootstrap/groups/links.js
Normal file
14
lib/query/testing/bootstrap/groups/links.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import Groups from './collection.js';
|
||||
import Authors from '../authors/collection.js';
|
||||
import Posts from '../posts/collection.js';
|
||||
|
||||
Groups.addLinks({
|
||||
authors: {
|
||||
collection: Authors,
|
||||
inversedBy: 'groups'
|
||||
},
|
||||
posts: {
|
||||
collection: Posts,
|
||||
inversedBy: 'group'
|
||||
}
|
||||
});
|
5
lib/query/testing/bootstrap/groups/schema.js
Normal file
5
lib/query/testing/bootstrap/groups/schema.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default new SimpleSchema({
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
});
|
11
lib/query/testing/bootstrap/index.js
Normal file
11
lib/query/testing/bootstrap/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import './comments/links';
|
||||
import './posts/links';
|
||||
import './authors/links';
|
||||
import './tags/links';
|
||||
import './groups/links';
|
||||
|
||||
import Posts from './posts/collection.js';
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Posts.expose();
|
||||
}
|
6
lib/query/testing/bootstrap/posts/collection.js
Normal file
6
lib/query/testing/bootstrap/posts/collection.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import PostSchema from './schema.js';
|
||||
|
||||
const Posts = new Mongo.Collection('posts');
|
||||
export default Posts;
|
||||
|
||||
Posts.attachSchema(PostSchema);
|
5
lib/query/testing/bootstrap/posts/expose.js
Normal file
5
lib/query/testing/bootstrap/posts/expose.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Posts from './collection.js';
|
||||
|
||||
Posts.expose((filters, options, userId) => {
|
||||
|
||||
});
|
34
lib/query/testing/bootstrap/posts/links.js
Normal file
34
lib/query/testing/bootstrap/posts/links.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Posts from './collection.js';
|
||||
import Authors from '../authors/collection.js';
|
||||
import Comments from '../comments/collection.js';
|
||||
import Tags from '../tags/collection.js';
|
||||
import Groups from '../groups/collection.js';
|
||||
|
||||
Posts.addLinks({
|
||||
author: {
|
||||
type: 'one',
|
||||
collection: Authors,
|
||||
field: 'ownerId',
|
||||
index: true
|
||||
},
|
||||
comments: {
|
||||
collection: Comments,
|
||||
inversedBy: 'post'
|
||||
},
|
||||
tags: {
|
||||
collection: Tags,
|
||||
type: 'many',
|
||||
field: 'tagIds',
|
||||
index: true
|
||||
},
|
||||
commentsCount: {
|
||||
resolve(post) {
|
||||
return Comments.find({postId: post._id}).count();
|
||||
}
|
||||
},
|
||||
group: {
|
||||
type: 'one',
|
||||
collection: Groups,
|
||||
metadata: {}
|
||||
}
|
||||
});
|
5
lib/query/testing/bootstrap/posts/schema.js
Normal file
5
lib/query/testing/bootstrap/posts/schema.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default new SimpleSchema({
|
||||
title: {
|
||||
type: String
|
||||
}
|
||||
})
|
6
lib/query/testing/bootstrap/tags/collection.js
Normal file
6
lib/query/testing/bootstrap/tags/collection.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import TagSchema from './schema.js';
|
||||
|
||||
const Tags = new Mongo.Collection('tags');
|
||||
export default Tags;
|
||||
|
||||
Tags.attachSchema(TagSchema);
|
9
lib/query/testing/bootstrap/tags/links.js
Normal file
9
lib/query/testing/bootstrap/tags/links.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Tags from './collection.js';
|
||||
import Posts from '../posts/collection.js';
|
||||
|
||||
Tags.addLinks({
|
||||
posts: {
|
||||
collection: Posts,
|
||||
inversedBy: 'tags'
|
||||
}
|
||||
});
|
5
lib/query/testing/bootstrap/tags/schema.js
Normal file
5
lib/query/testing/bootstrap/tags/schema.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default new SimpleSchema({
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
});
|
67
lib/query/testing/client.test.js
Normal file
67
lib/query/testing/client.test.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
|
||||
describe('Query Client Tests', function () {
|
||||
it('Should work with queries via method call', function (done) {
|
||||
const query = createQuery({
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
title: 1,
|
||||
comments: {
|
||||
$filters: {text: 'Good'},
|
||||
text: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
query.fetch((err, res) => {
|
||||
assert.isUndefined(err);
|
||||
|
||||
assert.isArray(res);
|
||||
_.each(res, post => {
|
||||
assert.isString(post.title);
|
||||
assert.isString(post._id);
|
||||
_.each(post.comments, comment => {
|
||||
assert.isString(comment._id);
|
||||
assert.equal('Good', comment.text);
|
||||
})
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should work with queries reactively', function (done) {
|
||||
const query = createQuery({
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
title: 1,
|
||||
comments: {
|
||||
$filters: {text: 'Good'},
|
||||
text: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handle = query.subscribe();
|
||||
|
||||
Tracker.autorun(c => {
|
||||
if (handle.ready()) {
|
||||
c.stop();
|
||||
|
||||
const res = query.fetch();
|
||||
|
||||
assert.isArray(res);
|
||||
_.each(res, post => {
|
||||
assert.isString(post.title);
|
||||
assert.isString(post._id);
|
||||
_.each(post.comments, comment => {
|
||||
assert.isString(comment._id);
|
||||
assert.equal('Good', comment.text);
|
||||
})
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
242
lib/query/testing/server.test.js
Normal file
242
lib/query/testing/server.test.js
Normal file
|
@ -0,0 +1,242 @@
|
|||
import { createQuery } from 'meteor/cultofcoders:grapher';
|
||||
import Comments from './bootstrap/comments/collection.js';
|
||||
|
||||
describe('Hypernova', function () {
|
||||
it('Should fetch One links correctly', function () {
|
||||
const data = createQuery({
|
||||
comments: {
|
||||
text: 1,
|
||||
author: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, Comments.find().count());
|
||||
assert.isTrue(data.length > 0);
|
||||
|
||||
_.each(data, comment => {
|
||||
assert.isObject(comment.author);
|
||||
assert.isString(comment.author.name);
|
||||
assert.isString(comment.author._id);
|
||||
assert.isTrue(_.keys(comment.author).length == 2);
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch One links with limit and options', function () {
|
||||
const data = createQuery({
|
||||
comments: {
|
||||
$options: {limit: 5},
|
||||
text: 1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, 5);
|
||||
});
|
||||
|
||||
it('Should fetch One-Inversed links with limit and options', function () {
|
||||
const data = createQuery({
|
||||
authors: {
|
||||
$options: {limit: 5},
|
||||
comments: {
|
||||
$filters: {text: 'Good'},
|
||||
$options: {limit: 2},
|
||||
text: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, 5);
|
||||
_.each(data, author => {
|
||||
assert.lengthOf(author.comments, 2);
|
||||
_.each(author.comments, comment => {
|
||||
assert.equal('Good', comment.text);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch Many links correctly', function () {
|
||||
const data = createQuery({
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
title: 1,
|
||||
tags: {
|
||||
text: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, 5);
|
||||
_.each(data, post => {
|
||||
assert.isString(post.title);
|
||||
assert.isArray(post.tags);
|
||||
assert.isTrue(post.tags.length > 0);
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch Many - inversed links correctly', function () {
|
||||
const data = createQuery({
|
||||
tags: {
|
||||
name: 1,
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
title: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(data, tag => {
|
||||
assert.isString(tag.name);
|
||||
assert.isArray(tag.posts);
|
||||
assert.lengthOf(tag.posts, 5);
|
||||
_.each(tag.posts, post => {
|
||||
assert.isString(post.title);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch One-Meta links correctly', function () {
|
||||
const data = createQuery({
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
title: 1,
|
||||
group: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, 5);
|
||||
_.each(data, post => {
|
||||
assert.isString(post.title);
|
||||
assert.isString(post._id);
|
||||
assert.isObject(post.group);
|
||||
assert.isString(post.group._id);
|
||||
assert.isString(post.group.name);
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch One-Meta inversed links correctly', function () {
|
||||
const data = createQuery({
|
||||
groups: {
|
||||
name: 1,
|
||||
posts: {
|
||||
title: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(data, group => {
|
||||
assert.isString(group.name);
|
||||
assert.isString(group._id);
|
||||
assert.lengthOf(_.keys(group), 3);
|
||||
assert.isArray(group.posts);
|
||||
_.each(group.posts, post => {
|
||||
assert.isString(post.title);
|
||||
assert.isString(post._id);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch Many-Meta links correctly', function () {
|
||||
const data = createQuery({
|
||||
authors: {
|
||||
name: 1,
|
||||
groups: {
|
||||
$options: {limit: 1},
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(data, author => {
|
||||
assert.isArray(author.groups);
|
||||
assert.lengthOf(author.groups, 1);
|
||||
|
||||
_.each(author.groups, group => {
|
||||
assert.isObject(group);
|
||||
assert.isString(group._id);
|
||||
assert.isString(group.name);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch Many-Meta inversed links correctly', function () {
|
||||
const data = createQuery({
|
||||
groups: {
|
||||
name: 1,
|
||||
authors: {
|
||||
$options: {limit: 2},
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(data, group => {
|
||||
assert.isArray(group.authors);
|
||||
assert.isTrue(group.authors.length <= 2);
|
||||
|
||||
_.each(group.authors, author => {
|
||||
assert.isObject(author);
|
||||
assert.isString(author._id);
|
||||
assert.isString(author.name);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
it('Should fetch Resolver links properly', function () {
|
||||
const data = createQuery({
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
commentsCount: 1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, 5);
|
||||
_.each(data, post => {
|
||||
assert.equal(10, post.commentsCount);
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fetch in depth properly at any given level.', function () {
|
||||
const data = createQuery({
|
||||
authors: {
|
||||
$options: {limit: 5},
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
comments: {
|
||||
$options: {limit: 5},
|
||||
author: {
|
||||
groups: {
|
||||
posts: {
|
||||
$options: {limit: 5},
|
||||
author: {
|
||||
name: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
assert.lengthOf(data, 5);
|
||||
let arrivedInDepth = false;
|
||||
|
||||
_.each(data, author => {
|
||||
_.each(author.posts, post => {
|
||||
_.each(post.comments, comment => {
|
||||
_.each(comment.author.groups, group => {
|
||||
_.each(group.posts, post => {
|
||||
assert.isString(post.author.name);
|
||||
arrivedInDepth = true;
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
assert.isTrue(arrivedInDepth);
|
||||
})
|
||||
});
|
34
lib/query/tests/bootstrap.js
vendored
34
lib/query/tests/bootstrap.js
vendored
|
@ -1,34 +0,0 @@
|
|||
PostCollection = new Mongo.Collection('test_query_post');
|
||||
CommentCollection = new Mongo.Collection(('test_query_comment'));
|
||||
GroupCollection = new Mongo.Collection(('test_query_group'));
|
||||
AuthorCollection = new Mongo.Collection(('test_query_author'));
|
||||
|
||||
PostCollection.addLinks({
|
||||
'comments': {
|
||||
collection: CommentCollection,
|
||||
type: '*'
|
||||
},
|
||||
'groups': {
|
||||
collection: GroupCollection,
|
||||
type: '*',
|
||||
metadata: {
|
||||
isAdmin: {type: String}
|
||||
}
|
||||
},
|
||||
author: {
|
||||
collection: AuthorCollection,
|
||||
type: 'one'
|
||||
},
|
||||
comment_resolve: {
|
||||
resolve(object) {
|
||||
return CommentCollection.find({resourceId: object._id}).fetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CommentCollection.addLinks({
|
||||
author: {
|
||||
collection: AuthorCollection,
|
||||
type: '1'
|
||||
}
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
import './bootstrap.js';
|
||||
import createQuery from '../createQuery.js';
|
||||
|
||||
describe('Query Client Tests', function () {
|
||||
it('Should return static data with helpers', function (done) {
|
||||
const query = PostCollection.createQuery({
|
||||
title: 1,
|
||||
groups: 1
|
||||
});
|
||||
|
||||
query.fetch((err, res) => {
|
||||
assert.lengthOf(res, 2);
|
||||
_.each(res, element => {
|
||||
assert.isArray(element.groups);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should work with global queries', function (done) {
|
||||
const query = createQuery({
|
||||
test_query_post: {
|
||||
title: 1,
|
||||
groups: 1
|
||||
}
|
||||
});
|
||||
|
||||
query.fetch((err, res) => {
|
||||
assert.lengthOf(res, 2);
|
||||
_.each(res, element => {
|
||||
assert.isArray(element.groups);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should work with global queries (reactively)', function (done) {
|
||||
const query = createQuery({
|
||||
test_query_post: {
|
||||
title: 1,
|
||||
groups: 1
|
||||
}
|
||||
});
|
||||
|
||||
const handle = query.subscribe();
|
||||
|
||||
Tracker.autorun(c => {
|
||||
if (handle.ready()) {
|
||||
c.stop();
|
||||
|
||||
const res = query.fetch();
|
||||
|
||||
assert.lengthOf(res, 2);
|
||||
_.each(res, element => {
|
||||
assert.isArray(element.groups);
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('Should subscribe to links properly', function (done) {
|
||||
const query = PostCollection.createQuery({
|
||||
title: 1,
|
||||
comments: {
|
||||
$filters: {isBanned: false}
|
||||
},
|
||||
groups: {}
|
||||
});
|
||||
|
||||
const subsHandle = query.subscribe();
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
if (subsHandle.ready()) {
|
||||
c.stop();
|
||||
|
||||
let posts = PostCollection.find().fetch();
|
||||
assert.lengthOf(posts, 2);
|
||||
let firstPost = posts[0];
|
||||
|
||||
const commentsLink = PostCollection.getLink(firstPost, 'comments');
|
||||
assert.lengthOf(commentsLink.find().fetch(), 2);
|
||||
|
||||
// check direct fetching
|
||||
posts = query.fetch();
|
||||
|
||||
assert.lengthOf(posts, 2);
|
||||
firstPost = posts[0];
|
||||
assert.lengthOf(firstPost.comments, 2);
|
||||
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
|
@ -1,136 +0,0 @@
|
|||
import createQuery from '../createQuery.js';
|
||||
|
||||
describe('Query Server Tests', function () {
|
||||
it('should return the propper data', function () {
|
||||
const query = PostCollection.createQuery({
|
||||
$filters: {'title': 'Hello'},
|
||||
title: 1,
|
||||
author: {
|
||||
name: 1
|
||||
},
|
||||
nested: {
|
||||
data: 1
|
||||
},
|
||||
comments: {
|
||||
text: 1,
|
||||
author: {
|
||||
name: 1
|
||||
}
|
||||
},
|
||||
groups: {
|
||||
name: 1
|
||||
},
|
||||
comment_resolve: {
|
||||
$filters: {test: '123'}
|
||||
}
|
||||
});
|
||||
|
||||
const data = query.fetch();
|
||||
|
||||
assert.equal(1, data.length);
|
||||
let post = data[0];
|
||||
|
||||
//assert.isString(post.testModelFunction());
|
||||
assert.equal('Yes', post.nested.data);
|
||||
assert.equal(3, post.comments.length);
|
||||
assert.equal(2, post.groups.length);
|
||||
assert.isObject(post.author);
|
||||
assert.equal('John McSmithie', post.author.name);
|
||||
|
||||
assert.lengthOf(post.comment_resolve, 1);
|
||||
|
||||
_.each(post.comments, comment => {
|
||||
assert.isString(comment.author.name);
|
||||
})
|
||||
});
|
||||
|
||||
it('should apply filter function recursively', function () {
|
||||
const query = PostCollection.createQuery({
|
||||
$filter({filters, params}) {
|
||||
if (params.title) {
|
||||
filters.title = params.title;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
query.setParams({title: 'Hello'});
|
||||
assert.equal(1, query.fetch().length);
|
||||
|
||||
query.setParams({title: undefined});
|
||||
assert.equal(2, query.fetch().length);
|
||||
});
|
||||
|
||||
|
||||
it('Should work with global createQuery', function () {
|
||||
const query = createQuery({
|
||||
test_query_post: {
|
||||
title: 1,
|
||||
groups: 1
|
||||
}
|
||||
});
|
||||
|
||||
const res = query.fetch();
|
||||
|
||||
assert.lengthOf(res, 2);
|
||||
_.each(res, element => {
|
||||
assert.isArray(element.groups);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fetch the fields properly', function () {
|
||||
let res;
|
||||
|
||||
res = createQuery({
|
||||
test_query_post: {
|
||||
}
|
||||
}).fetch();
|
||||
_.each(res, element => {
|
||||
assert.isDefined(element._id);
|
||||
assert.isUndefined(element.title);
|
||||
});
|
||||
|
||||
res = createQuery({
|
||||
test_query_post: {
|
||||
title: 1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(res, element => {
|
||||
assert.isDefined(element._id);
|
||||
assert.isDefined(element.title);
|
||||
});
|
||||
|
||||
res = createQuery({
|
||||
test_query_post: {
|
||||
groups: {
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(res, element => {
|
||||
assert.isDefined(element._id);
|
||||
_.each(element.groups, group => {
|
||||
assert.isDefined(group._id);
|
||||
assert.isUndefined(group.name);
|
||||
})
|
||||
});
|
||||
|
||||
res = createQuery({
|
||||
test_query_post: {
|
||||
$all: 1,
|
||||
groups: {
|
||||
$all: 1
|
||||
}
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
_.each(res, element => {
|
||||
assert.isDefined(element.title);
|
||||
assert.isDefined(element._id);
|
||||
_.each(element.groups, group => {
|
||||
assert.isDefined(group._id);
|
||||
assert.isDefined(group.name);
|
||||
})
|
||||
});
|
||||
})
|
||||
});
|
16
main.both.js
16
main.both.js
|
@ -5,22 +5,6 @@ export {
|
|||
default as createQuery
|
||||
} from './lib/query/createQuery.js';
|
||||
|
||||
export {
|
||||
default as createGraph
|
||||
} from './lib/query/lib/createGraph.js';
|
||||
|
||||
export {
|
||||
default as recursiveFetch
|
||||
} from './lib/query/lib/recursiveFetch.js';
|
||||
|
||||
export {
|
||||
default as recursiveCompose
|
||||
} from './lib/query/lib/recursiveCompose.js';
|
||||
|
||||
export {
|
||||
default as applyFilterFunction
|
||||
} from './lib/query/lib/applyFilterFunction.js';
|
||||
|
||||
export {
|
||||
default as restrictFields
|
||||
} from './lib/exposure/lib/restrictFields.js';
|
|
@ -1 +1,6 @@
|
|||
import './lib/exposure/extension.js';
|
||||
|
||||
import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions';
|
||||
checkNpmVersions({
|
||||
'sift': '3.2.x'
|
||||
}, 'cultofcoders:grapher');
|
20
package.js
20
package.js
|
@ -1,8 +1,8 @@
|
|||
Package.describe({
|
||||
name: 'cultofcoders:grapher',
|
||||
version: '1.0.10',
|
||||
version: '1.1.0',
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'Grapher is a way of linking/joining collections. And fetching data in a GraphQL style.',
|
||||
summary: 'Grapher makes linking collections easily. And fetching data as a graph.',
|
||||
// URL to the Git repository containing the source code for this package.
|
||||
git: 'https://github.com/cult-of-coders/grapher',
|
||||
// By default, Meteor will default to using README.md for documentation.
|
||||
|
@ -10,6 +10,10 @@ Package.describe({
|
|||
documentation: 'README.md'
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
'sift': '3.2.6'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.versionsFrom('1.3');
|
||||
|
||||
|
@ -21,6 +25,8 @@ Package.onUse(function (api) {
|
|||
'matb33:collection-hooks@0.8.4',
|
||||
'reywood:publish-composite@1.4.2',
|
||||
'dburles:mongo-collection-instances@0.3.5',
|
||||
'tmeasday:check-npm-versions@0.3.1',
|
||||
'meteorhacks:aggregate@1.3.0',
|
||||
'mongo'
|
||||
];
|
||||
|
||||
|
@ -37,14 +43,16 @@ Package.onTest(function (api) {
|
|||
|
||||
api.use('ecmascript');
|
||||
api.use('tracker');
|
||||
|
||||
api.use('practicalmeteor:mocha');
|
||||
api.use('practicalmeteor:chai');
|
||||
|
||||
api.mainModule('lib/links/tests/main.js', 'server');
|
||||
|
||||
api.addFiles(['lib/query/tests/bootstrap.js']);
|
||||
api.addFiles(['lib/query/tests/fixtures.js'], 'server');
|
||||
api.addFiles('lib/query/testing/bootstrap/index.js');
|
||||
|
||||
api.mainModule('lib/query/tests/client.test.js', 'client');
|
||||
api.mainModule('lib/query/tests/server.test.js', 'server');
|
||||
// When you play with tests you should comment this to make tests go faster.
|
||||
api.addFiles('lib/query/testing/bootstrap/fixtures.js', 'server');
|
||||
api.mainModule('lib/query/testing/server.test.js', 'server');
|
||||
api.mainModule('lib/query/testing/client.test.js', 'client');
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue