diff --git a/.travis.yml b/.travis.yml index 5dc6cbaa..2827b522 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: node_js node_js: + - "12" - "10" - "8" - - "6" script: "npm run lint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/README.md b/README.md index 392b2261..034ab59f 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Register a new middleware. before being committed to the database * `'commit'`: An operation was applied to a snapshot; The operation and new snapshot are about to be written to the database. - * `'afterSubmit'`: An operation was successfully submitted to + * `'afterWrite'`: An operation was successfully written to the database. * `'receive'`: Received a message from a client * `'reply'`: About to send a non-error reply to a client message @@ -321,7 +321,7 @@ Populate the fields on `doc` with a snapshot of the document from the server. Populate the fields on `doc` with a snapshot of the document from the server, and fire events on subsequent changes. -`doc.unsubscribe(function (err) {...})` +`doc.unsubscribe(function (err) {...})` Stop listening for document updates. The document data at the time of unsubscribing remains in memory, but no longer stays up-to-date. Resubscribe with `doc.subscribe`. `doc.ingestSnapshot(snapshot, callback)` diff --git a/examples/counter/client.js b/examples/counter/client.js index c8308ec4..1067d2a4 100644 --- a/examples/counter/client.js +++ b/examples/counter/client.js @@ -1,7 +1,8 @@ +var ReconnectingWebSocket = require('reconnecting-websocket'); var sharedb = require('sharedb/lib/client'); // Open WebSocket connection to ShareDB server -var socket = new WebSocket('ws://' + window.location.host); +var socket = new ReconnectingWebSocket('ws://' + window.location.host); var connection = new sharedb.Connection(socket); // Create local Doc instance mapped to 'examples' collection document with id 'counter' diff --git a/examples/counter/package.json b/examples/counter/package.json index 42b2e5c9..9c904f31 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -1,6 +1,6 @@ { "name": "sharedb-example-counter", - "version": "0.0.1", + "version": "1.0.0", "description": "A simple client/server app using ShareDB and WebSockets", "main": "server.js", "scripts": { @@ -15,12 +15,13 @@ ], "license": "MIT", "dependencies": { + "@teamwork/websocket-json-stream": "^2.0.0", "express": "^4.14.0", + "reconnecting-websocket": "^4.2.0", "sharedb": "^1.0.0-beta", - "@teamwork/websocket-json-stream": "^2.0.0", - "ws": "^1.1.0" + "ws": "^7.2.0" }, "devDependencies": { - "browserify": "^13.0.1" + "browserify": "^16.5.0" } } diff --git a/examples/leaderboard/README.md b/examples/leaderboard/README.md index e3b52050..50562609 100644 --- a/examples/leaderboard/README.md +++ b/examples/leaderboard/README.md @@ -9,24 +9,16 @@ In this demo, data is not persisted. To persist data, run a Mongo server and initialize ShareDB with the [ShareDBMongo](https://github.com/share/sharedb-mongo) database adapter. -## Run this example - -First, install dependencies. - -Note: Make sure you're in the `examples/leaderboard` folder so that it uses the `package.json` located here). +## Install dependencies +Make sure you're in the `examples/leaderboard` folder so that it uses the `package.json` located here). ``` npm install ``` -Then build the client JavaScript file. -``` -npm run build -``` - -Get the server running. +## Build JavaScript bundle and run server ``` -npm start +npm run build && npm start ``` Finally, open the example app in the browser. It runs on port 8080 by default: diff --git a/examples/leaderboard/client/Body.jsx b/examples/leaderboard/client/Body.jsx index 80206ea3..1c68a6a1 100644 --- a/examples/leaderboard/client/Body.jsx +++ b/examples/leaderboard/client/Body.jsx @@ -1,19 +1,17 @@ var React = require('react'); var Leaderboard = require('./Leaderboard.jsx'); -var Body = React.createClass({ - render: function() { - return ( -
-
-
-

Leaderboard

