Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"import/no-unresolved": "off",
"no-console": "off",
"global-require": "off",
"no-underscore-dangle": "off"
"no-underscore-dangle": "off",
"no-use-before-define": ["error", { "functions": false }]
}
}
75 changes: 70 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
@codesignal/meteor-protomongo<br>[![](http://img.shields.io/npm/dm/@codesignal/meteor-protomongo.svg?style=flat)](https://www.npmjs.com/package/@codesignal/meteor-protomongo) [![npm version](https://badge.fury.io/js/%40codesignal%2Fmeteor-protomongo.svg)](https://www.npmjs.com/package/@codesignal/meteor-protomongo)
=

**Adds asynchronous methods to `meteor/mongo`.**
**Monkey-patches to `meteor/mongo`.**

*When you don't want to deal with fibers.*
*Helpful for working with indexes and transitioning away from Fibers.*

**Update, April 2022:**

This proposed update to the core `meteor/mongo` package will add `*Async` versions of some of these methods by default, although the PR is currently pending and needs a new owner:
https://github.com/meteor/meteor/pull/11605

Until then, the `*Async` methods in this repo should have the same API as the proposed methods in `meteor/mongo`, so they might be helpful in getting a head start on the transition away from Fibers even before those change are released, or if you are on an earlier version of Meteor.

After those changes are finalized and released, we plan to release a new version of this package that removes our own monkey-patched `*Async` methods in favor of the official ones. However, other helpers like `getIndexes`, `ensureIndex`, and `ensureNoIndex` will still be provided.

## Description

This package extends `meteor/mongo` prototype with a few handy asynchronous method, and one more method you just couldn't live without. It means that it will only do something useful if your project is build with [Meteor](https://www.meteor.com/). Sorry about that!
This package extends the `Collection` and `Cursor` prototypes from `meteor/mongo` with a few handy asynchronous methods, as well as some index-related helpers we just couldn't live without. It's intended for use only with projects built with [Meteor](https://www.meteor.com/).

### API

#### Collection Prototype

```js
Collection.findOneAsync(selector, ?options);
```

Returns a Promise that is resolved with the found document, or null.


```js
Collection.updateAsync(selector, modifier, ?options);
```
Expand Down Expand Up @@ -66,7 +84,33 @@ The reverse of `ensureIndex`. You might want to call this in `Meteor.startup` to
Collection.aggregate(pipeline, ?options);
```

It's a wrapper for the mongodb [aggregate method](https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/). This removes the need to use rawCollection() everytime.
Exposes the [aggregate method](https://www.mongodb.com/docs/manual/reference/method/db.collection.aggregate/). This removes the need to use `rawCollection()` every time you want to aggregate.

#### Cursor Prototype

```js
Cursor.forEachAsync(callback, ?thisArg);
```

Returns a Promise that is resolved after the callback has been applied to each document.

```js
Cursor.mapAsync(callback, ?thisArg);
```

Returns a Promise that is resolved with an array of the transformed documents.

```js
Cursor.fetchAsync();
```

Returns a Promise that is resolved with an array of the found documents.

```js
Cursor.countAsync();
```

Returns a Promise that is resolved with the number of matching documents.

## Install

Expand All @@ -76,10 +120,31 @@ npm install @codesignal/meteor-protomongo

After the package is installed, add the following few lines in a file that's going to be loaded on startup:
Copy link

@mpowaga mpowaga Apr 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Can you add a line saying that accounts-base package is required?

Probably most meteor projects already have accounts installed and/or this requirement can be communicated dynamically with an error message but it would be great to know of this prerequisite in the readme (and the reason behind it since it might be a little bit strange to require a package that seems to be unrelated if someone doesn't know the details 😄 ).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would make sense to just ask the consumer to directly pass in the result of a find() instead, so that we can inject & remove that dependency?

The downside is that it exposes this "hack" to the consumer a little bit more. The upside is that it removes this dependency on accounts-base, which you're right is a little weird (I was trying to pick a collection I expect to exist in every Meteor instance, but I guess you're right that it's still a dep)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: Okay, I went ahead and made this change to ask the consumer to pass in a cursor, rather than assuming that Meteor.users is a valid collection. Thanks for pointing out the implicit dependency!

```js
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import ProtoMongo from '@codesignal/meteor-protomongo';

ProtoMongo.extend(Mongo);
ProtoMongo.extendCollection(Mongo);
ProtoMongo.extendCursor(Meteor.users.find()); // any cursor works here
```

To extend the Cursor prototype, we ask the consuming application to pass in any cursor instance (it does not matter the collection or the query, as long as it's from a Meteor Mongo collection driver), which can be found by calling `db.someCollection.find()`. A safe choice might be `Meteor.users.find()` (as shown in the example), since this is likely to exist in any app using `accounts-base`.

This is unfortunately necessary because `meteor/mongo` does not export the `Cursor` type used on the server. So, the only reliable way to find the `Cursor` prototype is to get it from a cursor instance.

## Building Locally

After checking out this repo, run...

```sh
npm install
npm run build
```

To do local checks:
```sh
npm run eslint
npm run flow
```

## Contributing
Expand Down
82 changes: 78 additions & 4 deletions src/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ const ERROR_CODES = Object.freeze({
});

// $FlowFixMe: meteor/mongo doesn't have nice Flow types
function extend(Mongo: Object) {
function extendCollection(Mongo: Object) {
if (!Mongo) {
throw new Error('Mongo should be truthy!');
throw new Error('Mongo object must exist');
}

if (!Mongo.Collection || typeof Mongo.Collection !== 'function') {
throw new Error('Mongo.Collection is not a prototypable constructor!');
throw new Error('Mongo.Collection must be a function/class');
}

Object.assign(Mongo.Collection.prototype, {
findOneAsync(selector, options) {
return new Promise((resolve, reject) => {
try {
// $FlowExpectedError[object-this-reference]
resolve(this.findOne(selector, options));
} catch (error) {
reject(error);
}
});
},

updateAsync(selector, modifier, options) {
return new Promise((resolve, reject) => {
// $FlowExpectedError[object-this-reference]
Expand Down Expand Up @@ -128,4 +139,67 @@ function extend(Mongo: Object) {
});
}

module.exports = { extend };
// $FlowFixMe: meteor/mongo doesn't have nice Flow types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should migrate to TypeScript at some point 😏

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right -- Flow seems especially useless for this package since we aren't even explicitly typing the inputs/outputs of these extended functions, and we are just wildly suppressing everything.

Buuuuuut I didn't want to bring that into scope here, so I left it alone and put more band-aids for now 😅

function extendCursor(anyCursor: Object) {
/*
On the server, meteor/mongo does not export its Cursor type directly.
So, the only reliable way to get the prototype is to actually find to create a cursor.
We ask the consumer to inject this so that we don't depend on any particular collection existing.
*/
const cursorPrototype = anyCursor && Object.getPrototypeOf(anyCursor);
if (!anyCursor || !cursorPrototype) {
throw new Error('Cursor and its prototype must exist');
}

Object.assign(cursorPrototype, {
forEachAsync(callback, thisArg) {
return new Promise((resolve, reject) => {
try {
// $FlowExpectedError[object-this-reference]
this.forEach(callback, thisArg);
resolve();
} catch (error) {
reject(error);
}
});
},

mapAsync(callback, thisArg) {
return new Promise((resolve, reject) => {
try {
// $FlowExpectedError[object-this-reference]
resolve(this.map(callback, thisArg));
} catch (error) {
reject(error);
}
});
},

fetchAsync() {
return new Promise((resolve, reject) => {
try {
// $FlowExpectedError[object-this-reference]
resolve(this.fetch());
} catch (error) {
reject(error);
}
});
},

countAsync() {
return new Promise((resolve, reject) => {
try {
// $FlowExpectedError[object-this-reference]
resolve(this.count());
} catch (error) {
reject(error);
}
});
}
});
}

module.exports = {
extendCollection,
extendCursor
};