Skip to content

Commit

Permalink
Merge pull request #7406 from Automattic/gh7079
Browse files Browse the repository at this point in the history
WIP: casting arrayFilters
  • Loading branch information
vkarpov15 committed Jan 14, 2019
2 parents 804e057 + 5b5c728 commit d8ac587
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 1 deletion.
2 changes: 1 addition & 1 deletion lib/cast.js
Expand Up @@ -337,4 +337,4 @@ function _cast(val, numbertype, context) {
}
}
}
}
}
54 changes: 54 additions & 0 deletions lib/helpers/query/castFilterPath.js
@@ -0,0 +1,54 @@
'use strict';

module.exports = function castFilterPath(query, schematype, val) {
const any$conditionals = Object.keys(val).some(function(k) {
return k.charAt(0) === '$' && k !== '$id' && k !== '$ref';
});

if (!any$conditionals) {
return schematype.castForQueryWrapper({
val: val,
context: query
});
}

const ks = Object.keys(val);

let k = ks.length;

while (k--) {
const $cond = ks[k];
const nested = val[$cond];

if ($cond === '$not') {
if (nested && schematype && !schematype.caster) {
const _keys = Object.keys(nested);
if (_keys.length && _keys[0].charAt(0) === '$') {
for (const key in nested) {
nested[key] = schematype.castForQueryWrapper({
$conditional: key,
val: nested[key],
context: context
});
}
} else {
val[$cond] = schematype.castForQueryWrapper({
$conditional: $cond,
val: nested,
context: context
});
}
continue;
}
// cast(schematype.caster ? schematype.caster.schema : schema, nested, options, context);
} else {
val[$cond] = schematype.castForQueryWrapper({
$conditional: $cond,
val: nested,
context: context
});
}
}

return val;
};
62 changes: 62 additions & 0 deletions lib/helpers/update/castArrayFilters.js
@@ -0,0 +1,62 @@
'use strict';

const castFilterPath = require('../query/castFilterPath');
const modifiedPaths = require('./modifiedPaths');

module.exports = function castArrayFilters(query) {
const arrayFilters = query.options.arrayFilters;
if (!Array.isArray(arrayFilters)) {
return;
}

const update = query.getUpdate();
const schema = query.schema;

const updatedPaths = modifiedPaths(update);

const updatedPathsByFilter = Object.keys(updatedPaths).reduce((cur, path) => {
const matches = path.match(/\$\[[^\]]+\]/g);
if (matches == null) {
return cur;
}
for (const match of matches) {
const firstMatch = path.indexOf(match);
if (firstMatch !== path.lastIndexOf(match)) {
throw new Error(`Path '${path}' contains the same array filter multiple times`);
}
cur[match.substring(2, match.length - 1)] = path.substr(0, firstMatch - 1);
}
return cur;
}, {});

for (const filter of arrayFilters) {
if (filter == null) {
throw new Error(`Got null array filter in ${arrayFilters}`);
}
const firstKey = Object.keys(filter)[0];

if (filter[firstKey] == null) {
continue;
}

const dot = firstKey.indexOf('.');
let filterPath = dot === -1 ?
updatedPathsByFilter[firstKey] + '.0' :
updatedPathsByFilter[firstKey.substr(0, dot)] + '.0' + firstKey.substr(dot);

if (filterPath == null) {
throw new Error(`Filter path not found for ${firstKey}`);
}

// If there are multiple array filters in the path being updated, make sure
// to replace them so we can get the schema path.
filterPath = filterPath.replace(/\$\[[^\]]+\]/g, '0');

const schematype = schema.path(filterPath);
if (typeof filter[firstKey] === 'object') {
filter[firstKey] = castFilterPath(query, schematype, filter[firstKey]);
} else {
filter[firstKey] = schematype.castForQuery(filter[firstKey]);
}
}
};
17 changes: 17 additions & 0 deletions lib/query.js
Expand Up @@ -11,6 +11,7 @@ const QueryCursor = require('./cursor/QueryCursor');
const ReadPreference = require('./driver').get().ReadPreference;
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
const cast = require('./cast');
const castArrayFilters = require('./helpers/update/castArrayFilters');
const castUpdate = require('./helpers/query/castUpdate');
const completeMany = require('./helpers/query/completeMany');
const get = require('./helpers/get');
Expand Down Expand Up @@ -1728,6 +1729,18 @@ Query.prototype._castConditions = function() {
}
};

