├── .gitignore ├── .versions ├── LICENSE ├── README.md ├── client └── publish-counts.js ├── package.js ├── server └── publish-counts.js └── tests ├── count_from_field_fn_deep_test.js ├── count_from_field_fn_shallow_test.js ├── count_from_field_length_fn_deep_test.js ├── count_from_field_length_fn_shallow_test.js ├── count_from_field_length_shallow_test.js ├── count_from_field_shallow_test.js ├── count_local_collection_test.js ├── count_non_reactive_test.js ├── count_test.js ├── field_limit_count_from_field_fn_test.js ├── field_limit_count_from_field_length_fn_test.js ├── field_limit_count_from_field_length_test.js ├── field_limit_count_from_field_test.js ├── field_limit_count_test.js ├── has_count_test.js ├── helper.js ├── no_ready_test.js ├── no_warn_test.js └── observe_handles_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@2.0.0-beta300.6 2 | babel-compiler@7.11.0-beta300.6 3 | babel-runtime@1.5.2-beta300.6 4 | base64@1.0.13-beta300.6 5 | binary-heap@1.0.12-beta300.6 6 | blaze@3.0.0-alpha300.17 7 | blaze-html-templates@3.0.0-alpha300.17 8 | blaze-tools@2.0.0-alpha300.17 9 | boilerplate-generator@2.0.0-beta300.6 10 | caching-compiler@2.0.0-beta300.6 11 | caching-html-compiler@2.0.0-alpha300.17 12 | callback-hook@1.6.0-beta300.6 13 | check@1.3.3-beta300.6 14 | core-runtime@1.0.0-beta300.6 15 | ddp@1.4.2-beta300.6 16 | ddp-client@3.0.0-beta300.6 17 | ddp-common@1.4.1-beta300.6 18 | ddp-server@3.0.0-beta300.6 19 | diff-sequence@1.1.3-beta300.6 20 | dynamic-import@0.7.4-beta300.6 21 | ecmascript@0.16.8-beta300.6 22 | ecmascript-runtime@0.8.2-beta300.6 23 | ecmascript-runtime-client@0.12.2-beta300.6 24 | ecmascript-runtime-server@0.11.1-beta300.6 25 | ejson@1.1.4-beta300.6 26 | facts-base@1.0.2-beta300.6 27 | fetch@0.1.4-beta300.6 28 | geojson-utils@1.0.12-beta300.6 29 | html-tools@2.0.0-alpha300.17 30 | htmljs@2.0.0-alpha300.17 31 | id-map@1.2.0-beta300.6 32 | inter-process-messaging@0.1.2-beta300.6 33 | jquery@3.0.1-alpha300.10 34 | local-test:tmeasday:publish-counts@1.0.0-beta.0 35 | logging@1.3.3-beta300.6 36 | meteor@2.0.0-beta300.6 37 | minimongo@2.0.0-beta300.6 38 | modern-browsers@0.1.10-beta300.6 39 | modules@0.19.1-beta300.6 40 | modules-runtime@0.13.2-beta300.6 41 | mongo@2.0.0-beta300.6 42 | mongo-decimal@0.1.4-beta300.6 43 | mongo-dev-server@1.1.1-beta300.6 44 | mongo-id@1.0.9-beta300.6 45 | npm-mongo@4.16.1-beta300.6 46 | observe-sequence@2.0.0-alpha300.17 47 | ordered-dict@1.2.0-beta300.6 48 | promise@1.0.0-beta300.6 49 | random@1.2.2-beta300.6 50 | react-fast-refresh@0.2.8-beta300.6 51 | reactive-var@1.0.13-beta300.6 52 | reload@1.3.2-beta300.6 53 | retry@1.1.1-beta300.6 54 | routepolicy@1.1.2-beta300.6 55 | socket-stream-client@0.5.2-beta300.6 56 | spacebars@2.0.0-alpha300.17 57 | spacebars-compiler@2.0.0-alpha300.17 58 | templating@1.4.4-alpha300.17 59 | templating-compiler@2.0.0-alpha300.17 60 | templating-runtime@2.0.0-alpha300.17 61 | templating-tools@2.0.0-alpha300.17 62 | tinytest@2.0.0-beta300.6 63 | tmeasday:publish-counts@1.0.0-beta.0 64 | tracker@1.3.3-beta300.6 65 | typescript@5.3.3-beta300.6 66 | underscore@1.0.14-beta300.6 67 | webapp@2.0.0-beta300.6 68 | webapp-hashing@1.1.2-beta300.6 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Percolate Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Publish Counts 2 | 3 | A package to help you publish the count of a cursor, in real time. 4 | 5 | Publish-counts is designed for counting a small number of documents around an 6 | order of 100. Due to the real-time capability, this package should not be used 7 | to count all documents in large datasets. Maybe some, but not all. Otherwise, 8 | you will maximize your server's CPU usage as each client connects. 9 | 10 | ## v1.x Breaking changes 11 | 12 | This version is only compatible with [Meteor 3.0](https://github.com/meteor/meteor/blob/release-3.0/docs/generators/changelog/versions/3.0.md) and higher. 13 | 14 | ## Table of Contents 15 | 16 | - [Installation](https://github.com/percolatestudio/publish-counts#installation) 17 | - [API](https://github.com/percolatestudio/publish-counts#api) 18 | - [Counts.publish](https://github.com/percolatestudio/publish-counts#countspublish-server) 19 | - [Counts.get](https://github.com/percolatestudio/publish-counts#countsget-client) 20 | - [Counts.has](https://github.com/percolatestudio/publish-counts#countshas-client) 21 | - [Counts.noWarnings](https://github.com/percolatestudio/publish-counts#countsnowarnings-server) 22 | - [Options](https://github.com/percolatestudio/publish-counts#options) 23 | - [noReady](https://github.com/percolatestudio/publish-counts#noready) 24 | - [nonReactive](https://github.com/percolatestudio/publish-counts#nonreactive) 25 | - [countFromField](https://github.com/percolatestudio/publish-counts#countfromfield) 26 | - [countFromFieldLength](https://github.com/percolatestudio/publish-counts#countfromfieldlength) 27 | - [noWarnings](https://github.com/percolatestudio/publish-counts#nowarnings) 28 | - [Template helpers](https://github.com/percolatestudio/publish-counts#template-helpers) 29 | - [Notes]() 30 | - [Observer handle leak testing](https://github.com/percolatestudio/publish-counts#observer-handle-leak-testing) 31 | - [Why doesn't this library count directly in Mongo? or...](https://github.com/percolatestudio/publish-counts#why-doesnt-this-library-count-directly-in-mongo-or) 32 | - [Scalable count packages](https://github.com/percolatestudio/publish-counts#scalable-count-packages) 33 | - [Compatiblity with Meteor < v1.3](https://github.com/percolatestudio/publish-counts#compatibility-with-meteor--13) 34 | - [Frequently Asked Questions](https://github.com/percolatestudio/publish-counts#frequently-asked-questions) \[[Issues](https://github.com/percolatestudio/publish-counts/issues?q=label%3Afaq)] 35 | - [License](https://github.com/percolatestudio/publish-counts#license) 36 | 37 | ## Installation 38 | 39 | ``` sh 40 | $ meteor add tmeasday:publish-counts 41 | ``` 42 | 43 | ## API 44 | 45 | ### Counts.publish [server] 46 | 47 | `Counts.publish(subscription, counter-name, cursor, options)` 48 | 49 | Simply call `Counts.publish` within a publication, passing in a name and a cursor: 50 | 51 | #### Example 1 52 | ##### JavaScript 53 | ```js 54 | Meteor.publish('publication', function() { 55 | Counts.publish(this, 'name-of-counter', Posts.find()); 56 | }); 57 | ``` 58 | 59 | ##### Coffeescript 60 | ```coffeescript 61 | Meteor.publish 'publication', -> 62 | Counts.publish this, 'name-of-counter', Posts.find() 63 | return undefined # otherwise coffeescript returns a Counts.publish 64 | # handle when Meteor expects a Mongo.Cursor object. 65 | ``` 66 | 67 | The `Counts.publish` function returns the observer handle that's used to maintain the counter. You can call its `stop` method in order to stop the observer from running. 68 | 69 | Warning: Make sure you call *collection*`.find()` separately for `Counts.publish` and the `Meteor.publish` return value, otherwise you'll get empty documents on the client. 70 | 71 | For more info regarding the `options` parameter, see [Options](#options). 72 | 73 | ### Counts.get [client] 74 | 75 | `Counts.get(counter-name)` 76 | 77 | Once you've subscribed to `'publication'` ([Ex 1](#example-1)), you can call `Counts.get('name-of-counter')` to get the value of the counter, reactively. 78 | 79 | This function will always return an integer, `0` is returned if the counter is neither published nor subscribed to. 80 | 81 | ### Counts.has [client] 82 | 83 | `Counts.has(counter-name)` 84 | 85 | Returns true if a counter is both published and subscribed to, otherwise returns false. This function is reactive. 86 | 87 | Useful for validating the existence of counters. 88 | 89 | ### Counts.noWarnings [server] 90 | 91 | `Counts.noWarnings()` 92 | 93 | This function disables all development warnings on the server from publish-counts. 94 | 95 | Not recommended for use by development teams, as warnings are meant to inform 96 | library users of potential conflicts, inefficiencies, etc in their use of 97 | publish-counts as a sanity check. Suppressing all warnings precludes this 98 | sanity check for future changes. See the [`noWarnings`](#nowarnings) option 99 | for fine-grained warning suppression. 100 | 101 | ## Options 102 | 103 | ### noReady 104 | 105 | If you publish a count within a publication that also returns cursor(s), you probably want to pass `{noReady: true}` as a final argument to ensure that the "data" publication sets the ready state. For example, the following publication sends down 10 posts, but allows us to see how many there are in total: 106 | 107 | ```js 108 | Meteor.publish('posts-with-count', function() { 109 | Counts.publish(this, 'posts', Posts.find(), { noReady: true }); 110 | return Posts.find({}, { limit: 10 }); 111 | }); 112 | ``` 113 | 114 | ### nonReactive 115 | 116 | If you specify `{nonReactive: true}` the cursor won't be observed and only the initial count will be sent on initially subscribing. This is useful in some cases where reactivity is not desired, and can improve performance. 117 | 118 | ### countFromField 119 | 120 | `countFromField` allows you to specify a field to calculate the sum of its numbers across all documents. 121 | For example if we were to store page visits as numbers on a field called `visits`: 122 | 123 | ``` 124 | { content: 'testing', visits: 100 }, 125 | { content: 'a comment', visits: 50 } 126 | ``` 127 | 128 | We could then publish them like: 129 | 130 | ```js 131 | Meteor.publish('posts-visits-count', function() { 132 | Counts.publish(this, 'posts-visits', Posts.find(), { countFromField: 'visits' }); 133 | }); 134 | ``` 135 | 136 | And calling `Counts.get('posts-visits')` returns `150` 137 | 138 | If the counter field is deeply nested, e.g.: 139 | 140 | ``` 141 | { content: 'testing', stats: { visits: 100 } }, 142 | { content: 'a comment', stats: { visits: 50 } } 143 | ``` 144 | 145 | Then use an accessor function instead like: 146 | 147 | ```js 148 | Meteor.publish('posts-visits-count', function() { 149 | Counts.publish(this, 'posts-visits', 150 | Posts.find({}, { fields: { _id: 1, 'stats.visits': 1 }}), 151 | { countFromField: function (doc) { return doc.stats.visits; } } 152 | ); 153 | }); 154 | ``` 155 | 156 | Note that when using an accessor function, you must limit the fields fetched if desired, otherwise Counts will fetch entire documents as it updates the count. 157 | 158 | ### countFromFieldLength 159 | 160 | `countFromFieldLength` allows you to specify a field to calculate the sum of its **length** across all documents. 161 | For example if we were to store the userIds in an array on a field called `likes`: 162 | 163 | ``` 164 | { content: 'testing', likes: ['6PNw4GQKMA8CLprZf', 'HKv4S7xQ52h6KsXQ7'] }, 165 | { content: 'a comment', likes: ['PSmYXrxpwg276aPf5'] } 166 | ``` 167 | 168 | We could then publish them like: 169 | 170 | ```js 171 | Meteor.publish('posts-likes-count', function() { 172 | Counts.publish(this, 'posts-likes', Posts.find(), { countFromFieldLength: 'likes' }); 173 | }); 174 | ``` 175 | 176 | If the counter field is deeply nested, e.g.: 177 | 178 | ``` 179 | { content: 'testing', popularity: { likes: ['6PNw4GQKMA8CLprZf', 'HKv4S7xQ52h6KsXQ7'] } }, 180 | { content: 'a comment', popularity: { likes: ['PSmYXrxpwg276aPf5'] } } 181 | ``` 182 | 183 | Then use an accessor function instead like: 184 | 185 | ```js 186 | Meteor.publish('posts-likes-count', function() { 187 | Counts.publish(this, 'posts-likes', 188 | Posts.find({}, { fields: { _id: 1, 'popularity.likes': 1 }}), 189 | { countFromFieldLength: function (doc) { return doc.popularity.likes; } } 190 | ); 191 | }); 192 | ``` 193 | 194 | Note that when using an accessor function, you must limit the fields fetched if desired, otherwise Counts will fetch entire documents as it updates the count. 195 | 196 | ### noWarnings 197 | 198 | Pass the option, `noWarnings: true`, to `Counts.publish` to disable its warnings in 199 | a development environment. 200 | 201 | Each call to `Counts.publish` may print warnings to the console to inform 202 | developers of non-fatal conflicts with publish-counts. In some situations, a 203 | developer may intentionally invoke `Counts.publish` in a way that generates a 204 | warnings. Use this option to disable warnings for a particular invocation of 205 | `Counts.publish`. 206 | 207 | This fine-grained method of warning suppression is recommended for development 208 | teams that rely on warnings with respect to future changes. 209 | 210 | ## Template helpers 211 | 212 | To easily show counter values within your templates, use the `getPublishedCount` or `hasPublishedCount` template helper. 213 | 214 | Example: 215 | ```html 216 |
There are {{getPublishedCount 'posts'}} posts.
217 |218 | {{#if hasPublishedCount 'posts'}} 219 | There are {{getPublishedCount 'posts'}} posts. 220 | {{else}} 221 | The number of posts is loading... 222 | {{/if}} 223 |
224 | ``` 225 | 226 | ## Notes 227 | 228 | ### Observer handle leak testing 229 | 230 | The package includes a test that checks the number of observer handles opened and closed (to check for memory leaks). You need to run the `enable-publication-tests-0.7.0.1` branch of `percolatestudio/meteor` to run it however. 231 | 232 | ### Why doesn't this library count directly in Mongo? or... 233 | **Why does my MongoDB connection time-out with large (1000+) datasets?** 234 | 235 | This package is designed primarily for correctness, not performance. That's why 236 | it's aimed at counting smaller datasets and keeping the count instantly up to 237 | date. 238 | 239 | To achieve perfect correctness in Meteor data layer, we use a database observer 240 | to know immediately if a relevant change has occurred. This approach does not 241 | necessarily scale to larger datasets, as the observer needs to cache the entire 242 | matching dataset (amongst other reasons). 243 | 244 | Counting large datasets in this manner is suspected to cause database 245 | connections to time out (see 246 | [#86](https://github.com/percolatestudio/publish-counts/issues/86)). 247 | 248 | An alternative approach would be to take a .count() of the relevant cursor (or 249 | perform an aggregation in more complex use cases), and poll it regularly to 250 | update the count. Bulletproof Meteor provides a proof of concept of this 251 | approach in their [bullet-counter][proof-of-concept] example. 252 | 253 | [proof-of-concept]: https://github.com/bulletproof-meteor/bullet-counter/blob/solution/lib/server.js 254 | 255 | We'd love to see someone publish a package for this use case! If you do end up 256 | making such a package, let us know and we'll link it here. 257 | 258 | #### Scalable Count Packages 259 | 260 | * [publish-performant-counts](https://atmospherejs.com/natestrauser/publish-performant-counts) - Performant solution derived directly from [bullet-counter](https://github.com/bulletproof-meteor/bullet-counter/tree/solution). 261 | * [meteor-publish-join](https://www.npmjs.com/package/meteor-publish-join) - Publish expensive-aggregated values 262 | 263 | #### Compatibility with Meteor < 1.3 264 | 265 | Publish-counts 0.8.0 introduces an explicit dependency on the underscore js 266 | library which may be incompatible with versions of Meteor below 1.3. Please 267 | upgrade Meteor to the latest version or, if you cannot, continue to use 268 | publish-counts 0.7.3. 269 | 270 | ## Frequently Asked Questions 271 | 272 | More information can be found in the [FAQ 273 | Section](https://github.com/percolatestudio/publish-counts/issues?q=label%3Afaq) 274 | of the issue tracker. 275 | 276 | ## License 277 | 278 | MIT. (c) Percolate Studio 279 | 280 | publish-counts was developed as part of the [Verso](http://versoapp.com) project. 281 | -------------------------------------------------------------------------------- /client/publish-counts.js: -------------------------------------------------------------------------------- 1 | Counts = new Mongo.Collection('counts'); 2 | 3 | Counts.get = function countsGet (name) { 4 | var count = this.findOne(name); 5 | return count && count.count || 0; 6 | }; 7 | 8 | Counts.has = function countsHas (name) { 9 | return !!this.findOne(name); 10 | }; 11 | 12 | if (Package.templating) { 13 | Package.templating.Template.registerHelper('getPublishedCount', function(name) { 14 | return Counts.get(name); 15 | }); 16 | 17 | Package.templating.Template.registerHelper('hasPublishedCount', function(name) { 18 | return Counts.has(name); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "tmeasday:publish-counts", 3 | summary: "Publish the count of a cursor, in real time", 4 | version: "1.0.0-beta.0", 5 | git: "https://github.com/percolatestudio/publish-counts.git" 6 | }); 7 | 8 | Package.onUse(function (api, where) { 9 | api.versionsFrom('3.0-beta.0'); 10 | api.use(['blaze@3.0.0-alpha300.17', 'templating@1.4.4-alpha300.17'], 'client', { weak: true }); 11 | api.use('mongo', 'client'); 12 | api.use('underscore', 'server'); 13 | api.addFiles('client/publish-counts.js', 'client'); 14 | api.addFiles('server/publish-counts.js', 'server'); 15 | api.export('Counts'); 16 | api.export('publishCount', 'server'); 17 | }); 18 | 19 | // meteor test-packages tmeasday:publish-counts --driver-package test-in-console 20 | Package.onTest(function (api) { 21 | api.use([ 22 | 'tmeasday:publish-counts', 23 | 'underscore', 24 | 'tinytest', 25 | 'mongo', 26 | 'facts-base' 27 | ]); 28 | 29 | api.use([ 30 | 'jquery', 31 | 'blaze-html-templates' 32 | ], 'client'); 33 | 34 | api.addFiles([ 35 | 'tests/helper.js', 36 | 'tests/has_count_test.js', 37 | 'tests/count_test.js', 38 | 'tests/count_local_collection_test.js', 39 | 'tests/count_non_reactive_test.js', 40 | 'tests/count_from_field_shallow_test.js', 41 | 'tests/count_from_field_fn_shallow_test.js', 42 | 'tests/count_from_field_fn_deep_test.js', 43 | 'tests/count_from_field_length_shallow_test.js', 44 | 'tests/count_from_field_length_fn_shallow_test.js', 45 | 'tests/count_from_field_length_fn_deep_test.js', 46 | 'tests/field_limit_count_test.js', 47 | 'tests/field_limit_count_from_field_test.js', 48 | 'tests/field_limit_count_from_field_fn_test.js', 49 | 'tests/field_limit_count_from_field_length_test.js', 50 | 'tests/field_limit_count_from_field_length_fn_test.js', 51 | 'tests/no_ready_test.js', 52 | 'tests/no_warn_test.js', 53 | 'tests/observe_handles_test.js', 54 | ]); 55 | }); 56 | -------------------------------------------------------------------------------- /server/publish-counts.js: -------------------------------------------------------------------------------- 1 | var noWarnings = false; 2 | 3 | Counts = {}; 4 | Counts.publish = async function(self, name, cursor, options) { 5 | var initializing = true; 6 | var handle; 7 | options = options || {}; 8 | 9 | var extraField, countFn; 10 | 11 | if (options.countFromField) { 12 | extraField = options.countFromField; 13 | if ('function' === typeof extraField) { 14 | countFn = Counts._safeAccessorFunction(extraField); 15 | } else { 16 | countFn = function(doc) { 17 | return doc[extraField] || 0; // return 0 instead of undefined. 18 | } 19 | } 20 | } else if (options.countFromFieldLength) { 21 | extraField = options.countFromFieldLength; 22 | if ('function' === typeof extraField) { 23 | countFn = Counts._safeAccessorFunction(function (doc) { 24 | return extraField(doc).length; 25 | }); 26 | } else { 27 | countFn = function(doc) { 28 | if (doc[extraField]) { 29 | return doc[extraField].length; 30 | } else { 31 | return 0; 32 | } 33 | } 34 | } 35 | } 36 | 37 | 38 | if (countFn && options.nonReactive) 39 | throw new Error("options.nonReactive is not yet supported with options.countFromFieldLength or options.countFromFieldSum"); 40 | 41 | if (cursor && cursor._cursorDescription) { 42 | cursor._cursorDescription.options.fields = 43 | Counts._optimizeQueryFields(cursor._cursorDescription.options.fields || cursor._cursorDescription.options.projection, extraField, options.noWarnings); 44 | } 45 | 46 | var count = 0; 47 | var observers = { 48 | added: function(doc) { 49 | if (countFn) { 50 | count += countFn(doc); 51 | } else { 52 | count += 1; 53 | } 54 | 55 | if (!initializing) 56 | self.changed('counts', name, {count: count}); 57 | }, 58 | removed: function(doc) { 59 | if (countFn) { 60 | count -= countFn(doc); 61 | } else { 62 | count -= 1; 63 | } 64 | self.changed('counts', name, {count: count}); 65 | } 66 | }; 67 | 68 | if (countFn) { 69 | observers.changed = function(newDoc, oldDoc) { 70 | if (countFn) { 71 | count += countFn(newDoc) - countFn(oldDoc); 72 | } 73 | 74 | self.changed('counts', name, {count: count}); 75 | }; 76 | } 77 | 78 | if (!countFn) { 79 | let count; 80 | // Cursor.count is officially deprecated, so reuse Meteor's stored cursor options, like 81 | // https://github.com/meteor/meteor/blob/release-3.0/packages/mongo/mongo_driver.js#L876, 82 | // but we reuse the method countDocuments available here 83 | // https://github.com/meteor/meteor/blob/release-3.0/packages/mongo/mongo_driver.js#L772 84 | // given that it applies the internal methods replaceTypes and replaceMeteorAtomWithMongo 85 | // to the `selector` and `options` arguments 86 | if (cursor._cursorDescription && typeof cursor._mongo?.countDocuments === 'function') { 87 | const { collectionName, selector, options } = cursor._cursorDescription; 88 | count = await cursor._mongo.countDocuments(collectionName, selector, options); 89 | } else { 90 | const isLocalCollection = cursor.collection && !cursor.collection.name; 91 | if (!isLocalCollection) { 92 | Counts._warn(null, 'publish-counts: Internal Meteor API not available, ' + 93 | 'loading cursor documents in the memory to perform the count'); 94 | } 95 | const array = await cursor.fetchAsync(); 96 | count = array.length; 97 | } 98 | self.added('counts', name, {count: count}); 99 | if (!options.noReady) 100 | self.ready(); 101 | } 102 | 103 | if (!options.nonReactive) 104 | handle = await cursor.observe(observers); 105 | 106 | if (countFn) 107 | self.added('counts', name, {count: count}); 108 | 109 | if (!options.noReady) 110 | self.ready(); 111 | 112 | initializing = false; 113 | 114 | self.onStop(function() { 115 | if (handle) 116 | handle.stop(); 117 | }); 118 | 119 | return { 120 | stop: function() { 121 | if (handle) { 122 | handle.stop(); 123 | handle = undefined; 124 | } 125 | } 126 | }; 127 | }; 128 | // back compatibility 129 | publishCount = Counts.publish; 130 | 131 | Counts.noWarnings = function (noWarn) { 132 | // suppress warnings if no arguments, or first argument is truthy 133 | noWarnings = (0 == arguments.length || !!noWarn); 134 | } 135 | 136 | Counts._safeAccessorFunction = function safeAccessorFunction (fn) { 137 | // ensure that missing fields don't corrupt the count. If the count field 138 | // doesn't exist, then it has a zero count. 139 | return function (doc) { 140 | try { 141 | return fn(doc) || 0; // return 0 instead of undefined 142 | } 143 | catch (err) { 144 | if (err instanceof TypeError) { // attempted to access property of undefined (i.e. deep access). 145 | return 0; 146 | } else { 147 | throw err; 148 | } 149 | } 150 | }; 151 | } 152 | 153 | Counts._optimizeQueryFields = function optimizeQueryFields (fields, extraField, noWarn) { 154 | switch (typeof extraField) { 155 | case 'function': // accessor function used. 156 | if (undefined === fields) { 157 | // user did not place restrictions on cursor fields. 158 | Counts._warn(noWarn, 159 | 'publish-counts: Collection cursor has no field limits and will fetch entire documents. ' + 160 | 'consider specifying only required fields.'); 161 | // if cursor field limits are empty to begin with, leave them empty. it is the 162 | // user's responsibility to specify field limits when using accessor functions. 163 | } 164 | // else user specified restrictions on cursor fields. Meteor will ensure _id is one of them. 165 | // WARNING: unable to verify user included appropriate field for accessor function to work. we can't hold their hand ;_; 166 | 167 | return fields; 168 | 169 | case 'string': // countFromField or countFromFieldLength has property name. 170 | // extra field is a property 171 | 172 | // automatically set limits if none specified. keep existing limits since user 173 | // may use a cursor transform and specify a dynamic field to count, but require other 174 | // fields in the transform process (e.g. https://github.com/percolatestudio/publish-counts/issues/47). 175 | fields = fields || {}; 176 | // _id and extraField are required 177 | fields._id = true; 178 | fields[extraField] = true; 179 | 180 | if (2 < _.keys(fields).length) 181 | Counts._warn(noWarn, 182 | 'publish-counts: unused fields detected in cursor fields option', 183 | _.omit(fields, ['_id', extraField])); 184 | 185 | // use modified field limits. automatically defaults to _id and extraField if none specified by user. 186 | return fields; 187 | 188 | case 'undefined': // basic count 189 | if (fields && 0 < _.keys(_.omit(fields, ['_id'])).length) 190 | Counts._warn(noWarn, 191 | 'publish-counts: unused fields removed from cursor fields option.', 192 | _.omit(fields, ['_id'])); 193 | 194 | // dispose of user field limits, only _id is required 195 | fields = { _id: true }; 196 | 197 | // use modified field limits. automatically defaults to _id if none specified by user. 198 | return fields; 199 | 200 | default: 201 | throw new Error("unknown invocation of Count.publish() detected."); 202 | } 203 | } 204 | 205 | Counts._warn = function warn (noWarn) { 206 | if (noWarnings || noWarn || 'production' == process.env.NODE_ENV) 207 | return; 208 | 209 | var args = Array.prototype.slice.call(arguments, 1); 210 | console.warn.apply(console, args); 211 | } 212 | -------------------------------------------------------------------------------- /tests/count_from_field_fn_deep_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_from_field_fn_deep', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({testId: testId}), 4 | {countFromField: function (doc) { return doc.a.b; }}); 5 | }); 6 | 7 | Meteor.methods({ 8 | setup_deep_countFromField_fn: async function (testId) { 9 | await H.insert(testId, 0, {a: {b: 2}}); 10 | await H.insert(testId, 1, {a: {b: 3}}); 11 | }, 12 | addDoc_deep_countFromField_fn: async function (testId) { 13 | await H.insert(testId, 2, {a: {b: 4}}); 14 | }, 15 | updateDoc_deep_countFromField_fn: async function (testId) { 16 | await H.update(testId, 0, {$set: {a: {b: 1}}}); 17 | }, 18 | removeDoc_deep_countFromField_fn: async function (testId) { 19 | await H.remove(testId, 0); 20 | }, 21 | addField_deep_countFromField_fn: async function (testId) { 22 | await H.update(testId, 0, {a: {b: 4}}); 23 | }, 24 | removeField_deep_countFromField_fn: async function (testId) { 25 | await H.update(testId, 0, {}); 26 | }, 27 | }); 28 | } 29 | 30 | if (Meteor.isClient) { 31 | Tinytest.addAsync("countFromField: (fn deep) - upon subscribe with no records, return zero", function (test, done) { 32 | Meteor.subscribe('count_from_field_fn_deep', test.id, function () { 33 | test.equal(H.getCount(test.id), 0); 34 | done(); 35 | }); 36 | }); 37 | 38 | Tinytest.addAsync("countFromField: (fn deep) - upon subscribe with records, return sum of count fields", function (test, done) { 39 | Meteor.call('setup_deep_countFromField_fn', test.id, function () { 40 | Meteor.subscribe('count_from_field_fn_deep', test.id, function () { 41 | test.equal(H.getCount(test.id), 5); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | Tinytest.addAsync("countFromField: (fn deep) - after adding a doc, increment sum by new count field", function (test, done) { 48 | Meteor.call('setup_deep_countFromField_fn', test.id, function () { 49 | Meteor.subscribe('count_from_field_fn_deep', test.id, function () { 50 | var before = H.getCount(test.id); 51 | Meteor.call('addDoc_deep_countFromField_fn', test.id, function () { 52 | var delta = H.getCount(test.id) - before; 53 | test.equal(delta, +4); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | 60 | Tinytest.addAsync("countFromField: (fn deep) - after updating the count field of a doc, adjust sum by change in count field", function (test, done) { 61 | Meteor.call('setup_deep_countFromField_fn', test.id, function () { 62 | Meteor.subscribe('count_from_field_fn_deep', test.id, function () { 63 | var before = H.getCount(test.id); 64 | Meteor.call('updateDoc_deep_countFromField_fn', test.id, function () { 65 | var delta = H.getCount(test.id) - before; 66 | test.equal(delta, -1); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | Tinytest.addAsync("countFromField: (fn deep) - after removing a doc, decrement sum by previous count value", function (test, done) { 74 | Meteor.call('setup_deep_countFromField_fn', test.id, function () { 75 | Meteor.subscribe('count_from_field_fn_deep', test.id, function () { 76 | var before = H.getCount(test.id); 77 | Meteor.call('removeDoc_deep_countFromField_fn', test.id, function () { 78 | var delta = H.getCount(test.id) - before; 79 | test.equal(delta, -2); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | Tinytest.addAsync("countFromField: (fn deep) - after 1) removing count field parent, 2) readding count field, adjust count by gain minus loss", function (test, done) { 87 | var delta; 88 | Meteor.call('setup_deep_countFromField_fn', test.id, function () { 89 | Meteor.subscribe('count_from_field_fn_deep', test.id, function () { 90 | var before = H.getCount(test.id); 91 | Meteor.call('removeField_deep_countFromField_fn', test.id, function () { 92 | delta = H.getCount(test.id) - before; 93 | test.equal(delta, -2, 'removing field did not update count'); 94 | 95 | Meteor.call('addField_deep_countFromField_fn', test.id, function () { 96 | delta = H.getCount(test.id) - before; 97 | test.equal(delta, +2, 'adding field did not update count'); 98 | done(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /tests/count_from_field_fn_shallow_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_from_field_fn_shallow', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({testId: testId}), 4 | {countFromField: function (doc) { return doc.number; }}); 5 | }); 6 | 7 | Meteor.methods({ 8 | setup_shallow_countFromField_fn: async function (testId) { 9 | await H.insert(testId, 0, {number: 2}); 10 | await H.insert(testId, 1, {number: 3}); 11 | }, 12 | addDoc_shallow_countFromField_fn: async function (testId) { 13 | await H.insert(testId, 2, {number: 4}); 14 | }, 15 | updateDoc_shallow_countFromField_fn: async function (testId) { 16 | await H.update(testId, 0, {$set: {number: 1}}); 17 | }, 18 | removeDoc_shallow_countFromField_fn: async function (testId) { 19 | await H.remove(testId, 0); 20 | }, 21 | addField_shallow_countFromField_fn: async function (testId) { 22 | await H.update(testId, 0, {number: 4}); 23 | }, 24 | removeField_shallow_countFromField_fn: async function (testId) { 25 | await H.update(testId, 0, {}); 26 | }, 27 | }); 28 | } 29 | 30 | if (Meteor.isClient) { 31 | Tinytest.addAsync("countFromField: (fn shallow) - upon subscribe with no records, return zero", function (test, done) { 32 | Meteor.subscribe('count_from_field_fn_shallow', test.id, function () { 33 | test.equal(H.getCount(test.id), 0); 34 | done(); 35 | }); 36 | }); 37 | 38 | Tinytest.addAsync("countFromField: (fn shallow) - upon subscribe with records, return sum of count fields", function (test, done) { 39 | Meteor.call('setup_shallow_countFromField_fn', test.id, function () { 40 | Meteor.subscribe('count_from_field_fn_shallow', test.id, function () { 41 | test.equal(H.getCount(test.id), 5); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | Tinytest.addAsync("countFromField: (fn shallow) - after adding a doc, increment sum by new count field", function (test, done) { 48 | Meteor.call('setup_shallow_countFromField_fn', test.id, function () { 49 | Meteor.subscribe('count_from_field_fn_shallow', test.id, function () { 50 | var before = H.getCount(test.id); 51 | Meteor.call('addDoc_shallow_countFromField_fn', test.id, function () { 52 | var delta = H.getCount(test.id) - before; 53 | test.equal(delta, +4); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | 60 | Tinytest.addAsync("countFromField: (fn shallow) - after updating the count field of a doc, adjust sum by change in count field", function (test, done) { 61 | Meteor.call('setup_shallow_countFromField_fn', test.id, function () { 62 | Meteor.subscribe('count_from_field_fn_shallow', test.id, function () { 63 | var before = H.getCount(test.id); 64 | Meteor.call('updateDoc_shallow_countFromField_fn', test.id, function () { 65 | var delta = H.getCount(test.id) - before; 66 | test.equal(delta, -1); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | Tinytest.addAsync("countFromField: (fn shallow) - after removing a doc, decrement sum by previous count value", function (test, done) { 74 | Meteor.call('setup_shallow_countFromField_fn', test.id, function () { 75 | Meteor.subscribe('count_from_field_fn_shallow', test.id, function () { 76 | var before = H.getCount(test.id); 77 | Meteor.call('removeDoc_shallow_countFromField_fn', test.id, function () { 78 | var delta = H.getCount(test.id) - before; 79 | test.equal(delta, -2); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | Tinytest.addAsync("countFromField: (fn shallow) - after 1) removing count field, 2) readding count field, adjust count by gain minus loss", function (test, done) { 87 | Meteor.call('setup_shallow_countFromField_fn', test.id, function () { 88 | Meteor.subscribe('count_from_field_fn_shallow', test.id, function () { 89 | var before = H.getCount(test.id); 90 | Meteor.call('removeField_shallow_countFromField_fn', test.id, function () { 91 | var delta = H.getCount(test.id) - before; 92 | test.equal(delta, -2, 'removing field did not update count'); 93 | 94 | Meteor.call('addField_shallow_countFromField_fn', test.id, function () { 95 | var delta = H.getCount(test.id) - before; 96 | test.equal(delta, +2, 'adding field did not update count'); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /tests/count_from_field_length_fn_deep_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_from_field_length_fn_deep', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({testId: testId}), 4 | {countFromFieldLength: function (doc) { return doc.a.b; }}); 5 | }); 6 | 7 | Meteor.methods({ 8 | setup_deep_countFromFieldLength_fn: async function (testId) { 9 | await H.insert(testId, 0, {a: {b: [1, 2, 3]}}); 10 | await H.insert(testId, 1, {a: {b: [1, 2, 3, 4]}}); 11 | }, 12 | addDoc_deep_countFromFieldLength_fn: async function (testId) { 13 | await H.insert(testId, 2, {a: {b: [1, 2]}}); 14 | }, 15 | updateDoc_deep_countFromFieldLength_fn: async function (testId) { 16 | // add 2 elements to array of doc 0 17 | await H.update(testId, 0, {$set: {'a.b': [1, 2, 3, 4, 5]}}); 18 | }, 19 | removeDoc_deep_countFromFieldLength_fn: async function (testId) { 20 | await H.remove(testId, 0); 21 | }, 22 | addField_deep_countFromFieldLength_fn: async function (testId) { 23 | await H.update(testId, 0, {a: {b: [1, 2, 3, 4, 5]}}); 24 | }, 25 | removeField_deep_countFromFieldLength_fn: async function (testId) { 26 | await H.update(testId, 0, {}); 27 | }, 28 | }); 29 | } 30 | 31 | if (Meteor.isClient) { 32 | Tinytest.addAsync("countFromFieldLength: (fn deep) - upon subscribe with no records, return zero", function (test, done) { 33 | Meteor.subscribe('count_from_field_length_fn_deep', test.id, function () { 34 | test.equal(H.getCount(test.id), 0); 35 | done(); 36 | }); 37 | }); 38 | 39 | Tinytest.addAsync("countFromFieldLength: (fn deep) - upon subscribe with records, return sum of lengths of array fields", function (test, done) { 40 | Meteor.call('setup_deep_countFromFieldLength_fn', test.id, function () { 41 | Meteor.subscribe('count_from_field_length_fn_deep', test.id, function () { 42 | test.equal(H.getCount(test.id), 7); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | Tinytest.addAsync("countFromFieldLength: (fn deep) - after adding a doc, increment count by new array length", function (test, done) { 49 | Meteor.call('setup_deep_countFromFieldLength_fn', test.id, function () { 50 | Meteor.subscribe('count_from_field_length_fn_deep', test.id, function () { 51 | var before = H.getCount(test.id); 52 | Meteor.call('addDoc_deep_countFromFieldLength_fn', test.id, function () { 53 | var delta = H.getCount(test.id) - before; 54 | test.equal(delta, +2); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | 61 | Tinytest.addAsync("countFromFieldLength: (fn deep) - after updating the count field of a doc, adjust count by change in array length", function (test, done) { 62 | Meteor.call('setup_deep_countFromFieldLength_fn', test.id, function () { 63 | Meteor.subscribe('count_from_field_length_fn_deep', test.id, function () { 64 | var before = H.getCount(test.id); 65 | Meteor.call('updateDoc_deep_countFromFieldLength_fn', test.id, function () { 66 | var delta = H.getCount(test.id) - before; 67 | test.equal(delta, +2); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | Tinytest.addAsync("countFromFieldLength: (fn deep) - after removing a doc, decrement count by array length", function (test, done) { 75 | Meteor.call('setup_deep_countFromFieldLength_fn', test.id, function () { 76 | Meteor.subscribe('count_from_field_length_fn_deep', test.id, function () { 77 | var before = H.getCount(test.id); 78 | Meteor.call('removeDoc_deep_countFromFieldLength_fn', test.id, function () { 79 | var delta = H.getCount(test.id) - before; 80 | test.equal(delta, -3); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | Tinytest.addAsync("countFromFieldLength: (fn deep) - after 1) removing count field parent, 2) readding count field, adjust count by gain minus loss", function (test, done) { 88 | Meteor.call('setup_deep_countFromFieldLength_fn', test.id, function () { 89 | Meteor.subscribe('count_from_field_length_fn_deep', test.id, function () { 90 | var before = H.getCount(test.id); 91 | Meteor.call('removeField_deep_countFromFieldLength_fn', test.id, function () { 92 | var delta = H.getCount(test.id) - before; 93 | test.equal(delta, -3, 'removing field did not update count'); 94 | 95 | Meteor.call('addField_deep_countFromFieldLength_fn', test.id, function () { 96 | var delta = H.getCount(test.id) - before; 97 | test.equal(delta, +2, 'adding field did not update count'); 98 | done(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /tests/count_from_field_length_fn_shallow_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_from_field_length_fn_shallow', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({testId: testId}), 4 | {countFromFieldLength: function (doc) { return doc.array; }}); 5 | }); 6 | 7 | Meteor.methods({ 8 | setup_shallow_countFromFieldLength_fn: async function (testId) { 9 | await H.insert(testId, 0, {array: [1, 2, 3]}); 10 | await H.insert(testId, 1, {array: [1, 2, 3, 4]}); 11 | }, 12 | addDoc_shallow_countFromFieldLength_fn: async function (testId) { 13 | await H.insert(testId, 2, {array: [1, 2]}); 14 | }, 15 | updateDoc_shallow_countFromFieldLength_fn: async function (testId) { 16 | // add 2 elements to array of doc 0 17 | await H.update(testId, 0, {$set: {array: [1, 2, 3, 4, 5]}}); 18 | }, 19 | removeDoc_shallow_countFromFieldLength_fn: async function (testId) { 20 | await H.remove(testId, 0); 21 | }, 22 | addField_shallow_countFromFieldLength_fn: async function (testId) { 23 | await H.update(testId, 0, {array: [1, 2, 3, 4, 5]}); 24 | }, 25 | removeField_shallow_countFromFieldLength_fn: async function (testId) { 26 | await H.update(testId, 0, {}); 27 | }, 28 | }); 29 | } 30 | 31 | if (Meteor.isClient) { 32 | Tinytest.addAsync("countFromFieldLength: (fn shallow) - upon subscribe with no records, return zero", function (test, done) { 33 | Meteor.subscribe('count_from_field_length_fn_shallow', test.id, function () { 34 | test.equal(H.getCount(test.id), 0); 35 | done(); 36 | }); 37 | }); 38 | 39 | Tinytest.addAsync("countFromFieldLength: (fn shallow) - upon subscribe with records, return sum of lengths of array fields", function (test, done) { 40 | Meteor.call('setup_shallow_countFromFieldLength_fn', test.id, function () { 41 | Meteor.subscribe('count_from_field_length_fn_shallow', test.id, function () { 42 | test.equal(H.getCount(test.id), 7); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | Tinytest.addAsync("countFromFieldLength: (fn shallow) - after adding a doc, increment count by new array length", function (test, done) { 49 | Meteor.call('setup_shallow_countFromFieldLength_fn', test.id, function () { 50 | Meteor.subscribe('count_from_field_length_fn_shallow', test.id, function () { 51 | var before = H.getCount(test.id); 52 | Meteor.call('addDoc_shallow_countFromFieldLength_fn', test.id, function () { 53 | var delta = H.getCount(test.id) - before; 54 | test.equal(delta, +2); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | 61 | Tinytest.addAsync("countFromFieldLength: (fn shallow) - after updating the count field of a doc, adjust count by change in array length", function (test, done) { 62 | Meteor.call('setup_shallow_countFromFieldLength_fn', test.id, function () { 63 | Meteor.subscribe('count_from_field_length_fn_shallow', test.id, function () { 64 | var before = H.getCount(test.id); 65 | Meteor.call('updateDoc_shallow_countFromFieldLength_fn', test.id, function () { 66 | var delta = H.getCount(test.id) - before; 67 | test.equal(delta, +2); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | Tinytest.addAsync("countFromFieldLength: (fn shallow) - after removing a doc, decrement count by array length", function (test, done) { 75 | Meteor.call('setup_shallow_countFromFieldLength_fn', test.id, function () { 76 | Meteor.subscribe('count_from_field_length_fn_shallow', test.id, function () { 77 | var before = H.getCount(test.id); 78 | Meteor.call('removeDoc_shallow_countFromFieldLength_fn', test.id, function () { 79 | var delta = H.getCount(test.id) - before; 80 | test.equal(delta, -3); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | 87 | Tinytest.addAsync("countFromFieldLength: (fn shallow) - after 1) removing count field, 2) readding count field, adjust count by gain minus loss", function (test, done) { 88 | Meteor.call('setup_shallow_countFromFieldLength_fn', test.id, function () { 89 | Meteor.subscribe('count_from_field_length_fn_shallow', test.id, function () { 90 | var before = H.getCount(test.id); 91 | Meteor.call('removeField_shallow_countFromFieldLength_fn', test.id, function () { 92 | var delta = H.getCount(test.id) - before; 93 | test.equal(delta, -3, 'removing field did not update count'); 94 | 95 | Meteor.call('addField_shallow_countFromFieldLength_fn', test.id, function () { 96 | var delta = H.getCount(test.id) - before; 97 | test.equal(delta, +2, 'adding field did not update count'); 98 | done(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /tests/count_from_field_length_shallow_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_from_field_length_shallow', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({testId: testId}), {countFromFieldLength: 'array'}); 4 | }); 5 | 6 | Meteor.methods({ 7 | setup_shallow_countFromFieldLength: async function (testId) { 8 | await H.insert(testId, 0, {array: [1, 2, 3]}); 9 | await H.insert(testId, 1, {array: [1, 2, 3, 4]}); 10 | }, 11 | addDoc_shallow_countFromFieldLength: async function (testId) { 12 | await H.insert(testId, 2, {array: [1, 2]}); 13 | }, 14 | updateDoc_shallow_countFromFieldLength: async function (testId) { 15 | // add 2 elements to array of doc 0 16 | await H.update(testId, 0, {$set: {array: [1, 2, 3, 4, 5]}}); 17 | }, 18 | removeDoc_shallow_countFromFieldLength: async function (testId) { 19 | await H.remove(testId, 0); 20 | }, 21 | addField_shallow_countFromFieldLength: async function (testId) { 22 | await H.update(testId, 0, {array: [1, 2, 3, 4, 5]}); 23 | }, 24 | removeField_shallow_countFromFieldLength: async function (testId) { 25 | await H.update(testId, 0, {}); 26 | }, 27 | }); 28 | } 29 | 30 | if (Meteor.isClient) { 31 | Tinytest.addAsync("countFromFieldLength: (shallow) - upon subscribe with no records, return zero", function (test, done) { 32 | Meteor.subscribe('count_from_field_length_shallow', test.id, function () { 33 | test.equal(H.getCount(test.id), 0); 34 | done(); 35 | }); 36 | }); 37 | 38 | Tinytest.addAsync("countFromFieldLength: (shallow) - upon subscribe with records, return sum of lengths of array fields", function (test, done) { 39 | Meteor.call('setup_shallow_countFromFieldLength', test.id, function () { 40 | Meteor.subscribe('count_from_field_length_shallow', test.id, function () { 41 | test.equal(H.getCount(test.id), 7); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | Tinytest.addAsync("countFromFieldLength: (shallow) - after adding a doc, increment count by new array length", function (test, done) { 48 | Meteor.call('setup_shallow_countFromFieldLength', test.id, function () { 49 | Meteor.subscribe('count_from_field_length_shallow', test.id, function () { 50 | var before = H.getCount(test.id); 51 | Meteor.call('addDoc_shallow_countFromFieldLength', test.id, function () { 52 | var delta = H.getCount(test.id) - before; 53 | test.equal(delta, +2); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | 60 | Tinytest.addAsync("countFromFieldLength: (shallow) - after updating the array field of a doc, adjust count by change in array length", function (test, done) { 61 | Meteor.call('setup_shallow_countFromFieldLength', test.id, function () { 62 | Meteor.subscribe('count_from_field_length_shallow', test.id, function () { 63 | var before = H.getCount(test.id); 64 | Meteor.call('updateDoc_shallow_countFromFieldLength', test.id, function () { 65 | var delta = H.getCount(test.id) - before; 66 | test.equal(delta, +2); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | Tinytest.addAsync("countFromFieldLength: (shallow) - after removing a doc, decrement count by array length", function (test, done) { 74 | Meteor.call('setup_shallow_countFromFieldLength', test.id, function () { 75 | Meteor.subscribe('count_from_field_length_shallow', test.id, function () { 76 | var before = H.getCount(test.id); 77 | Meteor.call('removeDoc_shallow_countFromFieldLength', test.id, function () { 78 | var delta = H.getCount(test.id) - before; 79 | test.equal(delta, -3); 80 | done(); 81 | }); 82 | }); 83 | }); 84 | }); 85 | 86 | Tinytest.addAsync("countFromFieldLength: (shallow) - after 1) removing count field, 2) readding count field, adjust count by gain minus loss", function (test, done) { 87 | Meteor.call('setup_shallow_countFromFieldLength', test.id, function () { 88 | Meteor.subscribe('count_from_field_length_shallow', test.id, function () { 89 | var before = H.getCount(test.id); 90 | Meteor.call('removeField_shallow_countFromFieldLength', test.id, function () { 91 | var delta = H.getCount(test.id) - before; 92 | test.equal(delta, -3, 'removing field did not update count'); 93 | 94 | Meteor.call('addField_shallow_countFromFieldLength', test.id, function () { 95 | var delta = H.getCount(test.id) - before; 96 | test.equal(delta, +2, 'adding field did not update count'); 97 | done(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /tests/count_from_field_shallow_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_from_field_shallow', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({testId: testId}), {countFromField: 'number'}); 4 | }); 5 | 6 | Meteor.methods({ 7 | setup_shallow_countFromField: async function (testId) { 8 | await H.insert(testId, 0, {number: 2}); 9 | await H.insert(testId, 1, {number: 3}); 10 | }, 11 | addDoc_shallow_countFromField: async function (testId) { 12 | await H.insert(testId, 2, {number: 4}); 13 | }, 14 | updateDoc_shallow_countFromField: async function (testId) { 15 | await H.update(testId, 0, {$set: {number: 1}}); 16 | }, 17 | removeDoc_shallow_countFromField: async function (testId) { 18 | await H.remove(testId, 0); 19 | }, 20 | }); 21 | } 22 | 23 | if (Meteor.isClient) { 24 | Tinytest.addAsync("countFromField: (shallow) - upon subscribe with no records, return zero", function (test, done) { 25 | Meteor.subscribe('count_from_field_shallow', test.id, function () { 26 | test.equal(H.getCount(test.id), 0); 27 | done(); 28 | }); 29 | }); 30 | 31 | Tinytest.addAsync("countFromField: (shallow) - upon subscribe with records, return sum of count fields", function (test, done) { 32 | Meteor.call('setup_shallow_countFromField', test.id, function () { 33 | Meteor.subscribe('count_from_field_shallow', test.id, function () { 34 | test.equal(H.getCount(test.id), 5); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | 40 | Tinytest.addAsync("countFromField: (shallow) - after adding a doc, increment sum by new count field", function (test, done) { 41 | Meteor.call('setup_shallow_countFromField', test.id, function () { 42 | Meteor.subscribe('count_from_field_shallow', test.id, function () { 43 | var before = H.getCount(test.id); 44 | Meteor.call('addDoc_shallow_countFromField', test.id, function () { 45 | var delta = H.getCount(test.id) - before; 46 | test.equal(delta, +4); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | 53 | Tinytest.addAsync("countFromField: (shallow) - after updating the count field of a doc, adjust sum by change in count field", function (test, done) { 54 | Meteor.call('setup_shallow_countFromField', test.id, function () { 55 | Meteor.subscribe('count_from_field_shallow', test.id, function () { 56 | var before = H.getCount(test.id); 57 | Meteor.call('updateDoc_shallow_countFromField', test.id, function () { 58 | var delta = H.getCount(test.id) - before; 59 | test.equal(delta, -1); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | }); 65 | 66 | Tinytest.addAsync("countFromField: (shallow) - after removing a doc, decrement sum by previous count value", function (test, done) { 67 | Meteor.call('setup_shallow_countFromField', test.id, function () { 68 | Meteor.subscribe('count_from_field_shallow', test.id, function () { 69 | var before = H.getCount(test.id); 70 | Meteor.call('removeDoc_shallow_countFromField', test.id, function () { 71 | var delta = H.getCount(test.id) - before; 72 | test.equal(delta, -2); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /tests/count_local_collection_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | var Locals = new Mongo.Collection(null); 3 | var insert = H.insertFactory(Locals); 4 | var remove = H.removeFactory(Locals); 5 | var update = H.updateFactory(Locals); 6 | 7 | Meteor.publish('count-locals', async function (testId) { 8 | await Counts.publish(this, 'locals' + testId, Locals.find({ testId: testId })); 9 | }); 10 | 11 | Meteor.methods({ 12 | setup_count_locals: async function (testId) { 13 | await insert(testId, 0, { name: "i'm a test local" }); 14 | await insert(testId, 1, { name: "i'm a test local" }); 15 | await insert(testId, 2, { name: "i'm a test local" }); 16 | }, 17 | addDoc_count_locals: async function (testId) { 18 | await insert(testId, 3, { name: "i'm a test local" }); 19 | }, 20 | updateDoc_count_locals: async function (testId) { 21 | await update(testId, 0, { $set: { name: "i'm an edited local" } }); 22 | }, 23 | removeDoc_count_locals: async function (testId) { 24 | await remove(testId, 0); 25 | }, 26 | }); 27 | } 28 | 29 | if (Meteor.isClient) { 30 | var getCount = H.getCountFactory('locals'); // name must match name used in `Counts.publish` above. 31 | 32 | Tinytest.addAsync("count: (local collection) - upon subscribe with no records, return zero", function (test, done) { 33 | Meteor.subscribe('count-locals', test.id, function () { 34 | test.equal(getCount(test.id), 0); 35 | done(); 36 | }); 37 | }); 38 | 39 | Tinytest.addAsync("count: (local collection) - upon subscribe with records, return number of records", function (test, done) { 40 | Meteor.call('setup_count_locals', test.id, function () { 41 | Meteor.subscribe('count-locals', test.id, function () { 42 | test.equal(getCount(test.id), 3); 43 | done(); 44 | }); 45 | }); 46 | }); 47 | 48 | Tinytest.addAsync("count: (local collection) - after adding a doc, increment count", function (test, done) { 49 | Meteor.call('setup_count_locals', test.id, function () { 50 | Meteor.subscribe('count-locals', test.id, function () { 51 | var before = getCount(test.id); 52 | Meteor.call('addDoc_count_locals', test.id, function () { 53 | var delta = getCount(test.id) - before; 54 | test.equal(delta, +1); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | 61 | Tinytest.addAsync("count: (local collection) - after updating a doc, count remains the same", function (test, done) { 62 | Meteor.call('setup_count_locals', test.id, function () { 63 | Meteor.subscribe('count-locals', test.id, function () { 64 | var before = getCount(test.id); 65 | Meteor.call('updateDoc_count_locals', test.id, function () { 66 | var delta = getCount(test.id) - before; 67 | test.equal(delta, 0); 68 | done(); 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | Tinytest.addAsync("count: (local collection) - after removing a doc, decrement count", function (test, done) { 75 | Meteor.call('setup_count_locals', test.id, function () { 76 | Meteor.subscribe('count-locals', test.id, function () { 77 | var before = getCount(test.id); 78 | Meteor.call('removeDoc_count_locals', test.id, function () { 79 | var delta = getCount(test.id) - before; 80 | test.equal(delta, -1); 81 | done(); 82 | }); 83 | }); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /tests/count_non_reactive_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count_non_reactive', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({ testId: testId }), {nonReactive: true}); 4 | }); 5 | 6 | Meteor.methods({ 7 | setup_countNonReactive: async function (testId) { 8 | await H.insert(testId, 0, {name: "i'm a test post" }); 9 | await H.insert(testId, 1, {name: "i'm a test post" }); 10 | await H.insert(testId, 2, {name: "i'm a test post" }); 11 | }, 12 | add_doc_countNonReactive: async function (testId) { 13 | await H.insert(testId, 3, {name: "i'm a test post" }); 14 | }, 15 | remove_doc_countNonReactive: async function (testId) { 16 | await H.remove(testId, 0); 17 | }, 18 | }); 19 | } 20 | 21 | if (Meteor.isClient) { 22 | Tinytest.addAsync("count: (non-reactive) - upon subscribe with no records, return zero", function (test, done) { 23 | Meteor.subscribe('count_non_reactive', test.id, function () { 24 | test.equal(H.getCount(test.id), 0); 25 | done(); 26 | }); 27 | }); 28 | 29 | Tinytest.addAsync("count: (non-reactive) - upon subscribe with records, return number of records", function (test, done) { 30 | Meteor.call('setup_countNonReactive', test.id, function () { 31 | Meteor.subscribe('count_non_reactive', test.id, function () { 32 | test.equal(H.getCount(test.id), 3); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | Tinytest.addAsync("count: (non-reactive) - after adding a doc, count remains same", function (test, done) { 39 | Meteor.call('setup_countNonReactive', test.id, function () { 40 | Meteor.subscribe('count_non_reactive', test.id, function () { 41 | var before = H.getCount(test.id); 42 | Meteor.call('add_doc_countNonReactive', test.id, function () { 43 | var delta = H.getCount(test.id) - before; 44 | test.equal(delta, 0); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | 51 | Tinytest.addAsync("count: (non-reactive) - after removing a doc, count remains same", function (test, done) { 52 | Meteor.call('setup_countNonReactive', test.id, function () { 53 | Meteor.subscribe('count_non_reactive', test.id, function () { 54 | var before = H.getCount(test.id); 55 | Meteor.call('remove_doc_countNonReactive', test.id, function () { 56 | var delta = H.getCount(test.id) - before; 57 | test.equal(delta, 0); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /tests/count_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('count', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({ testId: testId })); 4 | }); 5 | 6 | Meteor.methods({ 7 | setup_count: async function (testId) { 8 | await H.insert(testId, 0, { name: "i'm a test post" }); 9 | await H.insert(testId, 1, { name: "i'm a test post" }); 10 | await H.insert(testId, 2, { name: "i'm a test post" }); 11 | }, 12 | addDoc_count: async function (testId) { 13 | await H.insert(testId, 3, { name: "i'm a test post" }); 14 | }, 15 | updateDoc_count: async function (testId) { 16 | await H.update(testId, 0, { $set: { name: "i'm an edited post" } }); 17 | }, 18 | removeDoc_count: async function (testId) { 19 | await H.remove(testId, 0); 20 | }, 21 | }); 22 | } 23 | 24 | if (Meteor.isClient) { 25 | Tinytest.addAsync("count: - upon subscribe with no records, return zero", function (test, done) { 26 | Meteor.subscribe('count', test.id, function () { 27 | test.equal(H.getCount(test.id), 0); 28 | done(); 29 | }); 30 | }); 31 | 32 | Tinytest.addAsync("count: - upon subscribe with records, return number of records", function (test, done) { 33 | Meteor.call('setup_count', test.id, function () { 34 | Meteor.subscribe('count', test.id, function () { 35 | test.equal(H.getCount(test.id), 3); 36 | done(); 37 | }); 38 | }); 39 | }); 40 | 41 | Tinytest.addAsync("count: - after adding a doc, increment count", function (test, done) { 42 | Meteor.call('setup_count', test.id, function () { 43 | Meteor.subscribe('count', test.id, function () { 44 | var before = H.getCount(test.id); 45 | Meteor.call('addDoc_count', test.id, function () { 46 | var delta = H.getCount(test.id) - before; 47 | test.equal(delta, +1); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | Tinytest.addAsync("count: - after updating a doc, count remains the same", function (test, done) { 55 | Meteor.call('setup_count', test.id, function () { 56 | Meteor.subscribe('count', test.id, function () { 57 | var before = H.getCount(test.id); 58 | Meteor.call('updateDoc_count', test.id, function () { 59 | var delta = H.getCount(test.id) - before; 60 | test.equal(delta, 0); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | }); 66 | 67 | Tinytest.addAsync("count: - after removing a doc, decrement count", function (test, done) { 68 | Meteor.call('setup_count', test.id, function () { 69 | Meteor.subscribe('count', test.id, function () { 70 | var before = H.getCount(test.id); 71 | Meteor.call('removeDoc_count', test.id, function () { 72 | var delta = H.getCount(test.id) - before; 73 | test.equal(delta, -1); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /tests/field_limit_count_from_field_fn_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("fieldLimit: (countFromField fn) - upon publish without field limit, always leave field limit undefined", function (test) { 3 | var pub = new H.PubMock(); 4 | var cursor = Posts.find({ testId: test.id }); // no field limit 5 | 6 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 7 | 8 | var fields = cursor._cursorDescription.options.fields; 9 | 10 | // verify no restrictions were set. 11 | test.isUndefined(fields, 'Count must keep empty cursor fields limits when user uses accessor function'); 12 | }); 13 | 14 | { // WARNING TESTS 15 | 16 | // upon publish 17 | // without field limit 18 | // with warnings 19 | // in development 20 | // warn user 21 | Tinytest.add("fieldLimit: (countFromField fn) - upon publish without field limit, " + 22 | "with warnings, in development, warn user that entire documents are fetched", function (test) { 23 | var pub = new H.PubMock(); 24 | var cursor = Posts.find({ testId: test.id }); // no field limit 25 | var conmock = { warn: H.detectRegex(/Collection cursor has no field limits and will fetch entire documents. consider specifying only required fields./) }; 26 | 27 | H.withConsole(conmock, function () { 28 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 29 | }); 30 | 31 | // verify the warning was sent to user 32 | test.isTrue(conmock.warn.found(), 'expected warning'); 33 | }); 34 | 35 | // upon publish 36 | // without field limit 37 | // warning uses Counts._warn 38 | Tinytest.add("fieldLimit: (countFromField fn) - upon publish without field limit, " + 39 | "warn with Counts._warn()", function (test) { 40 | var pub = new H.PubMock(); 41 | var cursor = Posts.find({ testId: test.id }); // no field limit 42 | var flag = false; 43 | 44 | H.withWarn(function () { flag = true; }, function () { 45 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 46 | }); 47 | 48 | test.isTrue(flag, 'did not call Counts._warn()'); 49 | }); 50 | 51 | } // END WARNING TESTS 52 | 53 | Tinytest.add("fieldLimit: (countFromField fn) - upon publish with count field assigned to field limit, keep existing field limit", function (test) { 54 | var pub = new H.PubMock(); 55 | var cursor = Posts.find({ testId: test.id }, { fields: { likes: true }}); // field limit matches countFromField property. 56 | 57 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 58 | 59 | var fields = cursor._cursorDescription.options.fields; 60 | 61 | test.isNotUndefined(fields, 'cursor is missing fields property'); 62 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 63 | // verify only two fields are fetched. 64 | test.length(_.keys(fields), 1, 'cursor has more/less fields than specified'); 65 | }); 66 | 67 | // the user should always include the field used in the accessor function in the cursor field limit. 68 | // there is no means to automatically include the count field in the cursor field limit and chaos will ensue when 69 | // the user fails to handle this. 70 | Tinytest.add("fieldLimit: (countFromField fn) - upon publish with other fields assigned to field limit, keep existing field limit", function (test) { 71 | var pub = new H.PubMock(); 72 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true, likes: true }}); // field limit must match countFromField property. 73 | 74 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 75 | 76 | var fields = cursor._cursorDescription.options.fields; 77 | 78 | test.isNotUndefined(fields, 'cursor is missing fields property'); 79 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 80 | test.isNotUndefined(fields.name, 'cursor is missing field (name)'); 81 | // verify only three fields are fetched. 82 | test.length(_.keys(fields), 2, 'cursor has more/less fields than specified'); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /tests/field_limit_count_from_field_length_fn_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("fieldLimit: (countFromFieldLength fn) - upon publish without field limit, always leave field limit undefined", 3 | function (test) { 4 | var pub = new H.PubMock(); 5 | var cursor = Posts.find({ testId: test.id }); // no field limit 6 | 7 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 8 | 9 | var fields = cursor._cursorDescription.options.fields; 10 | 11 | // verify no restrictions were set. 12 | test.isUndefined(fields, 'Count must keep empty cursor fields limits when user uses accessor function'); 13 | }); 14 | 15 | { // WARNING TESTS 16 | 17 | // upon publish 18 | // without field limit 19 | // with warnings 20 | // in development 21 | // warn user 22 | Tinytest.add("fieldLimit: (countFromFieldLength fn) - upon publish without field limit, " + 23 | "with warnings, in development, warn user that entire documents are fetched", function (test) { 24 | var pub = new H.PubMock(); 25 | var cursor = Posts.find({ testId: test.id }); // no field limit 26 | var conmock = { warn: H.detectRegex(/Collection cursor has no field limits and will fetch entire documents. consider specifying only required fields./) }; 27 | 28 | H.withConsole(conmock, function () { 29 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 30 | }); 31 | 32 | // verify the warning was sent to user 33 | test.isTrue(conmock.warn.found(), 'expected warning'); 34 | }); 35 | 36 | // upon publish 37 | // without field limit 38 | // warning uses Counts._warn 39 | Tinytest.add("fieldLimit: (countFromFieldLength fn) - upon publish without field limit, " + 40 | "warn with Counts._warn()", function (test) { 41 | var pub = new H.PubMock(); 42 | var cursor = Posts.find({ testId: test.id }); // no field limit 43 | var flag = false; 44 | 45 | H.withWarn(function () { flag = true; }, function () { 46 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 47 | }); 48 | 49 | test.isTrue(flag, 'did not call Counts._warn()'); 50 | }); 51 | 52 | } // END WARNING TESTS 53 | 54 | Tinytest.add("fieldLimit: (countFromFieldLength fn) - upon publish with count field assigned to field limit, keep existing field limit", function (test) { 55 | var pub = new H.PubMock(); 56 | var cursor = Posts.find({ testId: test.id }, { fields: { likes: true }}); // field limit matches countFromField property. 57 | 58 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 59 | 60 | var fields = cursor._cursorDescription.options.fields; 61 | 62 | test.isNotUndefined(fields, 'cursor is missing fields property'); 63 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 64 | // verify only two fields are fetched. 65 | test.length(_.keys(fields), 1, 'cursor has more/less fields than specified'); 66 | }); 67 | 68 | // the user should always include the field used in the accessor function in the cursor field limit. 69 | // there is no means to automatically include the count field in the cursor field limit and chaos will ensue when 70 | // the user fails to handle this. 71 | Tinytest.add("fieldLimit: (countFromFieldLength fn) - upon publish with other fields assigned to field limit, keep existing field limit", function (test) { 72 | var pub = new H.PubMock(); 73 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true, likes: true }}); // field limit must match countFromField property. 74 | 75 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: function (doc) { return doc.likes; } }); 76 | 77 | var fields = cursor._cursorDescription.options.fields; 78 | 79 | test.isNotUndefined(fields, 'cursor is missing fields property'); 80 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 81 | test.isNotUndefined(fields.name, 'cursor is missing field (name)'); 82 | // verify only three fields are fetched. 83 | test.length(_.keys(fields), 2, 'cursor has more/less fields than specified'); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /tests/field_limit_count_from_field_length_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("fieldLimit: (countFromFieldLength) - upon publish without field limit, automatically limit cursor fields to _id and count field", function (test) { 3 | var pub = new H.PubMock(); 4 | var cursor = Posts.find({ testId: test.id }); // no field limit 5 | 6 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 7 | 8 | var fields = cursor._cursorDescription.options.fields; 9 | 10 | test.isNotUndefined(fields, 'cursor is missing fields property'); 11 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 12 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 13 | // verify only two fields are fetched. 14 | test.equal(_.keys(fields).length, 2, 'cursor has more/less than two fields'); 15 | }); 16 | 17 | Tinytest.add("fieldLimit: (countFromFieldLength) - upon publish with count field assigned to field limit, keep existing field limit plus _id", function (test) { 18 | var pub = new H.PubMock(); 19 | var cursor = Posts.find({ testId: test.id }, { fields: { likes: true }}); // field limit matches countFromField 20 | 21 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 22 | 23 | var fields = cursor._cursorDescription.options.fields; 24 | 25 | test.isNotUndefined(fields, 'cursor is missing fields property'); 26 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 27 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 28 | // verify only two fields are fetched. 29 | test.equal(_.keys(fields).length, 2, 'cursor has more/less fields than specified (plus _id)'); 30 | }); 31 | 32 | // honestly, devs should never have a reason to do this. the 'name' field in this example is never used, nor can it ever be. 33 | Tinytest.add("fieldLimit: (countFromFieldLength) - upon publish with other fields assigned to field limit, " + 34 | "always keep existing field limit plus _id and count field", function (test) { 35 | var pub = new H.PubMock(); 36 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); 37 | 38 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 39 | 40 | var fields = cursor._cursorDescription.options.fields; 41 | 42 | test.isNotUndefined(fields, 'cursor is missing fields property'); 43 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 44 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 45 | test.isNotUndefined(fields.name, 'cursor is missing field (name)'); 46 | // verify only three fields are fetched. 47 | test.equal(_.keys(fields).length, 3, 'cursor has more/less fields than specified (plus _id and likes)'); 48 | }); 49 | 50 | { // WARNING TESTS 51 | 52 | // upon publish 53 | // with other fields assigned to field limit 54 | // with warnings 55 | // in development 56 | // warn user 57 | Tinytest.add("fieldLimit: (countFromFieldLength) - upon publish with other fields assigned to field limit, " + 58 | "with warnings, in development, warn user about unused fields", 59 | function (test) { 60 | var pub = new H.PubMock(); 61 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); 62 | var conmock = { warn: H.detectRegex(/unused fields detected in cursor fields option/) }; 63 | 64 | H.withConsole(conmock, function () { 65 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 66 | }); 67 | 68 | // verify the warning was sent to user 69 | test.isTrue(conmock.warn.found(), 'expected warning'); 70 | }); 71 | 72 | // upon publish 73 | // with other fields assigned to field limit 74 | // warning uses Counts._warn 75 | Tinytest.add("fieldLimit: (countFromFieldLength) - upon publish with other fields assigned to field limit, " + 76 | "warn with Counts._warn()", 77 | function (test) { 78 | var pub = new H.PubMock(); 79 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); 80 | var flag = false; 81 | 82 | H.withWarn(function () { flag = true; }, function () { 83 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 84 | }); 85 | 86 | test.isTrue(flag, 'did not call Counts._warn()'); 87 | }); 88 | 89 | } // END WARNING TESTS 90 | } 91 | -------------------------------------------------------------------------------- /tests/field_limit_count_from_field_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("fieldLimit: (countFromField) - upon publish without field limit, automatically limit cursor fields to _id and count field", function (test) { 3 | var pub = new H.PubMock(); 4 | var cursor = Posts.find({ testId: test.id }); // no field limit 5 | 6 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 7 | 8 | var fields = cursor._cursorDescription.options.fields; 9 | 10 | test.isNotUndefined(fields, 'cursor is missing fields property'); 11 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 12 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 13 | // verify only two fields are fetched. 14 | test.equal(_.keys(fields).length, 2, 'cursor has more/less than two fields'); 15 | }); 16 | 17 | Tinytest.add("fieldLimit: (countFromField) - upon publish with count field assigned to field limit, keep existing field limit plus _id", function (test) { 18 | var pub = new H.PubMock(); 19 | var cursor = Posts.find({ testId: test.id }, { fields: { likes: true }}); // field limit matches countFromField 20 | 21 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 22 | 23 | var fields = cursor._cursorDescription.options.fields; 24 | 25 | test.isNotUndefined(fields, 'cursor is missing fields property'); 26 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 27 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 28 | // verify only two fields are fetched. 29 | test.equal(_.keys(fields).length, 2, 'cursor has more/less fields than specified (plus _id)'); 30 | }); 31 | 32 | // honestly, devs should never have a reason to do this. the 'name' field in this example is never used, nor can it ever be. 33 | Tinytest.add("fieldLimit: (countFromField) - upon publish with other fields assigned to field limit, always keep existing field limit plus _id and count field", function (test) { 34 | var pub = new H.PubMock(); 35 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); 36 | 37 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 38 | 39 | var fields = cursor._cursorDescription.options.fields; 40 | 41 | test.isNotUndefined(fields, 'cursor is missing fields property'); 42 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 43 | test.isNotUndefined(fields.likes, 'cursor is missing field (likes)'); 44 | test.isNotUndefined(fields.name, 'cursor is missing field (name)'); 45 | // verify only three fields are fetched. 46 | test.equal(_.keys(fields).length, 3, 'cursor has more/less fields than specified (plus _id and likes)'); 47 | }); 48 | 49 | { // WARNING TESTS 50 | 51 | // upon publish with other fields assigned to field limit 52 | // with warnings 53 | // in development 54 | // warn user 55 | Tinytest.add("fieldLimit: (countFromField) - upon publish with other fields assigned to field limit, " + 56 | "with warnings, in development, warn user about unused fields", function (test) { 57 | var pub = new H.PubMock(); 58 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); 59 | var conmock = { warn: H.detectRegex(/unused fields detected in cursor fields option/) }; 60 | 61 | H.withConsole(conmock, function () { 62 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 63 | }); 64 | 65 | // verify the warning was sent to user 66 | test.isTrue(conmock.warn.found(), 'expected warning'); 67 | }); 68 | 69 | // upon publish 70 | // with other fields assigned to field limit 71 | // warning uses Counts._warn 72 | Tinytest.add("fieldLimit: (countFromField) - upon publish with other fields assigned to field limit, " + 73 | "warn with Counts._warn()", function (test) { 74 | var pub = new H.PubMock(); 75 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); 76 | var flag = false; 77 | 78 | H.withWarn(function () { flag = true; }, function () { 79 | Counts.publish(pub, 'posts' + test.id, cursor, { countFromField: 'likes' }); 80 | }); 81 | 82 | test.isTrue(flag, 'did not call Counts._warn()'); 83 | }); 84 | 85 | } // END WARNING TESTS 86 | } 87 | -------------------------------------------------------------------------------- /tests/field_limit_count_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("fieldLimit: (count) - upon publish without field limit, automatically limit cursor fields to _id", function (test) { 3 | var pub = new H.PubMock(); 4 | var cursor = Posts.find({ testId: test.id }); // no field limit 5 | 6 | Counts.publish(pub, 'posts' + test.id, cursor); 7 | 8 | var fields = cursor._cursorDescription.options.fields; 9 | 10 | test.isNotUndefined(fields, 'cursor is missing fields property'); 11 | test.isNotUndefined(fields._id, 'cursor is missing _id field'); 12 | // verify only one field is fetched. 13 | test.equal(_.keys(fields).length, 1, 'cursor has more than one field') 14 | }); 15 | 16 | Tinytest.add("fieldLimit: (count) - upon publish with field limit, always limit cursor fields to _id", function (test) { 17 | var pub = new H.PubMock(); 18 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); // field manually limited to name 19 | 20 | Counts.publish(pub, 'posts' + test.id, cursor); 21 | 22 | var fields = cursor._cursorDescription.options.fields; 23 | 24 | test.isNotUndefined(fields, 'cursor is missing fields property'); 25 | test.isNotUndefined(fields._id, 'cursor is missing field (_id)'); 26 | // verify only two fields are fetched. 27 | test.equal(_.keys(fields).length, 1, 'cursor has more than one field'); 28 | }); 29 | 30 | { // WARNING TESTS 31 | 32 | // upon publish 33 | // with field limit 34 | // with warnings 35 | // in development 36 | // warn user 37 | Tinytest.add("fieldLimit: (count) - upon publish with field limit, with warnings, in development, " + 38 | "warn user unused fields were removed", function (test) { 39 | var pub = new H.PubMock(); 40 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); // field manually limited to name 41 | var conmock = { warn: H.detectRegex(/unused fields removed from cursor fields option/) }; 42 | 43 | H.withConsole(conmock, function () { 44 | Counts.publish(pub, 'posts' + test.id, cursor); 45 | }); 46 | 47 | // verify the warning was sent to user 48 | test.isTrue(conmock.warn.found(), 'expected warning'); 49 | }); 50 | 51 | // upon publish 52 | // with field limit 53 | // warning uses Counts._warn 54 | Tinytest.add("fieldLimit: (count) - upon publish with field limit, warn with Counts._warn()", function (test) { 55 | var pub = new H.PubMock(); 56 | var cursor = Posts.find({ testId: test.id }, { fields: { name: true }}); // field manually limited to name 57 | var flag = false; 58 | 59 | H.withWarn(function () { flag = true; }, function () { 60 | Counts.publish(pub, 'posts' + test.id, cursor); 61 | }); 62 | 63 | test.isTrue(flag, 'did not call Counts._warn()'); 64 | }); 65 | 66 | } // END WARNING TESTS 67 | } 68 | -------------------------------------------------------------------------------- /tests/has_count_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Meteor.publish('Counts.has', async function (testId) { 3 | await Counts.publish(this, 'posts' + testId, Posts.find({ testId: testId })); 4 | }); 5 | } 6 | 7 | if (Meteor.isClient) { 8 | var hasCount = function hasCount (testId) { 9 | return Counts.has('posts' + testId); 10 | } 11 | 12 | Tinytest.add('Counts.has: - when count is not published, return false', function (test, done) { 13 | test.isFalse(Counts.has('posts'), 'found unexpected count "posts"'); 14 | }); 15 | 16 | Tinytest.add('Counts.has: - when count is published but not subscribed, return false', function (test, done) { 17 | test.isFalse(hasCount(test.id), 'found unexpected count "posts' + test.id + '"'); 18 | }); 19 | 20 | Tinytest.addAsync('Counts.has: - when count is published and subscribed, return true', function (test, done) { 21 | Meteor.subscribe('Counts.has', test.id, function () { 22 | test.isTrue(hasCount(test.id), 'missing expected count "posts' + test.id + '"'); 23 | done(); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | this.Posts = new Mongo.Collection('posts'); 3 | 4 | // {this} is provided by IIFE that wraps this file 5 | // H is for helper 6 | this.H = {}; 7 | 8 | // added for use with console mock. used to detect when a method call's first 9 | // argument matches the regex. use found() for later assertion test. 10 | this.H.detectRegex = function (regex) { 11 | var found = false; 12 | 13 | fn = function (actual) { 14 | if (found) return actual; // once regex passes, never reset the found flag. 15 | found = regex.test(actual); 16 | return actual 17 | } 18 | 19 | fn.found = function () { return found; } // accessor for later assertion test. 20 | 21 | return fn; 22 | } 23 | 24 | // helper function to construct a unique, but consistent, id for each test 25 | // document. 26 | this.H.docId = function docId (testId, docNum) { 27 | return '' + testId + '-' + docNum; 28 | } 29 | 30 | // helper function to wrap console mock initialization and restoration around a 31 | // code block. to test warnings to user/devs. 32 | this.H.withConsole = (function withConsole (conmock, fn) { 33 | var mock = function (from, to, backup) { 34 | backup = backup || {}; 35 | 36 | _.each(from, function (v, k) { 37 | // only copy shallow properties 38 | if (from.hasOwnProperty(k)) { 39 | backup[k] = to[k]; 40 | to[k] = from[k]; 41 | } 42 | }); 43 | }; 44 | 45 | var backup = {}; 46 | mock(conmock, this.console, backup); // mock methods of console. 47 | fn(); 48 | mock(backup, this.console) // restore methods of console. 49 | }).bind(this); 50 | 51 | 52 | if (Meteor.isServer) { 53 | Posts.allow({ 54 | insert: function() { 55 | return true; 56 | }, 57 | remove: function() { 58 | return true; 59 | }, 60 | insertAsync: function() { 61 | return true; 62 | }, 63 | removeAsync: function() { 64 | return true; 65 | } 66 | }); 67 | 68 | var PubMock = function() { this._ready = false; }; 69 | PubMock.prototype.added = function(name, id) {}; 70 | PubMock.prototype.removed = function(name, id) {}; 71 | PubMock.prototype.changed = function(name, id) {}; 72 | PubMock.prototype.onStop = function(cb) { this._onStop = cb; }; 73 | PubMock.prototype.stop = function() { if (this._onStop) this._onStop(); }; 74 | PubMock.prototype.ready = function() { this._ready = true; }; 75 | this.H.PubMock = PubMock; 76 | 77 | this.H.insertFactory = function insertFactory (collection) { 78 | // return a DRY function that inserts docs into one collection. 79 | return function insert (testId, docNum, doc) { 80 | var testDoc = _.extend({}, doc, { 81 | _id: H.docId(testId, docNum), 82 | testId: testId, 83 | }); 84 | return collection.insertAsync(testDoc); 85 | }; 86 | } 87 | 88 | this.H.removeFactory = function removeFactory (collection) { 89 | // return a DRY function that removes docs from one collection. 90 | return function remove (testId, docNum) { 91 | return collection.removeAsync({_id: H.docId(testId, docNum)}); 92 | }; 93 | } 94 | 95 | this.H.updateFactory = function updateFactory (collection) { 96 | // return a DRY function that updates docs in one collection. 97 | return function update (testId, docNum, modifier) { 98 | // ensure testId is not modified. it's used to segregrate documents to 99 | // keep tests isolated. 100 | if (hasModifiers(modifier)) { 101 | modifier.$set = modifier.$set || {}; 102 | modifier.$set.testId = testId; 103 | } else { 104 | modifier.testId = testId; 105 | } 106 | 107 | return collection.updateAsync(H.docId(testId, docNum), modifier); 108 | }; 109 | } 110 | 111 | this.H.insert = this.H.insertFactory(Posts); 112 | this.H.remove = this.H.removeFactory(Posts); 113 | this.H.update = this.H.updateFactory(Posts); 114 | 115 | // helper function to modify node environment variables then restore them after testing. 116 | this.H.withNodeEnv = (function withNodeEnv (env, fn) { 117 | var backup = process.env; 118 | process.env = _.extend({}, backup, env); 119 | fn(); 120 | process.env = backup; 121 | }).bind(this); 122 | 123 | // helper function to disable then restore the global state for Counts.noWarnings(). 124 | this.H.withNoWarnings = (function withNoWarnings (fn) { 125 | Counts.noWarnings(); 126 | fn(); 127 | Counts.noWarnings(false); 128 | }).bind(this); 129 | 130 | // helper function to modify Counts._warn() then restore it after testing. 131 | this.H.withWarn = (function withWarn (warn, fn) { 132 | var backup = Counts._warn; 133 | Counts._warn = warn; 134 | fn(); 135 | Counts._warn = backup; 136 | }).bind(this); 137 | 138 | function hasModifiers (mongoModifier) { 139 | return _.keys(mongoModifier).some(function (key) { 140 | return /^\$/.test(key); 141 | }); 142 | } 143 | } 144 | 145 | if (Meteor.isClient) { 146 | this.H.getCountFactory = function getCountFactory (name) { 147 | // return a DRY function that uses a consistent naming scheme. 148 | // {name} allows each test suite to define a unique name for their 149 | // respective counter. 150 | return function getCount (testId) { 151 | return Counts.get('' + name + testId); 152 | }; 153 | } 154 | 155 | this.H.getCount = this.H.getCountFactory('posts'); 156 | } 157 | -------------------------------------------------------------------------------- /tests/no_ready_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("noReady: - when option is true, stop Counts.publish() from calling ready()", async function(test) { 3 | var pub = new H.PubMock(); 4 | await Counts.publish(pub, 'posts' + test.id, Posts.find({ testId: test.id })); 5 | test.isTrue(pub._ready); 6 | 7 | pub = new H.PubMock(); 8 | await Counts.publish(pub, 'posts' + test.id, Posts.find({ testId: test.id }), {noReady: true}); 9 | test.isFalse(pub._ready); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tests/no_warn_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | Tinytest.add("Counts._warn: - print warnings if noWarnings option is falsey", function (test) { 3 | var conmock = { warn: H.detectRegex(/test/) }; 4 | 5 | [false, null, undefined, '', 0].forEach(function (falsey) { 6 | H.withConsole(conmock, function () { 7 | Counts._warn(falsey, 'test'); 8 | }); 9 | 10 | // verify the warning was sent to user 11 | test.isTrue(conmock.warn.found(), 'warning did not print when noWarnings option is "' + String(falsey) + '"'); 12 | }); 13 | }); 14 | 15 | Tinytest.add("Counts._warn: - suppress warnings if noWarnings option is truthy", function (test) { 16 | var conmock = { warn: H.detectRegex(/test/) }; 17 | 18 | [true, 1, '2', {}].forEach(function (truthy) { 19 | H.withConsole(conmock, function () { 20 | Counts._warn(truthy, 'test'); 21 | }); 22 | 23 | // verify the warning was suppressed 24 | test.isFalse(conmock.warn.found(), 'warning not suppressed when noWarnings option is "' + String(truthy) + '"'); 25 | }); 26 | }); 27 | 28 | Tinytest.add("Counts._warn: - suppress warnings after Counts.noWarnings() is invoked", function (test) { 29 | var conmock = { warn: H.detectRegex(/test/) }; 30 | 31 | H.withConsole(conmock, function () { 32 | H.withNoWarnings(function () { 33 | Counts._warn(false, 'test'); 34 | }); 35 | }); 36 | 37 | // verify the warning was suppressed 38 | test.isFalse(conmock.warn.found(), 'Counts.noWarnings() did not suppress warning'); 39 | }); 40 | 41 | Tinytest.add("Counts._warn: - suppress warnings after Counts.noWarnings(true) is invoked", function (test) { 42 | var conmock = { warn: H.detectRegex(/test/) }; 43 | 44 | H.withConsole(conmock, function () { 45 | Counts.noWarnings(true); 46 | Counts._warn(false, 'test'); 47 | }); 48 | 49 | // verify the warning was suppressed 50 | test.isFalse(conmock.warn.found(), 'Counts.noWarnings(true) did not suppress warning'); 51 | }); 52 | 53 | // NOTE: Counts.noWarnings(false) cannot override { noWarnings: true }. 54 | Tinytest.add("Counts._warn: - print warnings after Counts.noWarnings(false) is invoked", function (test) { 55 | var conmock = { warn: H.detectRegex(/test/) }; 56 | 57 | H.withConsole(conmock, function () { 58 | H.withNoWarnings(function () { 59 | Counts.noWarnings(false); 60 | Counts._warn(false, 'test'); 61 | }); 62 | }); 63 | 64 | // verify the warning was sent to user 65 | test.isTrue(conmock.warn.found(), 'warning did not print after Count.noWarnings(false)'); 66 | }); 67 | 68 | Tinytest.add("Counts._warn: - suppress warnings in production environment", function (test) { 69 | var conmock = { warn: H.detectRegex(/test/) }; 70 | 71 | H.withConsole(conmock, function () { 72 | H.withNodeEnv({ NODE_ENV: 'production' }, function () { 73 | Counts._warn(false, 'test'); 74 | }); 75 | }); 76 | 77 | // verify the warning was suppressed 78 | test.isFalse(conmock.warn.found(), 'production environment did not suppress warning'); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /tests/observe_handles_test.js: -------------------------------------------------------------------------------- 1 | if (Meteor.isServer) { 2 | const factsByPackage = Package['facts-base']?.Facts?._factsByPackage; 3 | if ('undefined' !== typeof factsByPackage) { 4 | Tinytest.add("Confirm observe handles start and stop", async function(test) { 5 | var pub = new H.PubMock(); 6 | await Counts.publish(pub, 'posts' + test.id, Posts.find({ testId: test.id })); 7 | test.equal(factsByPackage['mongo-livedata']['observe-handles'], 1); 8 | pub.stop(); 9 | test.equal(factsByPackage['mongo-livedata']['observe-handles'], 0); 10 | }); 11 | } 12 | } 13 | --------------------------------------------------------------------------------