diff --git a/lib/document.js b/lib/document.js index 3ef33dcf..4f954218 100644 --- a/lib/document.js +++ b/lib/document.js @@ -87,7 +87,6 @@ function Document(model, options) { }); } - /** * Return the options of the document, not the instance of Document. * @return {Object=} @@ -176,6 +175,109 @@ Document.prototype._generateDefault = function() { } } +function getShadowObject() { + if ((this._shadowObject === undefined) || (this._shadowObject === null)) { + this._shadowObject = {}; + } + return this._shadowObject; +} + +Document.prototype.getShadowObject = getShadowObject + +Document.prototype._overrideProperties = function(typeObj, obj) { + if ((obj === undefined) || (obj === null)) { + return; + } + var _this = this; + var isArrayKey = false; + var schema = null; + if (type.isArray(typeObj)) { + schema = typeObj[0]._schema; + } else { + schema = typeObj._schema; + } + //for each key in the schema, if they have _methods present override the getter/setter + Object.keys(schema).forEach(function(schemaKey) { + var schemaDef = schema[schemaKey]; + if (schemaDef === undefined) { + return; + } + if (type.isArray(schemaDef)) { + if (schemaDef._schema === undefined) { + return; + } + schemaDef = schemaDef._schema; + if (schemaDef._schema === undefined) { + return; + } + if ((schemaDef._methods === undefined) + || (Object.keys(schemaDef._methods).length === 0)) { + return; + } + var methods = Object.assign({ + getShadowObject: getShadowObject + }, schemaDef._methods); + //need to override the arrays push method if the value is set + var existingValue = obj[schemaKey]; + Object.defineProperty(obj, schemaKey, { + set: function(value) { + var shadow = this.getShadowObject(); + shadow[schemaKey] = value; + if ((value === null) || (value === undefined)) { + //nothing to do + return; + } + var currentPush = shadow[schemaKey].push; + var newPush = function() { + var args = Array.prototype.slice.call(arguments); + args.forEach(function(value) { + if ((value !== null) && (value !== undefined)) { + util.changeProto(value, methods); + _this._overrideProperties(schemaDef, value); + } + }); + return currentPush.apply(this, args); + } + shadow[schemaKey].push = newPush; + shadow[schemaKey].forEach(function(value) { + util.changeProto(value, methods); + _this._overrideProperties(schemaDef, value); + }); + }, + get: function() { + return this.getShadowObject()[schemaKey]; + }, + enumerable: true + }); + if (existingValue !== undefined) { + obj[schemaKey] = existingValue; + } + } else if ((schemaDef._methods !== undefined) && (Object.keys(schemaDef._methods).length > 0)) { + //override the getters and setters to we can inject the custom functions + var methods = Object.assign({ + getShadowObject: getShadowObject + }, schemaDef._methods); + var existingValue = obj[schemaKey]; + Object.defineProperty(obj, schemaKey, { + set: function(value) { + var shadow = this.getShadowObject(); + shadow[schemaKey] = value; + if ((value !== null) && (value !== undefined)) { + util.changeProto(value, methods); + _this._overrideProperties(schemaDef, value); + } + }, + get: function() { + return this.getShadowObject()[schemaKey]; + }, + enumerable: true + }); + if (existingValue !== undefined) { + obj[schemaKey] = existingValue; + } + } + }); +} /* * Validate this document against the schema of its model and triggers all the hooks. @@ -473,6 +575,9 @@ Document.prototype.__makeSavableCopy = function(doc, schema, options, model, r) if (util.isPlainObject(doc) && (doc instanceof Buffer === false)) { result = {}; util.loopKeys(doc, function(doc, key) { + if (key === '_shadowObject') { + return true; + } copyFlag = true; if ((util.isPlainObject(model) === false) || (model._joins[key] === undefined)) { // We do not copy joined documents if ((schema !== undefined) && (schema._schema !== undefined) && (type.isVirtual(schema._schema[key]) === true)) { diff --git a/lib/model.js b/lib/model.js index b05485f8..02f06b5c 100644 --- a/lib/model.js +++ b/lib/model.js @@ -101,6 +101,8 @@ Model.new = function(name, schema, options, thinky) { util.changeProto(doc, new Document(model, options)); + doc._overrideProperties(model._schema, doc); + // Create joins document. We do it here because `options` are easily available util.loopKeys(proto._joins, function(joins, key) { if (doc[key] != null) { @@ -162,6 +164,13 @@ Model.new = function(name, schema, options, thinky) { model.__proto__ = proto; + + if (model._schema._methods !== undefined) { + Object.keys(model._schema._methods).forEach(function(key) { + model.define(key, model._schema._methods[key]); + }); + } + if (options.init !== false) { // Setup the model's table. model.tableReady().then(); diff --git a/lib/schema.js b/lib/schema.js index 252f8d71..336802a6 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -117,6 +117,9 @@ module.exports.generateDefault = generateDefault; function parse(schema, prefix, options, model) { var result; + var reservedFunctionKeys = ['default', 'validator'] + var dataTypeFunctions = [String, Number, Boolean, Date, Buffer, Object, Array]; + if ((prefix === '') && (type.isObject(schema) === false) && (util.isPlainObject(schema) === false)) { throw new Errors.ValidationError("The schema must be a plain object.") } @@ -164,7 +167,14 @@ function parse(schema, prefix, options, model) { result = type.object().options(options).validator(schema.validator); if (schema.default !== undefined) { result.default(schema.default); } util.loopKeys(schema.schema, function(_schema, key) { - result.setKey(key, parse(_schema[key], prefix+"["+key+"]", options)); + if ((typeof _schema[key] === 'function') && (dataTypeFunctions.indexOf(_schema[key]) === -1)) { + if (result._methods === undefined) { + result._methods = {}; + } + result._methods[key] = _schema[key]; + } else { + result.setKey(key, parse(_schema[key], prefix+"["+key+"]", options)); + } }) if (prefix === '') { result._setModel(model) @@ -233,7 +243,14 @@ function parse(schema, prefix, options, model) { else { result = type.object().options(options); util.loopKeys(schema, function(_schema, key) { - result.setKey(key, parse(_schema[key], prefix+"["+key+"]", options)); + if ((typeof _schema[key] === 'function') && (dataTypeFunctions.indexOf(_schema[key]) === -1)) { + if (result._methods === undefined) { + result._methods = {}; + } + result._methods[key] = _schema[key]; + } else { + result.setKey(key, parse(_schema[key], prefix+"["+key+"]", options)); + } }) if (prefix === '') { result._setModel(model) diff --git a/lib/type/object.js b/lib/type/object.js index b55beb39..aa49dab0 100644 --- a/lib/type/object.js +++ b/lib/type/object.js @@ -112,6 +112,7 @@ TypeObject.prototype.validator = function(fn) { TypeObject.prototype.validate = function(object, prefix, options) { var self = this; + var localOptions = util.mergeOptions(this._options, options); if (util.validateIfUndefined(object, prefix, "object", localOptions)) return; @@ -133,6 +134,9 @@ TypeObject.prototype.validate = function(object, prefix, options) { } else { util.loopKeys(self._schema, function(schema, key) { + if (key === '_shadowObject') { + return true; + } schema[key].validate(object[key], prefix+"["+key+"]", options); }); @@ -140,6 +144,9 @@ TypeObject.prototype.validate = function(object, prefix, options) { // https://github.com/neumino/thinky/pull/123#issuecomment-56254682 if (localOptions.enforce_extra === "remove") { util.loopKeys(object, function(object, key) { + if (key === '_shadowObject') { + return true; + } if ((self._model === undefined || self._model._joins.hasOwnProperty(key) === false) && (self._schema[key] === undefined)) { delete object[key]; @@ -148,6 +155,9 @@ TypeObject.prototype.validate = function(object, prefix, options) { } else if (localOptions.enforce_extra === "strict") { util.loopKeys(object, function(object, key) { + if (key === '_shadowObject') { + return true; + } if ((self._model === undefined || self._model._joins.hasOwnProperty(key) === false) && (self._schema[key] === undefined)) { util.extraField(prefix, key); diff --git a/test/sub_doc_functions.js b/test/sub_doc_functions.js new file mode 100644 index 00000000..ac80dd6b --- /dev/null +++ b/test/sub_doc_functions.js @@ -0,0 +1,175 @@ +var config = require(__dirname+'/../config.js'); + +var thinky = require(__dirname+'/../lib/thinky.js')(config); +var r = thinky.r; +var type = thinky.type; + +var util = require(__dirname+'/util.js'); +var assert = require('assert'); +var Promise = require('bluebird'); +var Errors = thinky.Errors; + + +var modelNameSet = {}; +modelNameSet[util.s8()] = true; +modelNameSet[util.s8()] = true; + +var modelNames = Object.keys(modelNameSet); + +var cleanTables = function(done) { + var promises = []; + var name; + for(var name in modelNameSet) { + promises.push(r.table(name).delete().run()); + } + Promise.settle(promises).error(function () {/*ignore*/}).finally(function() { + // Add the links table + for(var model in thinky.models) { + modelNameSet[model] = true; + } + modelNames = Object.keys(modelNameSet); + thinky._clean(); + done(); + }); +} + + + +describe('Sub Document Functions', function(){ + + afterEach(cleanTables); + + var basicModelOneSpec = { + id: String, + name: String, + email: type.string(), + address: { + street: String, + geoCode: function() { + return this.street; + } + }, + topLevelFunc: function() { + return "topLevel"; + } + } + var basicModelOne = null; + + it('can create a basic model', function(done){ + var testModelOne = thinky.createModel("TestModelOne", basicModelOneSpec); + assert.equal(type.isObject(testModelOne._schema._schema.address), true); + assert.equal(typeof testModelOne._schema._schema.address._methods, 'object'); + assert.equal(Object.keys(testModelOne._schema._schema.address._methods).length, 1); + assert.equal(testModelOne._schema._schema.address._methods.geoCode, basicModelOneSpec.address.geoCode); + basicModelOne = testModelOne; + done(); + }); + + var basicModelTwoSpec = { + id: String, + name: String, + email: type.string(), + addresses: [{ + street: String, + geoCode: function() { + return this.street; + } + }] + } + var basicModelTwo = null; + + it('can create a basic model with object array', function(done){ + var testModelTwo = thinky.createModel("TestModelTwo", basicModelTwoSpec); + assert.equal(type.isArray(testModelTwo._schema._schema.addresses), true); + assert.equal(typeof testModelTwo._schema._schema.addresses._schema._methods, 'object'); + assert.equal(Object.keys(testModelTwo._schema._schema.addresses._schema._methods).length, 1); + assert.equal(testModelTwo._schema._schema.addresses._schema._methods.geoCode, basicModelTwoSpec.addresses[0].geoCode); + basicModelTwo = testModelTwo; + done(); + }); + + it('can instantiate basic objects correctly', function(done){ + var one = new basicModelOne({ + id: 'db15e340-1ebf-4c24-871b-c383330cef7f', + name: 'Tester', + email: 'tester@test.com', + address: { + street: "123 Fake Street" + } + }); + assert.equal(typeof one.address, 'object'); + assert.equal(one.topLevelFunc(), 'topLevel'); + assert.equal(one.address.geoCode(), "123 Fake Street"); + var two = new basicModelTwo({ + id: '7777b2c1-26ac-45e0-a0f2-f11e613265b9', + name: 'Tester2', + email: 'tester2@test.com', + addresses: [{ + street: "456 Fake Street" + }] + }); + assert.equal(Array.isArray(two.addresses), true); + assert.equal(two.addresses.length, 1); + assert.equal(two.addresses[0].geoCode(), "456 Fake Street"); + done(); + }); + + var basicModelThreeSpec = { + id: String, + name: String, + email: type.string(), + address: { + street: String, + geoCode: function() { + return this.street; + }, + zipCode: { + num: Number, + lookup: function() { + return "North Pole"; + }, + source: { + name: String, + rep: function() { + return 24; + } + } + } + } + } + var basicModelThree = null; + + it('can build nested object models', function(done){ + var testModelThree = thinky.createModel("TestModelThree", basicModelThreeSpec); + basicModelThree = testModelThree; + done(); + }); + + it('can create nested object models', function(done){ + var three = new basicModelThree({ + id: 'e9c8111e-a09a-4268-b25e-e42583113058', + name: 'Tester3', + email: 'tester3@test.com', + address: { + street: "123 Fake Street", + zipCode: { + num: 4000, + source: { + name: "Test Source" + } + } + } + }); + assert.equal(three.address.zipCode.lookup(), "North Pole"); + assert.equal(three.address.zipCode.source.rep(), 24); + three.save().then(function(result) { + assert.equal(three.isSaved(), true); + basicModelThree.get(three.id).then(function(dbResult) { + assert.equal(dbResult.address.zipCode.lookup(), "North Pole"); + assert.equal(dbResult.address.zipCode.source.rep(), 24); + done(); + }).error(done); + }).error(done); + }); + +}); \ No newline at end of file