[4mRunning "mochacov:coverage" (mochacov) task[24m
Line | Hits | Source |
---|---|---|
1 | ||
2 | 1 | var util = require('util'); |
3 | /** | |
4 | * FormalError constructor | |
5 | * | |
6 | * @param {String} msg Error message | |
7 | * @inherits Error https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error | |
8 | */ | |
9 | ||
10 | 1 | function FormalError (msg) { |
11 | 28 | Error.call(this); |
12 | 28 | Error.captureStackTrace(this, FormalError); |
13 | 28 | this.message = msg; |
14 | 28 | this.name = 'FormalError'; |
15 | } | |
16 | ||
17 | /*! | |
18 | * Inherits from Error. | |
19 | */ | |
20 | ||
21 | 1 | util.inherits(FormalError, Error); |
22 | ||
23 | /*! | |
24 | * Module exports. | |
25 | */ | |
26 | ||
27 | 1 | module.exports = exports = FormalError; |
28 | ||
29 | /*! | |
30 | * Expose subclasses | |
31 | */ | |
32 | ||
33 | 1 | FormalError.CastError = require('./errors/cast'); |
34 | //FormalError.DocumentError = require('./errors/document'); | |
35 | 1 | FormalError.ValidationError = require('./errors/validation'); |
36 | 1 | FormalError.ValidatorError = require('./errors/validator'); |
37 | //FormalError.VersionError =require('./errors/version') | |
38 | //FormalError.OverwriteModelError = require('./errors/overwriteModel') | |
39 | //FormalError.MissingSchemaError = require('./errors/missingSchema') | |
40 | //FormalError.DivergentArrayError = require('./errors/divergentArray') | |
41 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module dependencies. | |
3 | */ | |
4 | 1 | var util = require('util'), |
5 | FormalError = require('../error'); | |
6 | ||
7 | /** | |
8 | * Casting Error constructor. | |
9 | * | |
10 | * @param {String} type | |
11 | * @param {String} value | |
12 | * @inherits FormalError | |
13 | * @api private | |
14 | */ | |
15 | ||
16 | 1 | function CastError (type, value, path) { |
17 | 3 | FormalError.call(this, 'Cast to ' + type + ' failed for value "' + value + '" at path "' + path + '"'); |
18 | 2 | Error.captureStackTrace(this, CastError); |
19 | 2 | this.name = 'CastError'; |
20 | 2 | this.type = type; |
21 | 2 | this.value = value; |
22 | 2 | this.path = path; |
23 | } | |
24 | ||
25 | /*! | |
26 | * Inherits from FormalError. | |
27 | */ | |
28 | ||
29 | 1 | util.inherits(CastError, FormalError); |
30 | ||
31 | /*! | |
32 | * exports | |
33 | */ | |
34 | ||
35 | 1 | module.exports = CastError; |
36 |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Module requirements | |
4 | */ | |
5 | ||
6 | 1 | var FormalError = require('../error'), |
7 | util = require('util'); | |
8 | ||
9 | /** | |
10 | * Document Validation Error | |
11 | * | |
12 | * @api private | |
13 | * @param {Document} instance | |
14 | * @inherits FormalError | |
15 | */ | |
16 | ||
17 | 1 | function ValidationError (instance) { |
18 | 7 | FormalError.call(this, 'Validation failed'); |
19 | 7 | Error.captureStackTrace(this, ValidationError); |
20 | 7 | this.name = 'ValidationError'; |
21 | 7 | this.errors = instance.errors = {}; |
22 | } | |
23 | ||
24 | /*! | |
25 | * Inherits from FormalError. | |
26 | */ | |
27 | ||
28 | 1 | util.inherits(ValidationError, FormalError); |
29 | ||
30 | /** | |
31 | * Console.log helper | |
32 | */ | |
33 | ||
34 | 1 | ValidationError.prototype.toString = function () { |
35 | 1 | var ret = this.name + ': '; |
36 | 1 | var msgs = []; |
37 | ||
38 | 1 | Object.keys(this.errors).forEach(function (key) { |
39 | 2 | if (this == this.errors[key]) return; |
40 | 2 | msgs.push(String(this.errors[key])); |
41 | }, this); | |
42 | ||
43 | 1 | return ret + msgs.join(', '); |
44 | }; | |
45 | ||
46 | /*! | |
47 | * Module exports | |
48 | */ | |
49 | ||
50 | 1 | module.exports = exports = ValidationError; |
51 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module dependencies. | |
3 | */ | |
4 | ||
5 | 1 | var FormalError = require('../error'), |
6 | util = require('util'); | |
7 | ||
8 | /** | |
9 | * Schema validator error | |
10 | * | |
11 | * @param {String} path | |
12 | * @param {String} msg | |
13 | * @param {String|Number|any} val | |
14 | * @inherits FormalError | |
15 | * @api private | |
16 | */ | |
17 | ||
18 | 1 | function ValidatorError (path, type, val) { |
19 | 19 | var msg = type ? '"' + type + '" ' : ''; |
20 | ||
21 | 19 | var message = 'Validator ' + msg + 'failed for path ' + path; |
22 | 38 | if (2 < arguments.length) message += ' with value `' + String(val) + '`'; |
23 | ||
24 | 19 | FormalError.call(this, message); |
25 | 19 | Error.captureStackTrace(this, ValidatorError); |
26 | 19 | this.name = 'ValidatorError'; |
27 | 19 | this.path = path; |
28 | 19 | this.type = type; |
29 | 19 | this.value = val; |
30 | } | |
31 | ||
32 | /*! | |
33 | * Inherits from FormalError | |
34 | */ | |
35 | ||
36 | 1 | util.inherits(ValidatorError, FormalError); |
37 | ||
38 | /*! | |
39 | * toString helper | |
40 | */ | |
41 | ||
42 | 1 | ValidatorError.prototype.toString = function () { |
43 | 2 | return this.message; |
44 | }; | |
45 | ||
46 | ||
47 | /*! | |
48 | * exports | |
49 | */ | |
50 | ||
51 | 1 | module.exports = ValidatorError; |
52 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module dependencies. | |
3 | */ | |
4 | ||
5 | 1 | var Field = require('./field'), |
6 | util = require('util'), | |
7 | CastError = Field.CastError, | |
8 | Types = { | |
9 | Boolean: require('./boolean'), | |
10 | Date: require('./date'), | |
11 | Number: require('./number'), | |
12 | String: require('./string') | |
13 | }; | |
14 | ||
15 | /** | |
16 | * Array Field constructor | |
17 | * | |
18 | * @param {String} key | |
19 | * @param {Field} cast | |
20 | * @param {Object} options | |
21 | * @inherits Field | |
22 | * @api private | |
23 | */ | |
24 | ||
25 | 1 | function FieldArray (key, cast, options) { |
26 | 10 | if (cast) { |
27 | 10 | var castOptions = {}; |
28 | ||
29 | // support { type: 'String' } | |
30 | 10 | var name = 'string' == typeof cast ? cast : cast.name; |
31 | ||
32 | 10 | var Caster = name in Types ? Types[name] : cast; |
33 | ||
34 | 10 | this.casterConstructor = Caster; |
35 | 10 | this.caster = new Caster(null, castOptions); |
36 | } | |
37 | ||
38 | 10 | Field.call(this, key, options); |
39 | ||
40 | 10 | var self = this, |
41 | defaultArr, fn; | |
42 | ||
43 | 10 | if (this.defaultValue) { |
44 | 1 | defaultArr = this.defaultValue; |
45 | 1 | fn = 'function' == typeof defaultArr; |
46 | } | |
47 | ||
48 | 10 | this.default(function(){ |
49 | 10 | var arr = fn ? defaultArr() : defaultArr || []; |
50 | 10 | return arr; |
51 | }); | |
52 | } | |
53 | ||
54 | /*! | |
55 | * Inherits from Field. | |
56 | */ | |
57 | ||
58 | 1 | util.inherits(FieldArray, Field); |
59 | ||
60 | /** | |
61 | * Check required | |
62 | * | |
63 | * @param {Array} value | |
64 | * @api private | |
65 | */ | |
66 | ||
67 | 1 | FieldArray.prototype.checkRequired = function (value) { |
68 | 1 | return !!(value && value.length); |
69 | }; | |
70 | ||
71 | /** | |
72 | * Overrides the getters application for the population special-case | |
73 | * | |
74 | * @param {Object} value | |
75 | * @param {Object} scope | |
76 | * @api private | |
77 | */ | |
78 | ||
79 | 1 | FieldArray.prototype.applyGetters = function (value, scope) { |
80 | 9 | if (this.caster.options && this.caster.options.ref) { |
81 | // means the object id was populated | |
82 | 0 | return value; |
83 | } | |
84 | ||
85 | 9 | return Field.prototype.applyGetters.call(this, value, scope); |
86 | }; | |
87 | ||
88 | /** | |
89 | * Casts contents | |
90 | * | |
91 | * @param {Object} value | |
92 | * @param {Form} form that triggers the casting | |
93 | * @param {Boolean} init whether this is an initialization cast | |
94 | * @api private | |
95 | */ | |
96 | ||
97 | 1 | FieldArray.prototype.cast = function (value, form, init) { |
98 | 18 | if (Array.isArray(value)) { |
99 | ||
100 | 17 | if (this.caster) { |
101 | 17 | try { |
102 | 17 | for (var i = 0, l = value.length; i < l; i++) { |
103 | 17 | value[i] = this.caster.cast(value[i], form, init); |
104 | } | |
105 | } catch (e) { | |
106 | // rethrow | |
107 | 0 | throw new CastError(e.type, value, this.path); |
108 | } | |
109 | } | |
110 | ||
111 | 17 | return value; |
112 | } else { | |
113 | 1 | return this.cast([value], form, init); |
114 | } | |
115 | }; | |
116 | ||
117 | /*! | |
118 | * Module exports. | |
119 | */ | |
120 | ||
121 | 1 | module.exports = FieldArray; |
122 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module dependencies. | |
3 | */ | |
4 | ||
5 | 1 | var util = require('util'), |
6 | Field = require('./field'); | |
7 | ||
8 | /** | |
9 | * Boolean Field constructor. | |
10 | * | |
11 | * @param {String} path | |
12 | * @param {Object} options | |
13 | * @inherits Field | |
14 | * @api private | |
15 | */ | |
16 | ||
17 | 1 | function FieldBoolean (path, options) { |
18 | 12 | Field.call(this, path, options); |
19 | } | |
20 | ||
21 | /*! | |
22 | * Inherits from Field. | |
23 | */ | |
24 | 1 | util.inherits(FieldBoolean, Field); |
25 | ||
26 | /** | |
27 | * Required validator | |
28 | * | |
29 | * @api private | |
30 | */ | |
31 | ||
32 | 1 | FieldBoolean.prototype.checkRequired = function (value) { |
33 | 1 | return value === true || value === false; |
34 | }; | |
35 | ||
36 | /** | |
37 | * Casts to boolean | |
38 | * | |
39 | * @param {Object} value | |
40 | * @api private | |
41 | */ | |
42 | ||
43 | 1 | FieldBoolean.prototype.cast = function (value) { |
44 | 10 | if (null === value) return value; |
45 | 11 | if ('0' === value) return false; |
46 | 9 | if ('true' === value) return true; |
47 | 10 | if ('false' === value) return false; |
48 | 8 | return !! value; |
49 | }; | |
50 | ||
51 | 1 | FieldBoolean.prototype.export = function() { |
52 | 1 | FieldBoolean.super_.prototype.export.call(this); |
53 | ||
54 | 1 | var attr = this.options.attributes || (this.options.attributes = {}); |
55 | ||
56 | 1 | attr.type = 'checkbox'; |
57 | ||
58 | 1 | return this.options; |
59 | }; | |
60 | ||
61 | /*! | |
62 | * Module exports. | |
63 | */ | |
64 | ||
65 | 1 | module.exports = FieldBoolean; |
66 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module requirements. | |
3 | */ | |
4 | ||
5 | 1 | var Field = require('./field'), |
6 | util = require('util'), | |
7 | CastError = Field.CastError; | |
8 | ||
9 | /** | |
10 | * Date Field constructor. | |
11 | * | |
12 | * @param {String} key | |
13 | * @param {Object} options | |
14 | * @inherits Field | |
15 | * @api private | |
16 | */ | |
17 | ||
18 | 1 | function FieldDate (key, options) { |
19 | 10 | Field.call(this, key, options); |
20 | } | |
21 | ||
22 | /*! | |
23 | * Inherits from Field. | |
24 | */ | |
25 | ||
26 | 1 | util.inherits(FieldDate, Field); |
27 | ||
28 | /** | |
29 | * Required validator for date | |
30 | * | |
31 | * @api private | |
32 | */ | |
33 | ||
34 | 1 | FieldDate.prototype.checkRequired = function (value) { |
35 | 2 | return value instanceof Date; |
36 | }; | |
37 | ||
38 | /** | |
39 | * Casts to date | |
40 | * | |
41 | * @param {Object} value to cast | |
42 | * @api private | |
43 | */ | |
44 | ||
45 | 1 | FieldDate.prototype.cast = function (value) { |
46 | 9 | if (value === null || value === '') |
47 | 1 | return null; |
48 | ||
49 | 8 | if (value instanceof Date) |
50 | 5 | return value; |
51 | ||
52 | 3 | var date; |
53 | ||
54 | // support for timestamps | |
55 | 3 | if (value instanceof Number || 'number' == typeof value || String(value) == Number(value)) |
56 | 1 | date = new Date(Number(value)); |
57 | ||
58 | // support for date strings | |
59 | 2 | else if (value.toString) |
60 | 2 | date = new Date(value.toString()); |
61 | ||
62 | 3 | if (date.toString() != 'Invalid Date') |
63 | 2 | return date; |
64 | ||
65 | 1 | throw new CastError('date', value, this.path); |
66 | }; | |
67 | ||
68 | 1 | FieldDate.prototype.export = function() { |
69 | 1 | FieldDate.super_.prototype.export.call(this); |
70 | ||
71 | 1 | var attr = this.options.attributes || (this.options.attributes = {}); |
72 | ||
73 | 1 | attr.type = 'date'; |
74 | ||
75 | 1 | return this.options; |
76 | }; | |
77 | ||
78 | ||
79 | /*! | |
80 | * Module exports. | |
81 | */ | |
82 | ||
83 | 1 | module.exports = FieldDate; |
84 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module dependencies. | |
3 | */ | |
4 | ||
5 | 1 | var CastError = require('../error').CastError, |
6 | ValidatorError = require('../error').ValidatorError, | |
7 | util = require('util'), | |
8 | _ = require('lodash'), | |
9 | EventEmitter = require('events').EventEmitter; | |
10 | ||
11 | /** | |
12 | * Field constructor | |
13 | * | |
14 | * @param {String} path | |
15 | * @param {Object} [options] | |
16 | * @param {String} [instance] | |
17 | * @api public | |
18 | */ | |
19 | ||
20 | 1 | function Field (path, options, instance) { |
21 | 95 | this.path = path; |
22 | 95 | this.instance = instance; |
23 | 95 | this.validators = []; |
24 | 95 | this.setters = []; |
25 | 95 | this.getters = []; |
26 | 95 | this.defaultValue = null; |
27 | ||
28 | 95 | this.options = options; |
29 | ||
30 | 95 | for (var opt in options) |
31 | 122 | if (this[opt] && 'function' == typeof this[opt]) { |
32 | // { unique: true, index: true } | |
33 | // if ('index' == opt && this._index) continue; | |
34 | ||
35 | 35 | var opts = Array.isArray(options[opt]) ? options[opt] : [options[opt]]; |
36 | ||
37 | 35 | this[opt].apply(this, opts); |
38 | } | |
39 | ||
40 | 95 | EventEmitter.call(this); |
41 | } | |
42 | ||
43 | 1 | util.inherits(Field, EventEmitter); |
44 | ||
45 | /** | |
46 | * Sets a default value for this Field. | |
47 | * | |
48 | * ####Example: | |
49 | * | |
50 | * var field = new Field({ n: { type: Number, default: 10 }) | |
51 | * var M = db.model('M', field) | |
52 | * var m = new M; | |
53 | * console.log(m.n) // 10 | |
54 | * | |
55 | * Defaults can be either `functions` which return the value to use as the default or the literal value itself. Either way, the value will be cast based on its field type before being set during document creation. | |
56 | * | |
57 | * ####Example: | |
58 | * | |
59 | * // values are cast: | |
60 | * var field = new Field({ aNumber: Number, default: "4.815162342" }) | |
61 | * var M = db.model('M', field) | |
62 | * var m = new M; | |
63 | * console.log(m.aNumber) // 4.815162342 | |
64 | * | |
65 | * // default unique objects for Mixed types: | |
66 | * var field = new Field({ mixed: Field.Types.Mixed }); | |
67 | * field.path('mixed').default(function () { | |
68 | * return {}; | |
69 | * }); | |
70 | * | |
71 | * // if we don't use a function to return object literals for Mixed defaults, | |
72 | * // each document will receive a reference to the same object literal creating | |
73 | * // a "shared" object instance: | |
74 | * var field = new Field({ mixed: Field.Types.Mixed }); | |
75 | * field.path('mixed').default({}); | |
76 | * var M = db.model('M', field); | |
77 | * var m1 = new M; | |
78 | * m1.mixed.added = 1; | |
79 | * console.log(m1.mixed); // { added: 1 } | |
80 | * var m2 = new M; | |
81 | * console.log(m2.mixed); // { added: 1 } | |
82 | * | |
83 | * @param {Function|any} val the default value | |
84 | * @return {defaultValue} | |
85 | * @api public | |
86 | */ | |
87 | ||
88 | 1 | Field.prototype.default = function (val) { |
89 | 14 | if (1 === arguments.length) { |
90 | 11 | this.defaultValue = typeof val === 'function' ? val : this.cast(val); |
91 | 11 | return this; |
92 | 3 | } else if (arguments.length > 1) { |
93 | 2 | this.defaultValue = _.toArray(arguments); |
94 | } | |
95 | 3 | if (arguments.length) |
96 | 2 | this.emit('default', this.defaultValue); |
97 | 3 | return this.defaultValue; |
98 | }; | |
99 | ||
100 | /** | |
101 | * Declares an unique index. | |
102 | * | |
103 | * ####Example: | |
104 | * | |
105 | * var s = new Field({ name: { type: String, unique: true }) | |
106 | * Field.path('name').index({ unique: true }); | |
107 | * | |
108 | * _NOTE: violating the constraint returns an `E11000` error from MongoDB when saving, not a Mongoose validation error._ | |
109 | * | |
110 | * @param {Boolean} bool | |
111 | * @return {Field} this | |
112 | * @api public | |
113 | ||
114 | Field.prototype.unique = function (bool) { | |
115 | //this._index.unique = bool; | |
116 | return this; | |
117 | }; | |
118 | */ | |
119 | ||
120 | /** | |
121 | * Adds a setter to this fieldtype. | |
122 | * | |
123 | * ####Example: | |
124 | * | |
125 | * function capitalize (val) { | |
126 | * if ('string' != typeof val) val = ''; | |
127 | * return val.charAt(0).toUpperCase() + val.substring(1); | |
128 | * } | |
129 | * | |
130 | * // defining within the field | |
131 | * var s = new Field({ name: { type: String, set: capitalize }}) | |
132 | * | |
133 | * // or by retreiving its Field | |
134 | * var s = new Field({ name: String }) | |
135 | * s.path('name').set(capitalize) | |
136 | * | |
137 | * Setters allow you to transform the data before it gets to the raw mongodb document and is set as a value on an actual key. | |
138 | * | |
139 | * Suppose you are implementing user registration for a website. Users provide an email and password, which gets saved to mongodb. The email is a string that you will want to normalize to lower case, in order to avoid one email having more than one account -- e.g., otherwise, avenue@q.com can be registered for 2 accounts via avenue@q.com and AvEnUe@Q.CoM. | |
140 | * | |
141 | * You can set up email lower case normalization easily via a Mongoose setter. | |
142 | * | |
143 | * function toLower (v) { | |
144 | * return v.toLowerCase(); | |
145 | * } | |
146 | * | |
147 | * var UserField = new Field({ | |
148 | * email: { type: String, set: toLower } | |
149 | * }) | |
150 | * | |
151 | * var User = db.model('User', UserField) | |
152 | * | |
153 | * var user = new User({email: 'AVENUE@Q.COM'}) | |
154 | * console.log(user.email); // 'avenue@q.com' | |
155 | * | |
156 | * // or | |
157 | * var user = new User | |
158 | * user.email = 'Avenue@Q.com' | |
159 | * console.log(user.email) // 'avenue@q.com' | |
160 | * | |
161 | * As you can see above, setters allow you to transform the data before it gets to the raw mongodb document and is set as a value on an actual key. | |
162 | * | |
163 | * _NOTE: we could have also just used the built-in `lowercase: true` Field option instead of defining our own function._ | |
164 | * | |
165 | * new Field({ email: { type: String, lowercase: true }}) | |
166 | * | |
167 | * Setters are also passed a second argument, the fieldtype on which the setter was defined. This allows for tailored behavior based on options passed in the field. | |
168 | * | |
169 | * function inspector (val, fieldtype) { | |
170 | * if (fieldtype.options.required) { | |
171 | * return fieldtype.path + ' is required'; | |
172 | * } else { | |
173 | * return val; | |
174 | * } | |
175 | * } | |
176 | * | |
177 | * var VirusField = new Field({ | |
178 | * name: { type: String, required: true, set: inspector }, | |
179 | * taxonomy: { type: String, set: inspector } | |
180 | * }) | |
181 | * | |
182 | * var Virus = db.model('Virus', VirusField); | |
183 | * var v = new Virus({ name: 'Parvoviridae', taxonomy: 'Parvovirinae' }); | |
184 | * | |
185 | * console.log(v.name); // name is required | |
186 | * console.log(v.taxonomy); // Parvovirinae | |
187 | * | |
188 | * @param {Function} fn | |
189 | * @return {Field} this | |
190 | * @api public | |
191 | */ | |
192 | ||
193 | 1 | Field.prototype.set = function (fn) { |
194 | 10 | if ('function' != typeof fn) |
195 | 1 | throw new TypeError('A setter must be a function.'); |
196 | 9 | this.setters.push(fn); |
197 | 9 | return this; |
198 | }; | |
199 | ||
200 | /** | |
201 | * Adds a getter to this fieldtype. | |
202 | * | |
203 | * ####Example: | |
204 | * | |
205 | * function dob (val) { | |
206 | * if (!val) return val; | |
207 | * return (val.getMonth() + 1) + "/" + val.getDate() + "/" + val.getFullYear(); | |
208 | * } | |
209 | * | |
210 | * // defining within the field | |
211 | * var s = new Field({ born: { type: Date, get: dob }) | |
212 | * | |
213 | * // or by retreiving its Field | |
214 | * var s = new Field({ born: Date }) | |
215 | * s.path('born').get(dob) | |
216 | * | |
217 | * Getters allow you to transform the representation of the data as it travels from the raw mongodb document to the value that you see. | |
218 | * | |
219 | * Suppose you are storing credit card numbers and you want to hide everything except the last 4 digits to the mongoose user. You can do so by defining a getter in the following way: | |
220 | * | |
221 | * function obfuscate (cc) { | |
222 | * return '****-****-****-' + cc.slice(cc.length-4, cc.length); | |
223 | * } | |
224 | * | |
225 | * var AccountField = new Field({ | |
226 | * creditCardNumber: { type: String, get: obfuscate } | |
227 | * }); | |
228 | * | |
229 | * var Account = db.model('Account', AccountField); | |
230 | * | |
231 | * Account.findById(id, function (err, found) { | |
232 | * console.log(found.creditCardNumber); // '****-****-****-1234' | |
233 | * }); | |
234 | * | |
235 | * Getters are also passed a second argument, the fieldtype on which the getter was defined. This allows for tailored behavior based on options passed in the field. | |
236 | * | |
237 | * function inspector (val, fieldtype) { | |
238 | * if (fieldtype.options.required) { | |
239 | * return fieldtype.path + ' is required'; | |
240 | * } else { | |
241 | * return fieldtype.path + ' is not'; | |
242 | * } | |
243 | * } | |
244 | * | |
245 | * var VirusField = new Field({ | |
246 | * name: { type: String, required: true, get: inspector }, | |
247 | * taxonomy: { type: String, get: inspector } | |
248 | * }) | |
249 | * | |
250 | * var Virus = db.model('Virus', VirusField); | |
251 | * | |
252 | * Virus.findById(id, function (err, virus) { | |
253 | * console.log(virus.name); // name is required | |
254 | * console.log(virus.taxonomy); // taxonomy is not | |
255 | * }) | |
256 | * | |
257 | * @param {Function} fn | |
258 | * @return {Field} this | |
259 | * @api public | |
260 | */ | |
261 | ||
262 | 1 | Field.prototype.get = function (fn) { |
263 | 4 | if ('function' != typeof fn) |
264 | 1 | throw new TypeError('A getter must be a function.'); |
265 | 3 | this.getters.push(fn); |
266 | 3 | return this; |
267 | }; | |
268 | ||
269 | /** | |
270 | * Adds validator(s) for this document path. | |
271 | * | |
272 | * Validators always receive the value to validate as their first argument and must return `Boolean`. Returning false is interpreted as validation failure. | |
273 | * | |
274 | * ####Examples: | |
275 | * | |
276 | * function validator (val) { | |
277 | * return val == 'something'; | |
278 | * } | |
279 | * | |
280 | * new Field({ name: { type: String, validate: validator }}); | |
281 | * | |
282 | * // with a custom error message | |
283 | * | |
284 | * var custom = [validator, 'validation failed'] | |
285 | * new Field({ name: { type: String, validate: custom }}); | |
286 | * | |
287 | * var many = [ | |
288 | * { validator: validator, msg: 'uh oh' } | |
289 | * , { validator: fn, msg: 'failed' } | |
290 | * ] | |
291 | * new Field({ name: { type: String, validate: many }}); | |
292 | * | |
293 | * // or utilizing Field methods directly: | |
294 | * | |
295 | * var field = new Field({ name: 'string' }); | |
296 | * field.path('name').validate(validator, 'validation failed'); | |
297 | * | |
298 | * ####Asynchronous validation: | |
299 | * | |
300 | * Passing a validator function that receives two arguments tells mongoose that the validator is an asynchronous validator. The second argument is an callback function that must be passed either `true` or `false` when validation is complete. | |
301 | * | |
302 | * field.path('name').validate(function (value, respond) { | |
303 | * doStuff(value, function () { | |
304 | * ... | |
305 | * respond(false); // validation failed | |
306 | * }) | |
307 | * }, 'my error type'); | |
308 | * | |
309 | * You might use asynchronous validators to retreive other documents from the database to validate against or to meet other I/O bound validation needs. | |
310 | * | |
311 | * Validation occurs `pre('save')` or whenever you manually execute [document#validate](#document_Document-validate). | |
312 | * | |
313 | * If validation fails during `pre('save')` and no callback was passed to receive the error, an `error` event will be emitted on your Models associated db [connection](#connection_Connection), passing the validation error object along. | |
314 | * | |
315 | * var conn = mongoose.createConnection(..); | |
316 | * conn.on('error', handleError); | |
317 | * | |
318 | * var Product = conn.model('Product', yourField); | |
319 | * var dvd = new Product(..); | |
320 | * dvd.save(); // emits error on the `conn` above | |
321 | * | |
322 | * If you desire handling these errors at the Model level, attach an `error` listener to your Model and the event will instead be emitted there. | |
323 | * | |
324 | * // registering an error listener on the Model lets us handle errors more locally | |
325 | * Product.on('error', handleError); | |
326 | * | |
327 | * @param {RegExp|Function|Object} obj validator | |
328 | * @param {String} [error] optional error message | |
329 | * @api public | |
330 | */ | |
331 | ||
332 | 1 | Field.prototype.validate = function (obj, error) { |
333 | 6 | if ('function' == typeof obj || obj && 'RegExp' === obj.constructor.name) { |
334 | 4 | this.validators.push([obj, error]); |
335 | 4 | return this; |
336 | } | |
337 | ||
338 | 2 | var i = arguments.length, |
339 | arg; | |
340 | ||
341 | 2 | while (i--) { |
342 | 2 | arg = arguments[i]; |
343 | 2 | if (!(arg && 'Object' == arg.constructor.name)) { |
344 | 1 | var msg = 'Invalid validator. Received (' + typeof arg + ') ' + |
345 | arg + | |
346 | '. See http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate'; | |
347 | ||
348 | 1 | throw new Error(msg); |
349 | } | |
350 | 1 | this.validate(arg.validator, arg.msg); |
351 | } | |
352 | ||
353 | 1 | return this; |
354 | }; | |
355 | ||
356 | /** | |
357 | * Adds a required validator to this fieldtype. | |
358 | * | |
359 | * ####Example: | |
360 | * | |
361 | * var s = new Field({ born: { type: Date, required: true }) | |
362 | * // or | |
363 | * form.path('name').required(true); | |
364 | * | |
365 | * | |
366 | * @param {Boolean} required enable/disable the validator | |
367 | * @return {Field} this | |
368 | * @api public | |
369 | */ | |
370 | ||
371 | 1 | Field.prototype.required = function (required) { |
372 | 10 | var self = this; |
373 | ||
374 | 10 | function __checkRequired (v) { |
375 | 9 | return self.checkRequired(v, this); |
376 | } | |
377 | ||
378 | 10 | if (false === required) { |
379 | 2 | delete this.options.required; |
380 | 2 | this.validators = this.validators.filter(function (v) { |
381 | 2 | return v[0].name !== '__checkRequired'; |
382 | }); | |
383 | } else { | |
384 | 8 | this.options.required = true; |
385 | 8 | this.validators.push([__checkRequired, 'required']); |
386 | } | |
387 | ||
388 | 10 | return this; |
389 | }; | |
390 | ||
391 | /** | |
392 | * Gets the default value | |
393 | * | |
394 | * @param {Object} scope the scope which callback are executed | |
395 | * @param {Boolean} init | |
396 | * @api private | |
397 | */ | |
398 | ||
399 | 1 | Field.prototype.getDefault = function (scope, init) { |
400 | 85 | var ret = 'function' === typeof this.defaultValue ? |
401 | this.defaultValue.call(scope) : | |
402 | this.defaultValue; | |
403 | ||
404 | 85 | if (null !== ret && undefined !== ret) { |
405 | 11 | return this.cast(ret, scope, init); |
406 | } else { | |
407 | 74 | return ret; |
408 | } | |
409 | }; | |
410 | ||
411 | /** | |
412 | * Applies setters | |
413 | * | |
414 | * @param {Object} value | |
415 | * @param {Object} scope | |
416 | * @param {Boolean} init | |
417 | * @api private | |
418 | */ | |
419 | ||
420 | 1 | Field.prototype.applySetters = function (value, scope, init, priorVal) { |
421 | 62 | var v = value, |
422 | setters = this.setters, | |
423 | len = setters.length; | |
424 | ||
425 | 62 | if (!len) { |
426 | 56 | if (null === v || undefined === v) return v; |
427 | 56 | return this.cast(v, scope, init, priorVal); |
428 | } | |
429 | ||
430 | 6 | while (len--) { |
431 | 7 | v = setters[len].call(scope, v, this); |
432 | } | |
433 | ||
434 | 6 | if (null === v || undefined === v) return v; |
435 | ||
436 | // do not cast until all setters are applied | |
437 | 6 | v = this.cast(v, scope, init, priorVal); |
438 | ||
439 | 6 | return v; |
440 | }; | |
441 | ||
442 | /** | |
443 | * Applies getters to a value | |
444 | * | |
445 | * @param {Object} value | |
446 | * @param {Object} scope | |
447 | * @api private | |
448 | */ | |
449 | ||
450 | 1 | Field.prototype.applyGetters = function (value, scope) { |
451 | 61 | var v = value, |
452 | getters = this.getters, | |
453 | len = getters.length; | |
454 | ||
455 | 61 | if (!len) { |
456 | 57 | return v; |
457 | } | |
458 | ||
459 | 4 | while (len--) { |
460 | 4 | v = getters[len].call(scope, v, this); |
461 | } | |
462 | ||
463 | 4 | return v; |
464 | }; | |
465 | ||
466 | /** | |
467 | * Performs a validation of `value` using the validators declared for this Field. | |
468 | * | |
469 | * @param {any} value | |
470 | * @param {Function} callback | |
471 | * @param {Object} scope | |
472 | * @api private | |
473 | */ | |
474 | ||
475 | 1 | Field.prototype.doValidate = function (value, fn, scope) { |
476 | 44 | var err = false, |
477 | path = this.path, | |
478 | count = this.validators.length; | |
479 | ||
480 | 58 | if (!count) return fn(null); |
481 | ||
482 | 30 | function validate (ok, msg, val) { |
483 | 31 | if (err) return; |
484 | 31 | if (ok === undefined || ok) { |
485 | 23 | if (!--count) fn(null); |
486 | } else { | |
487 | 19 | fn(err = new ValidatorError(path, msg, val)); |
488 | } | |
489 | } | |
490 | ||
491 | 30 | this.validators.forEach(function (v) { |
492 | 31 | var validator = v[0], |
493 | message = v[1]; | |
494 | ||
495 | 31 | if (validator instanceof RegExp) { |
496 | 3 | validate(validator.test(value), message, value); |
497 | 28 | } else if ('function' === typeof validator) { |
498 | 28 | if (2 === validator.length) { |
499 | 2 | validator.call(scope, value, function (ok) { |
500 | 2 | validate(ok, message, value); |
501 | }); | |
502 | } else { | |
503 | 26 | validate(validator.call(scope, value), message, value); |
504 | } | |
505 | } | |
506 | }); | |
507 | }; | |
508 | ||
509 | /** | |
510 | * Return field options | |
511 | * | |
512 | * @return {Object} options | |
513 | * @api public | |
514 | */ | |
515 | 1 | Field.prototype.export = function () { |
516 | 15 | var attr = this.options.attributes || (this.options.attributes = {}); |
517 | ||
518 | 15 | if (!this.options.required && attr.required) |
519 | 0 | delete attr.required; |
520 | ||
521 | 15 | if (this.options.required) |
522 | 4 | attr.required = null; // <input name="toto" required/> |
523 | ||
524 | 15 | return this.options; |
525 | }; | |
526 | ||
527 | /*! | |
528 | * Module exports. | |
529 | */ | |
530 | ||
531 | 1 | module.exports = exports = Field; |
532 | ||
533 | 1 | exports.CastError = CastError; |
534 | ||
535 | 1 | exports.ValidatorError = ValidatorError; |
536 |
Line | Hits | Source |
---|---|---|
1 | /* jshint -W053 */ | |
2 | /*! | |
3 | * Module requirements. | |
4 | */ | |
5 | ||
6 | 1 | var Field = require('./field'), |
7 | util = require('util'), | |
8 | CastError = Field.CastError, | |
9 | Document; | |
10 | ||
11 | /** | |
12 | * Number Field constructor. | |
13 | * | |
14 | * @param {String} key | |
15 | * @param {Object} options | |
16 | * @inherits Field | |
17 | * @api private | |
18 | */ | |
19 | ||
20 | 1 | function FieldNumber (key, options) { |
21 | 20 | Field.call(this, key, options, 'Number'); |
22 | } | |
23 | ||
24 | /*! | |
25 | * Inherits from Field. | |
26 | */ | |
27 | ||
28 | 1 | util.inherits(FieldNumber, Field); |
29 | ||
30 | /** | |
31 | * Required validator for number | |
32 | * | |
33 | * @api private | |
34 | */ | |
35 | ||
36 | 1 | FieldNumber.prototype.checkRequired = function checkRequired (value, doc) { |
37 | 1 | return typeof value == 'number' || value instanceof Number; |
38 | }; | |
39 | ||
40 | /** | |
41 | * Sets a minimum number validator. | |
42 | * | |
43 | * ####Example: | |
44 | * | |
45 | * var form = new Form({ | |
46 | * age: { | |
47 | * type: Number, | |
48 | * min: 18 | |
49 | * } | |
50 | * }); | |
51 | * | |
52 | * form.set('age', 17); | |
53 | * | |
54 | * form.validate(console.log); | |
55 | * | |
56 | * @param {Number} value minimum number | |
57 | * @param {String} message | |
58 | * @api public | |
59 | */ | |
60 | ||
61 | 1 | FieldNumber.prototype.min = function (value, message) { |
62 | 5 | if (this.minValidator) { |
63 | 1 | delete this.options.min; |
64 | 1 | this.validators = this.validators.filter(function(v){ |
65 | 1 | return v[1] != 'min'; |
66 | }); | |
67 | } | |
68 | 5 | if (value != null) { |
69 | 4 | this.validators.push([this.minValidator = function(v){ |
70 | 5 | return v === null || v >= value; |
71 | }, 'min']); | |
72 | 4 | this.options.min = value; |
73 | } | |
74 | 5 | return this; |
75 | }; | |
76 | ||
77 | /** | |
78 | * Sets a maximum number validator. | |
79 | * | |
80 | * ####Example: | |
81 | * | |
82 | * var form = new Form({ | |
83 | * price: Number | |
84 | * }); | |
85 | * | |
86 | * form.path('price').max(100000000, 'Price is too high, well... really?'); | |
87 | * | |
88 | * form.validate(console.log); | |
89 | * | |
90 | * @param {Number} maximum number | |
91 | * @param {String} message | |
92 | * @api public | |
93 | */ | |
94 | ||
95 | 1 | FieldNumber.prototype.max = function (value, message) { |
96 | 4 | if (this.maxValidator) { |
97 | 1 | this.maxValue = null; |
98 | 1 | this.validators = this.validators.filter(function(v){ |
99 | 1 | return v[1] != 'max'; |
100 | }); | |
101 | } | |
102 | 4 | if (value != null) { |
103 | 4 | this.validators.push([this.maxValidator = function(v){ |
104 | 4 | return v === null || v <= value; |
105 | }, 'max']); | |
106 | 4 | this.maxValue = value; |
107 | } | |
108 | 4 | return this; |
109 | }; | |
110 | ||
111 | /** | |
112 | * Casts to number | |
113 | * | |
114 | * @param {Object} value value to cast | |
115 | * @param {Document} doc document that triggers the casting | |
116 | * @param {Boolean} init | |
117 | * @api private | |
118 | */ | |
119 | ||
120 | 1 | FieldNumber.prototype.cast = function (value, doc, init) { |
121 | 19 | var val = value; |
122 | ||
123 | 19 | if (!isNaN(val)){ |
124 | 19 | if (null === val) return val; |
125 | 19 | if ('' === val) return null; |
126 | 21 | if ('string' == typeof val) val = Number(val); |
127 | 19 | if (val instanceof Number) return val; |
128 | 36 | if ('number' == typeof val) return val; |
129 | 2 | if (val.toString && !Array.isArray(val) && |
130 | val.toString() == Number(val)) { | |
131 | 1 | return new Number(val); |
132 | } | |
133 | } | |
134 | ||
135 | 1 | throw new CastError('number', value, this.path); |
136 | }; | |
137 | ||
138 | /** | |
139 | * Export | |
140 | * @return {[type]} [description] | |
141 | */ | |
142 | 1 | FieldNumber.prototype.export = function() { |
143 | 3 | FieldNumber.super_.prototype.export.call(this); |
144 | ||
145 | 3 | var attr = this.options.attributes || (this.options.attributes = {}); |
146 | ||
147 | 3 | attr.type = 'number'; |
148 | ||
149 | 3 | if (!this.options.min && attr.min) |
150 | 0 | delete attr.min; |
151 | 3 | if (this.options.min) |
152 | 2 | attr.min = this.options.min; |
153 | ||
154 | 3 | if (!this.options.max && attr.max) |
155 | 0 | delete attr.max; |
156 | ||
157 | 3 | if (this.options.max) |
158 | 2 | attr.max = this.options.max; |
159 | ||
160 | 3 | return this.options; |
161 | }; | |
162 | ||
163 | /*! | |
164 | * Module exports. | |
165 | */ | |
166 | ||
167 | 1 | module.exports = FieldNumber; |
168 |
Line | Hits | Source |
---|---|---|
1 | /* jshint bitwise: false */ | |
2 | /*! | |
3 | * Module dependencies. | |
4 | */ | |
5 | ||
6 | 1 | var Field = require('./field'), |
7 | util = require('util'), | |
8 | CastError = Field.CastError, | |
9 | Document; | |
10 | ||
11 | /** | |
12 | * String Field constructor. | |
13 | * | |
14 | * @param {String} key | |
15 | * @param {Object} options | |
16 | * @inherits Field | |
17 | * @api private | |
18 | */ | |
19 | ||
20 | 1 | function FieldString (key, options) { |
21 | 43 | this.enumValues = []; |
22 | 43 | this.regExp = null; |
23 | 43 | Field.call(this, key, options, 'String'); |
24 | } | |
25 | ||
26 | /*! | |
27 | * Inherits from Field. | |
28 | */ | |
29 | ||
30 | 1 | util.inherits(FieldString, Field); |
31 | ||
32 | /** | |
33 | * Adds enumeration values and a coinciding validator. | |
34 | * | |
35 | * ####Example: | |
36 | * | |
37 | * var states = 'opening open closing closed'.split(' ') | |
38 | * var f = new Form({ state: { type: String, enum: states }) | |
39 | * app.post('/url', f, function (req, res) { | |
40 | * if (!req.form.isValid()) | |
41 | * console.error(req.form.errors); | |
42 | * }); | |
43 | * | |
44 | * @param {String} [args...] enumeration values | |
45 | * @api public | |
46 | */ | |
47 | ||
48 | 1 | FieldString.prototype.enum = function () { |
49 | 4 | var len = arguments.length; |
50 | 4 | if (!len || undefined === arguments[0] || false === arguments[0]) { |
51 | 1 | if (this.enumValidator){ |
52 | 1 | this.enumValidator = false; |
53 | 1 | this.validators = this.validators.filter(function(v){ |
54 | 1 | return v[1] != 'enum'; |
55 | }); | |
56 | } | |
57 | 1 | return; |
58 | } | |
59 | ||
60 | 3 | for (var i = 0; i < len; i++) { |
61 | 8 | if (undefined !== arguments[i]) { |
62 | 8 | this.enumValues.push(this.cast(arguments[i])); |
63 | } | |
64 | } | |
65 | ||
66 | 3 | if (!this.enumValidator) { |
67 | 3 | var values = this.enumValues; |
68 | 3 | this.enumValidator = function(v){ |
69 | 4 | return undefined === v || ~values.indexOf(v); |
70 | }; | |
71 | 3 | this.validators.push([this.enumValidator, 'enum']); |
72 | } | |
73 | }; | |
74 | ||
75 | /** | |
76 | * Adds a lowercase setter. | |
77 | * | |
78 | * ####Example: | |
79 | * | |
80 | * var s = new FieldString({ email: { type: String, lowercase: true }}) | |
81 | * var M = db.model('M', s); | |
82 | * var m = new M({ email: 'SomeEmail@example.COM' }); | |
83 | * console.log(m.email) // someemail@example.com | |
84 | * | |
85 | * @api public | |
86 | */ | |
87 | ||
88 | 1 | FieldString.prototype.lowercase = function () { |
89 | 1 | return this.set(function (v, self) { |
90 | 1 | if ('string' != typeof v) v = self.cast(v); |
91 | 2 | if (v) return v.toLowerCase(); |
92 | 0 | return v; |
93 | }); | |
94 | }; | |
95 | ||
96 | /** | |
97 | * Adds an uppercase setter. | |
98 | * | |
99 | * ####Example: | |
100 | * | |
101 | * var s = new FieldString({ caps: { type: String, uppercase: true }}) | |
102 | * var M = db.model('M', s); | |
103 | * var m = new M({ caps: 'an example' }); | |
104 | * console.log(m.caps) // AN EXAMPLE | |
105 | * | |
106 | * @api public | |
107 | */ | |
108 | ||
109 | 1 | FieldString.prototype.uppercase = function () { |
110 | 2 | return this.set(function (v, self) { |
111 | 1 | if ('string' != typeof v) v = self.cast(v); |
112 | 2 | if (v) return v.toUpperCase(); |
113 | 0 | return v; |
114 | }); | |
115 | }; | |
116 | ||
117 | /** | |
118 | * Adds a trim setter. | |
119 | * | |
120 | * The string value will be trimmed when set. | |
121 | * | |
122 | * ####Example: | |
123 | * | |
124 | * var s = new FieldString({ name: { type: String, trim: true }}) | |
125 | * var M = db.model('M', s) | |
126 | * var string = ' some name ' | |
127 | * console.log(string.length) // 11 | |
128 | * var m = new M({ name: string }) | |
129 | * console.log(m.name.length) // 9 | |
130 | * | |
131 | * @api public | |
132 | */ | |
133 | ||
134 | 1 | FieldString.prototype.trim = function () { |
135 | 4 | return this.set(function (v, self) { |
136 | 3 | if ('string' != typeof v) v = self.cast(v); |
137 | 5 | if (v) return v.trim(); |
138 | 1 | return v; |
139 | }); | |
140 | }; | |
141 | ||
142 | /** | |
143 | * Sets a regexp validator. | |
144 | * | |
145 | * Any value that does not pass `regExp`.test(val) will fail validation. | |
146 | * | |
147 | * ####Example: | |
148 | * | |
149 | * var s = new FieldString({ name: { type: String, match: /^a/ }}) | |
150 | * var M = db.model('M', s) | |
151 | * var m = new M({ name: 'invalid' }) | |
152 | * m.validate(function (err) { | |
153 | * console.error(err) // validation error | |
154 | * m.name = 'apples' | |
155 | * m.validate(function (err) { | |
156 | * assert.ok(err) // success | |
157 | * }) | |
158 | * }) | |
159 | * | |
160 | * @param {RegExp} regExp regular expression to test against | |
161 | * @api public | |
162 | */ | |
163 | ||
164 | 1 | FieldString.prototype.match = function match (regExp) { |
165 | 1 | this.validators.push([function(v){ |
166 | 2 | return null != v && '' !== v ? regExp.test(v) : true; |
167 | }, 'regexp']); | |
168 | }; | |
169 | ||
170 | /** | |
171 | * Check required | |
172 | * | |
173 | * @param {String|null|undefined} value | |
174 | * @api private | |
175 | */ | |
176 | ||
177 | 1 | FieldString.prototype.checkRequired = function checkRequired (value, doc) { |
178 | 4 | return (value instanceof String || typeof value == 'string') && value.length; |
179 | }; | |
180 | ||
181 | /** | |
182 | * Casts to String | |
183 | * | |
184 | * @api private | |
185 | */ | |
186 | ||
187 | 1 | FieldString.prototype.cast = function (value, doc, init) { |
188 | 47 | if (value === null) { |
189 | 1 | return value; |
190 | } | |
191 | ||
192 | 46 | if ('undefined' !== typeof value) { |
193 | 46 | if (value.toString) { |
194 | 45 | return value.toString(); |
195 | } | |
196 | } | |
197 | ||
198 | ||
199 | 1 | throw new CastError('string', value, this.path); |
200 | }; | |
201 | ||
202 | 1 | FieldString.prototype.export = function() { |
203 | 9 | FieldString.super_.prototype.export.call(this); |
204 | ||
205 | 9 | var attr = this.options.attributes || (this.options.attributes = {}); |
206 | ||
207 | 9 | if (this.enumValues.length) { |
208 | 1 | if (this.enumValues.length === 2 && this.isRequired) |
209 | 0 | attr.type = 'radio'; |
210 | else | |
211 | 1 | attr.type = 'checkbox'; |
212 | } else { | |
213 | 8 | attr.type = 'text'; |
214 | } | |
215 | ||
216 | 9 | return this.options; |
217 | }; | |
218 | ||
219 | /*! | |
220 | * Module exports. | |
221 | */ | |
222 | ||
223 | 1 | module.exports = FieldString; |
224 |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /*! | |
3 | * Module exports. | |
4 | */ | |
5 | ||
6 | 1 | exports.String = require('./string'); |
7 | ||
8 | 1 | exports.Number = require('./number'); |
9 | ||
10 | 1 | exports.Boolean = require('./boolean'); |
11 | ||
12 | //exports.FormArray = require('./formarray'); | |
13 | ||
14 | 1 | exports.Array = require('./array'); |
15 | ||
16 | //exports.Buffer = require('./buffer'); | |
17 | ||
18 | 1 | exports.Date = require('./date'); |
19 | ||
20 | 1 | exports.Virtual = require('./virtualtype'); |
21 | ||
22 | // alias | |
23 | ||
24 | //exports.Bool = exports.Boolean; | |
25 |
Line | Hits | Source |
---|---|---|
1 | ||
2 | /** | |
3 | * VirtualType constructor | |
4 | * | |
5 | * This is what Formal uses to define virtual attributes via `Form.prototype.virtual`. | |
6 | * | |
7 | * ####Example: | |
8 | * | |
9 | * var fullname = form.virtual('fullname').get(function() { | |
10 | * return this.name.first + ' ' this.name.family; | |
11 | * }); | |
12 | * | |
13 | * @parma {Object} options | |
14 | * @api public | |
15 | */ | |
16 | ||
17 | 1 | function VirtualType (options, name, scope) { |
18 | 2 | this.path = name; |
19 | 2 | this.scope = scope; |
20 | 2 | this.getters = []; |
21 | 2 | this.setters = []; |
22 | 2 | this.options = options || {}; |
23 | } | |
24 | ||
25 | /** | |
26 | * Defines a getter. | |
27 | * | |
28 | * ####Example: | |
29 | * | |
30 | * var virtual = form.virtual('fullname'); | |
31 | * virtual.get(function () { | |
32 | * return this.name.first + ' ' + this.name.last; | |
33 | * }); | |
34 | * | |
35 | * @param {Function} fn | |
36 | * @return {VirtualType} this | |
37 | * @api public | |
38 | */ | |
39 | ||
40 | 1 | VirtualType.prototype.get = function (fn) { |
41 | 1 | this.getters.push(fn.bind(this.scope)); |
42 | 1 | return this; |
43 | }; | |
44 | ||
45 | /** | |
46 | * Defines a setter. | |
47 | * | |
48 | * ####Example: | |
49 | * | |
50 | * var virtual = form.virtual('fullname'); | |
51 | * virtual.set(function (v) { | |
52 | * var parts = v.split(' '); | |
53 | * this.name.first = parts[0]; | |
54 | * this.name.last = parts[1]; | |
55 | * }); | |
56 | * | |
57 | * @param {Function} fn | |
58 | * @return {VirtualType} this | |
59 | * @api public | |
60 | */ | |
61 | ||
62 | 1 | VirtualType.prototype.set = function (fn) { |
63 | 1 | this.setters.push(fn.bind(this.scope)); |
64 | 1 | return this; |
65 | }; | |
66 | ||
67 | /** | |
68 | * Applies getters to `value` using optional `scope`. | |
69 | * | |
70 | * @param {Object} value | |
71 | * @param {Object} scope | |
72 | * @return {any} the value after applying all getters | |
73 | * @api public | |
74 | */ | |
75 | ||
76 | 1 | VirtualType.prototype.applyGetters = function (value, scope) { |
77 | 1 | var v = value; |
78 | 1 | for (var l = this.getters.length - 1; l >= 0; l--) { |
79 | 1 | v = this.getters[l].call(scope, v, this); |
80 | } | |
81 | 1 | return v; |
82 | }; | |
83 | ||
84 | /** | |
85 | * Applies setters to `value` using optional `scope`. | |
86 | * | |
87 | * @param {Object} value | |
88 | * @param {Object} scope | |
89 | * @return {any} the value after applying all setters | |
90 | * @api public | |
91 | */ | |
92 | ||
93 | 1 | VirtualType.prototype.applySetters = function (value, scope) { |
94 | 1 | var v = value; |
95 | 1 | for (var l = this.setters.length - 1; l >= 0; l--) { |
96 | 1 | v = this.setters[l].call(scope, v, this); |
97 | } | |
98 | 1 | return v; |
99 | }; | |
100 | ||
101 | /*! | |
102 | * exports | |
103 | */ | |
104 | ||
105 | 1 | module.exports = VirtualType; |
106 |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Module dependencies. | |
3 | */ | |
4 | ||
5 | 1 | var EventEmitter = require('events').EventEmitter, |
6 | ValidatorError = require('./field/field').ValidatorError, | |
7 | ValidationError = require('./errors/validation'), | |
8 | mpath = require('mpath'), | |
9 | util = require('util'), | |
10 | _ = require('lodash'), | |
11 | Types; | |
12 | ||
13 | /** | |
14 | * Form constructor. | |
15 | * | |
16 | * ####Example: | |
17 | * | |
18 | * var form = new Form({ | |
19 | * email: { | |
20 | * type: String, | |
21 | * attributes: {type='email'} | |
22 | * }, | |
23 | * name: { | |
24 | * first: String, | |
25 | * family: String | |
26 | * } | |
27 | * }, options); | |
28 | * | |
29 | * ####Options: | |
30 | * | |
31 | * { | |
32 | * dataSources: ['body', 'query', 'params'], | |
33 | * autoTrim: false, // automatically add trim options to fields | |
34 | * autoLocals: true, // form.middleware adds form.export + validation results to res.locals | |
35 | * errors: { | |
36 | * required: 'This is a required field', | |
37 | * min: 'Value must be greater than or equal to <%= data.min %>', | |
38 | * max: 'Value must be less than or equal to <%= data.max %>', | |
39 | * } | |
40 | * } | |
41 | * | |
42 | * ####Note: | |
43 | * | |
44 | * Provides an alternative factory returning a route-middleware for express and connect | |
45 | * | |
46 | * app.post('/url', | |
47 | * // sames as (new Form({...})).middleware() | |
48 | * form({ | |
49 | * fieldA: String | |
50 | * }), | |
51 | * function (req, res) { | |
52 | * console.log(req.form); // instanceof Form | |
53 | * console.log(res.locals.form.fieldA); // {value: '...', error: '...', data: {...}} | |
54 | * } | |
55 | * ); | |
56 | * | |
57 | * @param {Object} definition | |
58 | * @param {Object} options | |
59 | * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter | |
60 | * @api public | |
61 | */ | |
62 | ||
63 | 1 | function Form (obj, options) { |
64 | 31 | if (!(this instanceof Form)) |
65 | 1 | return (new Form(obj, options)).middleware(); |
66 | ||
67 | 30 | this.data = {}; |
68 | 30 | this.paths = {}; |
69 | 30 | this.subpaths = {}; |
70 | 30 | this.virtuals = {}; |
71 | 30 | this.nested = {}; |
72 | 30 | this.tree = {}; |
73 | 30 | this._requiredpaths = undefined; |
74 | ||
75 | 30 | this.options = this.defaultOptions(options); |
76 | ||
77 | // build paths | |
78 | 30 | if (obj) { |
79 | 30 | this.field(obj); |
80 | } | |
81 | ||
82 | 30 | EventEmitter.call(this); |
83 | } | |
84 | ||
85 | /*! | |
86 | * Inherit from EventEmitter. | |
87 | */ | |
88 | ||
89 | 1 | util.inherits(Form, EventEmitter); |
90 | ||
91 | /** | |
92 | * Form as flat paths | |
93 | * | |
94 | * ####Example: | |
95 | * { | |
96 | * 'something' : Field, | |
97 | * , 'nested.key' : Field, | |
98 | * } | |
99 | * | |
100 | * @api private | |
101 | * @property paths | |
102 | */ | |
103 | ||
104 | 1 | Form.prototype.paths = {}; |
105 | ||
106 | /** | |
107 | * Form as a tree | |
108 | * | |
109 | * ####Example: | |
110 | * { | |
111 | * 'nested' : { | |
112 | * 'key' : String | |
113 | * } | |
114 | * } | |
115 | * | |
116 | * @api private | |
117 | * @property tree | |
118 | */ | |
119 | ||
120 | 1 | Form.prototype.tree = {}; |
121 | ||
122 | ||
123 | /** | |
124 | * Returns default options for this form, merged with `options`. | |
125 | * | |
126 | * @param {Object} options | |
127 | * @return {Object} | |
128 | * @api private | |
129 | */ | |
130 | ||
131 | 1 | Form.prototype.defaultOptions = function (options) { |
132 | 30 | options = _.merge({ |
133 | dataSources: ['body', 'query', 'params'], | |
134 | autoTrim: false, | |
135 | autoLocals: true, | |
136 | passThrough: false, | |
137 | errors: { | |
138 | required: 'This is a required field', | |
139 | min: 'Value must be greater than or equal to <%= data.min %>', | |
140 | max: 'Value must be less than or equal to <%= data.max %>', | |
141 | } | |
142 | }, options); | |
143 | ||
144 | 30 | return options; |
145 | }; | |
146 | ||
147 | /** | |
148 | * Adds key path / field type pairs to this form. | |
149 | * | |
150 | * ####Example: | |
151 | * | |
152 | * var ToyForm = new Form; | |
153 | * ToyForm.add({ name: 'string', color: 'string', price: 'number' }); | |
154 | * // alias | |
155 | * ToyForm.field(...) | |
156 | * | |
157 | * @param {Object} obj | |
158 | * @param {String} prefix | |
159 | * @api public | |
160 | */ | |
161 | ||
162 | 1 | Form.prototype.field = Form.prototype.add = function add (obj, prefix) { |
163 | 40 | prefix = prefix || ''; |
164 | 40 | for (var i in obj) { |
165 | 93 | if (null === obj[i]) { |
166 | 1 | throw new TypeError('Invalid value for field path `'+ prefix + i +'`'); |
167 | } | |
168 | ||
169 | 92 | if (obj[i].constructor.name == 'Object' && (!obj[i].type || obj[i].type.type)) { |
170 | 6 | if (Object.keys(obj[i]).length) { |
171 | // nested object { last: { name: String }} | |
172 | 6 | this.nested[prefix + i] = true; |
173 | 6 | this.add(obj[i], prefix + i + '.'); |
174 | } else { | |
175 | 0 | this.path(prefix + i, obj[i]); // mixed type |
176 | } | |
177 | } else { | |
178 | 86 | this.path(prefix + i, obj[i]); |
179 | } | |
180 | } | |
181 | }; | |
182 | ||
183 | /** | |
184 | * Gets/sets form paths. | |
185 | * | |
186 | * Sets a path (if arity 2) | |
187 | * Gets a path (if arity 1) | |
188 | * | |
189 | * ####Example | |
190 | * | |
191 | * form.path('name') // returns a Field | |
192 | * form.path('name', Number) // changes the fieldType of `name` to Number | |
193 | * | |
194 | * @param {String} path | |
195 | * @param {Object} constructor | |
196 | * @api public | |
197 | */ | |
198 | ||
199 | 1 | Form.prototype.path = function (path, obj) { |
200 | 298 | var self = this, |
201 | field; | |
202 | ||
203 | 298 | if (obj === undefined) { |
204 | 417 | if (this.paths[path]) return this.paths[path]; |
205 | 10 | if (this.subpaths[path]) return this.subpaths[path]; |
206 | ||
207 | // subpaths? | |
208 | 4 | return (/\.\d+\.?.*$/).test(path) ? getPositionalPath(this, path) : undefined; |
209 | } | |
210 | ||
211 | // update the tree | |
212 | 86 | var subpaths = path.split(/\./), |
213 | last = subpaths.pop(), | |
214 | branch = this.tree; | |
215 | ||
216 | 86 | subpaths.forEach(function(sub, i) { |
217 | 16 | if (!branch[sub]) branch[sub] = {}; |
218 | 10 | if (typeof branch[sub] != 'object' ) { |
219 | 0 | var msg = 'Cannot set nested path `' + |
220 | path + '`. ' + 'Parent path `' + | |
221 | subpaths.slice(0, i).concat([sub]).join('.') + | |
222 | '` already set to type ' + | |
223 | branch[sub].name + '.'; | |
224 | 0 | throw new Error(msg); |
225 | } | |
226 | 10 | branch = branch[sub]; |
227 | }); | |
228 | ||
229 | 86 | branch[last] = _.clone(obj, true); |
230 | ||
231 | 86 | field = this.interpretAsType(path, obj); |
232 | ||
233 | 85 | this.paths[path] = field; |
234 | ||
235 | // set default value | |
236 | 85 | this.setValue(path, field.getDefault()); |
237 | ||
238 | // when default() method is used on field | |
239 | 85 | field.on('default', function (def) { |
240 | 1 | var val = self.getValue(path); |
241 | 1 | if (!val || Array.isArray(val) && val.length === 0) |
242 | 1 | self.setValue(path, def); |
243 | }); | |
244 | ||
245 | 85 | return this; |
246 | }; | |
247 | ||
248 | /** | |
249 | * Converts type arguments into Formal Types. | |
250 | * | |
251 | * @param {String} path | |
252 | * @param {Object} obj constructor | |
253 | * @api private | |
254 | */ | |
255 | ||
256 | 1 | Form.prototype.interpretAsType = function (path, obj) { |
257 | 86 | if (obj.constructor.name != 'Object') |
258 | 55 | obj = { type: obj }; |
259 | ||
260 | 86 | if (this.options.autoTrim) |
261 | 2 | obj.trim = true; |
262 | ||
263 | // Get the type making sure to allow keys named "type" | |
264 | // and default to mixed if not specified. | |
265 | // { type: { type: String, default: 'freshcut' } } | |
266 | 86 | var type = obj.type && !obj.type.type ? obj.type : {}; |
267 | ||
268 | 86 | if (Array.isArray(type) || Array == type || 'array' == type) { |
269 | // if it was specified through { type } look for `cast` | |
270 | 10 | var cast = (Array == type || 'array' == type) ? obj.cast : type[0]; |
271 | ||
272 | 10 | if (cast instanceof Form) { |
273 | 0 | return new Types.FormArray(path, cast, obj); |
274 | } | |
275 | ||
276 | 10 | if ('string' == typeof cast) { |
277 | 0 | cast = Types[cast.charAt(0).toUpperCase() + cast.substring(1)]; |
278 | 10 | } else if (cast && (!cast.type || cast.type.type) && |
279 | 'Object' == cast.constructor.name && | |
280 | Object.keys(cast).length) { | |
281 | ||
282 | 0 | return new Types.FormArray(path, new Form(cast), obj); |
283 | } | |
284 | ||
285 | 10 | return new Types.Array(path, cast, obj); |
286 | } | |
287 | ||
288 | 76 | var name = 'string' == typeof type ? type : type.name; |
289 | ||
290 | 76 | if (name) { |
291 | 76 | name = name.charAt(0).toUpperCase() + name.substring(1); |
292 | } | |
293 | ||
294 | 76 | if (undefined === Types[name]) { |
295 | 1 | throw new TypeError('Undefined type at `' + path + |
296 | '`\n Did you try nesting Forms? ' + | |
297 | 'You can only nest using refs or arrays.'); | |
298 | } | |
299 | ||
300 | 75 | return new Types[name](path, obj); |
301 | }; | |
302 | ||
303 | ||
304 | /** | |
305 | * Returns an Array of path strings that are required by this form. | |
306 | * | |
307 | * @api public | |
308 | * @return {Array} | |
309 | */ | |
310 | ||
311 | 1 | Form.prototype.requiredPaths = function requiredPaths () { |
312 | 1 | if (this._requiredpaths) return this._requiredpaths; |
313 | ||
314 | 1 | var ret = []; |
315 | ||
316 | 1 | _.each(this.paths, function(field) { |
317 | 8 | if (field.options.required) ret.push(field.path); |
318 | }); | |
319 | ||
320 | 1 | this._requiredpaths = ret; |
321 | 1 | return this._requiredpaths; |
322 | }; | |
323 | ||
324 | /** | |
325 | * Returns the pathType of `path` for this form. | |
326 | * | |
327 | * Given a path, returns whether it is a real, virtual, nested, or ad-hoc/undefined path. | |
328 | * | |
329 | * @param {String} path | |
330 | * @return {String} | |
331 | * @api public | |
332 | */ | |
333 | ||
334 | 1 | Form.prototype.pathType = function (path) { |
335 | 265 | if (path in this.paths) return 'real'; |
336 | 11 | if (path in this.virtuals) return 'virtual'; |
337 | 9 | if (path in this.nested) return 'nested'; |
338 | 7 | if (path in this.subpaths) return 'real'; |
339 | ||
340 | 3 | if (/\.\d+\.|\.\d+$/.test(path) && getPositionalPath(this, path)) { |
341 | 1 | return 'real'; |
342 | } else { | |
343 | 2 | return 'adhocOrUndefined'; |
344 | } | |
345 | }; | |
346 | ||
347 | /*! | |
348 | * ignore | |
349 | */ | |
350 | ||
351 | 1 | function getPositionalPath (self, path) { |
352 | 2 | var subpaths = path.split(/\.(\d+)\.|\.(\d+)$/).filter(Boolean); |
353 | 2 | if (subpaths.length < 2) { |
354 | 0 | return self.paths[subpaths[0]]; |
355 | } | |
356 | ||
357 | 2 | var val = self.path(subpaths[0]); |
358 | 2 | if (!val) return val; |
359 | ||
360 | 2 | var last = subpaths.length - 1, |
361 | subpath, i = 1; | |
362 | ||
363 | 2 | for (; i < subpaths.length; ++i) { |
364 | 2 | subpath = subpaths[i]; |
365 | ||
366 | 2 | if (i === last && val && !val.form && !/\D/.test(subpath)) { |
367 | 2 | if (val instanceof Types.Array) { |
368 | // StringField, NumberField, etc | |
369 | 2 | val = val.caster; |
370 | } else { | |
371 | 0 | val = undefined; |
372 | } | |
373 | 2 | break; |
374 | } | |
375 | ||
376 | // ignore if its just a position segment: path.0.subpath | |
377 | 0 | if (!/\D/.test(subpath)) continue; |
378 | ||
379 | 0 | if (!(val && val.form)) { |
380 | 0 | val = undefined; |
381 | 0 | break; |
382 | } | |
383 | ||
384 | 0 | val = val.form.path(subpath); |
385 | } | |
386 | ||
387 | 2 | return self.subpaths[path] = val; |
388 | } | |
389 | ||
390 | /**! | |
391 | * ignore | |
392 | * Registers a plugin for this form. | |
393 | * | |
394 | * @param {Function} plugin callback | |
395 | * @param {Object} opts | |
396 | * @see plugins | |
397 | * @api public | |
398 | */ | |
399 | ||
400 | 1 | Form.prototype.plugin = function (fn, opts) { |
401 | 0 | fn(this, opts); |
402 | 0 | return this; |
403 | }; | |
404 | ||
405 | /** | |
406 | * Sets/gets a form option. | |
407 | * | |
408 | * Sets an option (if arity 2) | |
409 | * Gets an option (if arity 1) | |
410 | * | |
411 | * @param {String} key option name | |
412 | * @param {Object} [value] if not passed, the current option value is returned | |
413 | * @api public | |
414 | */ | |
415 | ||
416 | 1 | Form.prototype.option = function (key, value) { |
417 | 4 | if (1 === arguments.length) { |
418 | 2 | return this.options[key]; |
419 | } | |
420 | ||
421 | 2 | if (this.options[key] && typeof this.options[key] === 'object') |
422 | 1 | this.options[key] = _.merge(this.options[key], value); |
423 | else | |
424 | 1 | this.options[key] = value; |
425 | ||
426 | 2 | return this; |
427 | }; | |
428 | ||
429 | /** | |
430 | * Creates a virtual type with the given name. | |
431 | * | |
432 | * @param {String} name | |
433 | * @param {Object} [options] | |
434 | * @return {VirtualType} | |
435 | */ | |
436 | ||
437 | 1 | Form.prototype.virtual = function(name, options) { |
438 | 2 | var self = this; |
439 | 2 | var virtuals = this.virtuals; |
440 | 2 | var parts = name.split('.'); |
441 | 2 | return virtuals[name] = parts.reduce( |
442 | function(mem, part, i) { | |
443 | 4 | if (!mem[part]) mem[part] = (i === parts.length - 1) ? new Types.Virtual(options, name, self.data) : {}; |
444 | 2 | return mem[part]; |
445 | }, | |
446 | this.tree | |
447 | ); | |
448 | }; | |
449 | ||
450 | /** | |
451 | * Returns the virtual type with the given `name`. | |
452 | * | |
453 | * @param {String} name | |
454 | * @return {VirtualType} | |
455 | */ | |
456 | ||
457 | 1 | Form.prototype.virtualpath = function (name) { |
458 | 3 | return this.virtuals[name]; |
459 | }; | |
460 | ||
461 | /** | |
462 | * Sets the value of a path, or many paths. | |
463 | * | |
464 | * ####Example: | |
465 | * | |
466 | * // path, value | |
467 | * form.set(path, value); | |
468 | * | |
469 | * // object | |
470 | * form.set({ | |
471 | * path : value, | |
472 | * path2 : { | |
473 | * path : value | |
474 | * } | |
475 | * }); | |
476 | * | |
477 | * // only-the-fly cast to number | |
478 | * form.set(path, value, Number) | |
479 | * | |
480 | * // only-the-fly cast to string | |
481 | * form.set(path, value, String) | |
482 | * | |
483 | * @param {String|Object} path path or object of key/vals to set | |
484 | * @param {Any} val the value to set | |
485 | * @param {Schema|String|Number|Buffer|etc..} [type] optionally specify a type for "on-the-fly" attributes | |
486 | * @param {Object} [options] optionally specify options that modify the behavior of the set | |
487 | * @api public | |
488 | */ | |
489 | ||
490 | 1 | Form.prototype.set = function (path, val, type, options) { |
491 | 102 | if (type && 'Object' == type.constructor.name) { |
492 | 0 | options = type; |
493 | 0 | type = undefined; |
494 | } | |
495 | ||
496 | 102 | var merge = options && options.merge, |
497 | constructing = true === type; | |
498 | ||
499 | 102 | if (typeof path !== 'string') { |
500 | 35 | if (null === path || undefined === path) { |
501 | 0 | var v = path; |
502 | 0 | path = val; |
503 | 0 | val = v; |
504 | ||
505 | } else { | |
506 | 35 | var prefix = val ? val + '.' : ''; |
507 | ||
508 | 35 | if (path instanceof Form) |
509 | 0 | path = path.data; |
510 | ||
511 | 35 | var keys = Object.keys(path), |
512 | i = keys.length, | |
513 | pathtype, key; | |
514 | ||
515 | 35 | while (i--) { |
516 | 70 | key = keys[i]; |
517 | 70 | pathtype = this.pathType(prefix + key); |
518 | 70 | if (path[key] && 'Object' === path[key].constructor.name && 'virtual' !== pathtype) { |
519 | 3 | this.set(path[key], prefix + key, constructing); |
520 | 67 | } else if (undefined !== path[key] && 'real' === pathtype || 'virtual' === pathtype) { |
521 | 65 | this.set(prefix + key, path[key], constructing); |
522 | } | |
523 | } | |
524 | ||
525 | 35 | return this; |
526 | } | |
527 | } | |
528 | ||
529 | // form = new Form({ path: { nest: 'string' }}) | |
530 | // form.set('path', obj); | |
531 | 67 | var pathType = this.pathType(path); |
532 | 67 | if ('nested' == pathType && val && 'Object' == val.constructor.name) { |
533 | 0 | if (!merge) this.setValue(path, null); |
534 | 0 | this.set(val, path, constructing); |
535 | 0 | return this; |
536 | } | |
537 | ||
538 | 67 | var field; |
539 | 67 | var parts = path.split('.'); |
540 | ||
541 | 67 | if ('virtual' == pathType) { |
542 | 1 | field = this.virtualpath(path); |
543 | 1 | field.applySetters(val, this); |
544 | 1 | return this; |
545 | } else { | |
546 | 66 | field = this.path(path); |
547 | } | |
548 | ||
549 | 66 | if (!field || null === val || undefined === val) { |
550 | 4 | _set(this.data, parts, val); |
551 | 4 | return this; |
552 | } | |
553 | ||
554 | 62 | var shouldSet = true; |
555 | 62 | try { |
556 | 62 | val = field.applySetters(val, this, false, this.getValue(path)); |
557 | } catch (err) { | |
558 | 2 | shouldSet = false; |
559 | // casting error put on silence | |
560 | //this.invalidate(path, err, val); | |
561 | } | |
562 | ||
563 | 62 | if (shouldSet) { |
564 | 60 | _set(this.data, parts, val); |
565 | } | |
566 | ||
567 | 62 | return this; |
568 | }; | |
569 | ||
570 | /** | |
571 | * Returns the value of a path. | |
572 | * | |
573 | * ####Example | |
574 | * | |
575 | * // path | |
576 | * form.get('author.age') // 47 | |
577 | * | |
578 | * @param {String} path | |
579 | * @api public | |
580 | */ | |
581 | ||
582 | 1 | Form.prototype.get = function get (path) { |
583 | 63 | var field = this.path(path) || this.virtualpath(path), |
584 | pieces = path.split('.'), | |
585 | obj = this.data; | |
586 | ||
587 | 63 | for (var i = 0, l = pieces.length; i < l; i++) { |
588 | 72 | obj = undefined === obj || null === obj ? undefined : obj[pieces[i]]; |
589 | } | |
590 | ||
591 | ||
592 | 63 | if (field) { |
593 | 62 | obj = field.applyGetters(obj, this); |
594 | } | |
595 | ||
596 | 63 | return obj; |
597 | }; | |
598 | ||
599 | /** | |
600 | * Executes registered validation field rules for this form. | |
601 | * | |
602 | * ####Example: | |
603 | * | |
604 | * form.validate(function (err) { | |
605 | * if (err) handleError(err); | |
606 | * else // validation passed | |
607 | * }); | |
608 | * | |
609 | * @param {Function} cb called after validation completes, passing an error if one occurred | |
610 | * @api public | |
611 | */ | |
612 | ||
613 | 1 | Form.prototype.validate = function (cb) { |
614 | 12 | var self = this; |
615 | ||
616 | 12 | var paths = Object.keys(this.paths); |
617 | ||
618 | 12 | if (0 === paths.length) { |
619 | 0 | complete(); |
620 | 0 | return this; |
621 | } | |
622 | ||
623 | 12 | var validating = {}, total = 0; |
624 | ||
625 | 12 | paths.forEach(validatePath); |
626 | 12 | return this; |
627 | ||
628 | 0 | function validatePath (path) { |
629 | 44 | if (validating[path]) return; |
630 | ||
631 | 44 | validating[path] = true; |
632 | 44 | total++; |
633 | ||
634 | 44 | process.nextTick(function() { |
635 | 44 | var p = self.path(path); |
636 | 44 | if (!p) return --total || complete(); |
637 | ||
638 | 44 | var val = self.getValue(path); |
639 | 44 | p.doValidate(val, function(err) { |
640 | 44 | if (err) { |
641 | 19 | self.invalidate(path, err, undefined, true); // embedded docs |
642 | } | |
643 | 44 | if (!--total) |
644 | 12 | complete(); |
645 | }, self); | |
646 | }); | |
647 | } | |
648 | ||
649 | 0 | function complete () { |
650 | 12 | var err = self.validationError; |
651 | 12 | self.validationError = undefined; |
652 | 12 | self.emit('validate', self); |
653 | 12 | cb(err); |
654 | } | |
655 | }; | |
656 | ||
657 | /** | |
658 | * Gets a raw value from a path (no getters) | |
659 | * | |
660 | * @param {String} path | |
661 | * @api public | |
662 | */ | |
663 | ||
664 | 1 | Form.prototype.getValue = function (path) { |
665 | 108 | return mpath.get(path, this.data); |
666 | }; | |
667 | ||
668 | /** | |
669 | * Sets a raw value for a path (no casting, setters, transformations) | |
670 | * | |
671 | * @param {String} path | |
672 | * @param {Object} value | |
673 | * @api public | |
674 | */ | |
675 | 1 | Form.prototype.setValue = function (path, val) { |
676 | 86 | mpath.set(path, val, this.data); |
677 | 86 | return this; |
678 | }; | |
679 | ||
680 | /** | |
681 | * Marks a path as invalid, causing validation to fail. | |
682 | * | |
683 | * @param {String} path the field to invalidate | |
684 | * @param {String|Error} err the error which states the reason `path` was invalid | |
685 | * @param {Object|String|Number|any} value optional invalid value | |
686 | * @api public | |
687 | */ | |
688 | ||
689 | 1 | Form.prototype.invalidate = function (path, err, val) { |
690 | 19 | if (!this.validationError) { |
691 | 7 | this.validationError = new ValidationError(this); |
692 | } | |
693 | ||
694 | 19 | if (!err || 'string' === typeof err) { |
695 | // sniffing arguments: | |
696 | // need to handle case where user does not pass value | |
697 | // so our error message is cleaner | |
698 | 0 | err = 2 < arguments.length ? new ValidatorError(path, err, val) : new ValidatorError(path, err); |
699 | } | |
700 | ||
701 | 19 | this.validationError.errors[path] = err; |
702 | }; | |
703 | ||
704 | /** | |
705 | * Return a simple tree object containing data and errors for each field | |
706 | * | |
707 | * @param {Ojbect} err ValidationError object | |
708 | * @return {Object} Tree data | |
709 | * @api public | |
710 | */ | |
711 | 1 | Form.prototype.export = function (err) { |
712 | 3 | var form = this, |
713 | errorsTpl = this.options.errors, | |
714 | result = {}; | |
715 | ||
716 | 3 | Object.keys(this.paths).forEach(function (path, index) { |
717 | 15 | var field = { |
718 | value: form.get(path), | |
719 | data: form.path(path).export() | |
720 | }; | |
721 | ||
722 | 15 | var fielderror = err ? mpath.get(path, err.errors) : null; |
723 | ||
724 | 15 | if (fielderror) { |
725 | 7 | if (errorsTpl[fielderror.type]) { |
726 | 6 | field.error = _.template(errorsTpl[fielderror.type], field); |
727 | } else | |
728 | 1 | field.error = fielderror.message; |
729 | } | |
730 | ||
731 | 15 | _set(result, path.split('.'), field); |
732 | }); | |
733 | ||
734 | 3 | return result; |
735 | }; | |
736 | ||
737 | /** | |
738 | * Return the data tree with getters applied | |
739 | * | |
740 | * @return {Object} data tree object | |
741 | * @public true | |
742 | */ | |
743 | 1 | Form.prototype.getData = function() { |
744 | 1 | var self = this, |
745 | values = {}; | |
746 | ||
747 | 1 | Object.keys(this.paths).forEach(function (path, index) { |
748 | 2 | _set(values, path.split('.'), self.get(path)); |
749 | }); | |
750 | ||
751 | 1 | return values; |
752 | }; | |
753 | ||
754 | /** | |
755 | * Return a route-middleware for connect and express | |
756 | * | |
757 | * ####Example usage: | |
758 | * | |
759 | * var form = new Form({ | |
760 | * email: String | |
761 | * }); | |
762 | * | |
763 | * app.post('/url', | |
764 | * form.middleware(), | |
765 | * function(req, res) { | |
766 | * console.log(req.form); // instanceof Form | |
767 | * console.log(res.locals.form.fieldA); // {value: '...', error: '...', data: {...}} | |
768 | * } | |
769 | * ); | |
770 | * | |
771 | * #####Alternative usage: | |
772 | * | |
773 | * app.post('/url', | |
774 | * form({ | |
775 | * email: String | |
776 | * }), | |
777 | * function(req, res) { | |
778 | * console.log(req.form); // instanceof Form | |
779 | * console.log(res.locals.form.fieldA); // {value: '...', error: '...', data: {...}} | |
780 | * } | |
781 | * ); | |
782 | * | |
783 | * #### Notes | |
784 | * | |
785 | * Adds the form instance to the request. | |
786 | * | |
787 | * #####Example usage with mongoose | |
788 | * // in app.post callback | |
789 | * modelinstance.set(req.form.data); | |
790 | * model.save(function() { | |
791 | * console.log('VoilĂ !') | |
792 | * }); | |
793 | * | |
794 | * It also adds the result of form.export with the the validation result to res.locals | |
795 | * which is very useful for rendering | |
796 | * | |
797 | * @return {Function} route-middleware function | |
798 | * @api public | |
799 | */ | |
800 | 1 | Form.prototype.middleware = function () { |
801 | 3 | var form = this; |
802 | ||
803 | 3 | return function (req, res, next) { |
804 | 3 | var mergedSource = {}; |
805 | 3 | form.options.dataSources.forEach(function(source) { |
806 | 9 | if (req[source]) |
807 | 5 | _.merge(mergedSource, req[source]); |
808 | }); | |
809 | ||
810 | 3 | form.set(mergedSource); |
811 | ||
812 | 3 | form.validate(function (err) { |
813 | 3 | if (form.options.autoLocals) { |
814 | 2 | res.locals.form = form.export(err); |
815 | 2 | res.locals.isValid = !!err; |
816 | } | |
817 | 3 | req.form = form; |
818 | 3 | next(); |
819 | }); | |
820 | }; | |
821 | ||
822 | }; | |
823 | ||
824 | /*! | |
825 | * Module exports. | |
826 | */ | |
827 | ||
828 | 1 | module.exports = exports = Form; |
829 | ||
830 | // require down here because of reference issues | |
831 | ||
832 | /**! | |
833 | * ignore | |
834 | * The various built-in Field Types which mimic Mongoose Schema Type. | |
835 | * | |
836 | * ####Types: | |
837 | * | |
838 | * - [String](#field-string-js) | |
839 | * - [Number](#field-number-js) | |
840 | * - [Boolean](#field-boolean-js) | Bool | |
841 | * - [Array](#field-array-js) | |
842 | * - [Buffer](#field-buffer-js) // TODO? | |
843 | * - [Date](#field-date-js) | |
844 | * - [ObjectId](#field-objectid-js) | Oid // TODO | |
845 | * - [Mixed](#field-mixed-js) // mixed type removed | |
846 | * | |
847 | * @api private | |
848 | */ | |
849 | ||
850 | 1 | Form.FieldTypes = require('./field/types'); |
851 | ||
852 | /*! | |
853 | * ignore | |
854 | */ | |
855 | ||
856 | 1 | Types = Form.FieldTypes; |
857 | 1 | var ObjectId = exports.ObjectId = Types.ObjectId; |
858 | ||
859 | ||
860 | /** | |
861 | * Set a value to obj from parts of a path | |
862 | * | |
863 | * @api private | |
864 | * @memberOf Form | |
865 | */ | |
866 | ||
867 | 1 | var _set = function (obj, parts, val) { |
868 | ||
869 | 81 | var i = 0, |
870 | l = parts.length; | |
871 | ||
872 | 81 | for (; i < l; i++) { |
873 | 88 | var next = i + 1, |
874 | last = next === l; | |
875 | ||
876 | 88 | if (last) { |
877 | 81 | obj[parts[i]] = val; |
878 | 81 | return; |
879 | } | |
880 | ||
881 | 7 | if (obj[parts[i]] && 'Object' === obj[parts[i]].constructor.name) { |
882 | 1 | obj = obj[parts[i]]; |
883 | 6 | } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { |
884 | 2 | obj = obj[parts[i]]; |
885 | } else { | |
886 | 4 | obj = obj[parts[i]] = {}; |
887 | } | |
888 | } | |
889 | }; | |
890 | ||
891 |