Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,117 @@ Model.new = function(name, schema, options, thinky) {

util.changeProto(doc, new Document(model, options));

/*
* The strategy of this proxy/handler is to link
* the supplied object (obj) with a supplied schema
* for the purposes of supporting custom, non-virtual
* functions attached to sub-objects of documents.
* It does this hooking the basic 'set' function
* of the target object and altering the prototype
* of the value being set if custom functions need
* to be applied.
*/
function installHandlerForSchema(obj, schema) {
var handler = {
schema: schema,
set: function(target, property, value, receiver) {
/*
* There are two scenarios that need to be supported
* - Parent object has a singular sub-doc set by
* parent.property = value
* - Parent object has an array of sub-docs altered by
* parent.property[i] = value
* parent.property.push(value)
* Arrays also interact with their 'length' property
* automatically, so we only want to target numeric
* properties of arrays
*/
var propertySchema = null;
if (Array.isArray(target)) {
/*
* Only hook actual elements and not other properties
* or functions of arrays
*/
if (!isNaN(parseInt(property))) {
propertySchema = this.schema._schema;
}
} else {
propertySchema = this.schema[property];
}
var assignmentVal = null;
/*
* Only do work if the value is not null or undefined
*/
if ((value !== null) && (value !== undefined)) {
/*
* If this value is an array, we don't want to change it's
* prototype, but we do want to install a proxy/handler to
* capture any objects that get inserted into it. We only
* want to do this though if the objects require custom
* methods.
*
* If this value is an object and the schema defines that
* the object has custom functions, then we need to change
* it's prototype and install a handler
*
* If this value doesn't have custom methods, namely
* _methods.length === 0 or undefined or null
* but does define a deeper schema, then we still need
* to install a handler as there may potentially be deeper
* sub-docs with custom functions
*
* If none of that is the case, pass through the value assignment
*/
if (thinky.type.isArray(propertySchema)) {
var elementSchema = propertySchema._schema;
if ( (elementSchema !== undefined)
&& (elementSchema !== null)
&& (elementSchema._methods !== undefined)
&& (Object.keys(elementSchema._methods).length > 0)) {
assignmentVal = installHandlerForSchema(value, propertySchema);
} else {
assignmentVal = value;
}
} else if ((propertySchema !== undefined)
&& (propertySchema !== null)
&& (propertySchema._methods !== undefined)
&& (Object.keys(propertySchema._methods).length > 0)) {
util.changeProto(value, propertySchema._methods);
assignmentVal = installHandlerForSchema(value, propertySchema._schema);
} else if ((propertySchema !== undefined)
&& (propertySchema !== null)
&& (propertySchema._schema !== undefined)
&& (propertySchema._schema !== null )
&& (thinky.type.isObject(propertySchema._schema))){
assignmentVal = installHandlerForSchema(value, propertySchema._schema);
} else {
assignmentVal = value;
}
} else {
assignmentVal = value;
}
target[property] = assignmentVal;
return true;
}
}
var proxy = new Proxy(obj, handler);
/*
* Given that we've just altered the 'set' logic for the object,
* we need to perform set on all the keys to ensure that any
* existing values are altered correctly
*/
Object.keys(proxy).forEach((key) => {
if ((proxy[key] !== undefined) && (proxy[key] !== null)) {
proxy[key] = proxy[key];
}
});
return proxy;
}

if ('_usesCustomFunctions' in doc._getModel()._schema) {
doc = installHandlerForSchema(doc, doc._getModel()._schema._schema);
}

// Create joins document. We do it here because `options` are easily available
util.loopKeys(proto._joins, function(joins, key) {
if (doc[key] != null) {
Expand Down Expand Up @@ -165,6 +276,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();
Expand Down
38 changes: 36 additions & 2 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -233,7 +243,31 @@ 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];
//ignore for top level object since it leverages define()
if (prefix !== '') {
result._usesCustomFunctions = true;
}
} else {
var schemaObj = parse(_schema[key], prefix+"["+key+"]", options);
result.setKey(key, schemaObj);
/*
* If a sub-object of a property uses custom functions, ensure
* that it bubbles up to the parent
*/
if (schemaObj !== undefined) {
if ('_usesCustomFunctions' in schemaObj) {
result._usesCustomFunctions = schemaObj._usesCustomFunctions;
} else if ((schemaObj._schema !== undefined)
&& ('_usesCustomFunctions' in schemaObj._schema)) {
result._usesCustomFunctions = schemaObj._schema._usesCustomFunctions;
}
}
}
})
if (prefix === '') {
result._setModel(model)
Expand Down
Loading