sharedb-mongo 1.0.0-beta.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/mongodb.js ADDED
@@ -0,0 +1,3 @@
1
+ var version = process.env._SHAREDB_MONGODB_DRIVER || 'mongodb';
2
+ var mongodb = require(version);
3
+ module.exports = mongodb;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * This is a class for determining an op with a unique version number
3
+ * when presented with an **ordered** series of ops.
4
+ *
5
+ * For example, consider the following chain of op versions:
6
+ * 1 -> 1 -> 2 -> 2 -> 3 -> 4
7
+ * If we want to find the first unique version, we must consider a
8
+ * window of three versions. For example, if we consider the first
9
+ * three versions:
10
+ * 1 -> 1 -> 2
11
+ * Then we know that 1 is not unique. We don't know if 2 is unique
12
+ * yet, because we don't know what comes next. Therefore we push
13
+ * one more version and check again:
14
+ * 1 -> 2 -> 2
15
+ * Again we now see that 2 is not unique, so we keep pushing ops
16
+ * until we reach the final window:
17
+ * 2 -> 3 -> 4
18
+ * From here, **assuming the ops are well ordered** we can safely
19
+ * see that v3 is unique. We cannot make the same assumption of
20
+ * v4, because we don't know what comes next.
21
+ *
22
+ * Note that we also assume that the chain starts with **all**
23
+ * of the copies of an op version. That is that if we are provided
24
+ * 1 -> 2
25
+ * Then v1 is unique (because there are no other v1s).
26
+ *
27
+ * Similarly, if a null op is pushed into the class, it is assumed
28
+ * to be the end of the chain, and hence a unique version can be
29
+ * inferred, eg with this chain:
30
+ * 5 -> 6 -> null
31
+ * We say that 6 is unique, because we've reached the end of the
32
+ * list
33
+ */
34
+ function OpLinkValidator() {
35
+ this.currentOp = undefined;
36
+ this.previousOp = undefined;
37
+ this.oneBeforePreviousOp = undefined;
38
+ }
39
+
40
+ OpLinkValidator.prototype.push = function(op) {
41
+ this.oneBeforePreviousOp = this.previousOp;
42
+ this.previousOp = this.currentOp;
43
+ this.currentOp = op;
44
+ };
45
+
46
+ OpLinkValidator.prototype.opWithUniqueVersion = function() {
47
+ return this._previousVersionWasUnique() ? this.previousOp : null;
48
+ };
49
+
50
+ OpLinkValidator.prototype.isAtEndOfList = function() {
51
+ // We ascribe a special meaning to a current op of null
52
+ // being that we're at the end of the list, because this
53
+ // is the value that the Mongo cursor will return when
54
+ // the cursor is exhausted
55
+ return this.currentOp === null;
56
+ };
57
+
58
+ OpLinkValidator.prototype._previousVersionWasUnique = function() {
59
+ var previousVersion = this._previousVersion();
60
+
61
+ return typeof previousVersion === 'number'
62
+ && previousVersion !== this._currentVersion()
63
+ && previousVersion !== this._oneBeforePreviousVersion();
64
+ };
65
+
66
+ OpLinkValidator.prototype._currentVersion = function() {
67
+ return this.currentOp && this.currentOp.v;
68
+ };
69
+
70
+ OpLinkValidator.prototype._previousVersion = function() {
71
+ return this.previousOp && this.previousOp.v;
72
+ };
73
+
74
+ OpLinkValidator.prototype._oneBeforePreviousVersion = function() {
75
+ return this.oneBeforePreviousOp && this.oneBeforePreviousOp.v;
76
+ };
77
+
78
+ module.exports = OpLinkValidator;
package/package.json CHANGED
@@ -1,23 +1,37 @@
1
1
  {
2
2
  "name": "sharedb-mongo",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0",
4
4
  "description": "MongoDB database adapter for ShareDB",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
- "async": "^1.4.2",
8
- "mongodb": "^2.1.2",
9
- "sharedb": "^1.0.0-beta"
7
+ "async": "^2.6.3",
8
+ "mongodb": "^2.1.2 || ^3.0.0 || ^4.0.0",
9
+ "sharedb": "^1.9.1 || ^2.0.0"
10
10
  },