/*!
* ignore
*/

function _castArrayFilters(query) {
try {
castArrayFilters(query);
} catch (err) {
query.error(err);
}
}

/**
* Thunk around find()
*
Expand Down Expand Up @@ -3147,6 +3160,8 @@ Query.prototype._findAndModify = function(type, callback) {
return callback(castedQuery);
}

_castArrayFilters(this);

const opts = this._optionsForExec(model);

if ('strict' in opts) {
Expand Down Expand Up @@ -3385,6 +3400,8 @@ function _updateThunk(op, callback) {

this._castConditions();

_castArrayFilters(this);

if (this.error() != null) {
callback(this.error());
return null;
Expand Down
65 changes: 65 additions & 0 deletions test/helpers/update.castArrayFilters.test.js
@@ -0,0 +1,65 @@
'use strict';

const Query = require('../../lib/query');
const Schema = require('../../lib/schema');
const assert = require('assert');
const castArrayFilters = require('../../lib/helpers/update/castArrayFilters');

describe('castArrayFilters', function() {
it('works', function(done) {
const schema = new Schema({ comments: [{ date: Date }] });
const q = new Query();
q.schema = schema;

q.updateOne({}, { $set: { 'comments.$[x].date': '2018-01-01' } }, {
arrayFilters: [{ 'x.date': { $gte: '2018-01-01' } }]
});
castArrayFilters(q);

assert.ok(q.options.arrayFilters[0]['x.date'].$gte instanceof Date);

done();
});

it('casts multiple', function(done) {
const schema = new Schema({
comments: [{
text: String,
replies: [{ date: Date }]
}]
});
const q = new Query();
q.schema = schema;

q.updateOne({}, { $set: { 'comments.$[x].replies.$[y].date': '2018-01-01' } }, {
arrayFilters: [{ 'x.text': 123 }, { 'y.date': { $gte: '2018-01-01' } }]
});
castArrayFilters(q);

assert.strictEqual(q.options.arrayFilters[0]['x.text'], '123');
assert.ok(q.options.arrayFilters[1]['y.date'].$gte instanceof Date);

done();
});

it('sane error on same filter twice', function(done) {
const schema = new Schema({
comments: [{
text: String,
replies: [{ date: Date }]
}]
});
const q = new Query();
q.schema = schema;

q.updateOne({}, { $set: { 'comments.$[x].replies.$[x].date': '2018-01-01' } }, {
arrayFilters: [{ 'x.text': 123 }]
});

assert.throws(() => {
castArrayFilters(q);
}, /same array filter/);

done();
});
});
26 changes: 26 additions & 0 deletions test/model.test.js
Expand Up @@ -4947,6 +4947,32 @@ describe('Model', function() {
});
});

it('arrayFilter casting (gh-5965) (gh-7079)', function() {
return co(function*() {
const MyModel = db.model('gh7079', new Schema({
_id: Number,
grades: [Number]
}));

yield MyModel.create([
{ _id: 1, grades: [95, 92, 90] },
{ _id: 2, grades: [98, 100, 102] },
{ _id: 3, grades: [95, 110, 100] }
]);

yield MyModel.updateMany({}, { $set: { 'grades.$[element]': 100 } }, {
arrayFilters: [{
element: { $gte: '100', $lte: { valueOf: () => 109 } }
}]
});

const docs = yield MyModel.find().sort({ _id: 1 });
assert.deepEqual(docs[0].toObject().grades, [95, 92, 90]);
assert.deepEqual(docs[1].toObject().grades, [98, 100, 100]);
assert.deepEqual(docs[2].toObject().grades, [95, 110, 100]);
});
});

describe('watch()', function() {
before(function() {
if (!process.env.REPLICA_SET) {
Expand Down

0 comments on commit d8ac587

Please sign in to comment.