diff --git a/lib/dao.js b/lib/dao.js index cef7552d..d01cd468 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -1878,7 +1878,7 @@ DataAccessObject.prototype.setAttributes = function setAttributes(data) { }; DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullify) { - if (nullify) { + if (nullify || this.constructor.definition.settings.persistUndefinedAsNull) { this[name] = this.__data[name] = null; } else { delete this[name]; diff --git a/lib/model-builder.js b/lib/model-builder.js index 09694ac2..eeec2355 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -429,6 +429,12 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett } else if (typeof DataType === 'string') { DataType = modelBuilder.resolveType(DataType); } + + var persistUndefinedAsNull = ModelClass.definition.settings.persistUndefinedAsNull; + if (value === undefined && persistUndefinedAsNull) { + value = null; + } + if (ModelClass.setter[propertyName]) { ModelClass.setter[propertyName].call(this, value); // Try setter first } else { diff --git a/lib/model.js b/lib/model.js index b5be726c..417e9471 100644 --- a/lib/model.js +++ b/lib/model.js @@ -77,6 +77,8 @@ ModelBaseClass.prototype._initProperties = function (data, options) { strict = ctor.definition.settings.strict; } + var persistUndefinedAsNull = ctor.definition.settings.persistUndefinedAsNull; + if (ctor.hideInternalProperties) { // Object.defineProperty() is expensive. We only try to make the internal // properties hidden (non-enumerable) if the model class has the @@ -151,6 +153,11 @@ ModelBaseClass.prototype._initProperties = function (data, options) { if (typeof propVal === 'function') { continue; } + + if (propVal === undefined && persistUndefinedAsNull) { + propVal = null; + } + if (properties[p]) { // Managed property if (applySetters || properties[p].id) { @@ -257,6 +264,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) { self.__data[p] = propVal; } + if (propVal === undefined && persistUndefinedAsNull) { + self.__data[p] = propVal = null; + } + // Handle complex types (JSON/Object) if (!BASE_TYPES[type.name]) { @@ -345,6 +356,7 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr var strict = this.__strict; var schemaLess = (strict === false) || !onlySchema; + var persistUndefinedAsNull = Model.definition.settings.persistUndefinedAsNull; var props = Model.definition.properties; var keys = Object.keys(props); @@ -373,6 +385,9 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr if (val !== undefined && val !== null && val.toObject) { data[propertyName] = val.toObject(!schemaLess, removeHidden, true); } else { + if (val === undefined && persistUndefinedAsNull) { + val = null; + } data[propertyName] = val; } } @@ -398,8 +413,11 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr if (removeProtected && Model.isProtectedProperty(propertyName)) { continue; } + if (data[propertyName] !== undefined) { + continue; + } val = self[propertyName]; - if (val !== undefined && data[propertyName] === undefined) { + if (val !== undefined) { if (typeof val === 'function') { continue; } @@ -408,6 +426,8 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr } else { data[propertyName] = val; } + } else if (persistUndefinedAsNull) { + data[propertyName] = null; } } // Now continue to check __data @@ -434,6 +454,8 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr if (val !== undefined && val !== null && val.toObject) { data[propertyName] = val.toObject(!schemaLess, removeHidden, true); + } else if (val === undefined && persistUndefinedAsNull) { + data[propertyName] = null; } else { data[propertyName] = val; } diff --git a/test/datatype.test.js b/test/datatype.test.js index 8b2c550f..bd61abc3 100644 --- a/test/datatype.test.js +++ b/test/datatype.test.js @@ -149,4 +149,102 @@ describe('datatypes', function () { coerced.nested.constructor.name.should.equal('Object'); }); + describe('model option persistUndefinedAsNull', function() { + var TestModel; + before(function(done) { + TestModel = db.define( + 'TestModel', + { + desc: { type: String, required: false }, + stars: { type: Number, required: false } + }, + { + persistUndefinedAsNull: true + }); + + db.automigrate(done); + }); + + it('should set missing optional properties to null', function(done) { + var EXPECTED = { desc: null, stars: null }; + TestModel.create({ name: 'a-test-name' }, function(err, created) { + if (err) return done(err); + created.should.have.properties(EXPECTED); + + TestModel.findById(created.id, function(err, found) { + if (err) return done(err); + found.should.have.properties(EXPECTED); + done(); + }); + }); + }); + + it('should convert property value undefined to null', function(done) { + var EXPECTED = { desc: null, extra: null }; + var data ={ desc: undefined, extra: undefined }; + TestModel.create(data, function(err, created) { + if (err) return done(err); + created.should.have.properties(EXPECTED); + + TestModel.findById(created.id, function(err, found) { + if (err) return done(err); + found.should.have.properties(EXPECTED); + done(); + }); + }); + }); + + it('should convert undefined to null in the setter', function() { + var inst = new TestModel(); + inst.desc = undefined; + inst.should.have.property('desc', null); + inst.toObject().should.have.property('desc', null); + }); + + it('should use null in unsetAttribute()', function() { + var inst = new TestModel(); + inst.unsetAttribute('stars'); + inst.should.have.property('stars', null); + inst.toObject().should.have.property('stars', null); + }); + + it('should convert undefined to null on save', function(done) { + var EXPECTED = { desc: null, stars: null, extra: null }; + TestModel.create({}, function(err, created) { + if (err) return done(err); + created.desc = undefined; // Note: this is may be a no-op + created.unsetAttribute('stars'); + created.extra = undefined; + created.__data.dx = undefined; + + created.save(function(err, saved) { + if (err) return done(err); + created.should.have.properties(EXPECTED); + + TestModel.dataSource.connector.all( + TestModel.modelName, + { where: { id: created.id } }, + function(err, found) { + if (err) return done(err); + should.exist(found[0]); + found[0].should.have.properties(EXPECTED); + done(); + } + ); + }); + }); + }); + + it('should convert undefined to null in toObject()', function() { + var inst = new TestModel(); + inst.desc = undefined; // Note: this may be a no-op + inst.unsetAttribute('stars'); + inst.extra = undefined; + inst.__data.dx = undefined; + + inst.toObject(false).should.have.properties({ + desc: null, stars: null, extra: null, dx: null + }); + }); + }); });