11
11
  "devDependencies": {
12
- "coveralls": "^2.11.8",
13
- "expect.js": "^0.3.1",
14
- "istanbul": "^0.4.2",
15
- "mocha": "^2.3.3",
16
- "sharedb-mingo-memory": "^1.0.2"
12
+ "chai": "^4.2.0",
13
+ "coveralls": "^3.0.7",
14
+ "eslint": "^5.16.0",
15
+ "eslint-config-google": "^0.13.0",
16
+ "mocha": "^6.2.2",
17
+ "mongodb2": "npm:mongodb@^2.1.2",
18
+ "mongodb3": "npm:mongodb@^3.0.0",
19
+ "mongodb4": "npm:mongodb@^4.0.0",
20
+ "nyc": "^14.1.1",
21
+ "ot-json1": "^1.0.1",
22
+ "sharedb-mingo-memory": "^1.1.1",
23
+ "sinon": "^6.1.5",
24
+ "sinon-chai": "^3.7.0"
17
25
  },
18
26
  "scripts": {
19
- "test": "node_modules/.bin/mocha",
20
- "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha"
27
+ "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'",
28
+ "lint:fix": "npm run lint -- --fix",
29
+ "test": "mocha",
30
+ "test:mongodb2": "_SHAREDB_MONGODB_DRIVER=mongodb2 npm test",
31
+ "test:mongodb3": "_SHAREDB_MONGODB_DRIVER=mongodb3 npm test",
32
+ "test:mongodb4": "_SHAREDB_MONGODB_DRIVER=mongodb4 npm test",
33
+ "test:all": "npm run test:mongodb2 && npm run test:mongodb3 && npm run test:mongodb4",
34
+ "test-cover": "nyc --temp-dir=coverage -r text -r lcov npm run test:all"
21
35
  },
22
36
  "repository": "git://github.com/share/sharedb-mongo.git",
23
37
  "author": "Nate Smith and Joseph Gentle",
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ // Triggers before the call to write a new document is made
3
+ beforeCreate: 'beforeCreate',
4
+ // Triggers before the call to replace a document is made
5
+ beforeOverwrite: 'beforeOverwrite',
6
+ // Triggers directly before the call to issue a query for snapshots
7
+ // Applies for both a single lookup by ID and bulk lookups by a list of IDs
8
+ beforeSnapshotLookup: 'beforeSnapshotLookup'
9
+ };
@@ -0,0 +1,66 @@
1
+ var MIDDLEWARE_ACTIONS = require('./actions');
2
+
3
+ function MiddlewareHandler() {
4
+ this._middleware = {};
5
+ }
6
+
7
+ /**
8
+ * Add middleware to an action or array of actions
9
+ *
10
+ * @param action The action to use from MIDDLEWARE_ACTIONS (e.g. 'beforeOverwrite')
11
+ * @param fn The function to call when this middleware is triggered
12
+ * The fn receives a request object with information on the triggered action (e.g. the snapshot to write)
13
+ * and a next function to call once the middleware is complete
14
+ *
15
+ * NOTE: It is recommended not to add async or long running tasks to the sharedb-mongo middleware as it will
16
+ * be called very frequently during sensitive operations. It may have a significant performance impact.
17
+ */
18
+ MiddlewareHandler.prototype.use = function(action, fn) {
19
+ if (Array.isArray(action)) {
20
+ for (var i = 0; i < action.length; i++) {
21
+ this.use(action[i], fn);
22
+ }
23
+ return this;
24
+ }
25
+ if (!action) throw new Error('Expected action to be defined');
26
+ if (!fn) throw new Error('Expected fn to be defined');
27
+ if (!Object.values(MIDDLEWARE_ACTIONS).includes(action)) {
28
+ throw new Error('Unrecognized action name ' + action);
29
+ }
30
+
31
+ var fns = this._middleware[action] || (this._middleware[action] = []);
32
+ fns.push(fn);
33
+ return this;
34
+ };
35
+
36
+ /**
37
+ * Passes request through the middleware stack
38
+ *
39
+ * Middleware may modify the request object. After all middleware have been
40
+ * invoked we call `callback` with `null` and the modified request. If one of
41
+ * the middleware resturns an error the callback is called with that error.
42
+ *
43
+ * @param action The action to trigger from MIDDLEWARE_ACTIONS (e.g. 'beforeOverwrite')
44
+ * @param request Request details such as the snapshot to write, depends on the triggered action
45
+ * @param callback Function to call once the middleware has been processed.
46
+ */
47
+ MiddlewareHandler.prototype.trigger = function(action, request, callback) {
48
+ request.action = action;
49
+
50
+ var fns = this._middleware[action];
51
+ if (!fns) return callback();
52
+
53
+ // Copying the triggers we'll fire so they don't get edited while we iterate.
54
+ fns = fns.slice();
55
+ var next = function(err) {
56
+ if (err) return callback(err);
57
+ var fn = fns.shift();
58
+ if (!fn) return callback();
59
+ fn(request, next);
60
+ };
61
+ next();
62
+ };
63
+
64
+ MiddlewareHandler.Actions = MIDDLEWARE_ACTIONS;
65
+
66
+ module.exports = MiddlewareHandler;
package/test/mocha.opts CHANGED
@@ -2,3 +2,4 @@
2
2
  --check-leaks
