diff --git a/MIGRATION.md b/MIGRATION.md index 060efde..d3ae7ac 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -6,8 +6,31 @@ When you use reducers with a body that uses a link that should return a single r ### From 1.2 to 1.3 -SimpleSchema has been completely removed and it will no longer extend your Collection's schema automatically, therefore, -if you have configured links you have to manually add them. +SimpleSchema has been completely removed and it will no longer extend your Collection's schema automatically, therefore, if you have configured links you have to manually add them. + +For example the following link: + +```js +Users.addLinks({ + post: { + type: 'one', + collection: Posts, + field: 'postId' + } +}); +``` + +Requires the respective field in your Collection's schema: + +```js +// schema for Users +SimpleSchema({ + postId: { + type: String, + optional: true + } +}) +``` The `metadata` link configuration is no longer an object, but a `Boolean` diff --git a/docs/query_options.md b/docs/query_options.md index d24cfc1..d3ad3d6 100644 --- a/docs/query_options.md +++ b/docs/query_options.md @@ -74,7 +74,7 @@ Posts.createQuery({ }) ``` -The query above will fetch as `comments` only the ones that have been approved and that are linekd with the `post`. +The query above will fetch as `comments` only the ones that have been approved and that are linked with the `post`. The `$filter` function shares the same `params` across all collection nodes: diff --git a/lib/db.js b/lib/db.js index 0a4e83b..7457f8a 100644 --- a/lib/db.js +++ b/lib/db.js @@ -5,11 +5,14 @@ const db = new Proxy( {}, { get: function(obj, prop) { + if (typeof prop === 'symbol') { + return obj[prop]; + } + const collection = Mongo.Collection.get(prop); if (!collection) { - Meteor.isDevelopment && - console.warn(`There is no collection with the name: "${prop}"`); + return obj[prop]; } return collection; diff --git a/lib/namedQuery/namedQuery.client.js b/lib/namedQuery/namedQuery.client.js index 89d5d88..180ec73 100755 --- a/lib/namedQuery/namedQuery.client.js +++ b/lib/namedQuery/namedQuery.client.js @@ -77,7 +77,7 @@ export default class extends Base { throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.'); } - return await callWithPromise(this.name, prepareForProcess(this.body, this.params)); + return await callWithPromise(this.name, this.params); } /** @@ -129,7 +129,7 @@ export default class extends Base { throw new Meteor.Error('This query is reactive, meaning you cannot use promises to fetch the data.'); } - return await callWithPromise(this.name + '.count', prepareForProcess(this.body, this.params)); + return await callWithPromise(this.name + '.count', this.params); } /** diff --git a/lib/namedQuery/testing/client.test.js b/lib/namedQuery/testing/client.test.js index 609c5c3..8eda271 100755 --- a/lib/namedQuery/testing/client.test.js +++ b/lib/namedQuery/testing/client.test.js @@ -42,6 +42,20 @@ describe('Named Query', function() { }); }); + it('Should return proper values using query directly via import - sync', async function() { + const query = postListExposure.clone({ title: 'User Post - 3' }); + + const res = await query.fetchSync(); + + assert.isTrue(res.length > 0); + + _.each(res, post => { + assert.equal(post.title, 'User Post - 3'); + assert.isObject(post.author); + assert.isObject(post.group); + }); + }); + it('Should work with count', function(done) { const query = postListExposure.clone({ title: 'User Post - 3' }); @@ -51,6 +65,13 @@ describe('Named Query', function() { }); }); + it('Should work with count - sync', async function() { + const query = postListExposure.clone({ title: 'User Post - 3' }); + + const count = await query.getCountSync(); + assert.equal(6, count); + }); + it('Should work with reactive counts', function(done) { const query = postListExposure.clone({ title: 'User Post - 3' }); diff --git a/lib/query/hypernova/assembleAggregateResults.js b/lib/query/hypernova/assembleAggregateResults.js index 88e3b4c..9e1e5d1 100644 --- a/lib/query/hypernova/assembleAggregateResults.js +++ b/lib/query/hypernova/assembleAggregateResults.js @@ -61,23 +61,27 @@ export default function(childCollectionNode, aggregateResults, metaFilters) { aggregateResult._id == result._id; } - _.each(aggregateResults, aggregateResult => { - let parentResult = _.find( - childCollectionNode.parent.results, - result => { - return comparator(aggregateResult, result); - } + const childLinkName = childCollectionNode.linkName; + const parentResults = childCollectionNode.parent.results; + + parentResults.forEach(parentResult => { + // We are now finding the data from the pipeline that is related to the _id of the parent + const eligibleAggregateResults = aggregateResults.filter( + aggregateResult => comparator(aggregateResult, parentResult) ); - if (parentResult) { - parentResult[childCollectionNode.linkName] = - aggregateResult.data; - } - - _.each(aggregateResult.data, item => { - allResults.push(item); + eligibleAggregateResults.forEach(aggregateResult => { + if (Array.isArray(parentResult[childLinkName])) { + parentResult[childLinkName].push(...aggregateResult.data); + } else { + parentResult[childLinkName] = [...aggregateResult.data]; + } }); }); + + aggregateResults.forEach(aggregateResult => { + allResults.push(...aggregateResult.data); + }); } childCollectionNode.results = allResults; diff --git a/lib/query/hypernova/assembler.js b/lib/query/hypernova/assembler.js index 33679eb..ffdcb59 100644 --- a/lib/query/hypernova/assembler.js +++ b/lib/query/hypernova/assembler.js @@ -2,7 +2,11 @@ import createSearchFilters from '../../links/lib/createSearchFilters'; import cleanObjectForMetaFilters from './lib/cleanObjectForMetaFilters'; import sift from 'sift'; -export default (childCollectionNode, {limit, skip, metaFilters}) => { +export default (childCollectionNode, { limit, skip, metaFilters }) => { + if (childCollectionNode.results.length === 0) { + return; + } + const parent = childCollectionNode.parent; const linker = childCollectionNode.linker; @@ -16,29 +20,81 @@ export default (childCollectionNode, {limit, skip, metaFilters}) => { if (isMeta && metaFilters) { const metaFiltersTest = sift(metaFilters); _.each(parent.results, parentResult => { - cleanObjectForMetaFilters(parentResult, fieldStorage, metaFiltersTest); - }) + cleanObjectForMetaFilters( + parentResult, + fieldStorage, + metaFiltersTest + ); + }); } - _.each(parent.results, result => { - let data = assembleData(childCollectionNode, result, { - fieldStorage, strategy, isSingle + const resultsByKeyId = _.groupBy(childCollectionNode.results, '_id'); + + if (strategy === 'one') { + parent.results.forEach(parentResult => { + if (!parentResult[fieldStorage]) { + return; + } + + parentResult[childCollectionNode.linkName] = filterAssembledData( + resultsByKeyId[parentResult[fieldStorage]], + { limit, skip } + ); }); + } - result[childCollectionNode.linkName] = filterAssembledData(data, {limit, skip}) - }); -} + if (strategy === 'many') { + parent.results.forEach(parentResult => { + if (!parentResult[fieldStorage]) { + return; + } -function filterAssembledData(data, {limit, skip}) { - if (limit) { + let data = []; + parentResult[fieldStorage].forEach(_id => { + data.push(_.first(resultsByKeyId[_id])); + }); + + parentResult[childCollectionNode.linkName] = filterAssembledData( + data, + { limit, skip } + ); + }); + } + + if (strategy === 'one-meta') { + parent.results.forEach(parentResult => { + if (!parentResult[fieldStorage]) { + return; + } + + const _id = parentResult[fieldStorage]._id; + parentResult[childCollectionNode.linkName] = filterAssembledData( + resultsByKeyId[_id], + { limit, skip } + ); + }); + } + + if (strategy === 'many-meta') { + parent.results.forEach(parentResult => { + const _ids = _.pluck(parentResult[fieldStorage], '_id'); + let data = []; + _ids.forEach(_id => { + data.push(_.first(resultsByKeyId[_id])); + }); + + parentResult[childCollectionNode.linkName] = filterAssembledData( + data, + { limit, skip } + ); + }); + } +}; + +function filterAssembledData(data, { limit, skip }) { + if (limit && Array.isArray(data)) { return data.slice(skip, limit); } return data; } - -function assembleData(childCollectionNode, result, {fieldStorage, strategy}) { - const filters = createSearchFilters(result, fieldStorage, strategy, false); - - return sift(filters, childCollectionNode.results); -} diff --git a/lib/query/testing/link-cache/fixtures.js b/lib/query/testing/link-cache/fixtures.js old mode 100644 new mode 100755 diff --git a/lib/query/testing/link-cache/server.test.js b/lib/query/testing/link-cache/server.test.js old mode 100644 new mode 100755 index d49949a..cc60716 --- a/lib/query/testing/link-cache/server.test.js +++ b/lib/query/testing/link-cache/server.test.js @@ -9,8 +9,9 @@ import { } from './collections'; describe('Query Link Denormalization', function() { - before(() => { + before((done) => { createFixtures(); + done(); }); it('Should not cache work with nested options', function() { diff --git a/lib/query/testing/server.test.js b/lib/query/testing/server.test.js index cddad1a..8b550ba 100755 --- a/lib/query/testing/server.test.js +++ b/lib/query/testing/server.test.js @@ -1,10 +1,16 @@ import { createQuery } from 'meteor/cultofcoders:grapher'; import dot from 'dot-object'; import Comments from './bootstrap/comments/collection.js'; +import Posts from './bootstrap/posts/collection.js'; +import Tags from './bootstrap/tags/collection.js'; import './metaFilters.server.test'; import './reducers.server.test'; import './link-cache/server.test'; +// Used in some tests below +const Users = new Mongo.Collection('__many_inversed_users'); +const Restaurants = new Mongo.Collection('__many_inversed_restaurants'); + describe('Hypernova', function() { it('Should fetch One links correctly', function() { const data = createQuery({ @@ -105,6 +111,71 @@ describe('Hypernova', function() { }); }); + it('Should fetch Many - inversed links correctly #2', function() { + const post1Id = Posts.insert({ name: 'Post1' }); + const post2Id = Posts.insert({ name: 'Post2' }); + const post3Id = Posts.insert({ name: 'Post3' }); + const post4Id = Posts.insert({ name: 'Post4' }); + + const tag1Id = Tags.insert({ name: 'Tag1' }); + const tag2Id = Tags.insert({ name: 'Tag2' }); + const tag3Id = Tags.insert({ name: 'Tag3' }); + + function addTags(postId, tagIds) { + Posts.update(postId, { + $set: { + tagIds, + }, + }); + } + + addTags(post1Id, [tag1Id, tag2Id]); + addTags(post2Id, [tag1Id]); + addTags(post3Id, [tag2Id, tag3Id]); + addTags(post4Id, [tag3Id, tag1Id]); + + const data = createQuery({ + tags: { + $filters: { + _id: { $in: [tag1Id, tag2Id, tag3Id] }, + }, + name: 1, + posts: { + name: 1, + }, + }, + }).fetch(); + + console.log(JSON.stringify(data, null, 2)); + + const tag1Data = _.find(data, doc => doc.name === 'Tag1'); + const tag2Data = _.find(data, doc => doc.name === 'Tag2'); + const tag3Data = _.find(data, doc => doc.name === 'Tag3'); + + function hasPost(tag, postName) { + return !!_.find(tag.posts, post => post.name === postName); + } + assert.lengthOf(tag1Data.posts, 3); + assert.isTrue(hasPost(tag1Data, 'Post1')); + assert.isTrue(hasPost(tag1Data, 'Post2')); + assert.isTrue(hasPost(tag1Data, 'Post4')); + + assert.lengthOf(tag2Data.posts, 2); + assert.isTrue(hasPost(tag2Data, 'Post1')); + assert.isTrue(hasPost(tag2Data, 'Post3')); + + assert.lengthOf(tag3Data.posts, 2); + assert.isTrue(hasPost(tag3Data, 'Post3')); + assert.isTrue(hasPost(tag3Data, 'Post4')); + + Posts.remove({ + _id: { $in: [post1Id, post2Id, post3Id, post4Id] }, + }); + Tags.remove({ + _id: { $in: [tag1Id, tag2Id, tag3Id] }, + }); + }); + it('Should fetch One-Meta links correctly', function() { const data = createQuery({ posts: { @@ -171,16 +242,16 @@ describe('Hypernova', function() { }); }); - it('Should fetch Many-Meta links correctly where parent is One link', function () { + it('Should fetch Many-Meta links correctly where parent is One link', function() { const data = createQuery({ posts: { $options: { limit: 5 }, author: { groups: { - isAdmin: 1 - } - } - } + isAdmin: 1, + }, + }, + }, }).fetch(); _.each(data, post => { @@ -525,9 +596,6 @@ describe('Hypernova', function() { assert.isTrue(group.authors.length > 0); }); - const Users = new Mongo.Collection('__many_inversed_users'); - const Restaurants = new Mongo.Collection('__many_inversed_restaurants'); - it('Should fetch Many - inversed links correctly when the field is not the first', function() { Restaurants.addLinks({ users: { @@ -577,11 +645,10 @@ describe('Hypernova', function() { metadata: { language: { abbr: 1, - } - - } - } - } + }, + }, + }, + }, }); const data = query.fetch(); diff --git a/package.js b/package.js index b821adf..17063a6 100755 --- a/package.js +++ b/package.js @@ -1,6 +1,6 @@ Package.describe({ name: 'cultofcoders:grapher', - version: '1.3.6', + version: '1.3.7_4', // Brief, one-line summary of the package. summary: 'Grapher is a data fetching layer on top of Meteor', // URL to the Git repository containing the source code for this package.