////////////////////////////////////////////////////////////////////////// // // // This is a generated file. You can view the original // // source in your browser if your browser supports source maps. // // Source maps are supported by all recent versions of Chrome, Safari, // // and Firefox, and by Internet Explorer 11. // // // ////////////////////////////////////////////////////////////////////////// (function () { /* Imports */ var Meteor = Package.meteor.Meteor; var global = Package.meteor.global; var meteorEnv = Package.meteor.meteorEnv; var Tracker = Package.tracker.Tracker; var Deps = Package.tracker.Deps; var _ = Package.underscore._; var check = Package.check.check; var Match = Package.check.Match; var Random = Package.random.Random; /* Package-scope variables */ var humanize, MongoObject, Utility, SimpleSchema, doValidation1, doValidation2, SimpleSchemaValidationContext; (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/string-polyfills.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position) { position = position || 0; return this.indexOf(searchString, position) === position; }; } if (!String.prototype.trim) { String.prototype.trim = function () { return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); }; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/string-humanize.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* Code source: https://github.com/jxson/string-humanize https://github.com/jxson/string-capitalize */ function capitalize(string){ string = string || ''; string = string.trim(); if (string[0]) { string = string[0].toUpperCase() + string.substr(1).toLowerCase(); } return string; } humanize = function humanize(string){ string = string || ''; string = string.toString(); // might be a number string = string.trim(); string = string.replace(extname(string), ''); string = underscore(string); string = string.replace(/[\W_]+/g, ' '); return capitalize(string); } function underscore(string){ string = string || ''; string = string.toString(); // might be a number string = string.trim(); string = string.replace(/([a-z\d])([A-Z]+)/g, '$1_$2'); string = string.replace(/[-\s]+/g, '_').toLowerCase(); return string; } function extname(string){ var index = string.lastIndexOf('.'); var ext = string.substring(index, string.length); return (index === -1) ? '' : ext; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/mongo-object.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* global MongoObject:true */ var isObject = function(obj) { return obj === Object(obj); }; // getPrototypeOf polyfill if (typeof Object.getPrototypeOf !== "function") { if (typeof "".__proto__ === "object") { Object.getPrototypeOf = function(object) { return object.__proto__; }; } else { Object.getPrototypeOf = function(object) { // May break if the constructor has been tampered with return object.constructor.prototype; }; } } /* Tests whether "obj" is an Object as opposed to * something that inherits from Object * * @param {any} obj * @returns {Boolean} */ var isBasicObject = function(obj) { return isObject(obj) && Object.getPrototypeOf(obj) === Object.prototype; }; /* Takes a specific string that uses mongo-style dot notation * and returns a generic string equivalent. Replaces all numeric * "pieces" with a dollar sign ($). * * @param {type} name * @returns {unresolved} */ var makeGeneric = function makeGeneric(name) { if (typeof name !== "string") { return null; } return name.replace(/\.[0-9]+\./g, '.$.').replace(/\.[0-9]+$/g, '.$'); }; var appendAffectedKey = function appendAffectedKey(affectedKey, key) { if (key === "$each") { return affectedKey; } else { return (affectedKey ? affectedKey + "." + key : key); } }; // Extracts operator piece, if present, from position string var extractOp = function extractOp(position) { var firstPositionPiece = position.slice(0, position.indexOf("[")); return (firstPositionPiece.substring(0, 1) === "$") ? firstPositionPiece : null; }; /* * @constructor * @param {Object} objOrModifier * @param {string[]} blackBoxKeys - A list of the names of keys that shouldn't be traversed * @returns {undefined} * * Creates a new MongoObject instance. The object passed as the first argument * will be modified in place by calls to instance methods. Also, immediately * upon creation of the instance, the object will have any `undefined` keys * removed recursively. */ MongoObject = function(objOrModifier, blackBoxKeys) { var self = this; self._obj = objOrModifier; self._affectedKeys = {}; self._genericAffectedKeys = {}; self._parentPositions = []; self._positionsInsideArrays = []; self._objectPositions = []; function parseObj(val, currentPosition, affectedKey, operator, adjusted, isWithinArray) { // Adjust for first-level modifier operators if (!operator && affectedKey && affectedKey.substring(0, 1) === "$") { operator = affectedKey; affectedKey = null; } var affectedKeyIsBlackBox = false; var affectedKeyGeneric; var stop = false; if (affectedKey) { // Adjust for $push and $addToSet and $pull and $pop if (!adjusted) { if (operator === "$push" || operator === "$addToSet" || operator === "$pop") { // Adjust for $each // We can simply jump forward and pretend like the $each array // is the array for the field. This has the added benefit of // skipping past any $slice, which we also don't care about. if (isBasicObject(val) && "$each" in val) { val = val.$each; currentPosition = currentPosition + "[$each]"; } else { affectedKey = affectedKey + ".0"; } adjusted = true; } else if (operator === "$pull") { affectedKey = affectedKey + ".0"; if (isBasicObject(val)) { stop = true; } adjusted = true; } } // Make generic key affectedKeyGeneric = makeGeneric(affectedKey); // Determine whether affected key should be treated as a black box affectedKeyIsBlackBox = _.contains(blackBoxKeys, affectedKeyGeneric); // Mark that this position affects this generic and non-generic key if (currentPosition) { self._affectedKeys[currentPosition] = affectedKey; self._genericAffectedKeys[currentPosition] = affectedKeyGeneric; // If we're within an array, mark this position so we can omit it from flat docs isWithinArray && self._positionsInsideArrays.push(currentPosition); } } if (stop) { return; } // Loop through arrays if (_.isArray(val) && !_.isEmpty(val)) { if (currentPosition) { // Mark positions with arrays that should be ignored when we want endpoints only self._parentPositions.push(currentPosition); } // Loop _.each(val, function(v, i) { parseObj(v, (currentPosition ? currentPosition + "[" + i + "]" : i), affectedKey + '.' + i, operator, adjusted, true); }); } // Loop through object keys, only for basic objects, // but always for the passed-in object, even if it // is a custom object. else if ((isBasicObject(val) && !affectedKeyIsBlackBox) || !currentPosition) { if (currentPosition && !_.isEmpty(val)) { // Mark positions with objects that should be ignored when we want endpoints only self._parentPositions.push(currentPosition); // Mark positions with objects that should be left out of flat docs. self._objectPositions.push(currentPosition); } // Loop _.each(val, function(v, k) { if (v === void 0) { delete val[k]; } else if (k !== "$slice") { parseObj(v, (currentPosition ? currentPosition + "[" + k + "]" : k), appendAffectedKey(affectedKey, k), operator, adjusted, isWithinArray); } }); } } parseObj(self._obj); function reParseObj() { self._affectedKeys = {}; self._genericAffectedKeys = {}; self._parentPositions = []; self._positionsInsideArrays = []; self._objectPositions = []; parseObj(self._obj); } /** * @method MongoObject.forEachNode * @param {Function} func * @param {Object} [options] * @param {Boolean} [options.endPointsOnly=true] - Only call function for endpoints and not for nodes that contain other nodes * @returns {undefined} * * Runs a function for each endpoint node in the object tree, including all items in every array. * The function arguments are * (1) the value at this node * (2) a string representing the node position * (3) the representation of what would be changed in mongo, using mongo dot notation * (4) the generic equivalent of argument 3, with "$" instead of numeric pieces */ self.forEachNode = function(func, options) { if (typeof func !== "function") { throw new Error("filter requires a loop function"); } options = _.extend({ endPointsOnly: true }, options); var updatedValues = {}; _.each(self._affectedKeys, function(affectedKey, position) { if (options.endPointsOnly && _.contains(self._parentPositions, position)) { return; //only endpoints } func.call({ value: self.getValueForPosition(position), operator: extractOp(position), position: position, key: affectedKey, genericKey: self._genericAffectedKeys[position], updateValue: function(newVal) { updatedValues[position] = newVal; }, remove: function() { updatedValues[position] = void 0; } }); }); // Actually update/remove values as instructed _.each(updatedValues, function(newVal, position) { self.setValueForPosition(position, newVal); }); }; self.getValueForPosition = function(position) { var subkey, subkeys = position.split("["), current = self._obj; for (var i = 0, ln = subkeys.length; i < ln; i++) { subkey = subkeys[i]; // If the subkey ends in "]", remove the ending if (subkey.slice(-1) === "]") { subkey = subkey.slice(0, -1); } current = current[subkey]; if (!_.isArray(current) && !isBasicObject(current) && i < ln - 1) { return; } } return current; }; /** * @method MongoObject.prototype.setValueForPosition * @param {String} position * @param {Any} value * @returns {undefined} */ self.setValueForPosition = function(position, value) { var nextPiece, subkey, subkeys = position.split("["), current = self._obj; for (var i = 0, ln = subkeys.length; i < ln; i++) { subkey = subkeys[i]; // If the subkey ends in "]", remove the ending if (subkey.slice(-1) === "]") { subkey = subkey.slice(0, -1); } // If we've reached the key in the object tree that needs setting or // deleting, do it. if (i === ln - 1) { current[subkey] = value; //if value is undefined, delete the property if (value === void 0) { delete current[subkey]; } } // Otherwise attempt to keep moving deeper into the object. else { // If we're setting (as opposed to deleting) a key and we hit a place // in the ancestor chain where the keys are not yet created, create them. if (current[subkey] === void 0 && value !== void 0) { //see if the next piece is a number nextPiece = subkeys[i + 1]; nextPiece = parseInt(nextPiece, 10); current[subkey] = isNaN(nextPiece) ? {} : []; } // Move deeper into the object current = current[subkey]; // If we can go no further, then quit if (!_.isArray(current) && !isBasicObject(current) && i < ln - 1) { return; } } } reParseObj(); }; /** * @method MongoObject.prototype.removeValueForPosition * @param {String} position * @returns {undefined} */ self.removeValueForPosition = function(position) { self.setValueForPosition(position, void 0); }; /** * @method MongoObject.prototype.getKeyForPosition * @param {String} position * @returns {undefined} */ self.getKeyForPosition = function(position) { return self._affectedKeys[position]; }; /** * @method MongoObject.prototype.getGenericKeyForPosition * @param {String} position * @returns {undefined} */ self.getGenericKeyForPosition = function(position) { return self._genericAffectedKeys[position]; }; /** * @method MongoObject.getInfoForKey * @param {String} key - Non-generic key * @returns {undefined|Object} * * Returns the value and operator of the requested non-generic key. * Example: {value: 1, operator: "$pull"} */ self.getInfoForKey = function(key) { // Get the info var position = self.getPositionForKey(key); if (position) { return { value: self.getValueForPosition(position), operator: extractOp(position) }; } // If we haven't returned yet, check to see if there is an array value // corresponding to this key // We find the first item within the array, strip the last piece off the // position string, and then return whatever is at that new position in // the original object. var positions = self.getPositionsForGenericKey(key + ".$"), p, v; for (var i = 0, ln = positions.length; i < ln; i++) { p = positions[i]; v = self.getValueForPosition(p) || self.getValueForPosition(p.slice(0, p.lastIndexOf("["))); if (v) { return { value: v, operator: extractOp(p) }; } } }; /** * @method MongoObject.getPositionForKey * @param {String} key - Non-generic key * @returns {undefined|String} Position string * * Returns the position string for the place in the object that * affects the requested non-generic key. * Example: 'foo[bar][0]' */ self.getPositionForKey = function(key) { // Get the info for (var position in self._affectedKeys) { if (self._affectedKeys.hasOwnProperty(position)) { if (self._affectedKeys[position] === key) { // We return the first one we find. While it's // possible that multiple update operators could // affect the same non-generic key, we'll assume that's not the case. return position; } } } // If we haven't returned yet, we need to check for affected keys }; /** * @method MongoObject.getPositionsForGenericKey * @param {String} key - Generic key * @returns {String[]} Array of position strings * * Returns an array of position strings for the places in the object that * affect the requested generic key. * Example: ['foo[bar][0]'] */ self.getPositionsForGenericKey = function(key) { // Get the info var list = []; for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { if (self._genericAffectedKeys[position] === key) { list.push(position); } } } return list; }; /** * @deprecated Use getInfoForKey * @method MongoObject.getValueForKey * @param {String} key - Non-generic key * @returns {undefined|Any} * * Returns the value of the requested non-generic key */ self.getValueForKey = function(key) { var position = self.getPositionForKey(key); if (position) { return self.getValueForPosition(position); } }; /** * @method MongoObject.prototype.addKey * @param {String} key - Key to set * @param {Any} val - Value to give this key * @param {String} op - Operator under which to set it, or `null` for a non-modifier object * @returns {undefined} * * Adds `key` with value `val` under operator `op` to the source object. */ self.addKey = function(key, val, op) { var position = op ? op + "[" + key + "]" : MongoObject._keyToPosition(key); self.setValueForPosition(position, val); }; /** * @method MongoObject.prototype.removeGenericKeys * @param {String[]} keys * @returns {undefined} * * Removes anything that affects any of the generic keys in the list */ self.removeGenericKeys = function(keys) { for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { if (_.contains(keys, self._genericAffectedKeys[position])) { self.removeValueForPosition(position); } } } }; /** * @method MongoObject.removeGenericKey * @param {String} key * @returns {undefined} * * Removes anything that affects the requested generic key */ self.removeGenericKey = function(key) { for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { if (self._genericAffectedKeys[position] === key) { self.removeValueForPosition(position); } } } }; /** * @method MongoObject.removeKey * @param {String} key * @returns {undefined} * * Removes anything that affects the requested non-generic key */ self.removeKey = function(key) { // We don't use getPositionForKey here because we want to be sure to // remove for all positions if there are multiple. for (var position in self._affectedKeys) { if (self._affectedKeys.hasOwnProperty(position)) { if (self._affectedKeys[position] === key) { self.removeValueForPosition(position); } } } }; /** * @method MongoObject.removeKeys * @param {String[]} keys * @returns {undefined} * * Removes anything that affects any of the non-generic keys in the list */ self.removeKeys = function(keys) { for (var i = 0, ln = keys.length; i < ln; i++) { self.removeKey(keys[i]); } }; /** * @method MongoObject.filterGenericKeys * @param {Function} test - Test function * @returns {undefined} * * Passes all affected keys to a test function, which * should return false to remove whatever is affecting that key */ self.filterGenericKeys = function(test) { var gk, checkedKeys = [], keysToRemove = []; for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { gk = self._genericAffectedKeys[position]; if (!_.contains(checkedKeys, gk)) { checkedKeys.push(gk); if (gk && !test(gk)) { keysToRemove.push(gk); } } } } _.each(keysToRemove, function(key) { self.removeGenericKey(key); }); }; /** * @method MongoObject.setValueForKey * @param {String} key * @param {Any} val * @returns {undefined} * * Sets the value for every place in the object that affects * the requested non-generic key */ self.setValueForKey = function(key, val) { // We don't use getPositionForKey here because we want to be sure to // set the value for all positions if there are multiple. for (var position in self._affectedKeys) { if (self._affectedKeys.hasOwnProperty(position)) { if (self._affectedKeys[position] === key) { self.setValueForPosition(position, val); } } } }; /** * @method MongoObject.setValueForGenericKey * @param {String} key * @param {Any} val * @returns {undefined} * * Sets the value for every place in the object that affects * the requested generic key */ self.setValueForGenericKey = function(key, val) { // We don't use getPositionForKey here because we want to be sure to // set the value for all positions if there are multiple. for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { if (self._genericAffectedKeys[position] === key) { self.setValueForPosition(position, val); } } } }; /** * @method MongoObject.getObject * @returns {Object} * * Get the source object, potentially modified by other method calls on this * MongoObject instance. */ self.getObject = function() { return self._obj; }; /** * @method MongoObject.getFlatObject * @returns {Object} * * Gets a flat object based on the MongoObject instance. * In a flat object, the key is the name of the non-generic affectedKey, * with mongo dot notation if necessary, and the value is the value for * that key. * * With `keepArrays: true`, we don't flatten within arrays. Currently * MongoDB does not see a key such as `a.0.b` and automatically assume * an array. Instead it would create an object with key "0" if there * wasn't already an array saved as the value of `a`, which is rarely * if ever what we actually want. To avoid this confusion, we * set entire arrays. */ self.getFlatObject = function(options) { options = options || {}; var newObj = {}; _.each(self._affectedKeys, function(affectedKey, position) { if (typeof affectedKey === "string" && (options.keepArrays === true && !_.contains(self._positionsInsideArrays, position) && !_.contains(self._objectPositions, position)) || (!options.keepArrays && !_.contains(self._parentPositions, position)) ) { newObj[affectedKey] = self.getValueForPosition(position); } }); return newObj; }; /** * @method MongoObject.affectsKey * @param {String} key * @returns {Object} * * Returns true if the non-generic key is affected by this object */ self.affectsKey = function(key) { return !!self.getPositionForKey(key); }; /** * @method MongoObject.affectsGenericKey * @param {String} key * @returns {Object} * * Returns true if the generic key is affected by this object */ self.affectsGenericKey = function(key) { for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { if (self._genericAffectedKeys[position] === key) { return true; } } } return false; }; /** * @method MongoObject.affectsGenericKeyImplicit * @param {String} key * @returns {Object} * * Like affectsGenericKey, but will return true if a child key is affected */ self.affectsGenericKeyImplicit = function(key) { for (var position in self._genericAffectedKeys) { if (self._genericAffectedKeys.hasOwnProperty(position)) { var affectedKey = self._genericAffectedKeys[position]; // If the affected key is the test key if (affectedKey === key) { return true; } // If the affected key implies the test key because the affected key // starts with the test key followed by a period if (affectedKey.substring(0, key.length + 1) === key + ".") { return true; } // If the affected key implies the test key because the affected key // starts with the test key and the test key ends with ".$" var lastTwo = key.slice(-2); if (lastTwo === ".$" && key.slice(0, -2) === affectedKey) { return true; } } } return false; }; }; /** Takes a string representation of an object key and its value * and updates "obj" to contain that key with that value. * * Example keys and results if val is 1: * "a" -> {a: 1} * "a[b]" -> {a: {b: 1}} * "a[b][0]" -> {a: {b: [1]}} * "a[b.0.c]" -> {a: {'b.0.c': 1}} */ /** Takes a string representation of an object key and its value * and updates "obj" to contain that key with that value. * * Example keys and results if val is 1: * "a" -> {a: 1} * "a[b]" -> {a: {b: 1}} * "a[b][0]" -> {a: {b: [1]}} * "a[b.0.c]" -> {a: {'b.0.c': 1}} * * @param {any} val * @param {String} key * @param {Object} obj * @returns {undefined} */ MongoObject.expandKey = function(val, key, obj) { var nextPiece, subkey, subkeys = key.split("["), current = obj; for (var i = 0, ln = subkeys.length; i < ln; i++) { subkey = subkeys[i]; if (subkey.slice(-1) === "]") { subkey = subkey.slice(0, -1); } if (i === ln - 1) { //last iteration; time to set the value; always overwrite current[subkey] = val; //if val is undefined, delete the property if (val === void 0) { delete current[subkey]; } } else { //see if the next piece is a number nextPiece = subkeys[i + 1]; nextPiece = parseInt(nextPiece, 10); if (!current[subkey]) { current[subkey] = isNaN(nextPiece) ? {} : []; } } current = current[subkey]; } }; MongoObject._keyToPosition = function keyToPosition(key, wrapAll) { var position = ''; _.each(key.split("."), function (piece, i) { if (i === 0 && !wrapAll) { position += piece; } else { position += "[" + piece + "]"; } }); return position; }; /** * @method MongoObject._positionToKey * @param {String} position * @returns {String} The key that this position in an object would affect. * * This is different from MongoObject.prototype.getKeyForPosition in that * this method does not depend on the requested position actually being * present in any particular MongoObject. */ MongoObject._positionToKey = function positionToKey(position) { //XXX Probably a better way to do this, but this is //foolproof for now. var mDoc = new MongoObject({}); mDoc.setValueForPosition(position, 1); //value doesn't matter var key = mDoc.getKeyForPosition(position); mDoc = null; return key; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/simple-schema-utility.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* global Utility:true */ Utility = { appendAffectedKey: function appendAffectedKey(affectedKey, key) { if (key === "$each") { return affectedKey; } else { return (affectedKey ? affectedKey + "." + key : key); } }, shouldCheck: function shouldCheck(key) { if (key === "$pushAll") { throw new Error("$pushAll is not supported; use $push + $each"); } return !_.contains(["$pull", "$pullAll", "$pop", "$slice"], key); }, errorObject: function errorObject(errorType, keyName, keyValue) { return {name: keyName, type: errorType, value: keyValue}; }, // Tests whether it's an Object as opposed to something that inherits from Object isBasicObject: function isBasicObject(obj) { return _.isObject(obj) && Object.getPrototypeOf(obj) === Object.prototype; }, // The latest Safari returns false for Uint8Array, etc. instanceof Function // unlike other browsers. safariBugFix: function safariBugFix(type) { return (typeof Uint8Array !== "undefined" && type === Uint8Array) || (typeof Uint16Array !== "undefined" && type === Uint16Array) || (typeof Uint32Array !== "undefined" && type === Uint32Array) || (typeof Uint8ClampedArray !== "undefined" && type === Uint8ClampedArray); }, isNotNullOrUndefined: function isNotNullOrUndefined(val) { return val !== void 0 && val !== null; }, // Extracts operator piece, if present, from position string extractOp: function extractOp(position) { var firstPositionPiece = position.slice(0, position.indexOf("[")); return (firstPositionPiece.substring(0, 1) === "$") ? firstPositionPiece : null; }, deleteIfPresent: function deleteIfPresent(obj, key) { if (key in obj) { delete obj[key]; } }, looksLikeModifier: function looksLikeModifier(obj) { for (var key in obj) { if (obj.hasOwnProperty(key) && key.substring(0, 1) === "$") { return true; } } return false; }, dateToDateString: function dateToDateString(date) { var m = (date.getUTCMonth() + 1); if (m < 10) { m = "0" + m; } var d = date.getUTCDate(); if (d < 10) { d = "0" + d; } return date.getUTCFullYear() + '-' + m + '-' + d; } }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/simple-schema.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* global SimpleSchema:true */ /* global SimpleSchemaValidationContext */ /* global MongoObject */ /* global Utility */ var schemaDefinition = { type: Match.Any, label: Match.Optional(Match.OneOf(String, Function)), optional: Match.Optional(Match.OneOf(Boolean, Function)), min: Match.Optional(Match.OneOf(Number, Date, Function)), max: Match.Optional(Match.OneOf(Number, Date, Function)), minCount: Match.Optional(Match.OneOf(Number, Function)), maxCount: Match.Optional(Match.OneOf(Number, Function)), allowedValues: Match.Optional(Match.OneOf([Match.Any], Function)), decimal: Match.Optional(Boolean), exclusiveMax: Match.Optional(Boolean), exclusiveMin: Match.Optional(Boolean), regEx: Match.Optional(Match.OneOf(RegExp, [RegExp])), custom: Match.Optional(Function), blackbox: Match.Optional(Boolean), autoValue: Match.Optional(Function), defaultValue: Match.Optional(Match.Any), trim: Match.Optional(Boolean) }; /* * PRIVATE FUNCTIONS */ //called by clean() var typeconvert = function(value, type) { var parsedDate; if (_.isArray(value) || (_.isObject(value) && !(value instanceof Date))) { return value; //can't and shouldn't convert arrays or objects } if (type === String) { if (typeof value !== "undefined" && value !== null && typeof value !== "string") { return value.toString(); } return value; } if (type === Number) { if (typeof value === "string" && !_.isEmpty(value)) { //try to convert numeric strings to numbers var numberVal = Number(value); if (!isNaN(numberVal)) { return numberVal; } else { return value; //leave string; will fail validation } } return value; } // // If target type is a Date we can safely convert from either a // number (Integer value representing the number of milliseconds // since 1 January 1970 00:00:00 UTC) or a string that can be parsed // by Date. // if (type === Date) { if (typeof value === "string") { parsedDate = Date.parse(value); if (isNaN(parsedDate) === false) { return new Date(parsedDate); } } if (typeof value === "number") { return new Date(value); } } return value; }; var expandSchema = function(schema) { // Flatten schema by inserting nested definitions _.each(schema, function(val, key) { var dot, type; if (!val) { return; } if (Match.test(val.type, SimpleSchema)) { dot = '.'; type = val.type; val.type = Object; } else if (Match.test(val.type, [SimpleSchema])) { dot = '.$.'; type = val.type[0]; val.type = [Object]; } else { return; } //add child schema definitions to parent schema _.each(type._schema, function(subVal, subKey) { var newKey = key + dot + subKey; if (!(newKey in schema)) { schema[newKey] = subVal; } }); }); return schema; }; var adjustArrayFields = function(schema) { _.each(schema, function(def, existingKey) { if (_.isArray(def.type) || def.type === Array) { // Copy some options to array-item definition var itemKey = existingKey + ".$"; if (!(itemKey in schema)) { schema[itemKey] = {}; } if (_.isArray(def.type)) { schema[itemKey].type = def.type[0]; } if (def.label) { schema[itemKey].label = def.label; } schema[itemKey].optional = true; if (typeof def.min !== "undefined") { schema[itemKey].min = def.min; } if (typeof def.max !== "undefined") { schema[itemKey].max = def.max; } if (typeof def.allowedValues !== "undefined") { schema[itemKey].allowedValues = def.allowedValues; } if (typeof def.decimal !== "undefined") { schema[itemKey].decimal = def.decimal; } if (typeof def.exclusiveMax !== "undefined") { schema[itemKey].exclusiveMax = def.exclusiveMax; } if (typeof def.exclusiveMin !== "undefined") { schema[itemKey].exclusiveMin = def.exclusiveMin; } if (typeof def.regEx !== "undefined") { schema[itemKey].regEx = def.regEx; } if (typeof def.blackbox !== "undefined") { schema[itemKey].blackbox = def.blackbox; } // Remove copied options and adjust type def.type = Array; _.each(['min', 'max', 'allowedValues', 'decimal', 'exclusiveMax', 'exclusiveMin', 'regEx', 'blackbox'], function(k) { Utility.deleteIfPresent(def, k); }); } }); }; /** * Adds implied keys. * * If schema contains a key like "foo.$.bar" but not "foo", adds "foo". * * If schema contains a key like "foo" with an array type, adds "foo.$". * @param {Object} schema * @returns {Object} modified schema */ var addImplicitKeys = function(schema) { var arrayKeysToAdd = [], objectKeysToAdd = [], newKey, key, i, ln; // Pass 1 (objects) _.each(schema, function(def, existingKey) { var pos = existingKey.indexOf("."); while (pos !== -1) { newKey = existingKey.substring(0, pos); // It's an array item; nothing to add if (newKey.substring(newKey.length - 2) === ".$") { pos = -1; } // It's an array of objects; add it with type [Object] if not already in the schema else if (existingKey.substring(pos, pos + 3) === ".$.") { arrayKeysToAdd.push(newKey); // add later, since we are iterating over schema right now pos = existingKey.indexOf(".", pos + 3); // skip over next dot, find the one after } // It's an object; add it with type Object if not already in the schema else { objectKeysToAdd.push(newKey); // add later, since we are iterating over schema right now pos = existingKey.indexOf(".", pos + 1); // find next dot } } }); for (i = 0, ln = arrayKeysToAdd.length; i < ln; i++) { key = arrayKeysToAdd[i]; if (!(key in schema)) { schema[key] = {type: [Object], optional: true}; } } for (i = 0, ln = objectKeysToAdd.length; i < ln; i++) { key = objectKeysToAdd[i]; if (!(key in schema)) { schema[key] = {type: Object, optional: true}; } } // Pass 2 (arrays) adjustArrayFields(schema); return schema; }; var mergeSchemas = function(schemas) { // Merge all provided schema definitions. // This is effectively a shallow clone of each object, too, // which is what we want since we are going to manipulate it. var mergedSchema = {}; _.each(schemas, function(schema) { // Create a temporary SS instance so that the internal object // we use for merging/extending will be fully expanded if (Match.test(schema, SimpleSchema)) { schema = schema._schema; } else { schema = addImplicitKeys(expandSchema(schema)); } // Loop through and extend each individual field // definition. That way you can extend and overwrite // base field definitions. _.each(schema, function(def, field) { mergedSchema[field] = mergedSchema[field] || {}; _.extend(mergedSchema[field], def); }); }); // If we merged some schemas, do this again to make sure // extended definitions are pushed into array item field // definitions properly. schemas.length && adjustArrayFields(mergedSchema); return mergedSchema; }; // Returns an object relating the keys in the list // to their parent object. var getObjectKeys = function(schema, schemaKeyList) { var keyPrefix, remainingText, rKeys = {}, loopArray; _.each(schema, function(definition, fieldName) { if (definition.type === Object) { //object keyPrefix = fieldName + "."; } else { return; } loopArray = []; _.each(schemaKeyList, function(fieldName2) { if (fieldName2.startsWith(keyPrefix)) { remainingText = fieldName2.substring(keyPrefix.length); if (remainingText.indexOf(".") === -1) { loopArray.push(remainingText); } } }); rKeys[keyPrefix] = loopArray; }); return rKeys; }; // returns an inflected version of fieldName to use as the label var inflectedLabel = function(fieldName) { var label = fieldName, lastPeriod = label.lastIndexOf("."); if (lastPeriod !== -1) { label = label.substring(lastPeriod + 1); if (label === "$") { var pcs = fieldName.split("."); label = pcs[pcs.length - 2]; } } if (label === "_id") { return "ID"; } return humanize(label); }; /** * @method getAutoValues * @private * @param {MongoObject} mDoc * @param {Boolean} [isModifier=false] - Is it a modifier doc? * @param {Object} [extendedAutoValueContext] - Object that will be added to the context when calling each autoValue function * @returns {undefined} * * Updates doc with automatic values from autoValue functions or default * values from defaultValue. Modifies the referenced object in place. */ function getAutoValues(mDoc, isModifier, extendedAutoValueContext) { var self = this; var doneKeys = []; //on the client we can add the userId if not already in the custom context if (Meteor.isClient && extendedAutoValueContext.userId === void 0) { extendedAutoValueContext.userId = (Meteor.userId && Meteor.userId()) || null; } function runAV(func) { var affectedKey = this.key; // If already called for this key, skip it if (_.contains(doneKeys, affectedKey)) { return; } var lastDot = affectedKey.lastIndexOf('.'); var fieldParentName = lastDot === -1 ? '' : affectedKey.slice(0, lastDot + 1); var doUnset = false; var autoValue = func.call(_.extend({ isSet: (this.value !== void 0), unset: function() { doUnset = true; }, value: this.value, operator: this.operator, field: function(fName) { var keyInfo = mDoc.getInfoForKey(fName) || {}; return { isSet: (keyInfo.value !== void 0), value: keyInfo.value, operator: keyInfo.operator || null }; }, siblingField: function(fName) { var keyInfo = mDoc.getInfoForKey(fieldParentName + fName) || {}; return { isSet: (keyInfo.value !== void 0), value: keyInfo.value, operator: keyInfo.operator || null }; } }, extendedAutoValueContext || {}), mDoc.getObject()); // Update tracking of which keys we've run autovalue for doneKeys.push(affectedKey); if (autoValue === void 0) { if (doUnset) { mDoc.removeValueForPosition(this.position); } return; } // If the user's auto value is of the pseudo-modifier format, parse it // into operator and value. var op, newValue; if (_.isObject(autoValue)) { for (var key in autoValue) { if (autoValue.hasOwnProperty(key) && key.substring(0, 1) === "$") { op = key; newValue = autoValue[key]; break; } } } // Add $set for updates and upserts if necessary if (!op && isModifier && this.position.slice(0, 1) !== '$') { op = "$set"; newValue = autoValue; } // Update/change value if (op) { mDoc.removeValueForPosition(this.position); mDoc.setValueForPosition(op + '[' + affectedKey + ']', newValue); } else { mDoc.setValueForPosition(this.position, autoValue); } } _.each(self._autoValues, function(func, fieldName) { var positionSuffix, key, keySuffix, positions; // If we're under an array, run autovalue for all the properties of // any objects that are present in the nearest ancestor array. if (fieldName.indexOf("$") !== -1) { var testField = fieldName.slice(0, fieldName.lastIndexOf("$") + 1); keySuffix = fieldName.slice(testField.length + 1); positionSuffix = MongoObject._keyToPosition(keySuffix, true); keySuffix = '.' + keySuffix; positions = mDoc.getPositionsForGenericKey(testField); } else { // See if anything in the object affects this key positions = mDoc.getPositionsForGenericKey(fieldName); // Run autovalue for properties that are set in the object if (positions.length) { key = fieldName; keySuffix = ''; positionSuffix = ''; } // Run autovalue for properties that are NOT set in the object else { key = fieldName; keySuffix = ''; positionSuffix = ''; if (isModifier) { positions = ["$set[" + fieldName + "]"]; } else { positions = [MongoObject._keyToPosition(fieldName)]; } } } _.each(positions, function(position) { runAV.call({ key: (key || MongoObject._positionToKey(position)) + keySuffix, value: mDoc.getValueForPosition(position + positionSuffix), operator: Utility.extractOp(position), position: position + positionSuffix }, func); }); }); } //exported SimpleSchema = function(schemas, options) { var self = this; var firstLevelSchemaKeys = []; var fieldNameRoot; options = options || {}; schemas = schemas || {}; if (!_.isArray(schemas)) { schemas = [schemas]; } // adjust and store a copy of the schema definitions self._schema = mergeSchemas(schemas); // store the list of defined keys for speedier checking self._schemaKeys = []; // store autoValue functions by key self._autoValues = {}; // store the list of blackbox keys for passing to MongoObject constructor self._blackboxKeys = []; // a place to store custom validators for this instance self._validators = []; // a place to store custom error messages for this schema self._messages = {}; self._depsMessages = new Tracker.Dependency(); self._depsLabels = {}; _.each(self._schema, function(definition, fieldName) { // Validate the field definition if (!Match.test(definition, schemaDefinition)) { throw new Error('Invalid definition for ' + fieldName + ' field.'); } fieldNameRoot = fieldName.split(".")[0]; self._schemaKeys.push(fieldName); // We support defaultValue shortcut by converting it immediately into an // autoValue. if ('defaultValue' in definition) { if ('autoValue' in definition) { console.warn('SimpleSchema: Found both autoValue and defaultValue options for "' + fieldName + '". Ignoring defaultValue.'); } else { if (fieldName.slice(-2) === ".$") { throw new Error('An array item field (one that ends with ".$") cannot have defaultValue.'); } self._autoValues[fieldName] = (function defineAutoValue(v) { return function() { if (this.operator === null && !this.isSet) { return v; } }; })(definition.defaultValue); } } if ('autoValue' in definition) { if (fieldName.slice(-2) === ".$") { throw new Error('An array item field (one that ends with ".$") cannot have autoValue.'); } self._autoValues[fieldName] = definition.autoValue; } self._depsLabels[fieldName] = new Tracker.Dependency(); if (definition.blackbox === true) { self._blackboxKeys.push(fieldName); } if (!_.contains(firstLevelSchemaKeys, fieldNameRoot)) { firstLevelSchemaKeys.push(fieldNameRoot); } }); // Cache these lists self._firstLevelSchemaKeys = firstLevelSchemaKeys; self._objectKeys = getObjectKeys(self._schema, self._schemaKeys); // We will store named validation contexts here self._validationContexts = {}; }; // This allows other packages or users to extend the schema // definition options that are supported. SimpleSchema.extendOptions = function(options) { _.extend(schemaDefinition, options); }; // this domain regex matches all domains that have at least one . // sadly IPv4 Adresses will be caught too but technically those are valid domains // this expression is extracted from the original RFC 5322 mail expression // a modification enforces that the tld consists only of characters var RX_DOMAIN = '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z](?:[a-z-]*[a-z])?'; // this domain regex matches everythign that could be a domain in intranet // that means "localhost" is a valid domain var RX_NAME_DOMAIN = '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.|$))+'; // strict IPv4 expression which allows 0-255 per oktett var RX_IPv4 = '(?:(?:[0-1]?\\d{1,2}|2[0-4]\\d|25[0-5])(?:\\.|$)){4}'; // strict IPv6 expression which allows (and validates) all shortcuts var RX_IPv6 = '(?:(?:[\\dA-Fa-f]{1,4}(?::|$)){8}' // full adress + '|(?=(?:[^:\\s]|:[^:\\s])*::(?:[^:\\s]|:[^:\\s])*$)' // or min/max one '::' + '[\\dA-Fa-f]{0,4}(?:::?(?:[\\dA-Fa-f]{1,4}|$)){1,6})'; // and short adress // this allows domains (also localhost etc) and ip adresses var RX_WEAK_DOMAIN = '(?:' + [RX_NAME_DOMAIN,RX_IPv4,RX_IPv6].join('|') + ')'; SimpleSchema.RegEx = { // We use the RegExp suggested by W3C in http://www.w3.org/TR/html5/forms.html#valid-e-mail-address // This is probably the same logic used by most browsers when type=email, which is our goal. It is // a very permissive expression. Some apps may wish to be more strict and can write their own RegExp. Email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/, Domain: new RegExp('^' + RX_DOMAIN + '$'), WeakDomain: new RegExp('^' + RX_WEAK_DOMAIN + '$'), IP: new RegExp('^(?:' + RX_IPv4 + '|' + RX_IPv6 + ')$'), IPv4: new RegExp('^' + RX_IPv4 + '$'), IPv6: new RegExp('^' + RX_IPv6 + '$'), // URL RegEx from https://gist.github.com/dperini/729294 // http://mathiasbynens.be/demo/url-regex Url: /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/i, // unique id from the random package also used by minimongo // character list: https://github.com/meteor/meteor/blob/release/0.8.0/packages/random/random.js#L88 // string length: https://github.com/meteor/meteor/blob/release/0.8.0/packages/random/random.js#L143 Id: /^[23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz]{17}$/, // allows for a 5 digit zip code followed by a whitespace or dash and then 4 more digits // matches 11111 and 11111-1111 and 11111 1111 ZipCode: /^\d{5}(?:[-\s]\d{4})?$/, // taken from google's libphonenumber library // https://github.com/googlei18n/libphonenumber/blob/master/javascript/i18n/phonenumbers/phonenumberutil.js // reference the VALID_PHONE_NUMBER_PATTERN key // allows for common phone number symbols including + () and - Phone: /^[0-90-9٠-٩۰-۹]{2}$|^[++]*(?:[-x‐-―−ー--/  ­​⁠ ()()[].\[\]/~⁓∼~*]*[0-90-9٠-٩۰-۹]){3,}[-x‐-―−ー--/  ­​⁠ ()()[].\[\]/~⁓∼~0-90-9٠-٩۰-۹]*(?:;ext=([0-90-9٠-٩۰-۹]{1,7})|[  \t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|[,xx##~~]|int|anexo|int)[:\..]?[  \t,-]*([0-90-9٠-٩۰-۹]{1,7})#?|[- ]+([0-90-9٠-٩۰-۹]{1,5})#)?$/i }; SimpleSchema._makeGeneric = function(name) { if (typeof name !== "string") { return null; } return name.replace(/\.[0-9]+(?=\.|$)/g, '.$'); }; SimpleSchema._depsGlobalMessages = new Tracker.Dependency(); // Inherit from Match.Where // This allow SimpleSchema instance to be recognized as a Match.Where instance as well // as a SimpleSchema instance SimpleSchema.prototype = new Match.Where(); // If an object is an instance of Match.Where, Meteor built-in check API will look at // the function named `condition` and will pass it the document to validate SimpleSchema.prototype.condition = function(obj) { var self = this; //determine whether obj is a modifier var isModifier, isNotModifier; _.each(obj, function(val, key) { if (key.substring(0, 1) === "$") { isModifier = true; } else { isNotModifier = true; } }); if (isModifier && isNotModifier) { throw new Match.Error("Object cannot contain modifier operators alongside other keys"); } var ctx = self.newContext(); if (!ctx.validate(obj, {modifier: isModifier, filter: false, autoConvert: false})) { var error = ctx.getErrorObject(); var matchError = new Match.Error(error.message); matchError.invalidKeys = error.invalidKeys; if (Meteor.isServer) { matchError.sanitizedError = error.sanitizedError; } throw matchError; } return true; }; function logInvalidKeysForContext(context, name) { Meteor.startup(function() { Tracker.autorun(function() { if (!context.isValid()) { console.log('SimpleSchema invalid keys for "' + name + '" context:', context.invalidKeys()); } }); }); } SimpleSchema.prototype.namedContext = function(name) { var self = this; if (typeof name !== "string") { name = "default"; } if (!self._validationContexts[name]) { self._validationContexts[name] = new SimpleSchemaValidationContext(self); // In debug mode, log all invalid key errors to the browser console if (SimpleSchema.debug && Meteor.isClient) { Tracker.nonreactive(function() { logInvalidKeysForContext(self._validationContexts[name], name); }); } } return self._validationContexts[name]; }; // Global custom validators SimpleSchema._validators = []; SimpleSchema.addValidator = function(func) { SimpleSchema._validators.push(func); }; // Instance custom validators SimpleSchema.prototype.addValidator = function(func) { this._validators.push(func); }; /** * @method SimpleSchema.prototype.pick * @param {[fields]} The list of fields to pick to instantiate the subschema * @returns {SimpleSchema} The subschema */ SimpleSchema.prototype.pick = function(/* arguments */) { var self = this; var args = _.toArray(arguments); args.unshift(self._schema); var newSchema = _.pick.apply(null, args); return new SimpleSchema(newSchema); }; SimpleSchema.prototype.omit = function() { var self = this; var args = _.toArray(arguments); args.unshift(self._schema); var newSchema = _.omit.apply(null, args); return new SimpleSchema(newSchema); }; /** * @method SimpleSchema.prototype.clean * @param {Object} doc - Document or modifier to clean. Referenced object will be modified in place. * @param {Object} [options] * @param {Boolean} [options.filter=true] - Do filtering? * @param {Boolean} [options.autoConvert=true] - Do automatic type converting? * @param {Boolean} [options.removeEmptyStrings=true] - Remove keys in normal object or $set where the value is an empty string? * @param {Boolean} [options.trimStrings=true] - Trim string values? * @param {Boolean} [options.getAutoValues=true] - Inject automatic and default values? * @param {Boolean} [options.isModifier=false] - Is doc a modifier object? * @param {Object} [options.extendAutoValueContext] - This object will be added to the `this` context of autoValue functions. * @returns {Object} The modified doc. * * Cleans a document or modifier object. By default, will filter, automatically * type convert where possible, and inject automatic/default values. Use the options * to skip one or more of these. */ SimpleSchema.prototype.clean = function(doc, options) { var self = this; // By default, doc will be filtered and autoconverted options = _.extend({ filter: true, autoConvert: true, removeEmptyStrings: true, trimStrings: true, getAutoValues: true, isModifier: false, extendAutoValueContext: {} }, options || {}); // Convert $pushAll (deprecated) to $push with $each if ("$pushAll" in doc) { console.warn("SimpleSchema.clean: $pushAll is deprecated; converting to $push with $each"); doc.$push = doc.$push || {}; for (var field in doc.$pushAll) { doc.$push[field] = doc.$push[field] || {}; doc.$push[field].$each = doc.$push[field].$each || []; for (var i = 0, ln = doc.$pushAll[field].length; i < ln; i++) { doc.$push[field].$each.push(doc.$pushAll[field][i]); } delete doc.$pushAll; } } var mDoc = new MongoObject(doc, self._blackboxKeys); // Clean loop if (options.filter || options.autoConvert || options.removeEmptyStrings || options.trimStrings) { mDoc.forEachNode(function() { var gKey = this.genericKey, p, def, val; if (gKey) { def = self._schema[gKey]; val = this.value; // Filter out props if necessary; any property is OK for $unset because we want to // allow conversions to remove props that have been removed from the schema. if (options.filter && this.operator !== "$unset" && !self.allowsKey(gKey)) { // XXX Special handling for $each; maybe this could be made nicer if (this.position.slice(-7) === "[$each]") { mDoc.removeValueForPosition(this.position.slice(0, -7)); } else { this.remove(); } if (SimpleSchema.debug) { console.info('SimpleSchema.clean: filtered out value that would have affected key "' + gKey + '", which is not allowed by the schema'); } return; // no reason to do more } if (val !== void 0) { // Autoconvert values if requested and if possible var wasAutoConverted = false; if (options.autoConvert && this.operator !== "$unset" && def) { var newVal = typeconvert(val, def.type); // trim strings if (options.trimStrings && typeof newVal === "string") { newVal = newVal.trim(); } if (newVal !== void 0 && newVal !== val) { // remove empty strings if (options.removeEmptyStrings && (!this.operator || this.operator === "$set") && typeof newVal === "string" && !newVal.length) { // For a document, we remove any fields that are being set to an empty string newVal = void 0; // For a modifier, we $unset any fields that are being set to an empty string if (this.operator === "$set" && this.position.match(/\[.+?\]/g).length < 2) { p = this.position.replace("$set", "$unset"); mDoc.setValueForPosition(p, ""); } } // Change value; if undefined, will remove it SimpleSchema.debug && console.info('SimpleSchema.clean: autoconverted value ' + val + ' from ' + typeof val + ' to ' + typeof newVal + ' for ' + gKey); this.updateValue(newVal); wasAutoConverted = true; } } if (!wasAutoConverted) { // trim strings if (options.trimStrings && typeof val === "string" && (!def || (def && def.trim !== false))) { this.updateValue(val.trim()); } // remove empty strings if (options.removeEmptyStrings && (!this.operator || this.operator === "$set") && typeof val === "string" && !val.length) { // For a document, we remove any fields that are being set to an empty string this.remove(); // For a modifier, we $unset any fields that are being set to an empty string. But only if we're not already within an entire object that is being set. if (this.operator === "$set" && this.position.match(/\[.+?\]/g).length < 2) { p = this.position.replace("$set", "$unset"); mDoc.setValueForPosition(p, ""); } } } } } }, {endPointsOnly: false}); } // Set automatic values options.getAutoValues && getAutoValues.call(self, mDoc, options.isModifier, options.extendAutoValueContext); // Ensure we don't have any operators set to an empty object // since MongoDB 2.6+ will throw errors. if (options.isModifier) { for (var op in doc) { if (doc.hasOwnProperty(op) && _.isEmpty(doc[op])) { delete doc[op]; } } } return doc; }; // Returns the entire schema object or just the definition for one key // in the schema. SimpleSchema.prototype.schema = function(key) { var self = this; // if not null or undefined (more specific) if (key !== null && key !== void 0) { return self._schema[SimpleSchema._makeGeneric(key)]; } else { return self._schema; } }; // Returns the evaluated definition for one key in the schema // key = non-generic key // [propList] = props to include in the result, for performance // [functionContext] = used for evaluating schema options that are functions SimpleSchema.prototype.getDefinition = function(key, propList, functionContext) { var self = this; var defs = self.schema(key); if (!defs) { return; } if (_.isArray(propList)) { defs = _.pick(defs, propList); } else { defs = _.clone(defs); } // For any options that support specifying a function, // evaluate the functions. _.each(['min', 'max', 'minCount', 'maxCount', 'allowedValues', 'optional', 'label'], function (prop) { if (_.isFunction(defs[prop])) { defs[prop] = defs[prop].call(functionContext || {}); } }); // Inflect label if not defined defs.label = defs.label || inflectedLabel(key); return defs; }; // Check if the key is a nested dot-syntax key inside of a blackbox object SimpleSchema.prototype.keyIsInBlackBox = function(key) { var self = this; var parentPath = SimpleSchema._makeGeneric(key), lastDot, def; // Iterate the dot-syntax hierarchy until we find a key in our schema do { lastDot = parentPath.lastIndexOf('.'); if (lastDot !== -1) { parentPath = parentPath.slice(0, lastDot); // Remove last path component def = self.getDefinition(parentPath); } } while (lastDot !== -1 && !def); return !!(def && def.blackbox); }; // Use to dynamically change the schema labels. SimpleSchema.prototype.labels = function(labels) { var self = this; _.each(labels, function(label, fieldName) { if (!_.isString(label) && !_.isFunction(label)) { return; } if (!(fieldName in self._schema)) { return; } self._schema[fieldName].label = label; self._depsLabels[fieldName] && self._depsLabels[fieldName].changed(); }); }; // should be used to safely get a label as string SimpleSchema.prototype.label = function(key) { var self = this; // Get all labels if (key === null || key === void 0) { var result = {}; _.each(self.schema(), function(def, fieldName) { result[fieldName] = self.label(fieldName); }); return result; } // Get label for one field var def = self.getDefinition(key); if (def) { var genericKey = SimpleSchema._makeGeneric(key); self._depsLabels[genericKey] && self._depsLabels[genericKey].depend(); return def.label; } return null; }; // Global messages SimpleSchema._globalMessages = { required: "[label] is required", minString: "[label] must be at least [min] characters", maxString: "[label] cannot exceed [max] characters", minNumber: "[label] must be at least [min]", maxNumber: "[label] cannot exceed [max]", minNumberExclusive: "[label] must be greater than [min]", maxNumberExclusive: "[label] must be less than [max]", minDate: "[label] must be on or after [min]", maxDate: "[label] cannot be after [max]", badDate: "[label] is not a valid date", minCount: "You must specify at least [minCount] values", maxCount: "You cannot specify more than [maxCount] values", noDecimal: "[label] must be an integer", notAllowed: "[value] is not an allowed value", expectedString: "[label] must be a string", expectedNumber: "[label] must be a number", expectedBoolean: "[label] must be a boolean", expectedArray: "[label] must be an array", expectedObject: "[label] must be an object", expectedConstructor: "[label] must be a [type]", regEx: [ {msg: "[label] failed regular expression validation"}, {exp: SimpleSchema.RegEx.Email, msg: "[label] must be a valid e-mail address"}, {exp: SimpleSchema.RegEx.WeakEmail, msg: "[label] must be a valid e-mail address"}, {exp: SimpleSchema.RegEx.Domain, msg: "[label] must be a valid domain"}, {exp: SimpleSchema.RegEx.WeakDomain, msg: "[label] must be a valid domain"}, {exp: SimpleSchema.RegEx.IP, msg: "[label] must be a valid IPv4 or IPv6 address"}, {exp: SimpleSchema.RegEx.IPv4, msg: "[label] must be a valid IPv4 address"}, {exp: SimpleSchema.RegEx.IPv6, msg: "[label] must be a valid IPv6 address"}, {exp: SimpleSchema.RegEx.Url, msg: "[label] must be a valid URL"}, {exp: SimpleSchema.RegEx.Id, msg: "[label] must be a valid alphanumeric ID"} ], keyNotInSchema: "[key] is not allowed by the schema" }; SimpleSchema.messages = function(messages) { _.extend(SimpleSchema._globalMessages, messages); SimpleSchema._depsGlobalMessages.changed(); }; // Schema-specific messages SimpleSchema.prototype.messages = function(messages) { var self = this; _.extend(self._messages, messages); self._depsMessages.changed(); }; // Returns a string message for the given error type and key. Uses the // def and value arguments to fill in placeholders in the error messages. SimpleSchema.prototype.messageForError = function(type, key, def, value) { var self = this; // We proceed even if we can't get a definition because it might be a keyNotInSchema error def = def || self.getDefinition(key, ['regEx', 'label', 'minCount', 'maxCount', 'min', 'max', 'type']) || {}; // Adjust for complex types, currently only regEx, // where we might have regEx.1 meaning the second // expression in the array. var firstTypePeriod = type.indexOf("."), index = null; if (firstTypePeriod !== -1) { index = type.substring(firstTypePeriod + 1); index = parseInt(index, 10); type = type.substring(0, firstTypePeriod); } // Which regExp is it? var regExpMatch; if (type === "regEx") { if (index !== null && index !== void 0 && !isNaN(index)) { regExpMatch = def.regEx[index]; } else { regExpMatch = def.regEx; } if (regExpMatch) { regExpMatch = regExpMatch.toString(); } } // Prep some strings to be used when finding the correct message for this error var typePlusKey = type + " " + key; var genericKey = SimpleSchema._makeGeneric(key); var typePlusGenKey = type + " " + genericKey; // reactively update when message templates are changed SimpleSchema._depsGlobalMessages.depend(); self._depsMessages.depend(); // Prep a function that finds the correct message for regEx errors function findRegExError(message) { if (type !== "regEx" || !_.isArray(message)) { return message; } // Parse regEx messages, which are provided in a special object array format // [{exp: RegExp, msg: "Foo"}] // Where `exp` is optional var msgObj; // First see if there's one where exp matches this expression if (regExpMatch) { msgObj = _.find(message, function (o) { return o.exp && o.exp.toString() === regExpMatch; }); } // If not, see if there's a default message defined if (!msgObj) { msgObj = _.findWhere(message, {exp: null}); if (!msgObj) { msgObj = _.findWhere(message, {exp: void 0}); } } return msgObj ? msgObj.msg : null; } // Try finding the correct message to use at various levels, from most // specific to least specific. var message = self._messages[typePlusKey] || // (1) Use schema-specific message for specific key self._messages[typePlusGenKey] || // (2) Use schema-specific message for generic key self._messages[type]; // (3) Use schema-specific message for type message = findRegExError(message); if (!message) { message = SimpleSchema._globalMessages[typePlusKey] || // (4) Use global message for specific key SimpleSchema._globalMessages[typePlusGenKey] || // (5) Use global message for generic key SimpleSchema._globalMessages[type]; // (6) Use global message for type message = findRegExError(message); } if (!message) { return "Unknown validation error"; } // Now replace all placeholders in the message with the correct values // [key] message = message.replace("[key]", key); // [label] // The call to self.label() establishes a reactive dependency, too message = message.replace("[label]", self.label(key)); // [minCount] if (typeof def.minCount !== "undefined") { message = message.replace("[minCount]", def.minCount); } // [maxCount] if (typeof def.maxCount !== "undefined") { message = message.replace("[maxCount]", def.maxCount); } // [value] if (value !== void 0 && value !== null) { message = message.replace("[value]", value.toString()); } else { message = message.replace("[value]", 'null'); } // [min] and [max] var min = def.min; var max = def.max; if (def.type === Date || def.type === [Date]) { if (typeof min !== "undefined") { message = message.replace("[min]", Utility.dateToDateString(min)); } if (typeof max !== "undefined") { message = message.replace("[max]", Utility.dateToDateString(max)); } } else { if (typeof min !== "undefined") { message = message.replace("[min]", min); } if (typeof max !== "undefined") { message = message.replace("[max]", max); } } // [type] if (def.type instanceof Function) { message = message.replace("[type]", def.type.name); } // Now return the message return message; }; // Returns true if key is explicitly allowed by the schema or implied // by other explicitly allowed keys. // The key string should have $ in place of any numeric array positions. SimpleSchema.prototype.allowsKey = function(key) { var self = this; // Loop through all keys in the schema return _.any(self._schemaKeys, function(schemaKey) { // If the schema key is the test key, it's allowed. if (schemaKey === key) { return true; } // Black box handling if (self.schema(schemaKey).blackbox === true) { var kl = schemaKey.length; var compare1 = key.slice(0, kl + 2); var compare2 = compare1.slice(0, -1); // If the test key is the black box key + ".$", then the test // key is NOT allowed because black box keys are by definition // only for objects, and not for arrays. if (compare1 === schemaKey + '.$') { return false; } // Otherwise if (compare2 === schemaKey + '.') { return true; } } return false; }); }; SimpleSchema.prototype.newContext = function() { return new SimpleSchemaValidationContext(this); }; // Returns all the child keys for the object identified by the generic prefix, // or all the top level keys if no prefix is supplied. SimpleSchema.prototype.objectKeys = function(keyPrefix) { var self = this; if (!keyPrefix) { return self._firstLevelSchemaKeys; } return self._objectKeys[keyPrefix + "."] || []; }; SimpleSchema.prototype.validate = function (obj, options) { if (Package.check && Package['audit-argument-checks']) { // Call check but ignore the error to silence audit-argument-checks try { check(obj); } catch (e) { /* ignore error */ } } var validationContext = this.newContext(); var isValid = validationContext.validate(obj, options); if (isValid) return; var errors = validationContext.invalidKeys().map(function (error) { return { name: error.name, type: error.type, details: { value: error.value } }; }); // In order for the message at the top of the stack trace to be useful, // we set it to the first validation error message. var message = validationContext.keyErrorMessage(errors[0].name); throw new Package['mdg:validation-error'].ValidationError(errors, message); }; SimpleSchema.prototype.validator = function (options) { var self = this; options = options || {}; return function (obj) { if (options.clean === true) self.clean(obj, options); self.validate(obj); }; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/simple-schema-validation.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* global Utility */ /* global _ */ /* global SimpleSchema */ /* global MongoObject */ /* global doValidation1:true */ function doTypeChecks(def, keyValue, op) { var expectedType = def.type; // String checks if (expectedType === String) { if (typeof keyValue !== "string") { return "expectedString"; } else if (def.max !== null && def.max < keyValue.length) { return "maxString"; } else if (def.min !== null && def.min > keyValue.length) { return "minString"; } else if (def.regEx instanceof RegExp && !def.regEx.test(keyValue)) { return "regEx"; } else if (_.isArray(def.regEx)) { var regExError; _.every(def.regEx, function(re, i) { if (!re.test(keyValue)) { regExError = "regEx." + i; return false; } return true; }); if (regExError) { return regExError; } } } // Number checks else if (expectedType === Number) { if (typeof keyValue !== "number" || isNaN(keyValue)) { return "expectedNumber"; } else if (op !== "$inc" && def.max !== null && (!!def.exclusiveMax ? def.max <= keyValue : def.max < keyValue)) { return !!def.exclusiveMax ? "maxNumberExclusive" : "maxNumber"; } else if (op !== "$inc" && def.min !== null && (!!def.exclusiveMin ? def.min >= keyValue : def.min > keyValue)) { return !!def.exclusiveMin ? "minNumberExclusive" : "minNumber"; } else if (!def.decimal && keyValue.toString().indexOf(".") > -1) { return "noDecimal"; } } // Boolean checks else if (expectedType === Boolean) { if (typeof keyValue !== "boolean") { return "expectedBoolean"; } } // Object checks else if (expectedType === Object) { if (!Utility.isBasicObject(keyValue)) { return "expectedObject"; } } // Array checks else if (expectedType === Array) { if (!_.isArray(keyValue)) { return "expectedArray"; } else if (def.minCount !== null && keyValue.length < def.minCount) { return "minCount"; } else if (def.maxCount !== null && keyValue.length > def.maxCount) { return "maxCount"; } } // Constructor function checks else if (expectedType instanceof Function || Utility.safariBugFix(expectedType)) { // Generic constructor checks if (!(keyValue instanceof expectedType)) { return "expectedConstructor"; } // Date checks else if (expectedType === Date) { if (isNaN(keyValue.getTime())) { return "badDate"; } if (_.isDate(def.min) && def.min.getTime() > keyValue.getTime()) { return "minDate"; } else if (_.isDate(def.max) && def.max.getTime() < keyValue.getTime()) { return "maxDate"; } } } } doValidation1 = function doValidation1(obj, isModifier, isUpsert, keyToValidate, ss, extendedCustomContext) { // First do some basic checks of the object, and throw errors if necessary if (!_.isObject(obj)) { throw new Error("The first argument of validate() or validateOne() must be an object"); } if (!isModifier && Utility.looksLikeModifier(obj)) { throw new Error("When the validation object contains mongo operators, you must set the modifier option to true"); } var invalidKeys = []; var mDoc; // for caching the MongoObject if necessary // Validation function called for each affected key function validate(val, affectedKey, affectedKeyGeneric, def, op, skipRequiredCheck, isInArrayItemObject, isInSubObject) { // Get the schema for this key, marking invalid if there isn't one. if (!def) { invalidKeys.push(Utility.errorObject("keyNotInSchema", affectedKey, val, def, ss)); return; } // Check for missing required values. The general logic is this: // * If the operator is $unset or $rename, it's invalid. // * If the value is null, it's invalid. // * If the value is undefined and one of the following are true, it's invalid: // * We're validating a key of a sub-object. // * We're validating a key of an object that is an array item. // * We're validating a document (as opposed to a modifier). // * We're validating a key under the $set operator in a modifier, and it's an upsert. if (!skipRequiredCheck && !def.optional) { if ( val === null || op === "$unset" || op === "$rename" || (val === void 0 && (isInArrayItemObject || isInSubObject || !op || op === "$set")) ) { invalidKeys.push(Utility.errorObject("required", affectedKey, null, def, ss)); return; } } // For $rename, make sure that the new name is allowed by the schema if (op === "$rename" && typeof val === "string" && !ss.allowsKey(val)) { invalidKeys.push(Utility.errorObject("keyNotInSchema", val, null, null, ss)); return; } // Value checks are not necessary for null or undefined values // or for $unset or $rename values if (op !== "$unset" && op !== "$rename" && Utility.isNotNullOrUndefined(val)) { // Check that value is of the correct type var typeError = doTypeChecks(def, val, op); if (typeError) { invalidKeys.push(Utility.errorObject(typeError, affectedKey, val, def, ss)); return; } // Check value against allowedValues array if (def.allowedValues && !_.contains(def.allowedValues, val)) { invalidKeys.push(Utility.errorObject("notAllowed", affectedKey, val, def, ss)); return; } } // Perform custom validation var lastDot = affectedKey.lastIndexOf('.'); var fieldParentName = lastDot === -1 ? '' : affectedKey.slice(0, lastDot + 1); var validators = def.custom ? [def.custom] : []; validators = validators.concat(ss._validators).concat(SimpleSchema._validators); _.every(validators, function(validator) { var errorType = validator.call(_.extend({ key: affectedKey, genericKey: affectedKeyGeneric, definition: def, isSet: (val !== void 0), value: val, operator: op, field: function(fName) { mDoc = mDoc || new MongoObject(obj, ss._blackboxKeys); //create if necessary, cache for speed var keyInfo = mDoc.getInfoForKey(fName) || {}; return { isSet: (keyInfo.value !== void 0), value: keyInfo.value, operator: keyInfo.operator }; }, siblingField: function(fName) { mDoc = mDoc || new MongoObject(obj, ss._blackboxKeys); //create if necessary, cache for speed var keyInfo = mDoc.getInfoForKey(fieldParentName + fName) || {}; return { isSet: (keyInfo.value !== void 0), value: keyInfo.value, operator: keyInfo.operator }; } }, extendedCustomContext || {})); if (typeof errorType === "string") { invalidKeys.push(Utility.errorObject(errorType, affectedKey, val, def, ss)); return false; } return true; }); } // The recursive function function checkObj(val, affectedKey, operator, setKeys, isInArrayItemObject, isInSubObject) { var affectedKeyGeneric, def; if (affectedKey) { // When we hit a blackbox key, we don't progress any further if (ss.keyIsInBlackBox(affectedKey)) { return; } // Make a generic version of the affected key, and use that // to get the schema for this key. affectedKeyGeneric = SimpleSchema._makeGeneric(affectedKey); def = ss.getDefinition(affectedKey); // Perform validation for this key if (!keyToValidate || keyToValidate === affectedKey || keyToValidate === affectedKeyGeneric) { // We can skip the required check for keys that are ancestors // of those in $set or $setOnInsert because they will be created // by MongoDB while setting. var skipRequiredCheck = _.some(setKeys, function(sk) { return (sk.slice(0, affectedKey.length + 1) === affectedKey + "."); }); validate(val, affectedKey, affectedKeyGeneric, def, operator, skipRequiredCheck, isInArrayItemObject, isInSubObject); } } // Temporarily convert missing objects to empty objects // so that the looping code will be called and required // descendent keys can be validated. if ((val === void 0 || val === null) && (!def || (def.type === Object && !def.optional))) { val = {}; } // Loop through arrays if (_.isArray(val)) { _.each(val, function(v, i) { checkObj(v, affectedKey + '.' + i, operator, setKeys); }); } // Loop through object keys else if (Utility.isBasicObject(val) && (!def || !def.blackbox)) { // Get list of present keys var presentKeys = _.keys(val); // Check all present keys plus all keys defined by the schema. // This allows us to detect extra keys not allowed by the schema plus // any missing required keys, and to run any custom functions for other keys. var keysToCheck = _.union(presentKeys, ss.objectKeys(affectedKeyGeneric)); // If this object is within an array, make sure we check for // required as if it's not a modifier isInArrayItemObject = (affectedKeyGeneric && affectedKeyGeneric.slice(-2) === ".$"); // Check all keys in the merged list _.each(keysToCheck, function(key) { checkObj(val[key], Utility.appendAffectedKey(affectedKey, key), operator, setKeys, isInArrayItemObject, true); }); } } function checkModifier(mod) { // Check for empty modifier if (_.isEmpty(mod)) { throw new Error("When the modifier option is true, validation object must have at least one operator"); } // Get a list of all keys in $set and $setOnInsert combined, for use later var setKeys = _.keys(mod.$set || {}).concat(_.keys(mod.$setOnInsert || {})); // If this is an upsert, add all the $setOnInsert keys to $set; // since we don't know whether it will be an insert or update, we'll // validate upserts as if they will be an insert. if ("$setOnInsert" in mod) { if (isUpsert) { mod.$set = mod.$set || {}; mod.$set = _.extend(mod.$set, mod.$setOnInsert); } delete mod.$setOnInsert; } // Loop through operators _.each(mod, function (opObj, op) { // If non-operators are mixed in, throw error if (op.slice(0, 1) !== "$") { throw new Error("When the modifier option is true, all validation object keys must be operators. Did you forget `$set`?"); } if (Utility.shouldCheck(op)) { // For an upsert, missing props would not be set if an insert is performed, // so we add null keys to the modifier to force any "required" checks to fail if (isUpsert && op === "$set") { var presentKeys = _.keys(opObj); _.each(ss.objectKeys(), function (schemaKey) { if (!_.contains(presentKeys, schemaKey)) { checkObj(void 0, schemaKey, op, setKeys); } }); } _.each(opObj, function (v, k) { if (op === "$push" || op === "$addToSet") { if (Utility.isBasicObject(v) && "$each" in v) { v = v.$each; } else { k = k + ".0"; } } checkObj(v, k, op, setKeys); }); } }); } // Kick off the validation if (isModifier) { checkModifier(obj); } else { checkObj(obj); } // Make sure there is only one error per fieldName var addedFieldNames = []; invalidKeys = _.filter(invalidKeys, function(errObj) { if (!_.contains(addedFieldNames, errObj.name)) { addedFieldNames.push(errObj.name); return true; } return false; }); return invalidKeys; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/simple-schema-validation-new.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* global Utility */ /* global _ */ /* global SimpleSchema */ /* global MongoObject */ /* global Meteor */ /* global Random */ /* global doValidation2:true */ function doTypeChecks(def, keyValue, op) { var expectedType = def.type; // String checks if (expectedType === String) { if (typeof keyValue !== "string") { return "expectedString"; } else if (def.max !== null && def.max < keyValue.length) { return "maxString"; } else if (def.min !== null && def.min > keyValue.length) { return "minString"; } else if (def.regEx instanceof RegExp && !def.regEx.test(keyValue)) { return "regEx"; } else if (_.isArray(def.regEx)) { var regExError; _.every(def.regEx, function(re, i) { if (!re.test(keyValue)) { regExError = "regEx." + i; return false; } return true; }); if (regExError) { return regExError; } } } // Number checks else if (expectedType === Number) { if (typeof keyValue !== "number" || isNaN(keyValue)) { return "expectedNumber"; } else if (op !== "$inc" && def.max !== null && (!!def.exclusiveMax ? def.max <= keyValue : def.max < keyValue)) { return !!def.exclusiveMax ? "maxNumberExclusive" : "maxNumber"; } else if (op !== "$inc" && def.min !== null && (!!def.exclusiveMin ? def.min >= keyValue : def.min > keyValue)) { return !!def.exclusiveMin ? "minNumberExclusive" : "minNumber"; } else if (!def.decimal && keyValue.toString().indexOf(".") > -1) { return "noDecimal"; } } // Boolean checks else if (expectedType === Boolean) { if (typeof keyValue !== "boolean") { return "expectedBoolean"; } } // Object checks else if (expectedType === Object) { if (!Utility.isBasicObject(keyValue)) { return "expectedObject"; } } // Array checks else if (expectedType === Array) { if (!_.isArray(keyValue)) { return "expectedArray"; } else if (def.minCount !== null && keyValue.length < def.minCount) { return "minCount"; } else if (def.maxCount !== null && keyValue.length > def.maxCount) { return "maxCount"; } } // Constructor function checks else if (expectedType instanceof Function || Utility.safariBugFix(expectedType)) { // Generic constructor checks if (!(keyValue instanceof expectedType)) { return "expectedConstructor"; } // Date checks else if (expectedType === Date) { if (isNaN(keyValue.getTime())) { return "badDate"; } if (_.isDate(def.min) && def.min.getTime() > keyValue.getTime()) { return "minDate"; } else if (_.isDate(def.max) && def.max.getTime() < keyValue.getTime()) { return "maxDate"; } } } } doValidation2 = function doValidation2(obj, isModifier, isUpsert, keyToValidate, ss, extendedCustomContext) { // First do some basic checks of the object, and throw errors if necessary if (!_.isObject(obj)) { throw new Error("The first argument of validate() or validateOne() must be an object"); } if (isModifier) { if (_.isEmpty(obj)) { throw new Error("When the modifier option is true, validation object must have at least one operator"); } else { var allKeysAreOperators = _.every(obj, function(v, k) { return (k.substring(0, 1) === "$"); }); if (!allKeysAreOperators) { throw new Error("When the modifier option is true, all validation object keys must be operators"); } // We use a LocalCollection to figure out what the resulting doc // would be in a worst case scenario. Then we validate that doc // so that we don't have to validate the modifier object directly. obj = convertModifierToDoc(obj, ss.schema(), isUpsert); } } else if (Utility.looksLikeModifier(obj)) { throw new Error("When the validation object contains mongo operators, you must set the modifier option to true"); } var invalidKeys = []; var mDoc; // for caching the MongoObject if necessary // Validation function called for each affected key function validate(val, affectedKey, affectedKeyGeneric, def, op, skipRequiredCheck, strictRequiredCheck) { // Get the schema for this key, marking invalid if there isn't one. if (!def) { invalidKeys.push(Utility.errorObject("keyNotInSchema", affectedKey, val, def, ss)); return; } // Check for missing required values. The general logic is this: // * If the operator is $unset or $rename, it's invalid. // * If the value is null, it's invalid. // * If the value is undefined and one of the following are true, it's invalid: // * We're validating a key of a sub-object. // * We're validating a key of an object that is an array item. // * We're validating a document (as opposed to a modifier). // * We're validating a key under the $set operator in a modifier, and it's an upsert. if (!skipRequiredCheck && !def.optional) { if (val === null || val === void 0) { invalidKeys.push(Utility.errorObject("required", affectedKey, null, def, ss)); return; } } // Value checks are not necessary for null or undefined values if (Utility.isNotNullOrUndefined(val)) { // Check that value is of the correct type var typeError = doTypeChecks(def, val, op); if (typeError) { invalidKeys.push(Utility.errorObject(typeError, affectedKey, val, def, ss)); return; } // Check value against allowedValues array if (def.allowedValues && !_.contains(def.allowedValues, val)) { invalidKeys.push(Utility.errorObject("notAllowed", affectedKey, val, def, ss)); return; } } // Perform custom validation var lastDot = affectedKey.lastIndexOf('.'); var fieldParentName = lastDot === -1 ? '' : affectedKey.slice(0, lastDot + 1); var validators = def.custom ? [def.custom] : []; validators = validators.concat(ss._validators).concat(SimpleSchema._validators); _.every(validators, function(validator) { var errorType = validator.call(_.extend({ key: affectedKey, genericKey: affectedKeyGeneric, definition: def, isSet: (val !== void 0), value: val, operator: op, field: function(fName) { mDoc = mDoc || new MongoObject(obj, ss._blackboxKeys); //create if necessary, cache for speed var keyInfo = mDoc.getInfoForKey(fName) || {}; return { isSet: (keyInfo.value !== void 0), value: keyInfo.value, operator: keyInfo.operator }; }, siblingField: function(fName) { mDoc = mDoc || new MongoObject(obj, ss._blackboxKeys); //create if necessary, cache for speed var keyInfo = mDoc.getInfoForKey(fieldParentName + fName) || {}; return { isSet: (keyInfo.value !== void 0), value: keyInfo.value, operator: keyInfo.operator }; } }, extendedCustomContext || {})); if (typeof errorType === "string") { invalidKeys.push(Utility.errorObject(errorType, affectedKey, val, def, ss)); return false; } return true; }); } // The recursive function function checkObj(val, affectedKey, skipRequiredCheck, strictRequiredCheck) { var affectedKeyGeneric, def; if (affectedKey) { // When we hit a blackbox key, we don't progress any further if (ss.keyIsInBlackBox(affectedKey)) { return; } // Make a generic version of the affected key, and use that // to get the schema for this key. affectedKeyGeneric = SimpleSchema._makeGeneric(affectedKey); def = ss.getDefinition(affectedKey); // Perform validation for this key if (!keyToValidate || keyToValidate === affectedKey || keyToValidate === affectedKeyGeneric) { validate(val, affectedKey, affectedKeyGeneric, def, null, skipRequiredCheck, strictRequiredCheck); } } // Temporarily convert missing objects to empty objects // so that the looping code will be called and required // descendent keys can be validated. if ((val === void 0 || val === null) && (!def || (def.type === Object && !def.optional))) { val = {}; } // Loop through arrays if (_.isArray(val)) { _.each(val, function(v, i) { checkObj(v, affectedKey + '.' + i); }); } // Loop through object keys else if (Utility.isBasicObject(val) && (!def || !def.blackbox)) { // Get list of present keys var presentKeys = _.keys(val); // Check all present keys plus all keys defined by the schema. // This allows us to detect extra keys not allowed by the schema plus // any missing required keys, and to run any custom functions for other keys. var keysToCheck = _.union(presentKeys, ss._schemaKeys); // If this object is within an array, make sure we check for // required as if it's not a modifier strictRequiredCheck = (affectedKeyGeneric && affectedKeyGeneric.slice(-2) === ".$"); // Check all keys in the merged list _.each(keysToCheck, function(key) { if (Utility.shouldCheck(key)) { checkObj(val[key], Utility.appendAffectedKey(affectedKey, key), skipRequiredCheck, strictRequiredCheck); } }); } } // Kick off the validation checkObj(obj); // Make sure there is only one error per fieldName var addedFieldNames = []; invalidKeys = _.filter(invalidKeys, function(errObj) { if (!_.contains(addedFieldNames, errObj.name)) { addedFieldNames.push(errObj.name); return true; } return false; }); return invalidKeys; }; function convertModifierToDoc(mod, schema, isUpsert) { // Create unmanaged LocalCollection as scratchpad var t = new Meteor.Collection(null); // LocalCollections are in memory, and it seems // that it's fine to use them synchronously on // either client or server var id; if (isUpsert) { // We assume upserts will be inserts (conservative // validation of requiredness) id = Random.id(); t.upsert({_id: id}, mod); } else { var mDoc = new MongoObject(mod); // Create a ficticious existing document var fakeDoc = new MongoObject({}); _.each(schema, function (def, fieldName) { var setVal; // Prefill doc with empty arrays to avoid the // mongodb issue where it does not understand // that numeric pieces should create arrays. if (def.type === Array && mDoc.affectsGenericKey(fieldName)) { setVal = []; } // Set dummy values for required fields because // we assume any existing data would be valid. else if (!def.optional) { // TODO correct value type based on schema type if (def.type === Boolean) { setVal = true; } else if (def.type === Number) { setVal = def.min || 0; } else if (def.type === Date) { setVal = def.min || new Date(); } else if (def.type === Array) { setVal = []; } else if (def.type === Object) { setVal = {}; } else { setVal = "0"; } } if (setVal !== void 0) { var key = fieldName.replace(/\.\$/g, ".0"); var pos = MongoObject._keyToPosition(key, false); fakeDoc.setValueForPosition(pos, setVal); } }); fakeDoc = fakeDoc.getObject(); // Insert fake doc into local scratch collection id = t.insert(fakeDoc); // Now update it with the modifier t.update(id, mod); } var doc = t.findOne(id); // We're done with it t.remove(id); // Currently we don't validate _id unless it is // explicitly added to the schema if (!schema._id) { delete doc._id; } return doc; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); (function(){ ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/aldeed_simple-schema/simple-schema-context.js // // // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // /* global SimpleSchema */ /* global SimpleSchemaValidationContext:true */ /* global doValidation1 */ /* global doValidation2 */ function doValidation(obj, isModifier, isUpsert, keyToValidate, ss, extendedCustomContext) { var useOld = true; //for now this can be manually changed to try the experimental method, which doesn't yet work properly var func = useOld ? doValidation1 : doValidation2; return func(obj, isModifier, isUpsert, keyToValidate, ss, extendedCustomContext); } /* * PUBLIC API */ SimpleSchemaValidationContext = function SimpleSchemaValidationContext(ss) { var self = this; self._simpleSchema = ss; self._schema = ss.schema(); self._schemaKeys = _.keys(self._schema); self._invalidKeys = []; //set up validation dependencies self._deps = {}; self._depsAny = new Tracker.Dependency(); _.each(self._schemaKeys, function(name) { self._deps[name] = new Tracker.Dependency(); }); }; //validates the object against the simple schema and sets a reactive array of error objects SimpleSchemaValidationContext.prototype.validate = function simpleSchemaValidationContextValidate(doc, options) { var self = this; options = _.extend({ modifier: false, upsert: false, extendedCustomContext: {} }, options || {}); //on the client we can add the userId if not already in the custom context if (Meteor.isClient && options.extendedCustomContext.userId === void 0) { options.extendedCustomContext.userId = (Meteor.userId && Meteor.userId()) || null; } var invalidKeys = doValidation(doc, options.modifier, options.upsert, null, self._simpleSchema, options.extendedCustomContext); //now update self._invalidKeys and dependencies //note any currently invalid keys so that we can mark them as changed //due to new validation (they may be valid now, or invalid in a different way) var removedKeys = _.pluck(self._invalidKeys, "name"); //update self._invalidKeys = invalidKeys; //add newly invalid keys to changedKeys var addedKeys = _.pluck(self._invalidKeys, "name"); //mark all changed keys as changed var changedKeys = _.union(addedKeys, removedKeys); self._markKeysChanged(changedKeys); // Return true if it was valid; otherwise, return false return self._invalidKeys.length === 0; }; //validates doc against self._schema for one key and sets a reactive array of error objects SimpleSchemaValidationContext.prototype.validateOne = function simpleSchemaValidationContextValidateOne(doc, keyName, options) { var self = this, i, ln, k; options = _.extend({ modifier: false, upsert: false, extendedCustomContext: {} }, options || {}); //on the client we can add the userId if not already in the custom context if (Meteor.isClient && options.extendedCustomContext.userId === void 0) { options.extendedCustomContext.userId = (Meteor.userId && Meteor.userId()) || null; } var invalidKeys = doValidation(doc, options.modifier, options.upsert, keyName, self._simpleSchema, options.extendedCustomContext); //now update self._invalidKeys and dependencies //remove objects from self._invalidKeys where name = keyName var newInvalidKeys = []; for (i = 0, ln = self._invalidKeys.length; i < ln; i++) { k = self._invalidKeys[i]; if (k.name !== keyName) { newInvalidKeys.push(k); } } self._invalidKeys = newInvalidKeys; //merge invalidKeys into self._invalidKeys for (i = 0, ln = invalidKeys.length; i < ln; i++) { k = invalidKeys[i]; self._invalidKeys.push(k); } //mark key as changed due to new validation (they may be valid now, or invalid in a different way) self._markKeysChanged([keyName]); // Return true if it was valid; otherwise, return false return !self._keyIsInvalid(keyName); }; //reset the invalidKeys array SimpleSchemaValidationContext.prototype.resetValidation = function simpleSchemaValidationContextResetValidation() { var self = this; var removedKeys = _.pluck(self._invalidKeys, "name"); self._invalidKeys = []; self._markKeysChanged(removedKeys); }; SimpleSchemaValidationContext.prototype.isValid = function simpleSchemaValidationContextIsValid() { var self = this; self._depsAny.depend(); return !self._invalidKeys.length; }; SimpleSchemaValidationContext.prototype.invalidKeys = function simpleSchemaValidationContextInvalidKeys() { var self = this; self._depsAny.depend(); return self._invalidKeys; }; SimpleSchemaValidationContext.prototype.addInvalidKeys = function simpleSchemaValidationContextAddInvalidKeys(errors) { var self = this; if (!errors || !errors.length) { return; } var changedKeys = []; _.each(errors, function (errorObject) { changedKeys.push(errorObject.name); self._invalidKeys.push(errorObject); }); self._markKeysChanged(changedKeys); }; SimpleSchemaValidationContext.prototype._markKeysChanged = function simpleSchemaValidationContextMarkKeysChanged(keys) { var self = this; if (!keys || !keys.length) { return; } _.each(keys, function(name) { var genericName = SimpleSchema._makeGeneric(name); if (genericName in self._deps) { self._deps[genericName].changed(); } }); self._depsAny.changed(); }; SimpleSchemaValidationContext.prototype._getInvalidKeyObject = function simpleSchemaValidationContextGetInvalidKeyObject(name, genericName) { var self = this; genericName = genericName || SimpleSchema._makeGeneric(name); var errorObj = _.findWhere(self._invalidKeys, {name: name}); if (!errorObj) { errorObj = _.findWhere(self._invalidKeys, {name: genericName}); } return errorObj; }; SimpleSchemaValidationContext.prototype._keyIsInvalid = function simpleSchemaValidationContextKeyIsInvalid(name, genericName) { return !!this._getInvalidKeyObject(name, genericName); }; // Like the internal one, but with deps SimpleSchemaValidationContext.prototype.keyIsInvalid = function simpleSchemaValidationContextKeyIsInvalid(name) { var self = this, genericName = SimpleSchema._makeGeneric(name); self._deps[genericName] && self._deps[genericName].depend(); return self._keyIsInvalid(name, genericName); }; SimpleSchemaValidationContext.prototype.keyErrorMessage = function simpleSchemaValidationContextKeyErrorMessage(name) { var self = this, genericName = SimpleSchema._makeGeneric(name); self._deps[genericName] && self._deps[genericName].depend(); var errorObj = self._getInvalidKeyObject(name, genericName); if (!errorObj) { return ""; } return self._simpleSchema.messageForError(errorObj.type, errorObj.name, null, errorObj.value); }; SimpleSchemaValidationContext.prototype.getErrorObject = function simpleSchemaValidationContextGetErrorObject() { var self = this, message, invalidKeys = this._invalidKeys; if (invalidKeys.length) { message = self.keyErrorMessage(invalidKeys[0].name); // We add `message` prop to the invalidKeys. invalidKeys = _.map(invalidKeys, function (o) { return _.extend({message: self.keyErrorMessage(o.name)}, o); }); } else { message = "Failed validation"; } var error = new Error(message); error.invalidKeys = invalidKeys; // If on the server, we add a sanitized error, too, in case we're // called from a method. if (Meteor.isServer) { error.sanitizedError = new Meteor.Error(400, message); } return error; }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ Package._define("aldeed:simple-schema", { SimpleSchema: SimpleSchema, MongoObject: MongoObject, humanize: humanize }); })();