-
Select a scientist to give them points
- -
+function Body() { + return ( +
+
+
+

Leaderboard

+
Select a scientist to give them points
+
- ); - } -}); +
+ ); +} module.exports = Body; diff --git a/examples/leaderboard/client/Leaderboard.jsx b/examples/leaderboard/client/Leaderboard.jsx index a3356f7c..f530ed14 100644 --- a/examples/leaderboard/client/Leaderboard.jsx +++ b/examples/leaderboard/client/Leaderboard.jsx @@ -4,15 +4,18 @@ var React = require('react'); var _ = require('underscore'); var connection = require('./connection'); -var Leaderboard = React.createClass({ - getInitialState: function() { - return { +class Leaderboard extends React.Component { + constructor(props) { + super(props); + this.state = { selectedPlayerId: null, players: [] }; - }, + this.handlePlayerSelected = this.handlePlayerSelected.bind(this); + this.handleAddPoints = this.handleAddPoints.bind(this); + } - componentDidMount: function() { + componentDidMount() { var comp = this; var query = connection.createSubscribeQuery('players', {$sort: {score: -1}}); query.on('ready', update); @@ -21,30 +24,30 @@ var Leaderboard = React.createClass({ function update() { comp.setState({players: query.results}); } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { query.destroy(); - }, + } - selectedPlayer: function() { + selectedPlayer() { return _.find(this.state.players, function(x) { return x.id === this.state.selectedPlayerId; }.bind(this)); - }, + } - handlePlayerSelected: function(id) { + handlePlayerSelected(id) { this.setState({selectedPlayerId: id}); - }, + } - handleAddPoints: function() { + handleAddPoints() { var op = [{p: ['score'], na: 5}]; connection.get('players', this.state.selectedPlayerId).submitOp(op, function(err) { if (err) { console.error(err); return; } }); - }, + } - render: function() { + render() { return (
@@ -54,7 +57,7 @@ var Leaderboard = React.createClass({
); } -}); +} module.exports = Leaderboard; diff --git a/examples/leaderboard/client/Player.jsx b/examples/leaderboard/client/Player.jsx index 2580bd11..f7c08e3f 100644 --- a/examples/leaderboard/client/Player.jsx +++ b/examples/leaderboard/client/Player.jsx @@ -1,18 +1,18 @@ +var PropTypes = require('prop-types'); var React = require('react'); var classNames = require('classnames'); -var Player = React.createClass({ - propTypes: { - doc: React.PropTypes.object.isRequired, - onPlayerSelected: React.PropTypes.func.isRequired, - selected: React.PropTypes.bool.isRequired - }, +class Player extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } - handleClick: function(event) { + handleClick(event) { this.props.onPlayerSelected(this.props.doc.id); - }, + } - componentDidMount: function() { + componentDidMount() { var comp = this; var doc = comp.props.doc; doc.subscribe(); @@ -22,13 +22,13 @@ var Player = React.createClass({ // `comp.props.doc.data` is now updated. re-render component. comp.forceUpdate(); } - }, + } - componentWillUnmount: function() { + componentWillUnmount() { this.doc.unsubscribe(); - }, + } - render: function() { + render() { var classes = { 'player': true, 'selected': this.props.selected @@ -41,6 +41,12 @@ var Player = React.createClass({ ); } -}); +} + +Player.propTypes = { + doc: PropTypes.object.isRequired, + onPlayerSelected: PropTypes.func.isRequired, + selected: PropTypes.bool.isRequired +}; module.exports = Player; diff --git a/examples/leaderboard/client/PlayerList.jsx b/examples/leaderboard/client/PlayerList.jsx index 469ceef0..a7ea78b0 100644 --- a/examples/leaderboard/client/PlayerList.jsx +++ b/examples/leaderboard/client/PlayerList.jsx @@ -1,30 +1,29 @@ +var PropTypes = require('prop-types'); var React = require('react'); var Player = require('./Player.jsx'); var _ = require('underscore'); -var PlayerList = React.createClass({ - propTypes: { - players: React.PropTypes.array.isRequired, - selectedPlayerId: React.PropTypes.string - }, +function PlayerList(props) { + var { players, selectedPlayerId } = props; + var other = _.omit(props, 'players', 'selectedPlayerId'); - render: function() { - var { players, selectedPlayerId } = this.props; - var other = _.omit(this.props, 'players', 'selectedPlayerId'); + var playerNodes = players.map(function(player, index) { + var selected = selectedPlayerId === player.id; - var playerNodes = players.map(function(player, index) { - var selected = selectedPlayerId === player.id; - - return ( - - ); - }); return ( -
- {playerNodes} -
+ ); - } -}); + }); + return ( +
+ {playerNodes} +
+ ); +} + +PlayerList.propTypes = { + players: PropTypes.array.isRequired, + selectedPlayerId: PropTypes.string +}; -module.exports = PlayerList; \ No newline at end of file +module.exports = PlayerList; diff --git a/examples/leaderboard/client/PlayerSelector.jsx b/examples/leaderboard/client/PlayerSelector.jsx index 2a61d425..b778693a 100644 --- a/examples/leaderboard/client/PlayerSelector.jsx +++ b/examples/leaderboard/client/PlayerSelector.jsx @@ -1,24 +1,23 @@ +var PropTypes = require('prop-types'); var React = require('react'); -var PlayerSelector = React.createClass({ - propTypes: { - selectedPlayer: React.PropTypes.object - }, +function PlayerSelector({ selectedPlayer, onAddPoints }) { + var node; - render: function() { - var node; + if (selectedPlayer) { + node =
+
{selectedPlayer.data.name}
+ +
; + } else { + node =
Click a player to select
; + } - if (this.props.selectedPlayer) { - node =
-
{this.props.selectedPlayer.data.name}
- -
; - } else { - node =
Click a player to select
; - } + return node; +} - return node; - } -}); +PlayerSelector.propTypes = { + selectedPlayer: PropTypes.object +}; module.exports = PlayerSelector; diff --git a/examples/leaderboard/client/connection.js b/examples/leaderboard/client/connection.js index a3c03eaa..a9001050 100644 --- a/examples/leaderboard/client/connection.js +++ b/examples/leaderboard/client/connection.js @@ -1,6 +1,7 @@ +var ReconnectingWebSocket = require('reconnecting-websocket'); var sharedb = require('sharedb/lib/client'); // Expose a singleton WebSocket connection to ShareDB server -var socket = new WebSocket('ws://' + window.location.host); +var socket = new ReconnectingWebSocket('ws://' + window.location.host); var connection = new sharedb.Connection(socket); module.exports = connection; diff --git a/examples/leaderboard/package.json b/examples/leaderboard/package.json index 6ee5782c..705cf66b 100644 --- a/examples/leaderboard/package.json +++ b/examples/leaderboard/package.json @@ -1,10 +1,10 @@ { "name": "sharedb-example-leaderboard", - "version": "0.0.1", + "version": "1.0.0", "description": "React Leaderboard backed by ShareDB", "main": "server.js", "scripts": { - "build": "mkdir -p dist/ && ./node_modules/.bin/browserify -t [ babelify --presets [ react ] ] client/index.jsx -o dist/bundle.js", + "build": "mkdir -p static/dist/ && ./node_modules/.bin/browserify -t [ babelify --presets [ react ] ] client/index.jsx -o static/dist/bundle.js", "test": "echo \"Error: no test specified\" && exit 1", "start": "node server/index.js" }, @@ -15,21 +15,21 @@ ], "license": "MIT", "dependencies": { + "@teamwork/websocket-json-stream": "^2.0.0", "classnames": "^2.2.5", - "connect": "^3.4.1", - "react": "^15.1.0", - "react-dom": "^15.1.0", - "serve-static": "^1.11.1", + "express": "^4.17.1", + "prop-types": "^15.7.2", + "react": "^16.11.0", + "react-dom": "^16.11.0", + "reconnecting-websocket": "^4.2.0", "sharedb": "^1.0.0-beta", "sharedb-mingo-memory": "^1.0.0-beta", - "through2": "^2.0.1", "underscore": "^1.8.3", - "@teamwork/websocket-json-stream": "^2.0.0", - "ws": "^1.1.0" + "ws": "^7.2.0" }, "devDependencies": { "babel-preset-react": "^6.5.0", "babelify": "^7.3.0", - "browserify": "^13.0.1" + "browserify": "^16.5.0" } } diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js index 6aebe997..242d9dd3 100644 --- a/examples/leaderboard/server/index.js +++ b/examples/leaderboard/server/index.js @@ -1,7 +1,6 @@ var http = require('http'); var ShareDB = require('sharedb'); -var connect = require('connect'); -var serveStatic = require('serve-static'); +var express = require('express'); var ShareDBMingoMemory = require('sharedb-mingo-memory'); var WebSocketJSONStream = require('@teamwork/websocket-json-stream'); var WebSocket = require('ws'); @@ -10,8 +9,8 @@ var WebSocket = require('ws'); var share = new ShareDB({db: new ShareDBMingoMemory()}); // Create a WebSocket server -var app = connect(); -app.use(serveStatic('.')); +var app = express(); +app.use(express.static('static')); var server = http.createServer(app); var wss = new WebSocket.Server({server: server}); server.listen(8080); diff --git a/examples/leaderboard/index.html b/examples/leaderboard/static/index.html similarity index 100% rename from examples/leaderboard/index.html rename to examples/leaderboard/static/index.html diff --git a/examples/leaderboard/leaderboard.css b/examples/leaderboard/static/leaderboard.css similarity index 100% rename from examples/leaderboard/leaderboard.css rename to examples/leaderboard/static/leaderboard.css diff --git a/examples/rich-text/client.js b/examples/rich-text/client.js index 68541137..5785357d 100644 --- a/examples/rich-text/client.js +++ b/examples/rich-text/client.js @@ -1,10 +1,11 @@ +var ReconnectingWebSocket = require('reconnecting-websocket'); var sharedb = require('sharedb/lib/client'); var richText = require('rich-text'); var Quill = require('quill'); sharedb.types.register(richText.type); // Open WebSocket connection to ShareDB server -var socket = new WebSocket('ws://' + window.location.host); +var socket = new ReconnectingWebSocket('ws://' + window.location.host); var connection = new sharedb.Connection(socket); // For testing reconnection @@ -12,7 +13,7 @@ window.disconnect = function() { connection.close(); }; window.connect = function() { - var socket = new WebSocket('ws://' + window.location.host); + var socket = new ReconnectingWebSocket('ws://' + window.location.host); connection.bindToSocket(socket); }; diff --git a/examples/rich-text/package.json b/examples/rich-text/package.json index 2249e29c..461a2260 100644 --- a/examples/rich-text/package.json +++ b/examples/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "sharedb-example-rich-text", - "version": "0.0.1", + "version": "1.0.0", "description": "A simple rich-text editor example based on Quill and ShareDB", "main": "server.js", "scripts": { @@ -14,14 +14,15 @@ ], "license": "MIT", "dependencies": { - "express": "^4.14.0", - "quill": "^1.0.0-beta.11", - "rich-text": "^3.0.1", - "sharedb": "^1.0.0-beta", "@teamwork/websocket-json-stream": "^2.0.0", - "ws": "^1.1.0" + "express": "^4.17.1", + "quill": "^1.3.7", + "reconnecting-websocket": "^4.2.0", + "rich-text": "^4.0.0", + "sharedb": "^1.0.0-beta", + "ws": "^7.2.0" }, "devDependencies": { - "browserify": "^13.0.1" + "browserify": "^16.5.0" } } diff --git a/examples/textarea/client.js b/examples/textarea/client.js index 4cbf5520..c5111c2b 100644 --- a/examples/textarea/client.js +++ b/examples/textarea/client.js @@ -2,8 +2,8 @@ var sharedb = require('sharedb/lib/client'); var StringBinding = require('sharedb-string-binding'); // Open WebSocket connection to ShareDB server -var WebSocket = require('reconnecting-websocket'); -var socket = new WebSocket('ws://' + window.location.host); +var ReconnectingWebSocket = require('reconnecting-websocket'); +var socket = new ReconnectingWebSocket('ws://' + window.location.host); var connection = new sharedb.Connection(socket); var element = document.querySelector('textarea'); diff --git a/examples/textarea/package.json b/examples/textarea/package.json index 1a91a3aa..49c40f12 100644 --- a/examples/textarea/package.json +++ b/examples/textarea/package.json @@ -1,6 +1,6 @@ { "name": "sharedb-example-textarea", - "version": "0.0.1", + "version": "1.0.0", "description": "A simple client/server app using ShareDB and WebSockets", "main": "server.js", "scripts": { @@ -14,14 +14,14 @@ ], "license": "MIT", "dependencies": { - "express": "^4.14.0", - "reconnecting-websocket": "^3.0.3", + "@teamwork/websocket-json-stream": "^2.0.0", + "express": "^4.17.1", + "reconnecting-websocket": "^4.2.0", "sharedb": "^1.0.0-beta", "sharedb-string-binding": "^1.0.0", - "@teamwork/websocket-json-stream": "^2.0.0", - "ws": "^1.1.0" + "ws": "^7.2.0" }, "devDependencies": { - "browserify": "^13.0.1" + "browserify": "^16.5.0" } } diff --git a/lib/agent.js b/lib/agent.js index f548f4d7..9deb83d1 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,6 +1,6 @@ var hat = require('hat'); -var util = require('./util'); var types = require('./types'); +var util = require('./util'); var logger = require('./logger'); /** @@ -105,8 +105,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent._isOwnOp(collection, data)) return; - agent._sendOp(collection, id, data); + agent._onOp(collection, id, data); }); stream.on('end', function() { // The op stream is done sending, so release its reference @@ -149,13 +148,41 @@ Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query emitter.onOp = function(op) { var id = op.d; - if (agent._isOwnOp(collection, op)) return; - agent._sendOp(collection, id, op); + agent._onOp(collection, id, op); }; emitter._open(); }; +Agent.prototype._onOp = function(collection, id, op) { + if (this._isOwnOp(collection, op)) return; + + // Ops emitted here are coming directly from pubsub, which emits the same op + // object to listeners without making a copy. The pattern in middleware is to + // manipulate the passed in object, and projections are implemented the same + // way currently. + // + // Deep copying the op would be safest, but deep copies are very expensive, + // especially over arbitrary objects. This function makes a shallow copy of an + // op, and it requires that projections and any user middleware copy deep + // properties as needed when they modify the op. + // + // Polling of query subscriptions is determined by the same op objects. As a + // precaution against op middleware breaking query subscriptions, we delay + // before calling into projection and middleware code + var agent = this; + process.nextTick(function() { + var copy = shallowCopy(op); + agent.backend.sanitizeOp(agent, collection, id, copy, function(err) { + if (err) { + logger.error('Error sanitizing op emitted from subscription', collection, id, copy, err); + return; + } + agent._sendOp(collection, id, copy); + }); + }); +}; + Agent.prototype._isOwnOp = function(collection, op) { // Detect ops from this client on the same projection. Since the client sent // these in, the submit reply will be sufficient and we can silently ignore @@ -186,12 +213,17 @@ Agent.prototype._sendOp = function(collection, id, op) { this.send(message); }; - Agent.prototype._sendOps = function(collection, id, ops) { for (var i = 0; i < ops.length; i++) { this._sendOp(collection, id, ops[i]); } }; +Agent.prototype._sendOpsBulk = function(collection, opsMap) { + for (var id in opsMap) { + var ops = opsMap[id]; + this._sendOps(collection, id, ops); + } +}; function getReplyErrorObject(err) { if (typeof err === 'string') { @@ -201,7 +233,7 @@ function getReplyErrorObject(err) { }; } else { if (err.stack) { - logger.warn(err.stack); + logger.info(err.stack); } return { code: err.code, @@ -316,7 +348,8 @@ Agent.prototype._handleMessage = function(request, callback) { case 'u': return this._unsubscribe(request.c, request.d, callback); case 'op': - var op = this._createOp(request); + // Normalize the properties submitted + var op = createClientOp(request, this.clientId); if (!op) return callback({code: 4000, message: 'Invalid op message'}); return this._submit(request.c, request.d, op, callback); case 'nf': @@ -493,10 +526,7 @@ Agent.prototype._fetchBulkOps = function(collection, versions, callback) { var agent = this; this.backend.getOpsBulk(this, collection, versions, null, function(err, opsMap) { if (err) return callback(err); - for (var id in opsMap) { - var ops = opsMap[id]; - agent._sendOps(collection, id, ops); - } + agent._sendOpsBulk(collection, opsMap); callback(); }); }; @@ -505,8 +535,18 @@ Agent.prototype._subscribe = function(collection, id, version, callback) { // If the version is specified, catch the client up by sending all ops // since the specified version var agent = this; - this.backend.subscribe(this, collection, id, version, function(err, stream, snapshot) { + this.backend.subscribe(this, collection, id, version, function(err, stream, snapshot, ops) { if (err) return callback(err); + // If we're subscribing from a known version, send any ops committed since + // the requested version to bring the client's doc up to date + if (ops) { + agent._sendOps(collection, id, ops); + } + // In addition, ops may already be queued on the stream by pubsub. + // Subscribe is called before the ops or snapshot are fetched, so it is + // possible that some ops may be duplicates. Clients should ignore any + // duplicate ops they may receive. This will flush ops already queued and + // subscribe to ongoing ops from the stream agent._subscribeToStream(collection, id, stream); // Snapshot is returned only when subscribing from a null version. // Otherwise, ops will have been pushed into the stream @@ -519,9 +559,13 @@ Agent.prototype._subscribe = function(collection, id, version, callback) { }; Agent.prototype._subscribeBulk = function(collection, versions, callback) { + // See _subscribe() above. This function's logic should match but in bulk var agent = this; - this.backend.subscribeBulk(this, collection, versions, function(err, streams, snapshotMap) { + this.backend.subscribeBulk(this, collection, versions, function(err, streams, snapshotMap, opsMap) { if (err) return callback(err); + if (opsMap) { + agent._sendOpsBulk(collection, opsMap); + } for (var id in streams) { agent._subscribeToStream(collection, id, streams[id]); } @@ -572,45 +616,58 @@ Agent.prototype._submit = function(collection, id, op, callback) { }); }; -function CreateOp(src, seq, v, create) { +Agent.prototype._fetchSnapshot = function(collection, id, version, callback) { + this.backend.fetchSnapshot(this, collection, id, version, callback); +}; + +Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { + this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); +}; + + +function createClientOp(request, clientId) { + // src can be provided if it is not the same as the current agent, + // such as a resubmission after a reconnect, but it usually isn't needed + var src = request.src || clientId; + // c, d, and m arguments are intentionally undefined. These are set later + return (request.op) ? new EditOp(src, request.seq, request.v, request.op) : + (request.create) ? new CreateOp(src, request.seq, request.v, request.create) : + (request.del) ? new DeleteOp(src, request.seq, request.v, request.del) : + undefined; +} + +function shallowCopy(object) { + var out = {}; + for (var key in object) { + out[key] = object[key]; + } + return out; +} + +function CreateOp(src, seq, v, create, c, d, m) { this.src = src; this.seq = seq; this.v = v; this.create = create; - this.m = null; + this.c = c; + this.d = d; + this.m = m; } -function EditOp(src, seq, v, op) { +function EditOp(src, seq, v, op, c, d, m) { this.src = src; this.seq = seq; this.v = v; this.op = op; - this.m = null; + this.c = c; + this.d = d; + this.m = m; } -function DeleteOp(src, seq, v, del) { +function DeleteOp(src, seq, v, del, c, d, m) { this.src = src; this.seq = seq; this.v = v; this.del = del; - this.m = null; + this.c = c; + this.d = d; + this.m = m; } -// Normalize the properties submitted -Agent.prototype._createOp = function(request) { - // src can be provided if it is not the same as the current agent, - // such as a resubmission after a reconnect, but it usually isn't needed - var src = request.src || this.clientId; - if (request.op) { - return new EditOp(src, request.seq, request.v, request.op); - } else if (request.create) { - return new CreateOp(src, request.seq, request.v, request.create); - } else if (request.del) { - return new DeleteOp(src, request.seq, request.v, request.del); - } -}; - -Agent.prototype._fetchSnapshot = function(collection, id, version, callback) { - this.backend.fetchSnapshot(this, collection, id, version, callback); -}; - -Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { - this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); -}; diff --git a/lib/backend.js b/lib/backend.js index bdac897f..42c20bd0 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -40,8 +40,8 @@ module.exports = Backend; emitter.mixin(Backend); Backend.prototype.MIDDLEWARE_ACTIONS = { - // An operation was successfully submitted to the database. - afterSubmit: 'afterSubmit', + // An operation was successfully written to the database. + afterWrite: 'afterWrite', // An operation is about to be applied to a snapshot before being committed to the database apply: 'apply', // An operation was applied to a snapshot; The operation and new snapshot are about to be written to the database. @@ -154,7 +154,7 @@ Backend.prototype.use = function(action, fn) { for (var i = 0; i < action.length; i++) { this.use(action[i], fn); } - return; + return this; } var fns = this.middleware[action] || (this.middleware[action] = []); fns.push(fn); @@ -199,7 +199,7 @@ Backend.prototype.submit = function(agent, index, id, op, options, callback) { if (err) return callback(err); request.submit(function(err) { if (err) return callback(err); - backend.trigger(backend.MIDDLEWARE_ACTIONS.afterSubmit, agent, request, function(err) { + backend.trigger(backend.MIDDLEWARE_ACTIONS.afterWrite, agent, request, function(err) { if (err) return callback(err); backend._sanitizeOps(agent, request.projection, request.collection, id, request.ops, function(err) { if (err) return callback(err); @@ -211,6 +211,12 @@ Backend.prototype.submit = function(agent, index, id, op, options, callback) { }); }; +Backend.prototype.sanitizeOp = function(agent, index, id, op, callback) { + var projection = this.projections[index]; + var collection = (projection) ? projection.target : index; + this._sanitizeOp(agent, projection, collection, id, op, callback); +}; + Backend.prototype._sanitizeOp = function(agent, projection, collection, id, op, callback) { if (projection) { try { @@ -265,6 +271,28 @@ Backend.prototype._getSnapshotsFromMap = function(ids, snapshotMap) { return snapshots; }; +Backend.prototype._getSanitizedOps = function(agent, projection, collection, id, from, to, opsOptions, callback) { + var backend = this; + backend.db.getOps(collection, id, from, to, opsOptions, function(err, ops) { + if (err) return callback(err); + backend._sanitizeOps(agent, projection, collection, id, ops, function(err) { + if (err) return callback(err); + callback(null, ops); + }); + }); +}; + +Backend.prototype._getSanitizedOpsBulk = function(agent, projection, collection, fromMap, toMap, opsOptions, callback) { + var backend = this; + backend.db.getOpsBulk(collection, fromMap, toMap, opsOptions, function(err, opsMap) { + if (err) return callback(err); + backend._sanitizeOpsBulk(agent, projection, collection, opsMap, function(err) { + if (err) return callback(err); + callback(null, opsMap); + }); + }); +}; + // Non inclusive - gets ops from [from, to). Ie, all relevant ops. If to is // not defined (null or undefined) then it returns all ops. Backend.prototype.getOps = function(agent, index, id, from, to, options, callback) { @@ -285,13 +313,10 @@ Backend.prototype.getOps = function(agent, index, id, from, to, options, callbac to: to }; var opsOptions = options && options.opsOptions; - backend.db.getOps(collection, id, from, to, opsOptions, function(err, ops) { + backend._getSanitizedOps(agent, projection, collection, id, from, to, opsOptions, function(err, ops) { if (err) return callback(err); - backend._sanitizeOps(agent, projection, collection, id, ops, function(err) { - if (err) return callback(err); - backend.emit('timing', 'getOps', Date.now() - start, request); - callback(err, ops); - }); + backend.emit('timing', 'getOps', Date.now() - start, request); + callback(null, ops); }); }; @@ -312,13 +337,10 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, options, c toMap: toMap }; var opsOptions = options && options.opsOptions; - backend.db.getOpsBulk(collection, fromMap, toMap, opsOptions, function(err, opsMap) { + backend._getSanitizedOpsBulk(agent, projection, collection, fromMap, toMap, opsOptions, function(err, opsMap) { if (err) return callback(err); - backend._sanitizeOpsBulk(agent, projection, collection, opsMap, function(err) { - if (err) return callback(err); - backend.emit('timing', 'getOpsBulk', Date.now() - start, request); - callback(err, opsMap); - }); + backend.emit('timing', 'getOpsBulk', Date.now() - start, request); + callback(null, opsMap); }); }; @@ -419,21 +441,25 @@ Backend.prototype.subscribe = function(agent, index, id, version, options, callb }; backend.pubsub.subscribe(channel, function(err, stream) { if (err) return callback(err); - stream.initProjection(backend, agent, projection); if (version == null) { // Subscribing from null means that the agent doesn't have a document // and needs to fetch it as well as subscribing backend.fetch(agent, index, id, function(err, snapshot) { - if (err) return callback(err); + if (err) { + stream.destroy(); + return callback(err); + } backend.emit('timing', 'subscribe.snapshot', Date.now() - start, request); callback(null, stream, snapshot); }); } else { - backend.db.getOps(collection, id, version, null, null, function(err, ops) { - if (err) return callback(err); - stream.pushOps(collection, id, ops); + backend._getSanitizedOps(agent, projection, collection, id, version, null, null, function(err, ops) { + if (err) { + stream.destroy(); + return callback(err); + } backend.emit('timing', 'subscribe.ops', Date.now() - start, request); - callback(null, stream); + callback(null, stream, null, ops); }); } }); @@ -457,7 +483,6 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) { var channel = backend.getDocChannel(collection, id); backend.pubsub.subscribe(channel, function(err, stream) { if (err) return eachCb(err); - stream.initProjection(backend, agent, projection); streams[id] = stream; eachCb(); }); @@ -478,17 +503,13 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) { }); } else { // If a versions map, get ops since requested versions - backend.db.getOpsBulk(collection, versions, null, null, function(err, opsMap) { + backend._getSanitizedOpsBulk(agent, projection, collection, versions, null, null, function(err, opsMap) { if (err) { destroyStreams(streams); return callback(err); } - for (var id in opsMap) { - var ops = opsMap[id]; - streams[id].pushOps(collection, id, ops); - } backend.emit('timing', 'subscribeBulk.ops', Date.now() - start, request); - callback(null, streams); + callback(null, streams, null, opsMap); }); } }); @@ -530,7 +551,6 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba } backend.pubsub.subscribe(request.channel, function(err, stream) { if (err) return callback(err); - stream.initProjection(backend, agent, request.projection); if (options.ids) { var queryEmitter = new QueryEmitter(request, stream, options.ids); backend.emit('timing', 'querySubscribe.reconnect', Date.now() - start, request); diff --git a/lib/client/connection.js b/lib/client/connection.js index 779b7138..521a8ceb 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -293,8 +293,15 @@ Connection.prototype._setState = function(newState, reason) { // 'connecting' from anywhere other than 'disconnected' and getting to // 'connected' from anywhere other than 'connecting'. if ( - (newState === 'connecting' && this.state !== 'disconnected' && this.state !== 'stopped' && this.state !== 'closed') - || (newState === 'connected' && this.state !== 'connecting') + ( + newState === 'connecting' && + this.state !== 'disconnected' && + this.state !== 'stopped' && + this.state !== 'closed' + ) || ( + newState === 'connected' && + this.state !== 'connecting' + ) ) { var err = new ShareDBError(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState); return this.emit('error', err); @@ -303,7 +310,13 @@ Connection.prototype._setState = function(newState, reason) { this.state = newState; this.canSend = (newState === 'connected'); - if (newState === 'disconnected' || newState === 'stopped' || newState === 'closed') this._reset(); + if ( + newState === 'disconnected' || + newState === 'stopped' || + newState === 'closed' + ) { + this._reset(); + } // Group subscribes together to help server make more efficient calls this.startBulk(); diff --git a/lib/op-stream.js b/lib/op-stream.js index 3368c0dd..a2608fd2 100644 --- a/lib/op-stream.js +++ b/lib/op-stream.js @@ -5,12 +5,7 @@ var util = require('./util'); // Stream of operations. Subscribe returns one of these function OpStream() { Readable.call(this, {objectMode: true}); - this.id = null; - this.backend = null; - this.agent = null; - this.projection = null; - this.open = true; } module.exports = OpStream; @@ -24,33 +19,15 @@ inherits(OpStream, Readable); // themselves. OpStream.prototype._read = util.doNothing; -OpStream.prototype.initProjection = function(backend, agent, projection) { - this.backend = backend; - this.agent = agent; - this.projection = projection; -}; - -OpStream.prototype.pushOp = function(collection, id, op) { - if (this.backend) { - var stream = this; - this.backend._sanitizeOp(this.agent, this.projection, collection, id, op, function(err) { - if (!stream.open) return; - stream.push(err ? {error: err} : op); - }); - } else { - // Ignore any messages after unsubscribe - if (!this.open) return; - this.push(op); - } -}; - -OpStream.prototype.pushOps = function(collection, id, ops) { - for (var i = 0; i < ops.length; i++) { - this.pushOp(collection, id, ops[i]); - } +OpStream.prototype.pushData = function(data) { + // Ignore any messages after unsubscribe + if (!this.open) return; + // This data gets consumed in Agent#_subscribeToStream + this.push(data); }; OpStream.prototype.destroy = function() { + // Only close stream once if (!this.open) return; this.open = false; diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js index ff02ac00..e454a694 100644 --- a/lib/pubsub/index.js +++ b/lib/pubsub/index.js @@ -90,8 +90,7 @@ PubSub.prototype._emit = function(channel, data) { var channelStreams = this.streams[channel]; if (channelStreams) { for (var id in channelStreams) { - var copy = shallowCopy(data); - channelStreams[id].pushOp(copy.c, copy.d, copy); + channelStreams[id].pushData(data); } } }; @@ -129,11 +128,3 @@ PubSub.prototype._removeStream = function(channel, stream) { this._unsubscribe(channel, this._defaultCallback); }; - -function shallowCopy(object) { - var out = {}; - for (var key in object) { - out[key] = object[key]; - } - return out; -} diff --git a/lib/query-emitter.js b/lib/query-emitter.js index fe5c5b01..cfa33884 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -1,5 +1,5 @@ var arraydiff = require('arraydiff'); -var deepEquals = require('deep-is'); +var deepEqual = require('fast-deep-equal'); var ShareDBError = require('./error'); var util = require('./util'); @@ -63,7 +63,12 @@ QueryEmitter.prototype._emitTiming = function(action, start) { }; QueryEmitter.prototype._update = function(op) { + // Note that `op` should not be projected or sanitized yet. It's possible for + // a query to filter on a field that's not in the projection. skipPoll checks + // to see if an op could possibly affect a query, so it should get passed the + // full op. The onOp listener function must call backend.sanitizeOp() var id = op.d; + var pollCallback = this._defaultCallback; // Check if the op's id matches the query before updating the query results // and send it through immediately if it does. The current snapshot @@ -90,24 +95,35 @@ QueryEmitter.prototype._update = function(op) { // that removed the doc from the query to cause the client-side computed // list to update. if (this.ids.indexOf(id) !== -1) { - this.onOp(op); + var emitter = this; + pollCallback = function(err) { + // Send op regardless of polling error. Clients handle subscription to ops + // on the documents that currently match query results independently from + // updating which docs match the query + emitter.onOp(op); + if (err) emitter.onError(err); + }; } // Ignore if the database or user function says we don't need to poll try { - if (this.db.skipPoll(this.collection, id, op, this.query)) return this._defaultCallback(); - if (this.skipPoll(this.collection, id, op, this.query)) return this._defaultCallback(); + if ( + this.db.skipPoll(this.collection, id, op, this.query) || + this.skipPoll(this.collection, id, op, this.query) + ) { + return pollCallback(); + } } catch (err) { - return this._defaultCallback(err); + return pollCallback(err); } if (this.canPollDoc) { // We can query against only the document that was modified to see if the // op has changed whether or not it matches the results - this.queryPollDoc(id, this._defaultCallback); + this.queryPollDoc(id, pollCallback); } else { // We need to do a full poll of the query, because the query uses limits, // sorts, or something special - this.queryPoll(this._defaultCallback); + this.queryPoll(pollCallback); } }; @@ -173,7 +189,7 @@ QueryEmitter.prototype.queryPoll = function(callback) { emitter._emitTiming('queryEmitter.poll', start); // Be nice to not have to do this in such a brute force way - if (!deepEquals(emitter.extra, extra)) { + if (!deepEqual(emitter.extra, extra)) { emitter.extra = extra; emitter.onExtra(extra); } diff --git a/package.json b/package.json index b30da817..56a6d272 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,29 @@ { "name": "sharedb", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.27", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { "arraydiff": "^0.1.1", "async": "^1.4.2", - "deep-is": "^0.1.3", + "fast-deep-equal": "^2.0.1", "hat": "0.0.3", "make-error": "^1.1.1", "ot-json0": "^1.0.1" }, "devDependencies": { - "coveralls": "^2.11.8", - "eslint": "^5.16.0", - "eslint-config-google": "^0.13.0", - "expect.js": "^0.3.1", - "istanbul": "^0.4.2", - "lolex": "^3.0.0", - "mocha": "^5.2.0", - "sinon": "^6.1.5" + "chai": "^4.2.0", + "coveralls": "^3.0.7", + "eslint": "^6.5.1", + "eslint-config-google": "^0.14.0", + "lolex": "^5.1.1", + "mocha": "^6.2.2", + "nyc": "^14.1.1", + "sinon": "^7.5.0" }, "scripts": { "test": "./node_modules/.bin/mocha && npm run lint", - "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha", + "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha && npm run lint", "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'", "lint:fix": "npm run lint -- --fix" }, diff --git a/test/BasicQueryableMemoryDB.js b/test/BasicQueryableMemoryDB.js new file mode 100644 index 00000000..85d1ca51 --- /dev/null +++ b/test/BasicQueryableMemoryDB.js @@ -0,0 +1,91 @@ +var MemoryDB = require('../lib/db/memory'); + +module.exports = BasicQueryableMemoryDB; + +// Extension of MemoryDB that supports query filters and sorts on simple +// top-level properties, which is enough for the core ShareDB tests on +// query subscription updating. +function BasicQueryableMemoryDB() { + MemoryDB.apply(this, arguments); +} +BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype); +BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB; + +BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) { + if (query.filter) { + snapshots = snapshots.filter(function(snapshot) { + return querySnapshot(snapshot, query); + }); + } + + if (query.sort) { + if (!Array.isArray(query.sort)) { + throw new Error('query.sort must be an array'); + } + if (query.sort.length) { + snapshots.sort(snapshotComparator(query.sort)); + } + } + + return {snapshots: snapshots}; +}; + +BasicQueryableMemoryDB.prototype.queryPollDoc = function(collection, id, query, options, callback) { + var db = this; + process.nextTick(function() { + var snapshot = db._getSnapshotSync(collection, id); + try { + var matches = querySnapshot(snapshot, query); + } catch (err) { + return callback(err); + } + callback(null, matches); + }); +}; + +BasicQueryableMemoryDB.prototype.canPollDoc = function(collection, query) { + return !query.sort; +}; + +function querySnapshot(snapshot, query) { + // Never match uncreated or deleted snapshots + if (snapshot.type == null) return false; + // Match any snapshot when there is no query filter + if (!query.filter) return true; + // Check that each property in the filter equals the snapshot data + for (var queryKey in query.filter) { + // This fake only supports simple property equality filters, so + // throw an error on Mongo-like filter properties with dots. + if (queryKey.includes('.')) { + throw new Error('Only simple property filters are supported, got:', queryKey); + } + if (snapshot.data[queryKey] !== query.filter[queryKey]) { + return false; + } + } + return true; +} + +// sortProperties is an array whose items are each [propertyName, direction]. +function snapshotComparator(sortProperties) { + return function(snapshotA, snapshotB) { + for (var i = 0; i < sortProperties.length; i++) { + var sortProperty = sortProperties[i]; + var sortKey = sortProperty[0]; + var sortDirection = sortProperty[1]; + + var aPropVal = snapshotA.data[sortKey]; + var bPropVal = snapshotB.data[sortKey]; + if (aPropVal < bPropVal) { + return -1 * sortDirection; + } else if (aPropVal > bPropVal) { + return sortDirection; + } else if (aPropVal === bPropVal) { + continue; + } else { + throw new Error('Could not compare ' + aPropVal + ' and ' + bPropVal); + } + } + return 0; + }; +} diff --git a/test/backend.js b/test/backend.js index 991c6716..c6177d05 100644 --- a/test/backend.js +++ b/test/backend.js @@ -1,5 +1,5 @@ var Backend = require('../lib/backend'); -var expect = require('expect.js'); +var expect = require('chai').expect; describe('Backend', function() { var backend; @@ -39,8 +39,8 @@ describe('Backend', function() { backend.getOps(null, 'books', '1984', 0, null, options, function(error, ops) { if (error) return done(error); expect(ops).to.have.length(2); - expect(ops[0].m).to.be.ok(); - expect(ops[1].m).to.be.ok(); + expect(ops[0].m).to.be.ok; + expect(ops[1].m).to.be.ok; done(); }); }); @@ -64,7 +64,7 @@ describe('Backend', function() { }; backend.fetch(null, 'books', '1984', options, function(error, doc) { if (error) return done(error); - expect(doc.m).to.be.ok(); + expect(doc.m).to.be.ok; done(); }); }); @@ -74,7 +74,7 @@ describe('Backend', function() { it('subscribes to the document', function(done) { backend.subscribe(null, 'books', '1984', null, function(error, stream, snapshot) { if (error) return done(error); - expect(stream.open).to.be(true); + expect(stream.open).to.equal(true); expect(snapshot.data).to.eql({ title: '1984', author: 'George Orwell' @@ -95,7 +95,7 @@ describe('Backend', function() { opsOptions: {metadata: true} }; backend.subscribe(null, 'books', '1984', null, options, function(error) { - expect(error.code).to.be(4025); + expect(error.code).to.equal(4025); done(); }); }); diff --git a/test/client/connection.js b/test/client/connection.js index b38ff53f..a7a6cd82 100644 --- a/test/client/connection.js +++ b/test/client/connection.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var Backend = require('../../lib/backend'); var Connection = require('../../lib/client/connection'); @@ -69,7 +69,7 @@ describe('client connection', function() { expect(originalStream).to.have.property('open', false); var newStream = agent.subscribedDocs[collection][docId]; expect(newStream).to.have.property('open', true); - expect(newStream).to.not.be(originalStream); + expect(newStream).to.not.equal(originalStream); connection.close(); done(); }); diff --git a/test/client/doc.js b/test/client/doc.js index 9b5316e3..fd0e255a 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,5 +1,5 @@ var Backend = require('../../lib/backend'); -var expect = require('expect.js'); +var expect = require('chai').expect; var util = require('../util'); describe('Doc', function() { @@ -229,7 +229,7 @@ describe('Doc', function() { it('succeeds with valid op', function(done) { var doc = this.doc; doc.create({name: 'Scooby Doo'}, function(error) { - expect(error).to.not.be.ok(); + expect(error).to.not.exist; // Build valid op that deletes a substring at index 0 of name. var textOpComponents = [{p: 0, d: 'Scooby '}]; var op = [{p: ['name'], t: 'text0', o: textOpComponents}]; @@ -244,12 +244,12 @@ describe('Doc', function() { it('fails with invalid op', function(done) { var doc = this.doc; doc.create({name: 'Scooby Doo'}, function(error) { - expect(error).to.not.be.ok(); + expect(error).to.not.exist; // Build op that tries to delete an invalid substring at index 0 of name. var textOpComponents = [{p: 0, d: 'invalid'}]; var op = [{p: ['name'], t: 'text0', o: textOpComponents}]; doc.submitOp(op, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -286,7 +286,7 @@ describe('Doc', function() { util.callInSeries([ function(next) { doc.submitOp(invalidOp, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); next(); }); }, diff --git a/test/client/pending.js b/test/client/pending.js index 2b22c7a0..bfe84ddd 100644 --- a/test/client/pending.js +++ b/test/client/pending.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var Backend = require('../../lib/backend'); describe('client connection', function() { diff --git a/test/client/projections.js b/test/client/projections.js index 77359563..8b06c9d0 100644 --- a/test/client/projections.js +++ b/test/client/projections.js @@ -1,7 +1,10 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var util = require('../util'); -module.exports = function() { +module.exports = function(options) { + var getQuery = options.getQuery; + var matchAllQuery = getQuery({query: {}}); + describe('client projections', function() { beforeEach(function(done) { this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true}); @@ -26,7 +29,7 @@ module.exports = function() { ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) { it('snapshot ' + method, function(done) { var connection2 = this.backend.connect(); - connection2[method]('dogs_summary', {}, null, function(err, results) { + connection2[method]('dogs_summary', matchAllQuery, null, function(err, results) { if (err) return done(err); expect(results.length).eql(1); expect(results[0].data).eql({age: 3, owner: {name: 'jim'}}); @@ -139,7 +142,7 @@ module.exports = function() { if (err) return done(err); connection.get('dogs', 'fido').submitOp(op, function(err) { if (err) return done(err); - connection2.createFetchQuery('dogs_summary', {}, null, function(err) { + connection2.createFetchQuery('dogs_summary', matchAllQuery, null, function(err) { if (err) return done(err); expect(fido.data).eql(expected); expect(fido.version).eql(2); @@ -156,7 +159,7 @@ module.exports = function() { var connection = this.connection; var connection2 = this.backend.connect(); var fido = connection2.get('dogs_summary', 'fido'); - connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) { + connection2.createSubscribeQuery('dogs_summary', matchAllQuery, null, function(err) { if (err) return done(err); fido.on('op', function() { expect(fido.data).eql(expected); @@ -194,7 +197,7 @@ module.exports = function() { function test(trigger, callback) { var connection = this.connection; var connection2 = this.backend.connect(); - var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) { + var query = connection2.createSubscribeQuery('dogs_summary', matchAllQuery, null, function(err) { if (err) return callback(err); query.on('insert', function() { callback(null, query.results); @@ -203,6 +206,30 @@ module.exports = function() { }); } queryUpdateTests(test); + + it('query subscribe on projection will update based on fields not in projection', function(done) { + // Create a query on a field not in the projection. The query doesn't + // match the doc created in beforeEach + var fido = this.connection.get('dogs', 'fido'); + var connection2 = this.backend.connect(); + var dbQuery = getQuery({query: {color: 'black'}}); + var query = connection2.createSubscribeQuery('dogs_summary', dbQuery, null, function(err, results) { + if (err) return done(err); + expect(results).to.have.length(0); + + // Submit an op on a field not in the projection, where the op should + // cause the doc to be included in the query results + fido.submitOp({p: ['color'], od: 'gold', oi: 'black'}, null, function(err) { + if (err) return done(err); + setTimeout(function() { + expect(query.results).to.have.length(1); + expect(query.results[0].id).to.equal('fido'); + expect(query.results[0].data).to.eql({age: 3, owner: {name: 'jim'}}); + done(); + }, 10); + }); + }); + }); }); describe('fetch query', function() { @@ -211,7 +238,7 @@ module.exports = function() { var connection2 = this.backend.connect(); trigger(connection, function(err) { if (err) return callback(err); - connection2.createFetchQuery('dogs_summary', {}, null, callback); + connection2.createFetchQuery('dogs_summary', matchAllQuery, null, callback); }); } queryUpdateTests(test); @@ -240,7 +267,7 @@ module.exports = function() { projected.fetch(function(err) { if (err) return done(err); projected.submitOp(op, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); doc.fetch(function(err) { if (err) return done(err); expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}); @@ -318,7 +345,7 @@ module.exports = function() { var projected = this.backend.connect().get('dogs_summary', 'spot'); var data = {age: 5, foo: 'bar'}; projected.create(data, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); doc.fetch(function(err) { if (err) return done(err); expect(doc.data).eql(undefined); diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 166c859e..8667fc81 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var async = require('async'); var util = require('../util'); @@ -83,6 +83,40 @@ module.exports = function(options) { }); }); + it('subscribed query removes document from results before sending delete op to other clients', function(done) { + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection1.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection1.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { + if (err) return done(err); + var query = connection2.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + if (err) return done(err); + connection1.get('dogs', 'fido').del(); + }); + var removed = false; + connection2.get('dogs', 'fido').on('del', function() { + expect(removed).equal(true); + done(); + }); + query.on('remove', function(docs, index) { + removed = true; + expect(util.pluck(docs, 'id')).eql(['fido']); + expect(util.pluck(docs, 'data')).eql([{age: 3}]); + expect(index).a('number'); + var results = util.sortById(query.results); + expect(util.pluck(results, 'id')).eql(['spot']); + expect(util.pluck(results, 'data')).eql([{age: 5}]); + }); + }); + }); + it('subscribed query does not get updated after destroyed', function(done) { var connection = this.backend.connect(); var connection2 = this.backend.connect(); @@ -202,7 +236,9 @@ module.exports = function(options) { }); query.on('remove', function(docs) { expect(util.pluck(docs, 'id')).eql(['fido']); - expect(util.pluck(docs, 'data')).eql([undefined]); + // We don't assert the value of data, because the del op could be + // applied by the client before or after the query result is removed. + // Order of ops & query result updates is not currently guaranteed finish(); }); }); diff --git a/test/client/query.js b/test/client/query.js index 785c5521..b6efc1a2 100644 --- a/test/client/query.js +++ b/test/client/query.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var async = require('async'); var util = require('../util'); diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 8d7cc4bf..445c53d8 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -1,5 +1,5 @@ var Backend = require('../../lib/backend'); -var expect = require('expect.js'); +var expect = require('chai').expect; var util = require('../util'); var lolex = require('lolex'); var MemoryDb = require('../../lib/db/memory'); @@ -183,7 +183,7 @@ describe('SnapshotTimestampRequest', function() { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function() {}); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('throws without a callback', function() { @@ -191,7 +191,7 @@ describe('SnapshotTimestampRequest', function() { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine'); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('throws if the timestamp is -1', function() { @@ -199,7 +199,7 @@ describe('SnapshotTimestampRequest', function() { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function() { }); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('errors if the timestamp is a string', function() { @@ -207,7 +207,7 @@ describe('SnapshotTimestampRequest', function() { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function() { }); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('returns an empty snapshot if trying to fetch a non-existent document', function(done) { @@ -229,11 +229,11 @@ describe('SnapshotTimestampRequest', function() { connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error) { if (error) return done(error); - expect(connection.hasPending()).to.be(false); + expect(connection.hasPending()).to.equal(false); done(); }); - expect(connection.hasPending()).to.be(true); + expect(connection.hasPending()).to.equal(true); }); it('deletes the request from the connection', function(done) { @@ -269,7 +269,7 @@ describe('SnapshotTimestampRequest', function() { }); connection.whenNothingPending(function() { - expect(snapshotFetched).to.be(true); + expect(snapshotFetched).to.equal(true); done(); }); }); @@ -328,7 +328,7 @@ describe('SnapshotTimestampRequest', function() { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request) { expect(request.snapshots[0]).to.eql(v3); - expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp); + expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byTimestamp); done(); } ); @@ -346,7 +346,7 @@ describe('SnapshotTimestampRequest', function() { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) { if (error) return done(error); - expect(snapshot.data.title).to.be('Alice in Wonderland'); + expect(snapshot.data.title).to.equal('Alice in Wonderland'); done(); }); }); @@ -359,7 +359,7 @@ describe('SnapshotTimestampRequest', function() { ]; backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error) { - expect(error.message).to.be('foo'); + expect(error.message).to.equal('foo'); done(); }); }); @@ -374,8 +374,8 @@ describe('SnapshotTimestampRequest', function() { backend.connect().fetchSnapshotByTimestamp('bookTitles', 'time-machine', day2, function(error, snapshot) { if (error) return done(error); - expect(snapshot.data.title).to.be('The Time Machine'); - expect(snapshot.data.author).to.be(undefined); + expect(snapshot.data.title).to.equal('The Time Machine'); + expect(snapshot.data.author).to.equal(undefined); done(); }); }); @@ -441,11 +441,11 @@ describe('SnapshotTimestampRequest', function() { .fetchSnapshotByTimestamp('books', 'mocking-bird', halfwayBetweenDays3and4, function(error, snapshot) { if (error) return done(error); - expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); - expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true); - expect(db.getOps.calledWith('books', 'mocking-bird', 2, 4)).to.be(true); + expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true); + expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true); + expect(db.getOps.calledWith('books', 'mocking-bird', 2, 4)).to.equal(true); - expect(snapshot.v).to.be(3); + expect(snapshot.v).to.equal(3); expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'}); done(); }); @@ -459,10 +459,10 @@ describe('SnapshotTimestampRequest', function() { .fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function(error, snapshot) { if (error) return done(error); - expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); - expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true); + expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true); + expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true); - expect(snapshot.v).to.be(2); + expect(snapshot.v).to.equal(2); expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'}); done(); }); @@ -477,11 +477,11 @@ describe('SnapshotTimestampRequest', function() { .fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function(error, snapshot) { if (error) return done(error); - expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); - expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true); - expect(db.getOps.calledWith('books', 'mocking-bird', 0, 2)).to.be(true); + expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true); + expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true); + expect(db.getOps.calledWith('books', 'mocking-bird', 0, 2)).to.equal(true); - expect(snapshot.v).to.be(1); + expect(snapshot.v).to.equal(1); expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird'}); done(); }); @@ -496,11 +496,11 @@ describe('SnapshotTimestampRequest', function() { .fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function(error, snapshot) { if (error) return done(error); - expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); - expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true); - expect(db.getOps.calledWith('books', 'mocking-bird', 4, null)).to.be(true); + expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true); + expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true); + expect(db.getOps.calledWith('books', 'mocking-bird', 4, null)).to.equal(true); - expect(snapshot.v).to.be(5); + expect(snapshot.v).to.equal(5); expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lee', diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index a39355f3..e2155d45 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -1,5 +1,5 @@ var Backend = require('../../lib/backend'); -var expect = require('expect.js'); +var expect = require('chai').expect; var MemoryDb = require('../../lib/db/memory'); var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); @@ -105,7 +105,7 @@ describe('SnapshotVersionRequest', function() { backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {}); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('fetches the latest version when the optional version is not provided', function(done) { @@ -121,7 +121,7 @@ describe('SnapshotVersionRequest', function() { backend.connect().fetchSnapshot('books', 'don-quixote'); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('throws if the version is -1', function() { @@ -129,7 +129,7 @@ describe('SnapshotVersionRequest', function() { backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {}); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('errors if the version is a string', function() { @@ -137,13 +137,13 @@ describe('SnapshotVersionRequest', function() { backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { }); }; - expect(fetch).to.throwError(); + expect(fetch).to.throw(Error); }); it('errors if asking for a version that does not exist', function(done) { backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) { - expect(error.code).to.be(4024); - expect(snapshot).to.be(undefined); + expect(error.code).to.equal(4024); + expect(snapshot).to.equal(undefined); done(); }); }); @@ -167,11 +167,11 @@ describe('SnapshotVersionRequest', function() { connection.fetchSnapshot('books', 'don-quixote', null, function(error) { if (error) return done(error); - expect(connection.hasPending()).to.be(false); + expect(connection.hasPending()).to.equal(false); done(); }); - expect(connection.hasPending()).to.be(true); + expect(connection.hasPending()).to.equal(true); }); it('deletes the request from the connection', function(done) { @@ -207,7 +207,7 @@ describe('SnapshotVersionRequest', function() { }); connection.whenNothingPending(function() { - expect(snapshotFetched).to.be(true); + expect(snapshotFetched).to.equal(true); done(); }); }); @@ -266,7 +266,7 @@ describe('SnapshotVersionRequest', function() { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request) { expect(request.snapshots[0]).to.eql(v3); - expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byVersion); + expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byVersion); done(); } ); @@ -284,7 +284,7 @@ describe('SnapshotVersionRequest', function() { backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) { if (error) return done(error); - expect(snapshot.data.title).to.be('Alice in Wonderland'); + expect(snapshot.data.title).to.equal('Alice in Wonderland'); done(); }); }); @@ -297,7 +297,7 @@ describe('SnapshotVersionRequest', function() { ]; backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) { - expect(error.message).to.be('foo'); + expect(error.message).to.equal('foo'); done(); }); }); @@ -312,8 +312,8 @@ describe('SnapshotVersionRequest', function() { backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) { if (error) return done(error); - expect(snapshot.data.title).to.be('Don Quixote'); - expect(snapshot.data.author).to.be(undefined); + expect(snapshot.data.title).to.equal('Don Quixote'); + expect(snapshot.data.author).to.equal(undefined); done(); }); }); @@ -435,9 +435,9 @@ describe('SnapshotVersionRequest', function() { backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next); }, function(snapshot, next) { - expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.be(true); - expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.be(true); - expect(snapshot.v).to.be(3); + expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.equal(true); + expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.equal(true); + expect(snapshot.v).to.equal(3); expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'}); next(); }, diff --git a/test/client/submit.js b/test/client/submit.js index 2e47a444..f626f9eb 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1,5 +1,5 @@ var async = require('async'); -var expect = require('expect.js'); +var expect = require('chai').expect; var types = require('../../lib/types'); var deserializedType = require('./deserialized-type'); var numberType = require('./number-type'); @@ -96,7 +96,7 @@ module.exports = function() { if (err) return done(err); doc.version++; doc.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); done(); }); }); @@ -105,7 +105,7 @@ module.exports = function() { it('cannot submit op on an uncreated doc', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); done(); }); }); @@ -113,7 +113,7 @@ module.exports = function() { it('cannot delete an uncreated doc', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.del(function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); done(); }); }); @@ -207,7 +207,7 @@ module.exports = function() { doc.create({age: 3}, function(err) { if (err) return done(err); doc.create({age: 4}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); expect(doc.version).equal(1); expect(doc.data).eql({age: 3}); done(); @@ -221,7 +221,7 @@ module.exports = function() { doc.create({age: 3}, function(err) { if (err) return done(err); doc2.create({age: 4}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); expect(doc2.version).equal(1); expect(doc2.data).eql({age: 3}); done(); @@ -262,7 +262,7 @@ module.exports = function() { doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); doc2.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); done(); }); }); @@ -287,7 +287,7 @@ module.exports = function() { doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); doc2.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); done(); }); }); @@ -432,7 +432,7 @@ module.exports = function() { expect(doc.version).eql(1); expect(doc.data).eql({age: 3}); } else { - expect(err).ok(); + expect(err).instanceOf(Error); expect(doc.version).eql(1); expect(doc.data).eql({age: 5}); done(); @@ -445,7 +445,7 @@ module.exports = function() { expect(doc2.version).eql(1); expect(doc2.data).eql({age: 5}); } else { - expect(err).ok(); + expect(err).instanceOf(Error); expect(doc2.version).eql(1); expect(doc2.data).eql({age: 3}); done(); @@ -537,15 +537,15 @@ module.exports = function() { docCallback(); } }); - doc.submitOp({p: ['age'], na: 2}, function(error) { - if (error) return done(error); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); // When we know the first op has been committed, we try to commit the second op, which will // fail because it's working on an out-of-date snapshot. It will retry, but exceed the // maxSubmitRetries limit of 0 doc2Callback(); }); - doc2.submitOp({p: ['age'], na: 7}, function(error) { - expect(error).ok(); + doc2.submitOp({p: ['age'], na: 7}, function(err) { + expect(err).instanceOf(Error); done(); }); }); @@ -616,7 +616,7 @@ module.exports = function() { doc2.del(function(err) { if (err) return done(err); doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); expect(doc.version).equal(1); expect(doc.data).eql({age: 3}); done(); @@ -1070,7 +1070,7 @@ module.exports = function() { doc.create({age: 3}, function(err) { if (err) return done(err); doc._submit({}, null, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); done(); }); }); @@ -1094,8 +1094,9 @@ module.exports = function() { var doc = this.backend.connect().get('dogs', 'fido'); doc.create([3], deserializedType.type.uri, function(err) { if (err) return done(err); - expect(doc.data).a(deserializedType.Node); - expect(doc.data).eql({value: 3, next: null}); + expect(doc.data).instanceOf(deserializedType.Node); + expect(doc.data.value).equal(3); + expect(doc.data.next).equal(null); done(); }); }); @@ -1120,8 +1121,9 @@ module.exports = function() { if (err) return done(err); doc2.fetch(function(err) { if (err) return done(err); - expect(doc2.data).a(deserializedType.Node); - expect(doc2.data).eql({value: 3, next: null}); + expect(doc2.data).instanceOf(deserializedType.Node); + expect(doc2.data.value).equal(3); + expect(doc2.data.next).equal(null); done(); }); }); @@ -1133,7 +1135,9 @@ module.exports = function() { if (err) return done(err); doc.submitOp({insert: 0, value: 2}, function(err) { if (err) return done(err); - expect(doc.data).eql({value: 2, next: {value: 3, next: null}}); + expect(doc.data.value).eql(2); + expect(doc.data.next.value).equal(3); + expect(doc.data.next.next).equal(null); done(); }); }); @@ -1150,7 +1154,10 @@ module.exports = function() { if (err) return done(err); doc2.submitOp({insert: 1, value: 4}, function(err) { if (err) return done(err); - expect(doc2.data).eql({value: 2, next: {value: 3, next: {value: 4, next: null}}}); + expect(doc2.data.value).equal(2); + expect(doc2.data.next.value).equal(3); + expect(doc2.data.next.next.value).equal(4); + expect(doc2.data.next.next.next).equal(null); done(); }); }); @@ -1164,8 +1171,9 @@ module.exports = function() { var doc = this.backend.connect().get('dogs', 'fido'); doc.create([3], deserializedType.type2.uri, function(err) { if (err) return done(err); - expect(doc.data).a(deserializedType.Node); - expect(doc.data).eql({value: 3, next: null}); + expect(doc.data).instanceOf(deserializedType.Node); + expect(doc.data.value).equal(3); + expect(doc.data.next).equal(null); done(); }); }); @@ -1174,8 +1182,9 @@ module.exports = function() { var doc = this.backend.connect().get('dogs', 'fido'); doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) { if (err) return done(err); - expect(doc.data).a(deserializedType.Node); - expect(doc.data).eql({value: 3, next: null}); + expect(doc.data).instanceOf(deserializedType.Node); + expect(doc.data.value).equal(3); + expect(doc.data.next).equal(null); done(); }); }); diff --git a/test/client/subscribe.js b/test/client/subscribe.js index f1c3a63a..fe53e9c3 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var async = require('async'); module.exports = function() { @@ -339,7 +339,7 @@ module.exports = function() { doc.pause(); var calls = 0; doc.create({age: 3}, function(err) { - expect(err).ok(); + expect(err).instanceOf(Error); calls++; }); doc[method](function(err) { diff --git a/test/db-memory.js b/test/db-memory.js index 0f0d01a9..e27ce0fe 100644 --- a/test/db-memory.js +++ b/test/db-memory.js @@ -1,6 +1,6 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var DB = require('../lib/db'); -var MemoryDB = require('../lib/db/memory'); +var BasicQueryableMemoryDB = require('./BasicQueryableMemoryDB'); describe('DB base class', function() { it('can call db.close() without callback', function() { @@ -16,7 +16,7 @@ describe('DB base class', function() { it('returns an error if db.commit() is unimplemented', function(done) { var db = new DB(); db.commit('testcollection', 'test', {}, {}, null, function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); done(); }); }); @@ -24,7 +24,7 @@ describe('DB base class', function() { it('returns an error if db.getSnapshot() is unimplemented', function(done) { var db = new DB(); db.getSnapshot('testcollection', 'foo', null, null, function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); done(); }); }); @@ -32,7 +32,7 @@ describe('DB base class', function() { it('returns an error if db.getOps() is unimplemented', function(done) { var db = new DB(); db.getOps('testcollection', 'foo', 0, null, null, function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); done(); }); }); @@ -40,7 +40,7 @@ describe('DB base class', function() { it('returns an error if db.query() is unimplemented', function(done) { var db = new DB(); db.query('testcollection', {x: 5}, null, null, function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); done(); }); }); @@ -48,75 +48,12 @@ describe('DB base class', function() { it('returns an error if db.queryPollDoc() is unimplemented', function(done) { var db = new DB(); db.queryPollDoc('testcollection', 'foo', {x: 5}, null, function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); done(); }); }); }); - -// Extension of MemoryDB that supports query filters and sorts on simple -// top-level properties, which is enough for the core ShareDB tests on -// query subscription updating. -function BasicQueryableMemoryDB() { - MemoryDB.apply(this, arguments); -} -BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype); -BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB; - -BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) { - if (query.filter) { - snapshots = snapshots.filter(function(snapshot) { - for (var queryKey in query.filter) { - // This fake only supports simple property equality filters, so - // throw an error on Mongo-like filter properties with dots. - if (queryKey.includes('.')) { - throw new Error('Only simple property filters are supported, got:', queryKey); - } - if (snapshot.data[queryKey] !== query.filter[queryKey]) { - return false; - } - } - return true; - }); - } - - if (query.sort) { - if (!Array.isArray(query.sort)) { - throw new Error('query.sort must be an array'); - } - if (query.sort.length) { - snapshots.sort(snapshotComparator(query.sort)); - } - } - - return {snapshots: snapshots}; -}; - -// sortProperties is an array whose items are each [propertyName, direction]. -function snapshotComparator(sortProperties) { - return function(snapshotA, snapshotB) { - for (var i = 0; i < sortProperties.length; i++) { - var sortProperty = sortProperties[i]; - var sortKey = sortProperty[0]; - var sortDirection = sortProperty[1]; - - var aPropVal = snapshotA.data[sortKey]; - var bPropVal = snapshotB.data[sortKey]; - if (aPropVal < bPropVal) { - return -1 * sortDirection; - } else if (aPropVal > bPropVal) { - return sortDirection; - } else if (aPropVal === bPropVal) { - continue; - } else { - throw new Error('Could not compare ' + aPropVal + ' and ' + bPropVal); - } - } - return 0; - }; -} - // Run all the DB-based tests against the BasicQueryableMemoryDB. require('./db')({ create: function(options, callback) { diff --git a/test/db.js b/test/db.js index 701bdb7a..0fd9364b 100644 --- a/test/db.js +++ b/test/db.js @@ -1,5 +1,5 @@ var async = require('async'); -var expect = require('expect.js'); +var expect = require('chai').expect; var Backend = require('../lib/backend'); var ot = require('../lib/ot'); @@ -36,7 +36,7 @@ module.exports = function(options) { }); }); - require('./client/projections')(); + require('./client/projections')({getQuery: getQuery}); require('./client/query-subscribe')({getQuery: getQuery}); require('./client/query')({getQuery: getQuery}); require('./client/submit')(); @@ -87,7 +87,7 @@ module.exports = function(options) { function testCreateCommit(ops, snapshot) { expect(snapshot.v).eql(1); expect(ops.length).equal(1); - expect(ops[0].create).ok(); + expect(ops[0].create).ok; } it('one commit succeeds from 2 simultaneous creates', function(done) { @@ -117,8 +117,8 @@ module.exports = function(options) { function testOpCommit(ops, snapshot) { expect(snapshot.v).equal(2); expect(ops.length).equal(2); - expect(ops[0].create).ok(); - expect(ops[1].op).ok(); + expect(ops[0].create).ok; + expect(ops[1].op).ok; } function testDelCommit(ops, snapshot) { @@ -126,7 +126,7 @@ module.exports = function(options) { expect(ops.length).equal(2); expect(snapshot.data).equal(undefined); expect(snapshot.type).equal(null); - expect(ops[0].create).ok(); + expect(ops[0].create).ok; expect(ops[1].del).equal(true); } @@ -734,7 +734,8 @@ module.exports = function(options) { var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { if (err) return done(err); - db.queryPoll('testcollection', {x: 5}, null, function(err, ids) { + var dbQuery = getQuery({query: {x: 5}}); + db.queryPoll('testcollection', dbQuery, null, function(err, ids) { if (err) return done(err); expect(ids).eql(['test']); done(); @@ -743,7 +744,8 @@ module.exports = function(options) { }); it('returns nothing when there is no data', function(done) { - this.db.queryPoll('testcollection', {x: 5}, null, function(err, ids) { + var dbQuery = getQuery({query: {x: 5}}); + this.db.queryPoll('testcollection', dbQuery, null, function(err, ids) { if (err) return done(err); expect(ids).eql([]); done(); @@ -753,11 +755,11 @@ module.exports = function(options) { describe('queryPollDoc', function() { it('returns false when the document does not exist', function(done) { - var query = {}; - if (!this.db.canPollDoc('testcollection', query)) return done(); + var dbQuery = getQuery({query: {}}); + if (!this.db.canPollDoc('testcollection', dbQuery)) return done(); var db = this.db; - db.queryPollDoc('testcollection', 'doesnotexist', query, null, function(err, result) { + db.queryPollDoc('testcollection', 'doesnotexist', dbQuery, null, function(err, result) { if (err) return done(err); expect(result).equal(false); done(); @@ -765,14 +767,14 @@ module.exports = function(options) { }); it('returns true when the document matches', function(done) { - var query = {x: 5}; - if (!this.db.canPollDoc('testcollection', query)) return done(); + var dbQuery = getQuery({query: {x: 5}}); + if (!this.db.canPollDoc('testcollection', dbQuery)) return done(); var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}}; var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { if (err) return done(err); - db.queryPollDoc('testcollection', 'test', query, null, function(err, result) { + db.queryPollDoc('testcollection', 'test', dbQuery, null, function(err, result) { if (err) return done(err); expect(result).equal(true); done(); @@ -781,14 +783,14 @@ module.exports = function(options) { }); it('returns false when the document does not match', function(done) { - var query = {x: 6}; - if (!this.db.canPollDoc('testcollection', query)) return done(); + var dbQuery = getQuery({query: {x: 6}}); + if (!this.db.canPollDoc('testcollection', dbQuery)) return done(); var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}}; var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { if (err) return done(err); - db.queryPollDoc('testcollection', 'test', query, null, function(err, result) { + db.queryPollDoc('testcollection', 'test', dbQuery, null, function(err, result) { if (err) return done(err); expect(result).equal(false); done(); diff --git a/test/logger.js b/test/logger.js index 4efbb0cf..72f75e90 100644 --- a/test/logger.js +++ b/test/logger.js @@ -1,5 +1,5 @@ var Logger = require('../lib/logger/logger'); -var expect = require('expect.js'); +var expect = require('chai').expect; var sinon = require('sinon'); describe('Logger', function() { @@ -15,7 +15,7 @@ describe('Logger', function() { it('logs to console by default', function() { var logger = new Logger(); logger.warn('warning'); - expect(console.warn.calledOnceWithExactly('warning')).to.be(true); + expect(console.warn.calledOnceWithExactly('warning')).to.equal(true); }); it('overrides console', function() { @@ -27,8 +27,8 @@ describe('Logger', function() { logger.warn('warning'); - expect(console.warn.notCalled).to.be(true); - expect(customWarn.calledOnceWithExactly('warning')).to.be(true); + expect(console.warn.notCalled).to.equal(true); + expect(customWarn.calledOnceWithExactly('warning')).to.equal(true); }); it('only overrides if provided with a method', function() { @@ -40,7 +40,7 @@ describe('Logger', function() { logger.warn('warning'); - expect(console.warn.calledOnceWithExactly('warning')).to.be(true); + expect(console.warn.calledOnceWithExactly('warning')).to.equal(true); }); }); }); diff --git a/test/middleware.js b/test/middleware.js index 0da9f068..9f1ef9f4 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -1,5 +1,5 @@ var Backend = require('../lib/backend'); -var expect = require('expect.js'); +var expect = require('chai').expect; var util = require('./util'); var types = require('../lib/types'); @@ -24,6 +24,11 @@ describe('middleware', function() { var response = this.backend.use('submit', function() {}); expect(response).equal(this.backend); }); + + it('accepts an array of action names', function() { + var response = this.backend.use(['submit', 'connect'], function() {}); + expect(response).equal(this.backend); + }); }); describe('connect', function() { @@ -208,7 +213,7 @@ describe('middleware', function() { }); describe('submit lifecycle', function() { - ['submit', 'apply', 'commit', 'afterSubmit'].forEach(function(action) { + ['submit', 'apply', 'commit', 'afterWrite'].forEach(function(action) { it(action + ' gets options passed to backend.submit', function(done) { var doneAfter = util.callAfter(1, done); this.backend.use(action, function(request, next) { @@ -222,4 +227,89 @@ describe('middleware', function() { }); }); }); + + describe('access control', function() { + function setupOpMiddleware(backend) { + backend.use('apply', function(request, next) { + request.priorAccountId = request.snapshot.data && request.snapshot.data.accountId; + next(); + }); + backend.use('commit', function(request, next) { + var accountId = (request.snapshot.data) ? + // For created documents, get the accountId from the document data + request.snapshot.data.accountId : + // For deleted documents, get the accountId from before + request.priorAccountId; + // Store the accountId for the document on the op for efficient access control + request.op.accountId = accountId; + next(); + }); + backend.use('op', function(request, next) { + if (request.op.accountId === request.agent.accountId) { + return next(); + } + var err = {message: 'op accountId does not match', code: 'ERR_OP_READ_FORBIDDEN'}; + return next(err); + }); + } + + it('is possible to cache add additional top-level fields on ops for access control', function(done) { + setupOpMiddleware(this.backend); + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + connection2.agent.accountId = 'foo'; + + // Fetching the snapshot here will cause subsequent fetches to get ops + connection2.get('dogs', 'fido').fetch(function(err) { + if (err) return done(err); + var data = {accountId: 'foo', age: 2}; + connection1.get('dogs', 'fido').create(data, function(err) { + if (err) return done(err); + // This will go through the 'op' middleware and should pass + connection2.get('dogs', 'fido').fetch(done); + }); + }); + }); + + it('op middleware can reject ops', function(done) { + setupOpMiddleware(this.backend); + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + connection2.agent.accountId = 'baz'; + + // Fetching the snapshot here will cause subsequent fetches to get ops + connection2.get('dogs', 'fido').fetch(function(err) { + if (err) return done(err); + var data = {accountId: 'foo', age: 2}; + connection1.get('dogs', 'fido').create(data, function(err) { + if (err) return done(err); + // This will go through the 'op' middleware and fail; + connection2.get('dogs', 'fido').fetch(function(err) { + expect(err.code).equal('ERR_OP_READ_FORBIDDEN'); + done(); + }); + }); + }); + }); + + it('pubsub subscribe can check top-level fields for access control', function(done) { + setupOpMiddleware(this.backend); + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + connection2.agent.accountId = 'foo'; + + // Fetching the snapshot here will cause subsequent fetches to get ops + connection2.get('dogs', 'fido').subscribe(function(err) { + if (err) return done(err); + var data = {accountId: 'foo', age: 2}; + connection1.get('dogs', 'fido').create(data, function(err) { + if (err) return done(err); + // The subscribed op will go through the 'op' middleware and should pass + connection2.get('dogs', 'fido').on('create', function() { + done(); + }); + }); + }); + }); + }); }); diff --git a/test/milestone-db.js b/test/milestone-db.js index 223423d4..b7521636 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var Backend = require('../lib/backend'); var MilestoneDB = require('../lib/milestone-db'); var NoOpMilestoneDB = require('../lib/milestone-db/no-op'); @@ -14,14 +14,14 @@ describe('Base class', function() { it('calls back with an error when trying to get a snapshot', function(done) { db.getMilestoneSnapshot('books', '123', 1, function(error) { - expect(error.code).to.be(5019); + expect(error.code).to.equal(5019); done(); }); }); it('emits an error when trying to get a snapshot', function(done) { db.on('error', function(error) { - expect(error.code).to.be(5019); + expect(error.code).to.equal(5019); done(); }); @@ -30,14 +30,14 @@ describe('Base class', function() { it('calls back with an error when trying to save a snapshot', function(done) { db.saveMilestoneSnapshot('books', {}, function(error) { - expect(error.code).to.be(5020); + expect(error.code).to.equal(5020); done(); }); }); it('emits an error when trying to save a snapshot', function(done) { db.on('error', function(error) { - expect(error.code).to.be(5020); + expect(error.code).to.equal(5020); done(); }); @@ -46,14 +46,14 @@ describe('Base class', function() { it('calls back with an error when trying to get a snapshot before a time', function(done) { db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function(error) { - expect(error.code).to.be(5021); + expect(error.code).to.equal(5021); done(); }); }); it('calls back with an error when trying to get a snapshot after a time', function(done) { db.getMilestoneSnapshotAtOrAfterTime('books', '123', 1000, function(error) { - expect(error.code).to.be(5022); + expect(error.code).to.equal(5022); done(); }); }); @@ -83,7 +83,7 @@ describe('NoOpMilestoneDB', function() { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, done @@ -269,35 +269,35 @@ module.exports = function(options) { it('errors when fetching an undefined version', function(done) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('errors when fetching version -1', function(done) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('errors when fetching version "foo"', function(done) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('errors when fetching a null collection', function(done) { db.getMilestoneSnapshot(null, 'catcher-in-the-rye', 1, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('errors when fetching a null ID', function(done) { db.getMilestoneSnapshot('books', null, 1, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -312,7 +312,7 @@ module.exports = function(options) { ); db.saveMilestoneSnapshot(null, snapshot, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -323,7 +323,7 @@ module.exports = function(options) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, done @@ -340,7 +340,7 @@ module.exports = function(options) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, done @@ -357,7 +357,7 @@ module.exports = function(options) { ); db.on('save', function(collection, snapshot) { - expect(collection).to.be('books'); + expect(collection).to.equal('books'); expect(snapshot).to.eql(snapshot); done(); }); @@ -367,7 +367,7 @@ module.exports = function(options) { it('errors when the snapshot is undefined', function(done) { db.saveMilestoneSnapshot('books', undefined, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -471,14 +471,14 @@ module.exports = function(options) { it('returns an error for a string timestamp', function(done) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('returns an error for a negative timestamp', function(done) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', -1, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -489,7 +489,7 @@ module.exports = function(options) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 0, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, done @@ -498,14 +498,14 @@ module.exports = function(options) { it('errors if no collection is provided', function(done) { db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('errors if no ID is provided', function(done) { db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -553,14 +553,14 @@ module.exports = function(options) { it('returns an error for a string timestamp', function(done) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('returns an error for a negative timestamp', function(done) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', -1, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -571,7 +571,7 @@ module.exports = function(options) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 4000, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, done @@ -580,14 +580,14 @@ module.exports = function(options) { it('errors if no collection is provided', function(done) { db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); it('errors if no ID is provided', function(done) { db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function(error) { - expect(error).to.be.ok(); + expect(error).instanceOf(Error); done(); }); }); @@ -608,7 +608,7 @@ module.exports = function(options) { it('stores a milestone snapshot on commit', function(done) { db.on('save', function(collection, snapshot) { - expect(collection).to.be('books'); + expect(collection).to.equal('books'); expect(snapshot.data).to.eql({title: 'Catcher in the Rye'}); done(); }); @@ -639,28 +639,28 @@ module.exports = function(options) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, function(snapshot, next) { - expect(snapshot.v).to.be(2); + expect(snapshot.v).to.equal(2); next(); }, function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next); }, function(snapshot, next) { - expect(snapshot.v).to.be(2); + expect(snapshot.v).to.equal(2); next(); }, function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, function(snapshot, next) { - expect(snapshot.v).to.be(4); + expect(snapshot.v).to.equal(4); next(); }, done @@ -699,28 +699,28 @@ module.exports = function(options) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, function(snapshot, next) { - expect(snapshot).to.be(undefined); + expect(snapshot).to.equal(undefined); next(); }, function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next); }, function(snapshot, next) { - expect(snapshot.v).to.be(3); + expect(snapshot.v).to.equal(3); next(); }, function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, function(snapshot, next) { - expect(snapshot.v).to.be(4); + expect(snapshot.v).to.equal(4); next(); }, done diff --git a/test/ot.js b/test/ot.js index b58b9c89..379d017d 100644 --- a/test/ot.js +++ b/test/ot.js @@ -1,33 +1,33 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var ot = require('../lib/ot'); var type = require('../lib/types').defaultType; describe('ot', function() { describe('checkOp', function() { it('fails if op is not an object', function() { - expect(ot.checkOp('hi')).ok(); - expect(ot.checkOp()).ok(); - expect(ot.checkOp(123)).ok(); - expect(ot.checkOp([])).ok(); + expect(ot.checkOp('hi')).ok; + expect(ot.checkOp()).ok; + expect(ot.checkOp(123)).ok; + expect(ot.checkOp([])).ok; }); it('fails if op data is missing op, create and del', function() { - expect(ot.checkOp({v: 5})).ok(); + expect(ot.checkOp({v: 5})).ok; }); it('fails if src/seq data is invalid', function() { - expect(ot.checkOp({del: true, v: 5, src: 'hi'})).ok(); - expect(ot.checkOp({del: true, v: 5, seq: 123})).ok(); - expect(ot.checkOp({del: true, v: 5, src: 'hi', seq: 'there'})).ok(); + expect(ot.checkOp({del: true, v: 5, src: 'hi'})).ok; + expect(ot.checkOp({del: true, v: 5, seq: 123})).ok; + expect(ot.checkOp({del: true, v: 5, src: 'hi', seq: 'there'})).ok; }); it('fails if a create operation is missing its type', function() { - expect(ot.checkOp({create: {}})).ok(); - expect(ot.checkOp({create: 123})).ok(); + expect(ot.checkOp({create: {}})).ok; + expect(ot.checkOp({create: 123})).ok; }); it('fails if the type is missing', function() { - expect(ot.checkOp({create: {type: 'something that does not exist'}})).ok(); + expect(ot.checkOp({create: {type: 'something that does not exist'}})).ok; }); it('accepts valid create operations', function() { @@ -54,12 +54,12 @@ describe('ot', function() { describe('apply', function() { it('fails if the versions dont match', function() { - expect(ot.apply({v: 0}, {v: 1, create: {type: type.uri}})).ok(); - expect(ot.apply({v: 0}, {v: 1, del: true})).ok(); - expect(ot.apply({v: 0}, {v: 1, op: []})).ok(); - expect(ot.apply({v: 5}, {v: 4, create: {type: type.uri}})).ok(); - expect(ot.apply({v: 5}, {v: 4, del: true})).ok(); - expect(ot.apply({v: 5}, {v: 4, op: []})).ok(); + expect(ot.apply({v: 0}, {v: 1, create: {type: type.uri}})).ok; + expect(ot.apply({v: 0}, {v: 1, del: true})).ok; + expect(ot.apply({v: 0}, {v: 1, op: []})).ok; + expect(ot.apply({v: 5}, {v: 4, create: {type: type.uri}})).ok; + expect(ot.apply({v: 5}, {v: 4, del: true})).ok; + expect(ot.apply({v: 5}, {v: 4, op: []})).ok; }); it('allows the version field to be missing', function() { @@ -71,7 +71,7 @@ describe('ot', function() { describe('create', function() { it('fails if the document already exists', function() { var doc = {v: 6, create: {type: type.uri}}; - expect(ot.apply({v: 6, type: type.uri, data: 'hi'}, doc)).ok(); + expect(ot.apply({v: 6, type: type.uri, data: 'hi'}, doc)).ok; // The doc should be unmodified expect(doc).eql({v: 6, create: {type: type.uri}}); }); @@ -105,11 +105,11 @@ describe('ot', function() { describe('op', function() { it('fails if the document does not exist', function() { - expect(ot.apply({v: 6}, {v: 6, op: [1, 2, 3]})).ok(); + expect(ot.apply({v: 6}, {v: 6, op: [1, 2, 3]})).ok; }); it('fails if the type is missing', function() { - expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1, 2, 3]})).ok(); + expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1, 2, 3]})).ok; }); it('applies the operation to the document data', function() { @@ -138,21 +138,21 @@ describe('ot', function() { it('fails if the version is specified on both and does not match', function() { var op1 = {v: 5, op: [{p: [10], si: 'hi'}]}; var op2 = {v: 6, op: [{p: [5], si: 'abcde'}]}; - expect(ot.transform(type.uri, op1, op2)).ok(); + expect(ot.transform(type.uri, op1, op2)).ok; expect(op1).eql({v: 5, op: [{p: [10], si: 'hi'}]}); }); // There's 9 cases here. it('create by create fails', function() { - expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, create: {type: type.uri}})).ok(); + expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, create: {type: type.uri}})).ok; }); it('create by delete fails', function() { - expect(ot.transform(null, {create: {type: type.uri}}, {del: true})).ok(); + expect(ot.transform(null, {create: {type: type.uri}}, {del: true})).ok; }); it('create by op fails', function() { - expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, op: [15, 'hi']})).ok(); + expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, op: [15, 'hi']})).ok; }); it('create by noop ok', function() { @@ -162,7 +162,7 @@ describe('ot', function() { }); it('delete by create fails', function() { - expect(ot.transform(null, {del: true}, {create: {type: type.uri}})).ok(); + expect(ot.transform(null, {del: true}, {create: {type: type.uri}})).ok; }); it('delete by delete ok', function() { @@ -198,11 +198,11 @@ describe('ot', function() { }); it('op by create fails', function() { - expect(ot.transform(null, {op: {}}, {create: {type: type.uri}})).ok(); + expect(ot.transform(null, {op: {}}, {create: {type: type.uri}})).ok; }); it('op by delete fails', function() { - expect(ot.transform(type.uri, {v: 10, op: []}, {v: 10, del: true})).ok(); + expect(ot.transform(type.uri, {v: 10, op: []}, {v: 10, del: true})).ok; }); it('op by op ok', function() { diff --git a/test/projections.js b/test/projections.js index 52dc7261..7bf5eb2a 100644 --- a/test/projections.js +++ b/test/projections.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; var projections = require('../lib/projections'); var type = require('../lib/types').defaultType.uri; @@ -12,7 +12,7 @@ describe('projection utility methods', function() { it('throws on snapshots with the wrong type', function() { expect(function() { projections.projectSnapshot({}, {type: 'other', data: 123}); - }).throwException(); + }).throw(Error); }); it('empty object filters out all properties', function() { @@ -244,7 +244,7 @@ describe('projection utility methods', function() { it('throws on create ops with the wrong type', function() { expect(function() { projections.projectOp({}, {create: {type: 'other', data: 123}}); - }).throwException(); + }).throw(Error); }); it('strips data in creates', function() { diff --git a/test/pubsub-memory.js b/test/pubsub-memory.js index 498de699..371d982e 100644 --- a/test/pubsub-memory.js +++ b/test/pubsub-memory.js @@ -1,6 +1,6 @@ var MemoryPubSub = require('../lib/pubsub/memory'); var PubSub = require('../lib/pubsub'); -var expect = require('expect.js'); +var expect = require('chai').expect; require('./pubsub')(function(callback) { callback(null, new MemoryPubSub()); @@ -13,7 +13,7 @@ describe('PubSub base class', function() { it('returns an error if _subscribe is unimplemented', function(done) { var pubsub = new PubSub(); pubsub.subscribe('x', function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); expect(err.code).to.equal(5015); done(); }); @@ -22,7 +22,7 @@ describe('PubSub base class', function() { it('emits an error if _subscribe is unimplemented and callback is not provided', function(done) { var pubsub = new PubSub(); pubsub.on('error', function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); expect(err.code).to.equal(5015); done(); }); @@ -37,7 +37,7 @@ describe('PubSub base class', function() { pubsub.subscribe('x', function(err, stream) { if (err) return done(err); pubsub.on('error', function(err) { - expect(err).to.be.an(Error); + expect(err).instanceOf(Error); expect(err.code).to.equal(5016); done(); }); @@ -49,7 +49,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.on('error', done); pubsub.publish(['x', 'y'], {test: true}, function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); expect(err.code).to.equal(5017); done(); }); @@ -58,7 +58,7 @@ describe('PubSub base class', function() { it('emits an error if _publish is unimplemented and callback is not provided', function(done) { var pubsub = new PubSub(); pubsub.on('error', function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); expect(err.code).to.equal(5017); done(); }); @@ -68,7 +68,7 @@ describe('PubSub base class', function() { it('can emit events', function(done) { var pubsub = new PubSub(); pubsub.on('error', function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); expect(err.message).equal('test error'); done(); }); diff --git a/test/pubsub.js b/test/pubsub.js index 57c19309..123b9496 100644 --- a/test/pubsub.js +++ b/test/pubsub.js @@ -1,4 +1,4 @@ -var expect = require('expect.js'); +var expect = require('chai').expect; module.exports = function(create) { describe('pubsub', function() { @@ -78,7 +78,7 @@ module.exports = function(create) { it('can emit events', function(done) { this.pubsub.on('error', function(err) { - expect(err).an(Error); + expect(err).instanceOf(Error); expect(err.message).equal('test error'); done(); });