diff --git a/lib/dao.js b/lib/dao.js index 4c3485a8..a0506ab2 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -181,6 +181,31 @@ DataAccessObject.getConnector = function() { return this.getDataSource().connector; }; +/** + * Verify if allowExtendedOperators is enabled + * @options {Object} [options] Optional options to use. + * @property {Boolean} allowExtendedOperators. + * @returns {Boolean} Returns `true` if allowExtendedOperators is enabled, else `false`. + */ +DataAccessObject._allowExtendedOperators = function(options) { + options = options || {}; + + var Model = this; + var dsSettings = this.getDataSource().settings; + var allowExtendedOperators = dsSettings.allowExtendedOperators; + // options settings enable allowExtendedOperators per request (for example if + // enable allowExtendedOperators only server side); + // model settings enable allowExtendedOperators only for specific model. + // dataSource settings enable allowExtendedOperators globally (all models); + // options -> model -> dataSource (connector) + if (options.hasOwnProperty('allowExtendedOperators')) { + allowExtendedOperators = options.allowExtendedOperators === true; + } else if (Model.settings && Model.settings.hasOwnProperty('allowExtendedOperators')) { + allowExtendedOperators = Model.settings.allowExtendedOperators === true; + } + return allowExtendedOperators; +}; + // Empty callback function function noCallback(err, result) { // NOOP @@ -720,9 +745,9 @@ DataAccessObject.upsertWithWhere = function(where, data, options, cb) { function callConnector() { try { ctx.where = removeUndefined(ctx.where); - ctx.where = Model._coerce(ctx.where); + ctx.where = Model._coerce(ctx.where, options); update = removeUndefined(update); - update = Model._coerce(update); + update = Model._coerce(update, options); } catch (err) { return process.nextTick(function() { cb(err); @@ -1110,7 +1135,7 @@ DataAccessObject.findOrCreate = function findOrCreate(query, data, options, cb) query.limit = 1; try { - this._normalize(query); + this._normalize(query, options); } catch (err) { process.nextTick(function() { cb(err); @@ -1413,10 +1438,12 @@ var operators = { /* * Normalize the filter object and throw errors if invalid values are detected * @param {Object} filter The query filter object + * @options {Object} [options] Optional options to use. + * @property {Boolean} allowExtendedOperators. * @returns {Object} The normalized filter object * @private */ -DataAccessObject._normalize = function(filter) { +DataAccessObject._normalize = function(filter, options) { if (!filter) { return undefined; } @@ -1497,7 +1524,7 @@ DataAccessObject._normalize = function(filter) { var handleUndefined = this._getSetting('normalizeUndefinedInQuery'); // alter configuration of how removeUndefined handles undefined values filter = removeUndefined(filter, handleUndefined); - this._coerce(filter.where); + this._coerce(filter.where, options); return filter; }; @@ -1555,15 +1582,19 @@ function coerceArray(val) { /* * Coerce values based the property types * @param {Object} where The where clause + * @options {Object} [options] Optional options to use. + * @property {Boolean} allowExtendedOperators. * @returns {Object} The coerced where clause * @private */ -DataAccessObject._coerce = function(where) { +DataAccessObject._coerce = function(where, options) { var self = this; if (!where) { return where; } + options = options || {}; + var err; if (typeof where !== 'object' || Array.isArray(where)) { err = new Error(g.f('The where clause %j is not an {{object}}', where)); @@ -1585,7 +1616,7 @@ DataAccessObject._coerce = function(where) { } for (var k = 0; k < clauses.length; k++) { - self._coerce(clauses[k]); + self._coerce(clauses[k], options); } continue; @@ -1703,8 +1734,7 @@ DataAccessObject._coerce = function(where) { } } else { if (val != null) { - const dsSettings = this.getDataSource().settings; - const allowExtendedOperators = dsSettings.allowExtendedOperators; + const allowExtendedOperators = self._allowExtendedOperators(options); if (operator === null && val instanceof RegExp) { // Normalize {name: /A/} to {name: {regexp: /A/}} operator = 'regexp'; @@ -1831,7 +1861,7 @@ DataAccessObject.find = function find(query, options, cb) { 'all() must be implemented by the connector'); try { - this._normalize(query); + this._normalize(query, options); } catch (err) { process.nextTick(function() { cb(err); @@ -2189,7 +2219,7 @@ DataAccessObject.destroyAll = function destroyAll(where, options, cb) { try { // Support an optional where object where = removeUndefined(where); - where = Model._coerce(where); + where = Model._coerce(where, options); } catch (err) { return process.nextTick(function() { cb(err); @@ -2345,7 +2375,7 @@ DataAccessObject.count = function(where, options, cb) { try { where = removeUndefined(where); - where = this._coerce(where); + where = this._coerce(where, options); } catch (err) { process.nextTick(function() { cb(err); @@ -2616,9 +2646,9 @@ DataAccessObject.updateAll = function(where, data, options, cb) { function doUpdate(where, data) { try { where = removeUndefined(where); - where = Model._coerce(where); + where = Model._coerce(where, options); data = removeUndefined(data); - data = Model._coerce(data); + data = Model._coerce(data, options); } catch (err) { return process.nextTick(function() { cb(err); @@ -3083,9 +3113,7 @@ function(data, options, cb) { if (isPKMissing(Model, cb)) return cb.promise; - var allowExtendedOperators = connector.settings && - connector.settings.allowExtendedOperators; - + var allowExtendedOperators = Model._allowExtendedOperators(options); var strict = this.__strict; var model = Model.modelName; var hookState = {}; diff --git a/test/allow-extended-operators.test.js b/test/allow-extended-operators.test.js index 9b108ea9..359b73f3 100644 --- a/test/allow-extended-operators.test.js +++ b/test/allow-extended-operators.test.js @@ -8,32 +8,17 @@ const DataSource = require('..').DataSource; const should = require('should'); -describe('Model.settings.allowExtendedOperators', () => { - context('DAO.find()', () => { - it('converts extended operators to string value by default', () => { - const TestModel = createTestModel(); - return TestModel.find(extendedQuery()).then((results) => { - should(results[0].value).eql('[object Object]'); - }); - }); - - it('preserves extended operators wit allowExtendedOperators set', () => { - const TestModel = createTestModel({allowExtendedOperators: true}); - return TestModel.find(extendedQuery()).then((results) => { - should(results[0].value).eql({$exists: true}); - }); - }); - - function extendedQuery() { - // datasource modifies the query, - // we have to build a new object for each test - return {where: {value: {$exists: true}}}; - } - }); - - function createTestModel(connectorSettings) { +describe('allowExtendedOperators', () => { + function createTestModel(connectorSettings, modelSettings) { const ds = createTestDataSource(connectorSettings); - return ds.createModel('TestModel', {value: String}); + const TestModel = ds.createModel('TestModel', {value: String}, modelSettings); + + TestModel.observe('persist', function(ctx, next) { + ctx.Model.lastPersistedData = ctx.data; + next(); + }); + + return TestModel; } function createTestDataSource(connectorSettings) { @@ -47,16 +32,309 @@ describe('Model.settings.allowExtendedOperators', () => { return new DataSource(connectorSettings); } + function extendedQuery() { + // datasource modifies the query, + // we have to build a new object for each test + return {where: {value: {$exists: true}}}; + } + + function setCustomData() { + return {$set: {value: 'changed'}}; + } + + function updateShouldHaveFailed() { + throw new Error('updateAttributes() should have failed.'); + } + class TestConnector { constructor(dataSource) { } + create(model, data, options, callback) { + callback(); + } + + updateAttributes(model, id, data, options, callback) { + callback(); + } + all(model, filter, options, callback) { // return the raw "value" query - var instanceFound = { + let instanceFound = { value: filter.where.value, }; callback(null, [instanceFound]); } } + + describe('dataSource.settings.allowExtendedOperators', () => { + context('DAO.find()', () => { + it('converts extended operators to string value by default', () => { + const TestModel = createTestModel(); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('preserves extended operators with allowExtendedOperators set', () => { + const TestModel = createTestModel({allowExtendedOperators: true}); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + + it('`Model.settings.allowExtendedOperators` override data source settings - ' + + 'converts extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: true}, {allowExtendedOperators: false}); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('`Model.settings.allowExtendedOperators` override data source settings - ' + + 'preserves extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: false}, {allowExtendedOperators: true}); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + + it('`options.allowExtendedOperators` override data source settings - ' + + 'converts extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: true}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: false}).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('`options.allowExtendedOperators` override data source settings - ' + + 'preserves extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: false}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: true}).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + }); + + context('DAO.updateAttributes()', () => { + it('`options.allowExtendedOperators` override data source settings - disable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: false}, {strict: true}); + return TestModel.create({value: 'test'}).then((instance) => { + return instance.updateAttributes(setCustomData(), {allowExtendedOperators: true}) + .then(() => { + should(TestModel.lastPersistedData).eql(setCustomData()); + }); + }); + }); + + it('`options.allowExtendedOperators` override data source settings - enable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: true}, {strict: true}); + return TestModel.create({value: 'test'}).then((inst) => { + return inst.updateAttributes(setCustomData(), {allowExtendedOperators: false}) + .then(updateShouldHaveFailed, function onError(err) { + should.exist(err); + should(err.name).equal('ValidationError'); + }); + }); + }); + + it('`Model.settings.allowExtendedOperators` override data source settings - ' + + 'disable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: false}, + {strict: true, allowExtendedOperators: true}); + return TestModel.create({value: 'test'}).then((instance) => { + return instance.updateAttributes(setCustomData()).then(() => { + should(TestModel.lastPersistedData).eql(setCustomData()); + }); + }); + }); + + it('`Model.settings.allowExtendedOperators` override data source settings - ' + + 'enable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: true}, + {strict: true, allowExtendedOperators: false}); + return TestModel.create({value: 'test'}).then((inst) => { + return inst.updateAttributes(setCustomData()) + .then(updateShouldHaveFailed, function onError(err) { + should.exist(err); + should(err.name).equal('ValidationError'); + }); + }); + }); + }); + }); + + describe('Model.settings.allowExtendedOperators', () => { + context('DAO.find()', () => { + it('preserves extended operators with allowExtendedOperators set', () => { + const TestModel = createTestModel({}, {allowExtendedOperators: true}); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor Model settings - ' + + 'converts extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: true}, {allowExtendedOperators: false}); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor Model settings - ' + + 'preserves extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: false}, {allowExtendedOperators: true}); + return TestModel.find(extendedQuery()).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + + it('`options.allowExtendedOperators` override Model settings - converts extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: true}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: false}).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('`options.allowExtendedOperators` Model settings - preserves extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: false}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: true}).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + }); + + context('DAO.updateAttributes()', () => { + it('`options.allowExtendedOperators` override Model settings - disable strict check', () => { + const TestModel = createTestModel({}, {strict: true, allowExtendedOperators: false}); + return TestModel.create({value: 'test'}).then((instance) => { + return instance.updateAttributes(setCustomData(), {allowExtendedOperators: true}) + .then(() => { + should(TestModel.lastPersistedData).eql(setCustomData()); + }); + }); + }); + + it('`options.allowExtendedOperators` override Model settings - enabled strict check', () => { + const TestModel = createTestModel({}, {strict: true, allowExtendedOperators: true}); + return TestModel.create({value: 'test'}).then((inst) => { + return inst.updateAttributes(setCustomData(), {allowExtendedOperators: false}) + .then(updateShouldHaveFailed, function onError(err) { + should.exist(err); + should(err.name).equal('ValidationError'); + }); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor Model settings - disable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: false}, + {strict: true, allowExtendedOperators: true}); + return TestModel.create({value: 'test'}).then((instance) => { + return instance.updateAttributes(setCustomData()).then(() => { + should(TestModel.lastPersistedData).eql(setCustomData()); + }); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor Model settings - ' + + 'enable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: true}, + {strict: true, allowExtendedOperators: false}); + return TestModel.create({value: 'test'}).then((inst) => { + return inst.updateAttributes(setCustomData()) + .then(updateShouldHaveFailed, function onError(err) { + should.exist(err); + should(err.name).equal('ValidationError'); + }); + }); + }); + }); + }); + + describe('options.allowExtendedOperators', () => { + context('DAO.find()', () => { + it('preserves extended operators with allowExtendedOperators set', () => { + const TestModel = createTestModel(); + return TestModel.find(extendedQuery(), {allowExtendedOperators: true}).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor options settings - ' + + 'converts extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: true}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: false}).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor options settings - ' + + 'preserves extended operators', () => { + const TestModel = createTestModel({allowExtendedOperators: false}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: true}).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + + it('`Model.settings.allowExtendedOperators` honor options settings - ' + + 'converts extended operators', () => { + const TestModel = createTestModel({}, {allowExtendedOperators: true}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: false}).then((results) => { + should(results[0].value).eql('[object Object]'); + }); + }); + + it('`Model.settings.allowExtendedOperators` honor options settings - ' + + 'preserves extended operators', () => { + const TestModel = createTestModel({}, {allowExtendedOperators: false}); + return TestModel.find(extendedQuery(), {allowExtendedOperators: true}).then((results) => { + should(results[0].value).eql({$exists: true}); + }); + }); + }); + + context('DAO.updateAttributes()', () => { + it('`Model.settings.allowExtendedOperators` honor options settings - disable strict check', () => { + const TestModel = createTestModel({}, {strict: true, allowExtendedOperators: false}); + return TestModel.create({value: 'test'}).then((instance) => { + return instance.updateAttributes(setCustomData(), {allowExtendedOperators: true}) + .then(() => { + should(TestModel.lastPersistedData).eql(setCustomData()); + }); + }); + }); + + it('`Model.settings.allowExtendedOperators` honor options settings - enable strict check', () => { + const TestModel = createTestModel({}, {strict: true, allowExtendedOperators: true}); + return TestModel.create({value: 'test'}).then((inst) => { + return inst.updateAttributes(setCustomData(), {allowExtendedOperators: false}) + .then(updateShouldHaveFailed, function onError(err) { + should.exist(err); + should(err.name).equal('ValidationError'); + }); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor options settings - disable strict check', () => { + const TestModel = createTestModel({}, {strict: true}); + return TestModel.create({value: 'test'}).then((instance) => { + return instance.updateAttributes(setCustomData(), {allowExtendedOperators: true}) + .then(() => { + should(TestModel.lastPersistedData).eql(setCustomData()); + }); + }); + }); + + it('`dataSource.settings.allowExtendedOperators` honor options settings - enable strict check', () => { + const TestModel = createTestModel({allowExtendedOperators: true}, {strict: true}); + return TestModel.create({value: 'test'}).then((inst) => { + return inst.updateAttributes(setCustomData(), {allowExtendedOperators: false}) + .then(updateShouldHaveFailed, function onError(err) { + should.exist(err); + should(err.name).equal('ValidationError'); + }); + }); + }); + }); + }); });