diff --git a/index.js b/index.js index c283685..44cc085 100644 --- a/index.js +++ b/index.js @@ -1044,10 +1044,44 @@ function getInnerFields(params, fields) { function opContainsAnyField(op, fields) { for (var i = 0; i < op.length; i++) { var component = op[i]; - if (component.p.length === 0) { - return true; - } else if (fields[component.p[0]]) { + if (Array.isArray(component.p)) { + // json0 op component: + // + // Each op component has its own array of `p` path segments from doc root. + if (component.p.length === 0) { + return true; + } else if (fields[component.p[0]]) { + return true; + } + } else if (typeof component === 'string') { + // json1 field descent from root: + // + // First string in top-level array means all subsequent operations will be + // underneath that top-level field, no need to continue iterating. + return fields[component]; + } else if (hasOwnProperty(component, 'p') || + hasOwnProperty(component, 'r') || + hasOwnProperty(component, 'd') || + hasOwnProperty(component, 'i') || + hasOwnProperty(component, 'e')) { + // json1 root-level operation: + // + // If we encounter an operation at top level prior to encountering a string, + // (field descent) then the operation affects the entire document. return true; + } else if (Array.isArray(component)) { + // json1 child operation list: + // + // In a canonical json1 op, if we encounter a child op prior to encountering + // a string (field descent), then the child should start with a field descent. + // If that weren't the case, the op would be pulled up to the top-level array. + var descendant = component; + while (typeof descendant[0] !== 'string') { + descendant = descendant[0]; + } + if (fields[descendant[0]]) { + return true; + } } } return false; @@ -1457,6 +1491,10 @@ function isPlainObject(value) { ); } +function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + // Convert a simple map of fields that we want into a mongo projection. This // depends on the data being stored at the top level of the document. It will // only work properly for json documents--which are the only types for which diff --git a/package.json b/package.json index cd4a40d..5583c94 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "mongodb4": "npm:mongodb@^4.0.0", "nyc": "^14.1.1", "ot-json1": "^1.0.1", - "sharedb-mingo-memory": "^1.1.1", + "sharedb-mingo-memory": "^2.0.0", "sinon": "^6.1.5", "sinon-chai": "^3.7.0" }, diff --git a/test/test_skip_poll.js b/test/test_skip_poll.js index 587f405..9840b5b 100644 --- a/test/test_skip_poll.js +++ b/test/test_skip_poll.js @@ -1,4 +1,5 @@ var expect = require('chai').expect; +var json1 = require('ot-json1'); var ShareDbMongo = require('../index'); describe('skipPoll', function() { @@ -111,6 +112,51 @@ describe('skipPoll', function() { assertNotSkips({op: [{p: ['a'], dummyOp: 1}, {p: [], dummyOp: 1}]}, query); assertNotSkips({op: [{p: [], dummyOp: 1}, {p: ['x'], dummyOp: 1}]}, query); }); + + it('json1 root-level changes', function() { + // Root-level doc changes should always cause a query poll. + assertNotSkips({op: json1.insertOp('', {brandNew: 'value'})}, query); + assertNotSkips({op: json1.removeOp('')}, query); + }); + + it('json1 ops not affecting queried fields', function() { + assertSkips({op: json1.insertOp(['notQueried'], 'hello')}, query); + assertSkips({op: json1.moveOp(['notQueried'], ['alsoNotQueried'])}, query); + assertSkips({op: json1.removeOp(['notQueried'], 'hello')}, query); + }); + + it('json1 insert ops', function() { + assertIfSkips({op: json1.insertOp(['a'], 'hello')}, query, !has(fields, 'a')); + assertIfSkips({op: json1.insertOp(['a', 'b'], 'hello')}, query, !has(fields, 'a')); + assertIfSkips({op: json1.insertOp(['a', 0], 'hello')}, query, !has(fields, 'a')); + }); + + it('json1 remove ops', function() { + assertIfSkips({op: json1.removeOp(['a'], 'hello')}, query, !has(fields, 'a')); + assertIfSkips({op: json1.removeOp(['a', 'b'], 'hello')}, query, !has(fields, 'a')); + assertIfSkips({op: json1.removeOp(['a', 0], 'hello')}, query, !has(fields, 'a')); + }); + + it('json1 move ops', function() { + assertIfSkips({op: json1.moveOp(['a'], ['x'])}, query, !has(fields, 'a') && !has(fields, 'x')); + assertIfSkips({op: json1.moveOp(['x'], ['a'])}, query, !has(fields, 'x') && !has(fields, 'a')); + assertIfSkips({op: json1.moveOp(['a'], ['notQueried'])}, query, !has(fields, 'a')); + assertIfSkips({op: json1.moveOp(['notQueried'], ['a'])}, query, !has(fields, 'a')); + assertSkips({op: json1.moveOp(['notQueried'], ['alsoNotQueried'])}, query); + }); + + it('json1 composed ops', function() { + var compositeNotQueried = json1.type.compose( + json1.insertOp(['notQueried1'], 'hello'), + json1.moveOp(['notQueried2'], ['notQueried3']) + ); + assertSkips({op: compositeNotQueried}, query); + var compositeQueried = json1.type.compose( + json1.insertOp(['notQueried1'], 'hello'), + json1.moveOp(['notQueried2'], ['a']) + ); + assertIfSkips({op: compositeQueried}, query, !has(fields, 'a')); + }); }); } });