Add model setting "persistUndefinedAsNull"

When the setting "persistUndefinedAsNull" is true,
the model will use `null` instead of `undefined` in
all property values.

 - Known optional model properties are set to `null` when no value
   was provided.

 - When setting model properties, `undefined` is always converted
   to `null`. This applies to both known (model-defined) properties
   and additional (custom, dynamic) properties.

 - The instance method `toObject()` converts `undefined` to `null` too.
   Because `toJSON()` calls `toObject()` under the hood, the change
   applies to `toJSON()` too.
This commit is contained in:
Miroslav Bajtoš 2015-03-27 10:45:14 +01:00
parent abba2a88d9
commit 9fd4c00225
4 changed files with 128 additions and 2 deletions

View File

@ -1878,7 +1878,7 @@ DataAccessObject.prototype.setAttributes = function setAttributes(data) {
}; };
DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullify) { DataAccessObject.prototype.unsetAttribute = function unsetAttribute(name, nullify) {
if (nullify) { if (nullify || this.constructor.definition.settings.persistUndefinedAsNull) {
this[name] = this.__data[name] = null; this[name] = this.__data[name] = null;
} else { } else {
delete this[name]; delete this[name];

View File

@ -429,6 +429,12 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
} else if (typeof DataType === 'string') { } else if (typeof DataType === 'string') {
DataType = modelBuilder.resolveType(DataType); DataType = modelBuilder.resolveType(DataType);
} }
var persistUndefinedAsNull = ModelClass.definition.settings.persistUndefinedAsNull;
if (value === undefined && persistUndefinedAsNull) {
value = null;
}
if (ModelClass.setter[propertyName]) { if (ModelClass.setter[propertyName]) {
ModelClass.setter[propertyName].call(this, value); // Try setter first ModelClass.setter[propertyName].call(this, value); // Try setter first
} else { } else {

View File

@ -77,6 +77,8 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
strict = ctor.definition.settings.strict; strict = ctor.definition.settings.strict;
} }
var persistUndefinedAsNull = ctor.definition.settings.persistUndefinedAsNull;
if (ctor.hideInternalProperties) { if (ctor.hideInternalProperties) {
// Object.defineProperty() is expensive. We only try to make the internal // Object.defineProperty() is expensive. We only try to make the internal
// properties hidden (non-enumerable) if the model class has the // properties hidden (non-enumerable) if the model class has the
@ -151,6 +153,11 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
if (typeof propVal === 'function') { if (typeof propVal === 'function') {
continue; continue;
} }
if (propVal === undefined && persistUndefinedAsNull) {
propVal = null;
}
if (properties[p]) { if (properties[p]) {
// Managed property // Managed property
if (applySetters || properties[p].id) { if (applySetters || properties[p].id) {
@ -257,6 +264,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
self.__data[p] = propVal; self.__data[p] = propVal;
} }
if (propVal === undefined && persistUndefinedAsNull) {
self.__data[p] = propVal = null;
}
// Handle complex types (JSON/Object) // Handle complex types (JSON/Object)
if (!BASE_TYPES[type.name]) { if (!BASE_TYPES[type.name]) {
@ -345,6 +356,7 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr
var strict = this.__strict; var strict = this.__strict;
var schemaLess = (strict === false) || !onlySchema; var schemaLess = (strict === false) || !onlySchema;
var persistUndefinedAsNull = Model.definition.settings.persistUndefinedAsNull;
var props = Model.definition.properties; var props = Model.definition.properties;
var keys = Object.keys(props); var keys = Object.keys(props);
@ -373,6 +385,9 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr
if (val !== undefined && val !== null && val.toObject) { if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden, true); data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
} else { } else {
if (val === undefined && persistUndefinedAsNull) {
val = null;
}
data[propertyName] = val; data[propertyName] = val;
} }
} }
@ -398,8 +413,11 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr
if (removeProtected && Model.isProtectedProperty(propertyName)) { if (removeProtected && Model.isProtectedProperty(propertyName)) {
continue; continue;
} }
if (data[propertyName] !== undefined) {
continue;
}
val = self[propertyName]; val = self[propertyName];
if (val !== undefined && data[propertyName] === undefined) { if (val !== undefined) {
if (typeof val === 'function') { if (typeof val === 'function') {
continue; continue;
} }
@ -408,6 +426,8 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr
} else { } else {
data[propertyName] = val; data[propertyName] = val;
} }
} else if (persistUndefinedAsNull) {
data[propertyName] = null;
} }
} }
// Now continue to check __data // Now continue to check __data
@ -434,6 +454,8 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden, removePr
if (val !== undefined && val !== null && val.toObject) { if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden, true); data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
} else if (val === undefined && persistUndefinedAsNull) {
data[propertyName] = null;
} else { } else {
data[propertyName] = val; data[propertyName] = val;
} }

View File

@ -149,4 +149,102 @@ describe('datatypes', function () {
coerced.nested.constructor.name.should.equal('Object'); 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
});
});
});
}); });