3
3
  --globals Promise
4
4
  --timeout 18000
5
+ --file test/setup.js
package/test/setup.js ADDED
@@ -0,0 +1,10 @@
1
+ var logger = require('sharedb/lib/logger');
2
+
3
+ if (process.env.LOGGING !== 'true') {
4
+ // Silence the logger for tests by setting all its methods to no-ops
5
+ logger.setMethods({
6
+ info: function() {},
7
+ warn: function() {},
8
+ error: function() {}
9
+ });
10
+ }
@@ -0,0 +1,135 @@
1
+ var expect = require('chai').expect;
2
+ var ShareDbMongo = require('..');
3
+ var sinon = require('sinon');
4
+
5
+ var mongoUrl = process.env.TEST_MONGO_URL || 'mongodb://localhost:27017/test';
6
+
7
+ function create(options, callback) {
8
+ var opts = Object.assign({
9
+ mongoOptions: {},
10
+ getOpsWithoutStrictLinking: true
11
+ }, options);
12
+ var db = new ShareDbMongo(mongoUrl, opts);
13
+ db.getDbs(function(err, mongo) {
14
+ if (err) return callback(err);
15
+ mongo.dropDatabase(function(err) {
16
+ if (err) return callback(err);
17
+ callback(null, db, mongo);
18
+ });
19
+ });
20
+ };
21
+
22
+ // loop thru strict linking options
23
+ [true, false].forEach(function(strictLinkingOption) {
24
+ describe('getOps with strict linking ' + strictLinkingOption, function() {
25
+ beforeEach(function(done) {
26
+ var self = this;
27
+ create(
28
+ {getOpsWithoutStrictLinking: strictLinkingOption},
29
+ function(err, db, mongo) {
30
+ if (err) return done(err);
31
+ self.db = db;
32
+ self.mongo = mongo;
33
+ done();
34
+ });
35
+ });
36
+
37
+ afterEach(function(done) {
38
+ this.db.close(done);
39
+ });
40
+
41
+ describe('a chain of ops', function() {
42
+ var db;
43
+ var mongo;
44
+ var id;
45
+ var collection;
46
+
47
+ beforeEach(function(done) {
48
+ db = this.db;
49
+ mongo = this.mongo;
50
+ id = 'document1';
51
+ collection = 'testcollection';
52
+
53
+ sinon.spy(db, '_getOps');
54
+ sinon.spy(db, '_getSnapshotOpLink');
55
+
56
+ var ops = [
57
+ {v: 0, create: {}},
58
+ {v: 1, p: ['foo'], oi: 'bar'},
59
+ {v: 2, p: ['foo'], oi: 'baz'},
60
+ {v: 3, p: ['foo'], oi: 'qux'}
61
+ ];
62
+
63
+ commitOpChain(db, mongo, collection, id, ops, function(error) {
64
+ if (error) done(error);
65
+ mongo.collection('o_' + collection).deleteOne({v: 1}, done);
66
+ });
67
+ });
68
+
69
+ it('fetches ops 2-3 without fetching all ops', function(done) {
70
+ db.getOps(collection, id, 2, 4, null, function(error, ops) {
71
+ if (error) return done(error);
72
+ expect(ops.length).to.equal(2);
73
+ expect(ops[0].v).to.equal(2);
74
+ expect(ops[1].v).to.equal(3);
75
+ done();
76
+ });
77
+ });
78
+
79
+ it('default option errors when missing ops', function(done) {
80
+ db.getOps(collection, id, 0, 4, null, function(error) {
81
+ expect(error.code).to.equal(5103);
82
+ expect(error.message).to.equal('Missing ops from requested version testcollection.document1 0');
83
+ done();
84
+ });
85
+ });
86
+
87
+ it('ignoreMissingOps option returns ops up to the first missing op', function(done) {
88
+ db.getOps(collection, id, 0, 4, {ignoreMissingOps: true}, function(error, ops) {
89
+ if (error) return done(error);
90
+ expect(ops.length).to.equal(2);
91
+ expect(ops[0].v).to.equal(2);
92
+ expect(ops[1].v).to.equal(3);
93
+ done();
94
+ });
95
+ });
96
+
97
+ it('getOpsToSnapshot ignoreMissingOps option returns ops up to the first missing op', function(done) {
98
+ db.getSnapshot(collection, id, {$submit: true}, null, function(error, snapshot) {
99
+ if (error) done(error);
100
+ db.getOpsToSnapshot(collection, id, 0, snapshot, {ignoreMissingOps: true}, function(error, ops) {
101
+ if (error) return done(error);
102
+ expect(ops.length).to.equal(2);
103
+ expect(ops[0].v).to.equal(2);
104
+ expect(ops[1].v).to.equal(3);
105
+ done();
106
+ });
107
+ });
108
+ });
109
+ });
110
+ });
111
+ });
112
+
113
+ function commitOpChain(db, mongo, collection, id, ops, previousOpId, version, callback) {
114
+ if (typeof previousOpId === 'function') {
115
+ callback = previousOpId;
116
+ previousOpId = undefined;
117
+ version = 0;
118
+ }
119
+
120
+ ops = ops.slice();
121
+ var op = ops.shift();
122
+
123
+ if (!op) {
124
+ return callback();
125
+ }
126
+
127
+ var snapshot = {id: id, v: version + 1, type: 'json0', data: {}, m: null, _opLink: previousOpId};
128
+ db.commit(collection, id, op, snapshot, null, function(error) {
129
+ if (error) return callback(error);
130
+ mongo.collection('o_' + collection).find({d: id, v: version}).next(function(error, op) {
131
+ if (error) return callback(error);
132
+ commitOpChain(db, mongo, collection, id, ops, (op ? op._id : null), ++version, callback);
133
+ });
134
+ });
135
+ }
@@ -0,0 +1,189 @@
1
+ var expect = require('chai').expect;
2
+ var ShareDbMongo = require('..');
3
+ var getQuery = require('sharedb-mingo-memory/get-query');
4
+ var sinon = require('sinon');
5
+
6
+ var mongoUrl = process.env.TEST_MONGO_URL || 'mongodb://localhost:27017/test';
7
+
8
+ function create(callback) {
9
+ var db = new ShareDbMongo(mongoUrl, {
10
+ mongoOptions: {},
11
+ getOpsWithoutStrictLinking: true
12
+ });
13
+ db.getDbs(function(err, mongo) {
14
+ if (err) return callback(err);
15
+ mongo.dropDatabase(function(err) {
16
+ if (err) return callback(err);
17
+ callback(null, db, mongo);
18
+ });
19
+ });
20
+ };
21
+
22
+ require('sharedb/test/db')({create: create, getQuery: getQuery});
23
+
24
+ describe('getOpsWithoutStrictLinking: true', function() {
25
+ beforeEach(function(done) {
26
+ var self = this;
27
+ create(function(err, db, mongo) {
28
+ if (err) return done(err);
29
+ self.db = db;
30
+ self.mongo = mongo;
31
+ done();
32
+ });
33
+ });
34
+
35
+ afterEach(function(done) {
36
+ this.db.close(done);
37
+ });
38
+
39
+ describe('a chain of ops', function() {
40
+ var db;
41
+ var mongo;
42
+ var id;
43
+ var collection;
44
+
45
+ beforeEach(function(done) {
46
+ db = this.db;
47
+ mongo = this.mongo;
48
+ id = 'document1';
49
+ collection = 'testcollection';
50
+
51
+ sinon.spy(db, '_getOps');
52
+ sinon.spy(db, '_getSnapshotOpLink');
53
+
54
+ var ops = [
55
+ {v: 0, create: {}},
56
+ {v: 1, p: ['foo'], oi: 'bar'},
57
+ {v: 2, p: ['foo'], oi: 'baz'},
58
+ {v: 3, p: ['foo'], oi: 'qux'}
59
+ ];
60
+
61
+ commitOpChain(db, mongo, collection, id, ops, done);
62
+ });
63
+
64
+ it('fetches ops 0-1 without fetching all ops', function(done) {
65
+ db.getOps(collection, id, 0, 2, null, function(error, ops) {
66
+ if (error) return done(error);
67
+ expect(ops.length).to.equal(2);
68
+ expect(ops[0].v).to.equal(0);
69
+ expect(ops[1].v).to.equal(1);
70
+ expect(db._getSnapshotOpLink.notCalled).to.equal(true);
71
+ expect(db._getOps.calledOnceWith(collection, id, 0, 2)).to.equal(true);
72
+ done();
73
+ });
74
+ });
75
+
76
+ it('fetches ops 0-1 when v1 has a spurious duplicate', function(done) {
77
+ var spuriousOp = {v: 1, d: id, p: ['foo'], oi: 'corrupt', o: null};
78
+
79
+ callInSeries([
80
+ function(next) {
81
+ mongo.collection('o_' + collection).insertOne(spuriousOp, next);
82
+ },
83
+ function(result, next) {
84
+ db.getOps(collection, id, 0, 2, null, next);
85
+ },
86
+ function(ops, next) {
87
+ expect(ops.length).to.equal(2);
88
+ expect(ops[1].oi).to.equal('bar');
89
+ expect(db._getSnapshotOpLink.notCalled).to.equal(true);
90
+ expect(db._getOps.calledOnceWith(collection, id, 0, 2)).to.equal(true);
91
+ next();
92
+ },
93
+ done
94
+ ]);
95
+ });
96
+
97
+ it('fetches ops 0-1 when the next op v2 has a spurious duplicate', function(done) {
98
+ var spuriousOp = {v: 2, d: id, p: ['foo'], oi: 'corrupt', o: null};
99
+
100
+ callInSeries([
101
+ function(next) {
102
+ mongo.collection('o_' + collection).insertOne(spuriousOp, next);
103
+ },
104
+ function(result, next) {
105
+ db.getOps(collection, id, 0, 2, null, next);
106
+ },
107
+ function(ops, next) {
108
+ expect(ops.length).to.equal(2);
109
+ expect(ops[1].oi).to.equal('bar');
110
+ expect(db._getSnapshotOpLink.notCalled).to.equal(true);
111
+ expect(db._getOps.calledOnceWith(collection, id, 0, 3)).to.equal(true);
112
+ next();
113
+ },
114
+ done
115
+ ]);
116
+ });
117
+
118
+ it('fetches ops 0-1 when all the ops have spurious duplicates', function(done) {
119
+ var spuriousOps = [
120
+ {v: 0, d: id, p: ['foo'], oi: 'corrupt', o: null},
121
+ {v: 1, d: id, p: ['foo'], oi: 'corrupt', o: null},
122
+ {v: 2, d: id, p: ['foo'], oi: 'corrupt', o: null},
123
+ {v: 3, d: id, p: ['foo'], oi: 'corrupt', o: null}
124
+ ];
125
+
126
+ callInSeries([
127
+ function(next) {
128
+ mongo.collection('o_' + collection).insertMany(spuriousOps, next);
129
+ },
130
+ function(result, next) {
131
+ db.getOps(collection, id, 0, 2, null, next);
132
+ },
133
+ function(ops, next) {
134
+ expect(ops.length).to.equal(2);
135
+ expect(ops[0].create).to.eql({});
136
+ expect(ops[1].oi).to.equal('bar');
137
+ expect(db._getSnapshotOpLink.calledOnce).to.equal(true);
138
+ next();
139
+ },
140
+ done
141
+ ]);
142
+ });
143
+ });
144
+ });
145
+
146
+ function commitOpChain(db, mongo, collection, id, ops, previousOpId, version, callback) {
147
+ if (typeof previousOpId === 'function') {
148
+ callback = previousOpId;
149
+ previousOpId = undefined;
150
+ version = 0;
151
+ }
152
+
153
+ ops = ops.slice();
154
+ var op = ops.shift();
155
+
156
+ if (!op) {
157
+ return callback();
158
+ }
159
+
160
+ var snapshot = {id: id, v: version + 1, type: 'json0', data: {}, m: null, _opLink: previousOpId};
161
+ db.commit(collection, id, op, snapshot, null, function(error) {
162
+ if (error) return callback(error);
163
+ mongo.collection('o_' + collection).find({d: id, v: version}).next(function(error, op) {
164
+ if (error) return callback(error);
165
+ commitOpChain(db, mongo, collection, id, ops, op._id, ++version, callback);
166
+ });
167
+ });
168
+ }
169
+
170
+ function callInSeries(callbacks, args) {
171
+ if (!callbacks.length) return;
172
+ args = args || [];
173
+ var error = args.shift();
174
+
175
+ if (error) {
176
+ var finalCallback = callbacks[callbacks.length - 1];
177
+ return finalCallback(error);
178
+ }
179
+
180
+ var callback = callbacks.shift();
181
+ if (callbacks.length) {
182
+ args.push(function() {
183
+ var args = Array.from(arguments);
184
+ callInSeries(callbacks, args);
185
+ });
186
+ }
187
+
188
+ callback.apply(callback, args);
189
+ }