Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5e341b7
Add the ability to trigger and to use middleware
Jan 27, 2021
5632c4c
Add some comments for talking points
Jan 27, 2021
914b2c6
Some cleanup and documentation updates
Feb 1, 2021
1a7760f
Fix naming of opLink to opId
Feb 3, 2021
14f7b0c
Add test for middleware
Feb 3, 2021
1f87aee
Add tests
Feb 3, 2021
15eab51
Fix linting issues
Feb 3, 2021
b1e6040
Add support for middleware on snapshot retrieval
Feb 3, 2021
e7b30a8
Remove only on test
Feb 3, 2021
3b6d227
Reduce information that is not currently needed on the request
Feb 3, 2021
38ca0ef
Rename queryFilter to query
Feb 3, 2021
03b3436
Add tests for definition of request object
Feb 3, 2021
96ed2aa
Small wording fix in comments
Feb 3, 2021
ca71737
Fix confusion on baz vs fuzz in commit tests
Feb 3, 2021
2f68ab9
Update documentation
Feb 3, 2021
d4a552e
Minor docs updates
Feb 3, 2021
12418b0
Update README.md
pypmannetjies Feb 5, 2021
3ed9f54
Update middleware handler to use OOP approach instead of mixin
Feb 5, 2021
5ec4873
Update to move middleware to subpath in src
Feb 5, 2021
6291841
Don't save db on the test context
Feb 5, 2021
3a53f56
Add error handling for the `use` function
Feb 5, 2021
825d1fd
Minor refactor on variable names and linting issues
Feb 5, 2021
bdb11cc
Rename doc -> documentToWrite
Feb 5, 2021
3d69a06
Update index.js
pypmannetjies Feb 5, 2021
d81dfda
Rename beforeEdit -> beforeOverwrite
Feb 10, 2021
1619765
Merge branch 'add-middleware' of github.com:share/sharedb-mongo into …
Feb 10, 2021
546fead
Apply suggestions from code review
pypmannetjies Feb 10, 2021
d82a7dc
Update README.md
pypmannetjies Feb 10, 2021
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
104 changes: 65 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# sharedb-mongo

