1 /*
  2  * Geddy JavaScript Web development framework
  3  * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
  4  *
  5  * Licensed under the Apache License, Version 2.0 (the "License");
  6  * you may not use this file except in compliance with the License.
  7  * You may obtain a copy of the License at
  8  *
  9  *         http://www.apache.org/licenses/LICENSE-2.0
 10  *
 11  * Unless required by applicable law or agreed to in writing, software
 12  * distributed under the License is distributed on an "AS IS" BASIS,
 13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  * See the License for the specific language governing permissions and
 15  * limitations under the License.
 16  *
 17 */
 18 
 19 /*
 20 Example model file, would be app/models/user.js:
 21 
 22 var User = function () {
 23   this.property('login', 'string', {required: true});
 24   this.property('password', 'string', {required: true});
 25   this.property('lastName', 'string');
 26   this.property('firstName', 'string');
 27 
 28   this.validatesPresent('login');
 29   this.validatesFormat('login', /[a-z]+/, {message: 'Subdivisions!'});
 30   this.validatesLength('login', {min: 3});
 31   this.validatesConfirmed('password', 'confirmPassword');
 32   this.validatesWithFunction('password',
 33       function (s) { return s.length > 0; // Could be anything
 34   });
 35 };
 36 
 37 User.prototype.someMethod = function () {
 38   // Do some stuff on a User instance
 39 };
 40 
 41 geddy.model.register('User', User);
 42 */
 43 
 44 var model = {}
 45   , utils = require('../utils');
 46 
 47 model.datatypes = require('./datatypes.js');
 48 model.validators = require('./validators.js');
 49 model.formatters = require('./formatters.js');
 50 
 51 utils.mixin(model, new (function () {
 52 
 53   var _createModelItemConstructor = function (def) {
 54     // Base constructor function for all model items
 55     var ModelItemConstructor = function (params) {
 56       this.type = def.name;
 57 
 58       this.isValid = function () {
 59         return !this.errors;
 60       };
 61 
 62       this.toString = function () {
 63         var obj = {};
 64         obj.id = this.id;
 65         obj.type = this.type;
 66         var props = this.properties;
 67         var formatter;
 68         for (var p in props) {
 69           formatter = model.formatters[props[p].datatype];
 70           obj[p] = typeof formatter == 'function' ? formatter(this[p]) : this[p];
 71         }
 72         return JSON.stringify(obj);
 73       };
 74 
 75       this.toJson = this.toString;
 76     };
 77 
 78     return ModelItemConstructor;
 79   };
 80 
 81   var _createStaticMethodsMixin = function (name) {
 82     var obj = {};
 83 
 84     obj.create = function () {
 85       var args = Array.prototype.slice.call(arguments);
 86       args.unshift(name);
 87       return model.createItem.apply(model, args);
 88     };
 89 
 90     obj.load = function () {
 91       var args = Array.prototype.slice.call(arguments);
 92       if (!model.adapter) {
 93         throw new Error('adapter is not defined.');
 94       }
 95       args.unshift(name);
 96       return model.adapter.load.apply(model.adapter, args);
 97     };
 98 
 99     return obj;
100   };
101 
102 
103   this.reg = {};
104   this.useTimestamps = false;
105   this.adapter = {};
106 
107   this.register = function (name, ModelDefinition) {
108     var origProto = ModelDefinition.prototype
109       , defined
110       , ModelCtor;
111 
112     // Create the place to store the metadata about the model structure
113     // to use to do validations, etc. when constructing
114     model.reg[name] = new model.ModelDescription(name);
115     // Execute all the definition methods to create that metadata
116     ModelDefinition.prototype = new model.ModelDefinitionBase(name);
117     defined = new ModelDefinition();
118 
119     // Create the constructor function to use when calling static
120     // ModelCtor.create. Gives them the proper instanceof value,
121     // and .valid, etc. instance-methods.
122     ModelCtor = _createModelItemConstructor(defined);
123 
124     // Mix in the static methods like .create and .load
125     utils.mixin(ModelCtor, _createStaticMethodsMixin(name));
126     // Same with statics
127     utils.mixin(ModelCtor, defined);
128 
129     // Mix any functions defined directly in the model-item definition
130     // 'constructor' into the original prototype, and set it as the prototype of the
131     // actual constructor
132     utils.mixin(origProto, defined);
133 
134     ModelCtor.prototype = origProto;
135 
136     model[name] = ModelCtor;
137   };
138 
139   this.createItem = function (name, params) {
140     var item = new model[name](params);
141     item = this.validateAndUpdateFromParams(item, params);
142 
143     if (this.useTimestamps && !item.createdAt) {
144       item.createdAt = new Date();
145     }
146 
147     // After-create hook
148     if (typeof item.afterCreate == 'function') {
149       item.afterCreate();
150     }
151     return item;
152   };
153 
154   this.updateItem = function (item, params) {
155     item = this.validateAndUpdateFromParams(item, params);
156 
157     // After-update hook
158     if (typeof item.afterUpdate == 'function') {
159       item.afterUpdate();
160     }
161     return item;
162   };
163 
164   this.validateAndUpdateFromParams = function (item, params) {
165     var type = model.reg[item.type]
166       , properties = type.properties
167       , validated = null
168       , errs = null
169       , val
170 
171     // May be revalidating, clear errors
172     delete item.errors;
173 
174     // User-input should never contain these -- but we still want
175     // to validate them
176     if (typeof item.createdAt != 'undefined') {
177       params.createdAt = item.createdAt;
178     }
179     if (typeof item.updatedAt != 'undefined') {
180       params.updatedAt = item.updatedAt;
181     }
182 
183     for (var p in properties) {
184       validated = this.validateProperty(properties[p], params);
185       // If there are any failed validations, the errs param
186       // contains an Object literal keyed by field name, and the
187       // error message for the first failed validation for that
188       // property
189       if (validated.err) {
190         errs = errs || {};
191         errs[p] = validated.err;
192       }
193       // Otherwise add this property the the return item
194       else {
195         item[p] = validated.val;
196       }
197     }
198 
199     // Should never be incuded in user input
200     delete params.createdAt;
201     delete params.updatedAt;
202 
203     if (errs) {
204       item.errors = errs;
205     }
206 
207     return item;
208   };
209 
210   this.validateProperty = function (prop, params) {
211     var name = prop.name
212       , val = params[name];
213 
214     // Validate for the base datatype only if there actually is a value --
215     // e.g., undefined will fail the validation for Number, even if the
216     // field is optional
217     if (val) {
218       var result = model.datatypes[prop.datatype.toLowerCase()](prop.name, val);
219       if (result.err) {
220         return {
221           err: result.err,
222           val: null
223         };
224       }
225       // Value may have been modified in the datatype check -- e.g.,
226       // 'false' changed to false, '8.0' changed to 8, '2112' changed to
227       // 2112, etc.
228       val = result.val;
229     }
230 
231     // Now go through all the base validations for this property
232     var validations = prop.validations;
233     var validator;
234     var err;
235     for (var p in validations) {
236       validator = model.validators[p]
237       if (typeof validator != 'function') {
238         throw new Error(p + ' is not a valid validator');
239       }
240       err = validator(name, val, params, validations[p]);
241       // If there's an error for a validation, don't bother
242       // trying to continue with more validations -- just return
243       // this first error message
244       if (err) {
245         return {
246           err: err,
247           val: null
248         };
249       }
250     }
251 
252     // If there weren't any errors, return the value for this property
253     // and no error
254     return {
255       err: null,
256       val: val
257     };
258   };
259 
260 })());
261 
262 model.ModelDefinitionBase = function (name) {
263   var self = this
264     , _getValidator = function (p) {
265     return function () {
266       var args = Array.prototype.slice.call(arguments);
267       args.unshift(p);
268       return self.validates.apply(self, args);
269     };
270   };
271 
272   this.name = name;
273   this.property = function (name, datatype, o) {
274     model.reg[this.name].properties[name] =
275       new model.PropertyDescription(name, datatype, o);
276   };
277 
278   this.validates = function (condition, name, qualifier, opts) {
279     var rule = utils.mixin({}, opts, true);
280     rule.qualifier = qualifier;
281     model.reg[this.name].properties[name]
282         .validations[condition] = rule;
283   };
284 
285   // For each of the validators, create a validatesFooBar from
286   // validates('fooBar' ...
287   for (var p in model.validators) {
288     this['validates' + utils.string.capitalize(p)] = _getValidator(p);
289   }
290 
291   // Add the base model properties -- these should not be handled by user input
292   if (model.useTimestamps) {
293     this.property('createdAt', 'datetime');
294     this.property('updatedAt', 'datetime');
295   }
296 
297 };
298 
299 model.ModelDescription = function (name) {
300   this.name = name;
301   this.properties = {};
302 };
303 
304 model.PropertyDescription = function (name, datatype, o) {
305   var opts = o || {};
306   this.name = name;
307   this.datatype = datatype;
308   this.options = opts;
309   var validations = {};
310   for (var p in opts) {
311     if (opts.required || opts.length) {
312       validations.present = true;
313     }
314     if (opts.length) {
315       validations.length = opts.length;
316     }
317     if (opts.format) {
318       validations.format = opts.format;
319     }
320   }
321   this.validations = validations;
322 };
323 
324 module.exports = model;
325 
326