win-schema
Version:
WIN module that handles all things schema related! Schema validation, schema loading, schema remote loads, etc etc etc
518 lines (410 loc) • 17 kB
JavaScript
//pull in the validating workhorse -- checks schema and stuff
var tv4 = require('tv4');
//pull in traverse object from the repo please!
var traverse = require('optimuslime-traverse');
//pull in the object that knows what all schema look like!
var schemaSpec = require('./schemaSpec.js');
var addSchemaSupport = require('./addSchema');
module.exports = winSchema;
function winSchema(winback, globalConfiguration, localConfiguration)
{
//load up basic win-module stufffff
var self = this;
self.winFunction = "schema";
self.log = winback.getLogger(self);
//limited only by global -- or we could limit our own verboseness
self.log.logLevel = localConfiguration.logLevel || self.log.normal;
self.pathDelimiter = "///";
//this creates "internalAddSchema" to handle the weighty add logic
//need to thoroughly test and modify incoming schema to align with
//logical schema setup for WIN
addSchemaSupport(self, globalConfiguration, localConfiguration);
self.validator = tv4.freshApi();
//set the backbone and our logging function
self.bbEmit = winback.getEmitter(self);
//config setups
self.multipleErrors = (localConfiguration.multipleErrors == true || localConfiguration.multipleErrors == "true");
//by default you can have unknown keys -- the server environment may desire to change this
//if you don't want to be storing extra info
//by default, on lockdown -- better that way -- no sneaky stuff
self.allowUnknownKeys = localConfiguration.allowUnknownKeys || false;
//all keys are required by default -- this adds in required objects for everything
self.requireByDefault = localConfiguration.requireByDefault || true;
//do we allow properties with just the type "object" or "array"
//this would allow ANY data to be fit in there with no validation checks (other than it is an object or array)
//shouldn't allow this because people could slip in all sorts of terrible things without validation
self.allowAnyObjects = localConfiguration.allowAnyObjects || false;
self.eventCallbacks = function()
{
var callbacks = {};
//add callbacks to the object-- these are the functions called when the full event is emitted
callbacks["schema:validate"] = self.validateData;
callbacks["schema:validateMany"] = self.validateDataArray;
callbacks["schema:addSchema"] = self.addSchema;
callbacks["schema:getSchema"] = self.getSchema;
callbacks["schema:getSchemaReferences"] = self.getSchemaReferences;
callbacks["schema:getFullSchema"] = self.getFullSchema;
callbacks["schema:getSchemaProperties"] = self.getSchemaProperties;
//these are for deealing with pulling references from a specific object -- it's easier done in this module
callbacks["schema:getReferencesAndParents"] = self.getReferencesAndParents;
callbacks["schema:replaceParentReferences"] = self.replaceParentReferences;
//send back our callbacks
return callbacks;
}
self.requiredEvents = function()
{
//don't need no one for nuffin'
return [];
}
self.initialize = function(done)
{
setTimeout(function()
{
done();
}, 0);
}
//cache all our schema by type
self.allSchema = {};
self.schemaReferences = {};
self.requiredReferences = {};
self.fullSchema = {};
self.primaryPaths = {};
self.typeProperties = {};
self.validTypes = "\\b" + schemaSpec.definitions.simpleTypes.enum.join('\\b|\\b') + "\\b"; //['object', 'array', 'number', 'string', 'boolean', 'null'].join('|');
self.typeRegExp = new RegExp(self.validTypes);
self.specKeywords = ["\\$ref|\\babcdefg"];
for(var key in schemaSpec.properties)
self.specKeywords.push(key.replace('$', '\\$'));
//join using exact phrasing checks
self.specKeywords = self.specKeywords.join('\\b|\\b') + "\\b";
self.keywordRegExp = new RegExp(self.specKeywords);
self.log(self.log.testing, "--Specced types: ".green, self.validTypes);
self.log(self.log.testing, "--Specced keywords: ".green, self.specKeywords);
self.validateFunction = (self.multipleErrors ? self.validator.validateMultiple : self.validator.validateResult);
self.errorKey = self.multipleErrors ? "errors" : "error";
function listTypeIssues(type)
{
if(!self.allSchema[type]){
return "Schema type not loaded: " + type;
}
//we have to manually detect missing references -- since the validator is not concerned with such things
//FOR WHATEVER REASON
var missing = self.validator.getMissingUris();
for(var i=0; i < missing.length; i++)
{
//if we have this type inside our refernces for this object, it means we're missing a ref schema for this type!
if(self.requiredReferences[type][missing[i]])
{
return "Missing at least 1 schema definition: " + missing[i];
}
}
}
function internalValidate(schema, object)
{
//validate against what type?
var result = self.validateFunction.apply(self.validator, [object, schema, true, !self.allowUnknownKeys]);
//if it's not an array, make it an array
//if it's empty, make it a damn array
var errors = result[self.errorKey];
//if you are multiple errors, then you are a non-undefined array, just return as usual
//otherwise, you are an error but not in an array
//if errors is undefined then this will deefault to []
errors = (errors && !Array.isArray(errors)) ? [errors] : errors || [];
return {valid : result.valid, errors : errors};
}
self.validateDataArray = function(type, objects, finished)
{
var typeIssues = listTypeIssues(type);
//stop if we have type issues
if(typeIssues)
{
finished(typeIssues);
return;
}
else if(typeof type != "string" || !Array.isArray(objects))
{
finished("ValidateMany requires type [string], objects [array]");
return;
}
var schema = self.validator.getSchema(type);
// self.log('validate many against: ', schema);
var allValid = true;
var allErrors = [];
for(var i=0; i < objects.length; i++)
{
var result = internalValidate(schema, objects[i]);
if(!result.valid){
allValid = false;
allErrors.push(result.errors);
}
else //no error? just push empty array!
allErrors.push([]);
}
//if we have errors during validation, they'll be passed on thank you!
//if you're valid, and there are no errors, then don't send nuffin
finished(undefined, allValid, (!allValid ? allErrors : undefined));
}
self.validateData = function(type, object, finished)
{
var typeIssues = listTypeIssues(type);
//stop if we have type issues
if(typeIssues)
{
finished(typeIssues);
return;
}
//log object being checked
self.log("Validate: ", object);
//now we need to validate, we definitely have all the refs we need
var schema = self.validator.getSchema(type);
//log what's being validated
self.log('validate against: ', schema);
var result = internalValidate(schema, object);
//if we have errors during validation, they'll be passed on thank you!
//if you're valid, and there are no errors, then don't send nuffin
finished(undefined, result.valid, (result.errors.length ? result.errors : undefined));
}
//todo: pull reference objects from schema -- make sure those exist as well?
self.addSchema = function(type, schemaJSON, options, finished)
{
//pass args into internal adds
return self.internalAddSchema.apply(self, arguments);
}
//todo: pull reference objects from schema -- make sure those exist as well?
self.getSchema = function(typeOrArray, finished)
{
//did we request one or many?
var typeArray = typeOrArray;
if(typeof typeOrArray == "string")
{
//make single type to return
typeArray = [typeOrArray];
}
var refArray = [];
for(var i=0; i < typeArray.length; i++)
{
var sType = typeArray[i];
//failed to get schema for some very odd reason?
if(!self.allSchema[sType]){
finished("Schema type not loaded: ", sType);
return;
}
//push our reference information as a clone
refArray.push(traverse(self.validator.getSchema(sType)).clone());
//if you hit an error -send back
if(self.validator.error){
finished(self.validator.error);
return;
}
}
//send the schema objects back
//send an array regardless of how many requested -- standard behavior
finished(undefined, refArray);
}
self.getSchemaReferences = function(typeOrArray, finished)
{
var typeArray = typeOrArray;
if(typeof typeOrArray == "string")
{
//make single type to return
typeArray = [typeOrArray];
}
var refArray = [];
for(var i=0; i < typeArray.length; i++)
{
var sType = typeArray[i];
if(!self.allSchema[sType]){
finished("Schema type not loaded: ", sType);
return;
}
//push our reference information as a clone
refArray.push(traverse(self.requiredReferences[sType]).clone());
}
//send the refernece objects back
//if you are a single object, just send the one -- otherwise send an array
finished(undefined, refArray);
}
var buildFullSchema = function(type)
{
var schema = self.validator.getSchema(type);
var tSchema = traverse(schema);
var clone = tSchema.clone();
var tClone = traverse(clone);
var references = self.schemaReferences[type];
for(var path in references)
{
//we get the type of reference
var schemaInfo = references[path];
var refType = schemaInfo.schemaType;
//this is recursive behavior -- itwill call buidl full schema if not finished yet
var fullRefSchema = internalGetFullSchema(refType);
if(!fullRefSchema)
throw new Error("No schema could be created for: " + refType + ". Please check it's defined.");
//now we ahve teh full object to replace
var tPath = path.split(self.pathDelimiter);
// self.log(self.log.testing, 'Path to ref: ', tPath, " replacement: ", fullRefSchema);
//use traverse to set the path object as our full ref object
tClone.set(tPath, fullRefSchema);
}
// self.log(self.log.testing, "Returning schema: ", type, " full: ", clone);
return clone;
}
var inprogressSchema = {};
function internalGetFullSchema(type)
{
if(inprogressSchema[type])
{
throw new Error("Infinite schema reference loop: " + JSON.stringify(Object.keys(inprogressSchema)));
}
inprogressSchema[type] = true;
//if we don't have a full type yet, we build it
if(!self.fullSchema[type])
{
//need to build a full schema object
var fSchema = buildFullSchema(type);
self.fullSchema[type] = fSchema;
}
//mark the in progress as false!
delete inprogressSchema[type];
return self.fullSchema[type];
}
self.getFullSchema = function(typeOrArray, finished)
{
var typeArray = typeOrArray;
if(typeof typeOrArray == "string")
{
//make single type to return
typeArray = [typeOrArray];
}
var fullArray = [];
for(var i=0; i < typeArray.length; i++)
{
var sType = typeArray[i];
if(!self.allSchema[sType]){
finished("Schema type not loaded: ", sType);
return;
}
try
{
//get the full schema from internal function
//throws error if something is wrong
var fullSchema = internalGetFullSchema(sType);
//pull the full object -- guaranteed to exist -- send a clone
fullArray.push(traverse(fullSchema).clone());
}
catch(e)
{
//send the error if we have one
finished(e);
return;
}
}
//send the refernece objects back
//if you are a single object, just send the one -- otherwise send an array
finished(undefined, fullArray);
}
self.getSchemaProperties = function(typeOrArray, finished)
{
var typeArray = typeOrArray;
if(typeof typeOrArray == "string")
{
//make single type to return
typeArray = [typeOrArray];
}
var propArray = [];
for(var i=0; i < typeArray.length; i++)
{
var sType = typeArray[i];
if(!self.allSchema[sType]){
finished("Schema type not loaded: ", sType);
return;
}
//get our schema properties
propArray.push({type: sType, primaryPaths: traverse(self.primaryPaths[sType]).clone(), properties: traverse(self.typeProperties[sType]).clone()});
}
//send the refernece objects back
//if you are a single object, just send the one -- otherwise send an array
finished(undefined, propArray);
}
//we're going to clone the object, then replace all of the reference wids with new an improved parents
self.replaceParentReferences = function(type, object, parentMapping, finished)
{
var tClone = traverse(object).clone();
traverse(tClone).forEach(function(node)
{
//if we have a wid object and parents object -- likely we are a tracked reference
if(this.node.wid && this.node.parents)
{
var replaceParents = parentMapping[this.node.wid];
if(replaceParents)
{
//straight up replacement therapy yo
this.node.parents = replaceParents;
//then make sure to update and continue onwards!
this.update(this.node);
}
}
})
//we've replaced the innards of the object with a better future!
finished(undefined, tClone);
}
self.getReferencesAndParents = function(type, widObjects, finished)
{
//listed by some identifier, then the object, we need to look through the type, and pull references
var fSchema = internalGetFullSchema(type);
// self.log("Full schema: ", fSchema);
if(!fSchema)
{
finished("Full schema undefined, might be missing a reference type within " + type);
return;
}
var widToParents = {};
var traverseObjects = {};
for(var wid in widObjects){
widToParents[wid] = {};
traverseObjects[wid] = traverse(widObjects[wid]);
}
//for every wid object we see, we loop through and pull that
traverse(fSchema).forEach(function(node)
{
// self.log("Node: ", this.node, "", " key: ", this.key);
//if we have a wid object and parents object -- likely we are a tracked reference
if(this.node.wid && this.node.parents)
{
//let's pull these bad boys our of our other objects
var pathToObject = self.stripObjectPath(this.path);
//if you aren't root, split it up!
if(pathToObject != "")
pathToObject = pathToObject.split(self.pathDelimiter);
// self.log("\nKey before :", this.key, " node-p: ", this.parent.node, " path: ", this.path);
var isArray = (this.key == "properties" && this.path[this.path.length - 2] == "items");
// self.log("Is array? : ", isArray);
for(var wid in traverseObjects)
{
var wtp = widToParents[wid];
var tob = traverseObjects[wid];
//fetch object from our traverse thing using this path
var arrayOrObject = tob.get(pathToObject);
// self.log("Path to obj: ", pathToObject, " get: ", arrayOrObject);
//grab wid to parent mappings, always cloning parent arrays
if(isArray)
{
for(var i=0; i < arrayOrObject.length; i++)
{
var aobj = arrayOrObject[i];
//map wid objects to parent wids always thank you
wtp[aobj.wid] = aobj.parents.slice(0);
}
}
else
wtp[arrayOrObject.wid] = arrayOrObject.parents.slice(0);
//now we've aded a mapping from the objects wid to the parents for that wid
}
}
});
//we send back the wids of the original widObjects mapped to the wid internal reference && the corresponding parents
//so many damn layers -- it gets confusing
finished(undefined, widToParents);
}
return self;
}