├── .gitignore ├── .travis.yml ├── .versions ├── LICENSE ├── README.md ├── package.js ├── server.coffee └── tests.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | .build* 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | sudo: required 5 | before_install: 6 | - "curl -L http://git.io/ejPSng | /bin/sh" 7 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.1.0 2 | babel-compiler@7.3.4 3 | babel-runtime@1.3.0 4 | base64@1.0.12 5 | binary-heap@1.0.11 6 | blaze@2.3.3 7 | boilerplate-generator@1.6.0 8 | caching-compiler@1.2.1 9 | callback-hook@1.1.0 10 | check@1.3.1 11 | coffeescript@2.4.1 12 | coffeescript-compiler@2.4.1 13 | ddp@1.4.0 14 | ddp-client@2.3.3 15 | ddp-common@1.4.0 16 | ddp-server@2.3.0 17 | deps@1.0.12 18 | diff-sequence@1.1.1 19 | dynamic-import@0.5.1 20 | ecmascript@0.12.7 21 | ecmascript-runtime@0.7.0 22 | ecmascript-runtime-client@0.8.0 23 | ecmascript-runtime-server@0.7.1 24 | ejson@1.1.0 25 | fetch@0.1.1 26 | geojson-utils@1.0.10 27 | htmljs@1.0.11 28 | id-map@1.1.0 29 | insecure@1.0.7 30 | inter-process-messaging@0.1.0 31 | jquery@1.11.11 32 | local-test:peerlibrary:related@0.3.1 33 | logging@1.1.20 34 | meteor@1.9.3 35 | minimongo@1.4.5 36 | modern-browsers@0.1.4 37 | modules@0.13.0 38 | modules-runtime@0.10.3 39 | mongo@1.6.3 40 | mongo-decimal@0.1.1 41 | mongo-dev-server@1.1.0 42 | mongo-id@1.0.7 43 | npm-mongo@3.1.2 44 | observe-sequence@1.0.16 45 | ordered-dict@1.1.0 46 | peerlibrary:assert@0.3.0 47 | peerlibrary:classy-test@0.4.0 48 | peerlibrary:related@0.3.1 49 | promise@0.11.2 50 | random@1.1.0 51 | reactive-var@1.0.11 52 | reload@1.3.0 53 | retry@1.1.0 54 | routepolicy@1.1.0 55 | socket-stream-client@0.2.2 56 | test-helpers@1.1.0 57 | tinytest@1.1.0 58 | tracker@1.2.0 59 | underscore@1.0.10 60 | webapp@1.7.4 61 | webapp-hashing@1.0.9 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, The PeerLibrary Project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the PeerLibrary Project nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Publish with reactive dependencies 2 | ================================== 3 | 4 | Adding this package to your [Meteor](http://www.meteor.com/) application augments 5 | [Meteor.publish](http://docs.meteor.com/#meteor_publish) handler object with method 6 | `related` which allows you to define publish endpoints with reactive dependencies on 7 | additional queries. It allows easy wrapping of existing publish functions without any 8 | change needed. You can use publish functions which return query cursors, or one which 9 | uses publish `added`/`changed`/`removed` API for custom publish functions. 10 | 11 | This is useful in all situations where you want to publish documents which have a 12 | query based on data from some other document and you want that everything behaves 13 | reactively, if any of those documents change, published documents should change as 14 | well. Examples are any queries which limit published based documents on a list 15 | of IDs in another document. Or a permission system where you want to limit published 16 | documents based on flags and other properties of currently logged-in user. 17 | 18 | Server side only. 19 | 20 | See also the follow-up package [peerlibrary:reactive-publish](https://github.com/peerlibrary/meteor-reactive-publish) 21 | which provides an alternative API through Meteor reactivity and familiar server-side `autorun`. 22 | 23 | Installation 24 | ------------ 25 | 26 | ``` 27 | meteor add peerlibrary:related 28 | ``` 29 | 30 | `this.related` 31 | -------------- 32 | 33 | Along with existing properties and methods, `this` inside `Meteor.publish` callback now 34 | has `related` method available. Method accepts: 35 | 36 | * a callback – a new publish callback which will get as arguments results of your queries 37 | * one or more query cursors which should each return at most one document at any given moment; 38 | documents returned from query cursors will then be passed to the callback reactively, every 39 | time any of them changes callback will be rerun 40 | 41 | Example 42 | ------- 43 | 44 | Let's say that you have a list of blog posts user is following as `follows` field in Meteor's 45 | `users` documents. You want to create a publish endpoint which will publish only those blog posts 46 | which current user follows. But if `follows` field changes, published blog posts should also 47 | change (or if published blog posts themselves change, changes should be pushed to the client). 48 | 49 | ``` 50 | Meteor.publish('followed-blog-posts', function () { 51 | if (!this.userId) return; 52 | 53 | this.related(function (user) { 54 | return Posts.find({_id: {$in: user.follows}}); 55 | }, 56 | Meteor.users.find(this.userId, {fields: {follows: 1}}) 57 | ); 58 | }); 59 | ``` 60 | 61 | Every time `follows` field of currently logged-in user changes, related publish callback is 62 | rerun with new `user` document as an argument (as returned from `Meteor.users.find` query). 63 | Callback should do anything a normal publish callback should: or call `added`/`changed`/`removed`, 64 | or simply return a query to publish like in our example. 65 | 66 | Documents are tracked between reruns and are not republished if they remain the same between 67 | reruns. 68 | 69 | Known limitations 70 | ----------------- 71 | 72 | Currently each of related queries is expected to return at most one document at all times. 73 | In theory returning multiple documents could be supported, but this means that related publish 74 | callback would be rerun at any change of any of those multiple documents. This is probably not 75 | a very efficient approach. 76 | 77 | Nested calls to `related` should probably work, but were not yet tested. 78 | 79 | Related projects 80 | ---------------- 81 | 82 | There are few other similar projects trying to address a similar feature. We needed something 83 | production grade, with tests, and simple code base built upon existing Meteor features 84 | instead of trying to replace them. Most of our code just wraps existing Meteor code into the 85 | reactive loop, and allowing existing publish functions to be reused without any change needed, 86 | you can return queries or you can use `added`/`changed`/`removed`, all this is supported. Just 87 | instead of having static arguments to your publish function, publish function is rerun when any 88 | of arguments changes. Its API is thus simple and intuitive. 89 | 90 | * [meteor-reactive-publish](https://github.com/Diggsey/meteor-reactive-publish) – uses API based on server-side dependency 91 | tracking, but no tests and no support for `added`/`changed`/`removed` 92 | * [meteor-publish-with-relations](https://github.com/tmeasday/meteor-publish-with-relations) – complicated custom API not 93 | allowing to reuse existing publish functions, which means no support for `added`/`changed`/`removed` as well 94 | * [meteor-smart-publish](https://github.com/yeputons/meteor-smart-publish) – complicated way of defining dependencies 95 | and works only with query cursors and not custom `added`/`changed`/`removed` functions 96 | * [reywood:publish-composite](https://github.com/englue/meteor-publish-composite) – allow you to define a nested structure 97 | of cursors, which get documents from higher levels in a reactive manner, but it works only with only with query cursors 98 | and not custom `added`/`changed`/`removed` functions 99 | * [copleyjk:simple-publish](https://github.com/copleykj/meteor-simple-publish) – seems similar to 100 | `meteor-publish-with-relations`, but a more developed version covering more edge cases; on the other hand it 101 | has the same limitations of no support for `added`/`changed`/`removed` 102 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Publish with reactive dependencies on related queries", 3 | version: '0.3.1', 4 | name: 'peerlibrary:related', 5 | git: 'https://github.com/peerlibrary/meteor-related.git' 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom('METEOR@1.8.1'); 10 | 11 | // Core dependencies. 12 | api.use([ 13 | 'coffeescript@2.4.1', 14 | 'underscore' 15 | ], 'server'); 16 | 17 | // 3rd party dependencies. 18 | api.use([ 19 | 'peerlibrary:assert@0.3.0' 20 | ], 'server'); 21 | 22 | api.addFiles([ 23 | 'server.coffee' 24 | ], 'server'); 25 | }); 26 | 27 | Package.onTest(function (api) { 28 | api.versionsFrom('METEOR@1.8.1'); 29 | 30 | // Core dependencies. 31 | api.use([ 32 | 'tinytest', 33 | 'test-helpers', 34 | 'coffeescript', 35 | 'insecure', 36 | 'random', 37 | 'underscore' 38 | ]); 39 | 40 | // Internal dependencies. 41 | api.use([ 42 | 'peerlibrary:related' 43 | ]); 44 | 45 | // 3rd party dependencies. 46 | api.use([ 47 | 'peerlibrary:assert@0.3.0', 48 | 'peerlibrary:classy-test@0.4.0' 49 | ]); 50 | 51 | api.addFiles([ 52 | 'tests.coffee' 53 | ]); 54 | }); 55 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | unless originalPublish 2 | originalPublish = Meteor.publish 3 | 4 | Meteor.publish = (name, func) -> 5 | originalPublish name, (args...) -> 6 | publish = @ 7 | 8 | # This function wraps the logic of publishing related documents. publishFunction gets 9 | # as arguments documents returned from related querysets. Every time any related document 10 | # changes, publishFunction is rerun. The requirement is that related querysets return 11 | # exactly one document. publishFunction can be anything a normal publish endpoint function 12 | # can be, it can return querysets or can call added/changed/removed. It does not have to 13 | # care about unpublishing documents which are not published anymore after the rerun, or 14 | # care about publishing only changes to documents after the rerun. 15 | 16 | # TODO: Should we use try/except around the code so that if there is any exception we stop handlers? 17 | publish.related = (publishFunction, related...) -> 18 | relatedPublish = null 19 | ready = false 20 | 21 | publishDocuments = (relatedDocuments) -> 22 | oldRelatedPublish = relatedPublish 23 | # Check if we are using Meteor 1.8.1+ which uses Maps/Sets instead of objects 24 | newDDP = (oldRelatedPublish?._documents instanceof Map) 25 | 26 | relatedPublish = publish._recreate() 27 | 28 | # We copy overridden methods if they exist 29 | for own key, value of publish when key in ['added', 'changed', 'removed', 'ready', 'stop', 'error'] 30 | relatedPublish[key] = value 31 | 32 | # If there are any extra fields which do not exist in recreated related publish 33 | # (because they were added by some other code), copy them over 34 | # TODO: This copies also @related, test how recursive @related works 35 | for own key, value of publish when key not of relatedPublish 36 | relatedPublish[key] = value 37 | 38 | relatedPublishAdded = relatedPublish.added 39 | relatedPublish.added = (collectionName, id, fields) -> 40 | stringId = @_idFilter.idStringify id 41 | # If document as already present in oldRelatedPublish then we just set 42 | # relatedPublish's _documents and call changed to send updated fields 43 | # (Meteor sends only a diff). 44 | if newDDP 45 | runAdded = oldRelatedPublish?._documents.get(collectionName)?.has(stringId) 46 | else 47 | runAdded = oldRelatedPublish?._documents[collectionName]?[stringId] 48 | if runAdded 49 | if newDDP 50 | ids = @_documents.get(collectionName) 51 | if !ids 52 | ids = new Set 53 | @_documents.set(collectionName, ids) 54 | ids.add(stringId) 55 | else 56 | Meteor._ensure(@_documents, collectionName)[stringId] = true 57 | oldFields = {} 58 | # If some field existed before, but does not exist anymore, we have to remove it by calling "changed" 59 | # with value set to "undefined". So we look into current session's state and see which fields are currently 60 | # known and create an object of same fields, just all values set to "undefined". We then override some fields 61 | # with new values. Only top-level fields matter. 62 | if newDDP 63 | for field in Array.from(@_session.getCollectionView(collectionName)?.documents?.get(id)?.dataByKey.keys()) or [] 64 | oldFields[field] = undefined 65 | else 66 | for field of @_session.getCollectionView(collectionName)?.documents?[id]?.dataByKey or {} 67 | oldFields[field] = undefined 68 | @changed collectionName, id, _.extend oldFields, fields 69 | else 70 | relatedPublishAdded.call @, collectionName, id, fields 71 | 72 | relatedPublish.ready = -> 73 | # Mark it as ready only the first time 74 | publish.ready() unless ready 75 | ready = true 76 | # To return nothing, so that it can be used at the end of the 77 | # publish function in CoffeeScript without an error 78 | return 79 | 80 | relatedPublish.stop = (relatedChange) -> 81 | if relatedChange 82 | # We only deactivate (which calls stop callbacks as well) because we 83 | # have manually removed only documents which are not published again. 84 | @_deactivate() 85 | else 86 | # We do manually what would _stopSubscription do, but without 87 | # subscription handling. This should be done by the parent publish. 88 | @_removeAllDocuments() 89 | @_deactivate() 90 | publish.stop() 91 | 92 | if Package['audit-argument-checks'] 93 | relatedPublish._handler = (args...) -> 94 | # Related parameters are trusted 95 | check arg, Match.Any for arg in args 96 | publishFunction.apply @, args 97 | else 98 | relatedPublish._handler = publishFunction 99 | relatedPublish._params = relatedDocuments 100 | relatedPublish._runHandler() 101 | 102 | return unless oldRelatedPublish 103 | 104 | # We remove those which are not published anymore 105 | if newDDP 106 | for collectionName in Array.from(oldRelatedPublish._documents.keys()) 107 | for id in _.difference(Array.from((oldRelatedPublish._documents.get(collectionName) or new Map).keys()), Array.from((relatedPublish._documents.get(collectionName) or new Map).keys())) 108 | oldRelatedPublish.removed collectionName, id 109 | else 110 | for collectionName in _.keys(oldRelatedPublish._documents) 111 | for id in _.difference _.keys(oldRelatedPublish._documents[collectionName] or {}), _.keys(relatedPublish._documents[collectionName] or {}) 112 | oldRelatedPublish.removed collectionName, id 113 | 114 | oldRelatedPublish.stop true 115 | oldRelatedPublish = null 116 | 117 | currentRelatedDocuments = [] 118 | handleRelatedDocuments = [] 119 | 120 | relatedInitializing = related.length 121 | 122 | for r, i in related 123 | do (r, i) -> 124 | currentRelatedDocuments[i] = null 125 | handleRelatedDocuments[i] = r.observe 126 | added: (doc) -> 127 | # There should be only one document with the id at every given moment 128 | assert.equal currentRelatedDocuments[i], null 129 | 130 | currentRelatedDocuments[i] = doc 131 | publishDocuments currentRelatedDocuments if relatedInitializing is 0 132 | 133 | changed: (newDoc, oldDoc) -> 134 | # Document should already be added 135 | assert.equal currentRelatedDocuments[i]?._id, newDoc._id 136 | 137 | currentRelatedDocuments[i] = newDoc 138 | 139 | # We are checking relatedInitializing even here because it could happen that this is triggered why other related documents are still being initialized 140 | publishDocuments currentRelatedDocuments if relatedInitializing is 0 141 | 142 | removed: (oldDoc) -> 143 | # We cannot remove the document if we never added the document before 144 | assert.equal currentRelatedDocuments[i]?._id, oldDoc._id 145 | 146 | currentRelatedDocuments[i] = null 147 | 148 | # We are checking relatedInitializing even here because it could happen that this is triggered why other related documents are still being initialized 149 | publishDocuments currentRelatedDocuments if relatedInitializing is 0 150 | 151 | # We initialized this related document 152 | relatedInitializing-- 153 | 154 | assert.equal relatedInitializing, 0 155 | 156 | # We call publishDocuments for the first time 157 | publishDocuments currentRelatedDocuments 158 | 159 | publish.onStop -> 160 | for handle, i in handleRelatedDocuments 161 | handle?.stop() 162 | handleRelatedDocuments[i] = null 163 | relatedPublish?.stop() 164 | relatedPublish = null 165 | 166 | func.apply publish, args 167 | -------------------------------------------------------------------------------- /tests.coffee: -------------------------------------------------------------------------------- 1 | Users = new Meteor.Collection 'Users_meteor_related_tests' 2 | Posts = new Meteor.Collection 'Posts_meteor_related_tests' 3 | Addresses = new Meteor.Collection 'Addresses_meteor_related_tests' 4 | Fields = new Meteor.Collection 'Fields_meteor_related_tests' 5 | 6 | if Meteor.isServer 7 | Meteor.publish null, -> 8 | Users.find() 9 | 10 | Meteor.publish 'posts', (ids) -> 11 | Posts.find 12 | _id: 13 | $in: ids 14 | 15 | Meteor.publish 'users-posts', (userId) -> 16 | @copyIn = true 17 | 18 | @related (user, fields) -> 19 | assert @copyIn, "copyIn not copied into related publish" 20 | 21 | Posts.find( 22 | _id: 23 | $in: user?.posts or [] 24 | , 25 | fields: _.omit (fields or {}), '_id' 26 | ).observeChanges 27 | added: (id, fields) => 28 | fields.dummyField = true 29 | @added 'Posts_meteor_related_tests', id, fields 30 | changed: (id, fields) => 31 | @changed 'Posts_meteor_related_tests', id, fields 32 | removed: (id) => 33 | @removed 'Posts_meteor_related_tests', id 34 | 35 | @ready() 36 | , 37 | Users.find userId, 38 | fields: 39 | posts: 1 40 | , 41 | Fields.find userId 42 | 43 | Meteor.publish 'users-posts-and-addresses', (userId) -> 44 | @related (user) -> 45 | Posts.find( 46 | _id: 47 | $in: user?.posts or [] 48 | ) 49 | , 50 | Users.find userId, 51 | fields: 52 | posts: 1 53 | 54 | @related (user) -> 55 | Addresses.find( 56 | _id: 57 | $in: user?.addresses or [] 58 | ) 59 | , 60 | Users.find userId, 61 | fields: 62 | addresses: 1 63 | 64 | Meteor.publish 'users-posts-and-addresses-together', (userId) -> 65 | @related (user) -> 66 | [ 67 | Posts.find( 68 | _id: 69 | $in: user?.posts or [] 70 | ) 71 | , 72 | Addresses.find( 73 | _id: 74 | $in: user?.addresses or [] 75 | ) 76 | ] 77 | , 78 | Users.find userId, 79 | fields: 80 | posts: 1 81 | addresses: 1 82 | 83 | Meteor.publish 'users-posts-count', (userId, countId) -> 84 | @related (user) -> 85 | count = 0 86 | initializing = true 87 | 88 | Posts.find( 89 | _id: 90 | $in: user?.posts or [] 91 | ).observeChanges 92 | added: (id) => 93 | count++ 94 | @changed 'Counts', countId, count: count unless initializing 95 | removed: (id) => 96 | count-- 97 | @changed 'Counts', countId, count: count unless initializing 98 | 99 | initializing = false 100 | 101 | @added 'Counts', countId, 102 | count: count 103 | 104 | @ready() 105 | , 106 | Users.find userId, 107 | fields: 108 | posts: 1 109 | 110 | class RelatedTestCase extends ClassyTestCase 111 | @testName: 'related' 112 | 113 | setUpServer: -> 114 | # Initialize the database. 115 | Users.remove {} 116 | Posts.remove {} 117 | Addresses.remove {} 118 | Fields.remove {} 119 | 120 | setUpClient: -> 121 | @countsCollection ?= new Meteor.Collection 'Counts' 122 | 123 | testClientBasic: [ 124 | -> 125 | @userId = Random.id() 126 | @countId = Random.id() 127 | 128 | @assertSubscribeSuccessful 'users-posts', @userId, @expect() 129 | @assertSubscribeSuccessful 'users-posts-count', @userId, @countId, @expect() 130 | , 131 | -> 132 | @assertEqual Posts.find().fetch(), [] 133 | @assertEqual @countsCollection.findOne(@countId)?.count, 0 134 | 135 | @posts = [] 136 | 137 | for i in [0...10] 138 | Posts.insert {}, @expect (error, id) => 139 | @assertFalse error, error?.toString?() or error 140 | @assertTrue id 141 | @posts.push id 142 | , 143 | -> 144 | @assertEqual Posts.find().fetch(), [] 145 | @assertEqual @countsCollection.findOne(@countId)?.count, 0 146 | 147 | Users.insert 148 | _id: @userId 149 | posts: @posts 150 | , 151 | @expect (error, userId) => 152 | @assertFalse error, error?.toString?() or error 153 | @assertTrue userId 154 | @assertEqual userId, @userId 155 | , 156 | -> 157 | Posts.find().forEach (post) => 158 | @assertTrue post.dummyField 159 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts 160 | @assertEqual @countsCollection.findOne(@countId)?.count, @posts.length 161 | 162 | @shortPosts = @posts[0...5] 163 | 164 | Users.update @userId, 165 | posts: @shortPosts 166 | , 167 | @expect (error, count) => 168 | @assertFalse error, error?.toString?() or error 169 | @assertEqual count, 1 170 | , 171 | -> 172 | Posts.find().forEach (post) => 173 | @assertTrue post.dummyField 174 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @shortPosts 175 | @assertEqual @countsCollection.findOne(@countId)?.count, @shortPosts.length 176 | 177 | Users.update @userId, 178 | posts: [] 179 | , 180 | @expect (error, count) => 181 | @assertFalse error, error?.toString?() or error 182 | @assertEqual count, 1 183 | , 184 | -> 185 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), [] 186 | @assertEqual @countsCollection.findOne(@countId)?.count, 0 187 | 188 | Users.update @userId, 189 | posts: @posts 190 | , 191 | @expect (error, count) => 192 | @assertFalse error, error?.toString?() or error 193 | @assertEqual count, 1 194 | , 195 | -> 196 | Posts.find().forEach (post) => 197 | @assertTrue post.dummyField, true 198 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts 199 | @assertEqual @countsCollection.findOne(@countId)?.count, @posts.length 200 | 201 | Posts.remove @posts[0], @expect (error, count) => 202 | @assertFalse error, error?.toString?() or error 203 | @assertEqual count, 1 204 | , 205 | -> 206 | Posts.find().forEach (post) => 207 | @assertTrue post.dummyField 208 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts[1..] 209 | @assertEqual @countsCollection.findOne(@countId)?.count, @posts.length - 1 210 | 211 | Users.remove @userId, 212 | @expect (error) => 213 | @assertFalse error, error?.toString?() or error 214 | , 215 | -> 216 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), [] 217 | @assertEqual @countsCollection.findOne(@countId)?.count, 0 218 | ] 219 | 220 | testClientUnsubscribing: [ 221 | -> 222 | @userId = Random.id() 223 | @countId = Random.id() 224 | 225 | @assertSubscribeSuccessful 'users-posts', @userId, @expect() 226 | @assertSubscribeSuccessful 'users-posts-count', @userId, @countId, @expect() 227 | , 228 | -> 229 | @assertEqual Posts.find().fetch(), [] 230 | @assertEqual @countsCollection.findOne(@countId)?.count, 0 231 | 232 | @posts = [] 233 | 234 | for i in [0...10] 235 | Posts.insert {}, @expect (error, id) => 236 | @assertFalse error, error?.toString?() or error 237 | @assertTrue id 238 | @posts.push id 239 | , 240 | -> 241 | @assertEqual Posts.find().fetch(), [] 242 | @assertEqual @countsCollection.findOne(@countId)?.count, 0 243 | 244 | Users.insert 245 | _id: @userId 246 | posts: @posts 247 | , 248 | @expect (error, userId) => 249 | @assertFalse error, error?.toString?() or error 250 | @assertTrue userId 251 | @assertEqual userId, @userId 252 | , 253 | -> 254 | Posts.find().forEach (post) => 255 | @assertTrue post.dummyField 256 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts 257 | @assertEqual @countsCollection.findOne(@countId)?.count, @posts.length 258 | 259 | # We have to update posts to trigger at least one rerun. 260 | Users.update @userId, 261 | posts: _.shuffle @posts 262 | , 263 | @expect (error, count) => 264 | @assertFalse error, error?.toString?() or error 265 | @assertEqual count, 1 266 | , 267 | -> 268 | Posts.find().forEach (post) => 269 | @assertTrue post.dummyField 270 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts 271 | @assertEqual @countsCollection.findOne(@countId)?.count, @posts.length 272 | 273 | callback = @expect() 274 | @postsSubscribe = Meteor.subscribe 'posts', @posts, 275 | onReady: callback 276 | onError: (error) => 277 | @assertFail 278 | type: 'subscribe' 279 | message: "Subscrption to endpoint failed, but should have succeeded." 280 | callback() 281 | @unsubscribeAll() 282 | 283 | # Let's wait a but for subscription to really stop 284 | Meteor.setTimeout @expect(), 1000 285 | , 286 | -> 287 | # After unsubscribing from the related-based publish which added dummyField, 288 | # dummyField should be removed from documents available on the client side 289 | Posts.find().forEach (post) => 290 | @assertIsUndefined post.dummyField 291 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts 292 | 293 | @postsSubscribe.stop() 294 | ] 295 | 296 | testClientRemoveField: [ 297 | -> 298 | @userId = Random.id() 299 | 300 | @assertSubscribeSuccessful 'users-posts', @userId, @expect() 301 | , 302 | -> 303 | @assertEqual Posts.find().fetch(), [] 304 | 305 | Fields.insert 306 | _id: @userId 307 | foo: 1 308 | dummyField: 1 309 | , 310 | @expect (error, id) => 311 | @assertFalse error, error?.toString?() or error 312 | @assertTrue id 313 | @fieldsId = id 314 | 315 | Posts.insert {foo: 'bar'}, @expect (error, id) => 316 | @assertFalse error, error?.toString?() or error 317 | @assertTrue id 318 | @postId = id 319 | , 320 | -> 321 | @assertEqual Posts.find().fetch(), [] 322 | 323 | Users.insert 324 | _id: @userId 325 | posts: [@postId] 326 | , 327 | @expect (error, userId) => 328 | @assertFalse error, error?.toString?() or error 329 | @assertTrue userId 330 | @assertEqual userId, @userId 331 | , 332 | -> 333 | @assertItemsEqual Posts.find().fetch(), [ 334 | _id: @postId 335 | foo: 'bar' 336 | dummyField: true 337 | ] 338 | 339 | Posts.update @postId, 340 | $set: 341 | foo: 'baz' 342 | , 343 | @expect (error, count) => 344 | @assertFalse error, error?.toString?() or error 345 | @assertEqual count, 1 346 | , 347 | -> 348 | @assertItemsEqual Posts.find().fetch(), [ 349 | _id: @postId 350 | foo: 'baz' 351 | dummyField: true 352 | ] 353 | 354 | Posts.update @postId, 355 | $unset: 356 | foo: '' 357 | , 358 | @expect (error, count) => 359 | @assertFalse error, error?.toString?() or error 360 | @assertEqual count, 1 361 | , 362 | -> 363 | @assertItemsEqual Posts.find().fetch(), [ 364 | _id: @postId 365 | dummyField: true 366 | ] 367 | 368 | Posts.update @postId, 369 | $set: 370 | foo: 'bar' 371 | , 372 | @expect (error, count) => 373 | @assertFalse error, error?.toString?() or error 374 | @assertEqual count, 1 375 | , 376 | -> 377 | @assertItemsEqual Posts.find().fetch(), [ 378 | _id: @postId 379 | foo: 'bar' 380 | dummyField: true 381 | ] 382 | 383 | Fields.update @userId, 384 | $unset: 385 | foo: '' 386 | , 387 | @expect (error, count) => 388 | @assertFalse error, error?.toString?() or error 389 | @assertEqual count, 1 390 | , 391 | -> 392 | @assertItemsEqual Posts.find().fetch(), [ 393 | _id: @postId 394 | dummyField: true 395 | ] 396 | ] 397 | 398 | @multiple: [ 399 | -> 400 | @assertEqual Posts.find().fetch(), [] 401 | @assertEqual Addresses.find().fetch(), [] 402 | 403 | @posts = [] 404 | 405 | for i in [0...10] 406 | Posts.insert {}, @expect (error, id) => 407 | @assertFalse error, error?.toString?() or error 408 | @assertTrue id 409 | @posts.push id 410 | 411 | @addresses = [] 412 | 413 | for i in [0...10] 414 | Addresses.insert {}, @expect (error, id) => 415 | @assertFalse error, error?.toString?() or error 416 | @assertTrue id 417 | @addresses.push id 418 | , 419 | -> 420 | @assertEqual Posts.find().fetch(), [] 421 | @assertEqual Addresses.find().fetch(), [] 422 | 423 | Users.insert 424 | _id: @userId 425 | posts: @posts 426 | addresses: @addresses 427 | , 428 | @expect (error, userId) => 429 | @assertFalse error, error?.toString?() or error 430 | @assertTrue userId 431 | @assertEqual userId, @userId 432 | , 433 | -> 434 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts 435 | @assertItemsEqual _.pluck(Addresses.find().fetch(), '_id'), @addresses 436 | 437 | Users.update @userId, 438 | $set: 439 | posts: @posts[0..5] 440 | , 441 | @expect (error, count) => 442 | @assertFalse error, error?.toString?() or error 443 | @assertEqual count, 1 444 | , 445 | -> 446 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts[0..5] 447 | @assertItemsEqual _.pluck(Addresses.find().fetch(), '_id'), @addresses 448 | 449 | Users.update @userId, 450 | $set: 451 | addresses: @addresses[0..5] 452 | , 453 | @expect (error, count) => 454 | @assertFalse error, error?.toString?() or error 455 | @assertEqual count, 1 456 | , 457 | -> 458 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts[0..5] 459 | @assertItemsEqual _.pluck(Addresses.find().fetch(), '_id'), @addresses[0..5] 460 | 461 | Users.update @userId, 462 | $unset: 463 | addresses: '' 464 | , 465 | @expect (error, count) => 466 | @assertFalse error, error?.toString?() or error 467 | @assertEqual count, 1 468 | , 469 | -> 470 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), @posts[0..5] 471 | @assertItemsEqual _.pluck(Addresses.find().fetch(), '_id'), [] 472 | 473 | Users.remove @userId, @expect (error, count) => 474 | @assertFalse error, error?.toString?() or error 475 | @assertEqual count, 1 476 | , 477 | -> 478 | @assertItemsEqual _.pluck(Posts.find().fetch(), '_id'), [] 479 | @assertItemsEqual _.pluck(Addresses.find().fetch(), '_id'), [] 480 | ] 481 | 482 | testClientMultiple: [ 483 | -> 484 | @userId = Random.id() 485 | 486 | @assertSubscribeSuccessful 'users-posts-and-addresses', @userId, @expect() 487 | ].concat @multiple 488 | 489 | testClientMultipleTogether: [ 490 | -> 491 | @userId = Random.id() 492 | 493 | @assertSubscribeSuccessful 'users-posts-and-addresses-together', @userId, @expect() 494 | ].concat @multiple 495 | 496 | # Register the test case. 497 | ClassyTestCase.addTest new RelatedTestCase() 498 | --------------------------------------------------------------------------------