[![NPM Version](https://img.shields.io/npm/v/sharedb-mongo.svg)](https://npmjs.org/package/sharedb-mongo)
[![Build Status](https://travis-ci.org/share/sharedb-mongo.svg?branch=master)](https://travis-ci.org/share/sharedb-mongo)
[![Coverage Status](https://coveralls.io/repos/github/share/sharedb-mongo/badge.svg?branch=master)](https://coveralls.io/github/share/sharedb-mongo?branch=master)
[![NPM Version](https://img.shields.io/npm/v/sharedb-mongo.svg)](https://npmjs.org/package/sharedb-mongo)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

My linter kicked in on this README

[![Build Status](https://travis-ci.org/share/sharedb-mongo.svg?branch=master)](https://travis-ci.org/share/sharedb-mongo)
[![Coverage Status](https://coveralls.io/repos/github/share/sharedb-mongo/badge.svg?branch=master)](https://coveralls.io/github/share/sharedb-mongo?branch=master)

MongoDB database adapter for [sharedb](https://github.com/share/sharedb). This
driver can be used both as a snapshot store and oplog.

Snapshots are stored where you'd expect (the named collection with _id=id). In
Snapshots are stored where you'd expect (the named collection with \_id=id). In
addition, operations are stored in `o_COLLECTION`. For example, if you have
a `users` collection, the operations are stored in `o_users`.

Expand All @@ -17,38 +17,36 @@ the form of `_v` and `_type`). It is safe to query documents directly with the
MongoDB driver or command line. Any read only mongo features, including find,
aggregate, and map reduce are safe to perform concurrent with ShareDB.

However, you must *always* use ShareDB to edit documents. Never use the
However, you must _always_ use ShareDB to edit documents. Never use the
MongoDB driver or command line to directly modify any documents that ShareDB
might create or edit. ShareDB must be used to properly persist operations
together with snapshots.


## Usage

`sharedb-mongo` uses the [MongoDB NodeJS Driver](https://github.com/mongodb/node-mongodb-native), and it supports the same configuration options.

There are two ways to instantiate a sharedb-mongo wrapper:

1. The simplest way is to invoke the module and pass in your mongo DB
arguments as arguments to the module function. For example:

```javascript
const db = require('sharedb-mongo')('mongodb://localhost:27017/test', {mongoOptions: {...}});
const backend = new ShareDB({db});
```
1. The simplest way is to invoke the module and pass in your mongo DB
arguments as arguments to the module function. For example:

2. If you'd like to reuse a mongo db connection or handle mongo driver
instantiation yourself, you can pass in a function that calls back with
a mongo instance.
```javascript
const db = require('sharedb-mongo')('mongodb://localhost:27017/test', {mongoOptions: {...}});
const backend = new ShareDB({db});
```

```javascript
const mongodb = require('mongodb');
const db = require('sharedb-mongo')({mongo: function(callback) {
mongodb.connect('mongodb://localhost:27017/test', callback);
}});
const backend = new ShareDB({db});
```
2. If you'd like to reuse a mongo db connection or handle mongo driver
instantiation yourself, you can pass in a function that calls back with
a mongo instance.

```javascript
const mongodb = require('mongodb');
const db = require('sharedb-mongo')({mongo: function(callback) {
mongodb.connect('mongodb://localhost:27017/test', callback);
}});
const backend = new ShareDB({db});
```

## Queries

Expand Down Expand Up @@ -172,7 +170,7 @@ failed ops:
- v4: collision 4
- v5: unique
- v6: unique
...
...
- v1000: unique

If I want to fetch ops v1-v3, then we:
Expand All @@ -190,6 +188,34 @@ In the case where a valid op cannot be determined, we still
fall back to fetching all ops and working backwards from the
current version.

### Middlewares
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only real change to the docs


Middlewares let you hook into the `sharedb-mongo` pipeline for certain actions. They are distinct from [middleware in `ShareDB`](https://github.com/share/sharedb) as they are closer to the concrete calls that are made to `MongoDB` itself.

The original intent for middleware on `sharedb-mongo` is to support running in a sharded `MongoDB` cluster to satisfy the requirements on shard keys for versions 4.2 and greater of `MongoDB`. For more information see [the MongoDB docs](https://docs.mongodb.com/manual/core/sharding-shard-key/#shard-keys).

#### Usage

`share.use(action, fn)`
Register a new middleware.

- `action` _(String)_
One of:
- `'beforeOverwrite'`: directly before the call to replace a document, can include edits as well as deletions
- `'beforeSnapshotLookup'`: directly before the call to issue a query for one or more snapshots by ID
- `fn` _(Function(context, callback))_
Call this function at the time specified by `action`
- `context` will always have the following properties:
- `action`: The action this middleware is handling
- `collectionName`: The collection name being handled
- `options`: Original options as they were passed into the relevant function that triggered the action
- `'beforeOverwrite'` actions have additional context properties:
- `documentToWrite` - The document to be written
- `op` - The op that represents the changes that will be made to the document
- `query` - A filter that will be used to lookup the document that is about to be edited, which should always include an ID and snapshot version e.g. `{_id: 'uuid', _v: 1}`
- `'beforeSnapshotLookup'` actions have additional context properties:
- `query` - A filter that will be used to lookup the snapshot. When a single snapshot is looked up the query will take the shape `{_id: docId}` while a bulk lookup by a list of IDs will resemble `{_id: {$in: docIdsArray}}`.

### Limitations

#### Integrity
Expand Down Expand Up @@ -226,26 +252,26 @@ Mongo errors are passed back directly. Additional error codes:

#### 4100 -- Bad request - DB

* 4101 -- Invalid op version
* 4102 -- Invalid collection name
* 4103 -- $where queries disabled
* 4104 -- $mapReduce queries disabled
* 4105 -- $aggregate queries disabled
* 4106 -- $query property deprecated in queries
* 4107 -- Malformed query operator
* 4108 -- Only one collection operation allowed
* 4109 -- Only one cursor operation allowed
* 4110 -- Cursor methods can't run after collection method
- 4101 -- Invalid op version
- 4102 -- Invalid collection name
- 4103 -- $where queries disabled
- 4104 -- $mapReduce queries disabled
- 4105 -- $aggregate queries disabled
- 4106 -- $query property deprecated in queries
- 4107 -- Malformed query operator
- 4108 -- Only one collection operation allowed
- 4109 -- Only one cursor operation allowed
- 4110 -- Cursor methods can't run after collection method

#### 5100 -- Internal error - DB

* 5101 -- Already closed
* 5102 -- Snapshot missing last operation field
* 5103 -- Missing ops from requested version
* 5104 -- Failed to parse query

- 5101 -- Already closed
- 5102 -- Snapshot missing last operation field
- 5103 -- Missing ops from requested version
- 5104 -- Failed to parse query

## MIT License

Copyright (c) 2015 by Joseph Gentle and Nate Smith

Permission is hereby granted, free of charge, to any person obtaining a copy
Expand Down
99 changes: 71 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var async = require('async');
var mongodb = require('mongodb');
var DB = require('sharedb').DB;
var OpLinkValidator = require('./op-link-validator');
var MiddlewareHandler = require('./src/middleware/middlewareHandler');

module.exports = ShareDbMongo;

Expand Down Expand Up @@ -66,6 +67,8 @@ function ShareDbMongo(mongo, options) {
} else {
throw new Error('deprecated: pass mongo as url string or function with callback');
}

this._middleware = new MiddlewareHandler();
};

ShareDbMongo.prototype = Object.create(DB.prototype);
Expand Down Expand Up @@ -215,20 +218,31 @@ ShareDbMongo.prototype.close = function(callback) {

ShareDbMongo.prototype.commit = function(collectionName, id, op, snapshot, options, callback) {
var self = this;
var request = createRequestForMiddleware(options, collectionName, op);
this._writeOp(collectionName, id, op, snapshot, function(err, result) {
if (err) return callback(err);
var opId = result.insertedId;
self._writeSnapshot(collectionName, id, snapshot, opId, function(err, succeeded) {
self._writeSnapshot(request, id, snapshot, opId, function(err, succeeded) {
if (succeeded) return callback(err, succeeded);
// Cleanup unsuccessful op if snapshot write failed. This is not
// neccessary for data correctness, but it gets rid of clutter
self._deleteOp(collectionName, opId, function(removeErr) {
self._deleteOp(request.collectionName, opId, function(removeErr) {
callback(err || removeErr, succeeded);
});
});
});
};

function createRequestForMiddleware(options, collectionName, op) {
// Create a new request object which will be passed to helper functions and middleware
var request = {
options: options,
collectionName: collectionName
};
if (op) request.op = op;
return request;
}

ShareDbMongo.prototype._writeOp = function(collectionName, id, op, snapshot, callback) {
if (typeof op.v !== 'number') {
var err = ShareDbMongo.invalidOpVersionError(collectionName, id, op.v);
Expand All @@ -250,12 +264,13 @@ ShareDbMongo.prototype._deleteOp = function(collectionName, opId, callback) {
});
};

ShareDbMongo.prototype._writeSnapshot = function(collectionName, id, snapshot, opLink, callback) {
this.getCollection(collectionName, function(err, collection) {
ShareDbMongo.prototype._writeSnapshot = function(request, id, snapshot, opId, callback) {
var self = this;
this.getCollection(request.collectionName, function(err, collection) {
if (err) return callback(err);
var doc = castToDoc(id, snapshot, opLink);
if (doc._v === 1) {
collection.insertOne(doc, function(err) {
request.documentToWrite = castToDoc(id, snapshot, opId);
if (request.documentToWrite._v === 1) {
collection.insertOne(request.documentToWrite, function(err) {
if (err) {
// Return non-success instead of duplicate key error, since this is
// expected to occur during simultaneous creates on the same id
Expand All @@ -267,10 +282,16 @@ ShareDbMongo.prototype._writeSnapshot = function(collectionName, id, snapshot, o
callback(null, true);
});
} else {
collection.replaceOne({_id: id, _v: doc._v - 1}, doc, function(err, result) {
if (err) return callback(err);
var succeeded = !!result.modifiedCount;
callback(null, succeeded);
request.query = {_id: id, _v: request.documentToWrite._v - 1};
self._middleware.trigger(MiddlewareHandler.Actions.beforeOverwrite, request, function(middlewareErr) {
if (middlewareErr) {
return callback(middlewareErr);
}
collection.replaceOne(request.query, request.documentToWrite, function(err, result) {
if (err) return callback(err);
var succeeded = !!result.modifiedCount;
callback(null, succeeded);
});
});
}
});
Expand All @@ -280,36 +301,50 @@ ShareDbMongo.prototype._writeSnapshot = function(collectionName, id, snapshot, o
// **** Snapshot methods

ShareDbMongo.prototype.getSnapshot = function(collectionName, id, fields, options, callback) {
var self = this;
this.getCollection(collectionName, function(err, collection) {
if (err) return callback(err);
var query = {_id: id};
var projection = getProjection(fields, options);
collection.find(query).limit(1).project(projection).next(function(err, doc) {
if (err) return callback(err);
var snapshot = (doc) ? castToSnapshot(doc) : new MongoSnapshot(id, 0, null, undefined);
callback(null, snapshot);
var request = createRequestForMiddleware(options, collectionName);
request.query = query;
self._middleware.trigger(MiddlewareHandler.Actions.beforeSnapshotLookup, request, function(middlewareErr) {
if (middlewareErr) return callback(middlewareErr);

collection.find(request.query).limit(1).project(projection).next(function(err, doc) {
if (err) return callback(err);
var snapshot = (doc) ? castToSnapshot(doc) : new MongoSnapshot(id, 0, null, undefined);
callback(null, snapshot);
});
});
});
};

ShareDbMongo.prototype.getSnapshotBulk = function(collectionName, ids, fields, options, callback) {
var self = this;
this.getCollection(collectionName, function(err, collection) {
if (err) return callback(err);
var query = {_id: {$in: ids}};
var projection = getProjection(fields, options);
collection.find(query).project(projection).toArray(function(err, docs) {
if (err) return callback(err);
var snapshotMap = {};
for (var i = 0; i < docs.length; i++) {
var snapshot = castToSnapshot(docs[i]);
snapshotMap[snapshot.id] = snapshot;
}
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
if (snapshotMap[id]) continue;
snapshotMap[id] = new MongoSnapshot(id, 0, null, undefined);
}
callback(null, snapshotMap);
var request = createRequestForMiddleware(options, collectionName);
request.query = query;
self._middleware.trigger(MiddlewareHandler.Actions.beforeSnapshotLookup, request, function(middlewareErr) {
if (middlewareErr) return callback(middlewareErr);

collection.find(request.query).project(projection).toArray(function(err, docs) {
if (err) return callback(err);
var snapshotMap = {};
for (var i = 0; i < docs.length; i++) {
var snapshot = castToSnapshot(docs[i]);
snapshotMap[snapshot.id] = snapshot;
}
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
if (snapshotMap[id]) continue;
snapshotMap[id] = new MongoSnapshot(id, 0, null, undefined);
}
callback(null, snapshotMap);
});
});
});
};
Expand Down Expand Up @@ -1576,3 +1611,11 @@ ShareDbMongo.parseQueryError = function(err) {
err.code = 5104;
return err;
};

// Middleware

ShareDbMongo.prototype.use = function(action, fn) {
this._middleware.use(action, fn);
};

ShareDbMongo.MiddlewareActions = MiddlewareHandler.Actions;
7 changes: 7 additions & 0 deletions src/middleware/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
// Triggers before the call to replace a document is made
beforeOverwrite: 'beforeOverwrite',
// Triggers directly before the call to issue a query for snapshots
// Applies for both a single lookup by ID and bulk lookups by a list of IDs
beforeSnapshotLookup: 'beforeSnapshotLookup'
};
Loading