diff --git a/.npm/package/.gitignore b/.npm/package/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.npm/package/README b/.npm/package/README new file mode 100644 index 0000000..3d49255 --- /dev/null +++ b/.npm/package/README @@ -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. diff --git a/.npm/package/npm-shrinkwrap.json b/.npm/package/npm-shrinkwrap.json new file mode 100644 index 0000000..6cdb574 --- /dev/null +++ b/.npm/package/npm-shrinkwrap.json @@ -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" + } + } +} diff --git a/README.md b/README.md index fd26e7c..1649791 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,19 @@ Welcome to Grapher [![Build Status](https://api.travis-ci.org/cult-of-coders/grapher.svg)](https://travis-ci.org/cult-of-coders/grapher) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](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 ------- diff --git a/docs/api.md b/docs/api.md index 96df5a5..4c2d391 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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; diff --git a/docs/query.md b/docs/query.md index 3a04bf7..b8a80e8 100644 --- a/docs/query.md +++ b/docs/query.md @@ -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 diff --git a/lib/exposure/exposure.js b/lib/exposure/exposure.js index 9cda819..0d869b3 100644 --- a/lib/exposure/exposure.js +++ b/lib/exposure/exposure.js @@ -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); } diff --git a/lib/links/config.schema.js b/lib/links/config.schema.js index 163b57e..20fb0a3 100644 --- a/lib/links/config.schema.js +++ b/lib/links/config.schema.js @@ -35,6 +35,11 @@ export default new SimpleSchema({ type: Boolean, defaultValue: false, optional: true + }, + unique: { + type: Boolean, + defaultValue: false, + optional: true } }); diff --git a/lib/links/lib/createFieldSchema.js b/lib/links/lib/createFieldSchema.js index 45d8696..ee5a6c1 100644 --- a/lib/links/lib/createFieldSchema.js +++ b/lib/links/lib/createFieldSchema.js @@ -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; diff --git a/lib/links/lib/createSearchFilters.js b/lib/links/lib/createSearchFilters.js new file mode 100644 index 0000000..cf257c3 --- /dev/null +++ b/lib/links/lib/createSearchFilters.js @@ -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 + }; +} \ No newline at end of file diff --git a/lib/links/linkTypes/base.js b/lib/links/linkTypes/base.js index 5e49c69..d79e301 100644 --- a/lib/links/linkTypes/base.js +++ b/lib/links/linkTypes/base.js @@ -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 }); } diff --git a/lib/links/linker.js b/lib/links/linker.js index 2d96a9e..cccddf1 100644 --- a/lib/links/linker.js +++ b/lib/links/linker.js @@ -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'; + field = field + '._id'; } - this.mainCollection._ensureIndex({ - [field]: 1 - }) + 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}) + } + } } } diff --git a/lib/query/hypernova/README.md b/lib/query/hypernova/README.md new file mode 100644 index 0000000..cf07c3b --- /dev/null +++ b/lib/query/hypernova/README.md @@ -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. \ No newline at end of file diff --git a/lib/query/hypernova/aggregateSearchFilters.js b/lib/query/hypernova/aggregateSearchFilters.js new file mode 100644 index 0000000..041538a --- /dev/null +++ b/lib/query/hypernova/aggregateSearchFilters.js @@ -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') + } + }; + } + } +} \ No newline at end of file diff --git a/lib/query/hypernova/assembleAggregateResults.js b/lib/query/hypernova/assembleAggregateResults.js new file mode 100644 index 0000000..ea4011c --- /dev/null +++ b/lib/query/hypernova/assembleAggregateResults.js @@ -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; +} \ No newline at end of file diff --git a/lib/query/hypernova/assembler.js b/lib/query/hypernova/assembler.js new file mode 100644 index 0000000..d9396f9 --- /dev/null +++ b/lib/query/hypernova/assembler.js @@ -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); +} \ No newline at end of file diff --git a/lib/query/hypernova/buildAggregatePipeline.js b/lib/query/hypernova/buildAggregatePipeline.js new file mode 100644 index 0000000..d892caa --- /dev/null +++ b/lib/query/hypernova/buildAggregatePipeline.js @@ -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; +} \ No newline at end of file diff --git a/lib/query/hypernova/hypernova.js b/lib/query/hypernova/hypernova.js new file mode 100644 index 0000000..53fb468 --- /dev/null +++ b/lib/query/hypernova/hypernova.js @@ -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; +} + diff --git a/lib/query/hypernova/storeHypernovaResults.js b/lib/query/hypernova/storeHypernovaResults.js new file mode 100644 index 0000000..654261e --- /dev/null +++ b/lib/query/hypernova/storeHypernovaResults.js @@ -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); + } +} \ No newline at end of file diff --git a/lib/query/lib/createGraph.js b/lib/query/lib/createGraph.js index 665be23..2cfce3a 100644 --- a/lib/query/lib/createGraph.js +++ b/lib/query/lib/createGraph.js @@ -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); } diff --git a/lib/query/lib/recursiveCompose.js b/lib/query/lib/recursiveCompose.js index d7ef811..09a154c 100644 --- a/lib/query/lib/recursiveCompose.js +++ b/lib/query/lib/recursiveCompose.js @@ -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 }); } diff --git a/lib/query/lib/recursiveFetch.js b/lib/query/lib/recursiveFetch.js index fc5b1ef..5292cfd 100644 --- a/lib/query/lib/recursiveFetch.js +++ b/lib/query/lib/recursiveFetch.js @@ -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(); + } } - - - results = accessor.fetch(filters, options, userId); } else { if (node.collection.findSecure) { results = node.collection.findSecure(filters, options, userId).fetch(); @@ -34,4 +43,4 @@ export default function fetch(node, parentObject, userId) { }); return results; -} \ No newline at end of file +} diff --git a/lib/query/nodes/collectionNode.js b/lib/query/nodes/collectionNode.js index ef52a3c..0eafb5f 100644 --- a/lib/query/nodes/collectionNode.js +++ b/lib/query/nodes/collectionNode.js @@ -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; + } } diff --git a/lib/query/nodes/fieldNode.js b/lib/query/nodes/fieldNode.js index b35108a..28a108d 100644 --- a/lib/query/nodes/fieldNode.js +++ b/lib/query/nodes/fieldNode.js @@ -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) { diff --git a/lib/query/query.js b/lib/query/query.js index bf0627d..04bb37c 100644 --- a/lib/query/query.js +++ b/lib/query/query.js @@ -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); } } diff --git a/lib/query/testing/bootstrap/authors/collection.js b/lib/query/testing/bootstrap/authors/collection.js new file mode 100644 index 0000000..86efb73 --- /dev/null +++ b/lib/query/testing/bootstrap/authors/collection.js @@ -0,0 +1,6 @@ +import AuthorSchema from './schema.js'; + +const Authors = new Mongo.Collection('authors'); +export default Authors; + +Authors.attachSchema(AuthorSchema); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/authors/links.js b/lib/query/testing/bootstrap/authors/links.js new file mode 100644 index 0000000..c2adf9f --- /dev/null +++ b/lib/query/testing/bootstrap/authors/links.js @@ -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' + } +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/authors/schema.js b/lib/query/testing/bootstrap/authors/schema.js new file mode 100644 index 0000000..3513ff6 --- /dev/null +++ b/lib/query/testing/bootstrap/authors/schema.js @@ -0,0 +1,5 @@ +export default new SimpleSchema({ + name: { + type: String + } +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/comments/collection.js b/lib/query/testing/bootstrap/comments/collection.js new file mode 100644 index 0000000..9613c91 --- /dev/null +++ b/lib/query/testing/bootstrap/comments/collection.js @@ -0,0 +1,6 @@ +import CommentSchema from './schema.js'; + +const Comments = new Mongo.Collection('comments'); +export default Comments; + +Comments.attachSchema(CommentSchema); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/comments/links.js b/lib/query/testing/bootstrap/comments/links.js new file mode 100644 index 0000000..3906ea2 --- /dev/null +++ b/lib/query/testing/bootstrap/comments/links.js @@ -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 + } +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/comments/schema.js b/lib/query/testing/bootstrap/comments/schema.js new file mode 100644 index 0000000..c4c14be --- /dev/null +++ b/lib/query/testing/bootstrap/comments/schema.js @@ -0,0 +1,5 @@ +export default new SimpleSchema({ + text: { + type: String + } +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/fixtures.js b/lib/query/testing/bootstrap/fixtures.js new file mode 100644 index 0000000..40a8a50 --- /dev/null +++ b/lib/query/testing/bootstrap/fixtures.js @@ -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.'); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/groups/collection.js b/lib/query/testing/bootstrap/groups/collection.js new file mode 100644 index 0000000..6a7ae17 --- /dev/null +++ b/lib/query/testing/bootstrap/groups/collection.js @@ -0,0 +1,6 @@ +import GroupSchema from './schema.js'; + +const Groups = new Mongo.Collection('groups'); +export default Groups; + +Groups.attachSchema(GroupSchema); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/groups/expose.js b/lib/query/testing/bootstrap/groups/expose.js new file mode 100644 index 0000000..fe1d821 --- /dev/null +++ b/lib/query/testing/bootstrap/groups/expose.js @@ -0,0 +1,3 @@ +import Groups from './collection.js'; + +Groups.expose(); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/groups/links.js b/lib/query/testing/bootstrap/groups/links.js new file mode 100644 index 0000000..8a46908 --- /dev/null +++ b/lib/query/testing/bootstrap/groups/links.js @@ -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' + } +}); diff --git a/lib/query/testing/bootstrap/groups/schema.js b/lib/query/testing/bootstrap/groups/schema.js new file mode 100644 index 0000000..3513ff6 --- /dev/null +++ b/lib/query/testing/bootstrap/groups/schema.js @@ -0,0 +1,5 @@ +export default new SimpleSchema({ + name: { + type: String + } +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/index.js b/lib/query/testing/bootstrap/index.js new file mode 100644 index 0000000..25ccf84 --- /dev/null +++ b/lib/query/testing/bootstrap/index.js @@ -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(); +} diff --git a/lib/query/testing/bootstrap/posts/collection.js b/lib/query/testing/bootstrap/posts/collection.js new file mode 100644 index 0000000..160bb1a --- /dev/null +++ b/lib/query/testing/bootstrap/posts/collection.js @@ -0,0 +1,6 @@ +import PostSchema from './schema.js'; + +const Posts = new Mongo.Collection('posts'); +export default Posts; + +Posts.attachSchema(PostSchema); diff --git a/lib/query/testing/bootstrap/posts/expose.js b/lib/query/testing/bootstrap/posts/expose.js new file mode 100644 index 0000000..5916744 --- /dev/null +++ b/lib/query/testing/bootstrap/posts/expose.js @@ -0,0 +1,5 @@ +import Posts from './collection.js'; + +Posts.expose((filters, options, userId) => { + +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/posts/links.js b/lib/query/testing/bootstrap/posts/links.js new file mode 100644 index 0000000..23b2c0c --- /dev/null +++ b/lib/query/testing/bootstrap/posts/links.js @@ -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: {} + } +}); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/posts/schema.js b/lib/query/testing/bootstrap/posts/schema.js new file mode 100644 index 0000000..41283fd --- /dev/null +++ b/lib/query/testing/bootstrap/posts/schema.js @@ -0,0 +1,5 @@ +export default new SimpleSchema({ + title: { + type: String + } +}) \ No newline at end of file diff --git a/lib/query/testing/bootstrap/tags/collection.js b/lib/query/testing/bootstrap/tags/collection.js new file mode 100644 index 0000000..5ac8629 --- /dev/null +++ b/lib/query/testing/bootstrap/tags/collection.js @@ -0,0 +1,6 @@ +import TagSchema from './schema.js'; + +const Tags = new Mongo.Collection('tags'); +export default Tags; + +Tags.attachSchema(TagSchema); \ No newline at end of file diff --git a/lib/query/testing/bootstrap/tags/links.js b/lib/query/testing/bootstrap/tags/links.js new file mode 100644 index 0000000..c7f1bdf --- /dev/null +++ b/lib/query/testing/bootstrap/tags/links.js @@ -0,0 +1,9 @@ +import Tags from './collection.js'; +import Posts from '../posts/collection.js'; + +Tags.addLinks({ + posts: { + collection: Posts, + inversedBy: 'tags' + } +}); diff --git a/lib/query/testing/bootstrap/tags/schema.js b/lib/query/testing/bootstrap/tags/schema.js new file mode 100644 index 0000000..3513ff6 --- /dev/null +++ b/lib/query/testing/bootstrap/tags/schema.js @@ -0,0 +1,5 @@ +export default new SimpleSchema({ + name: { + type: String + } +}); \ No newline at end of file diff --git a/lib/query/testing/client.test.js b/lib/query/testing/client.test.js new file mode 100644 index 0000000..67255c9 --- /dev/null +++ b/lib/query/testing/client.test.js @@ -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(); + } + }) + }); +}); \ No newline at end of file diff --git a/lib/query/tests/fixtures.js b/lib/query/testing/fixtures.js similarity index 100% rename from lib/query/tests/fixtures.js rename to lib/query/testing/fixtures.js diff --git a/lib/query/testing/server.test.js b/lib/query/testing/server.test.js new file mode 100644 index 0000000..3307214 --- /dev/null +++ b/lib/query/testing/server.test.js @@ -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); + }) +}); \ No newline at end of file diff --git a/lib/query/tests/bootstrap.js b/lib/query/tests/bootstrap.js deleted file mode 100644 index 2f6cea8..0000000 --- a/lib/query/tests/bootstrap.js +++ /dev/null @@ -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' - } -}); \ No newline at end of file diff --git a/lib/query/tests/client.test.js b/lib/query/tests/client.test.js deleted file mode 100644 index 2eb4be6..0000000 --- a/lib/query/tests/client.test.js +++ /dev/null @@ -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(); - } - }) - }); -}); \ No newline at end of file diff --git a/lib/query/tests/server.test.js b/lib/query/tests/server.test.js deleted file mode 100644 index 1d4dd60..0000000 --- a/lib/query/tests/server.test.js +++ /dev/null @@ -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); - }) - }); - }) -}); \ No newline at end of file diff --git a/main.both.js b/main.both.js index 45ed6f7..e6588fd 100644 --- a/main.both.js +++ b/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'; \ No newline at end of file diff --git a/main.server.js b/main.server.js index e569bce..235914d 100644 --- a/main.server.js +++ b/main.server.js @@ -1 +1,6 @@ import './lib/exposure/extension.js'; + +import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; +checkNpmVersions({ + 'sift': '3.2.x' +}, 'cultofcoders:grapher'); \ No newline at end of file diff --git a/package.js b/package.js index 3fc6e5f..58c4fab 100644 --- a/package.js +++ b/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'); });