alchemymvc
Version:
MVC framework for Node.js
393 lines (323 loc) • 8.73 kB
JavaScript
/**
* The Schema class
*
* @constructor
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.1.0
*
* @param {Schema} parent Optional parent schema
*/
var Schema = Function.inherits(['Deck', 'Alchemy.Base'], function Schema(parent) {
Classes.Deck.call(this);
Schema.super.call(this);
// Default index options
this.indexOptions = {
unique: false,
alternate: false, // Alternates can act like primary keys
order: 1 // Ascending
};
// The parent class this schema belongs to
if (parent != null) {
if (parent.model === true) {
this.setParent(parent.schema);
this.setModel(parent);
} else {
this.setParent(parent);
}
}
this.init();
});
/**
* Add a relation creator method
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.4
* @version 1.3.4
*
* @param {string} relation_type
*/
Schema.setStatic(function addRelationCreator(relation_type, relation_config) {
let method_name = relation_type[0].toLowerCase() + relation_type.slice(1);
this.setMethod(method_name, function _addAssociation(alias, model_name, options) {
this.addAssociation(relation_type, relation_config, alias, model_name, options);
});
});
/**
* Revive a dried schema
*
* @TODO: Make Client.Schema & Schema use the same implementation
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.3
* @version 1.1.3
*
* @param {Object} value
*
* @return {Schema}
*/
Schema.setStatic(function unDry(value) {
var result = new this(),
field,
i;
result.associations = value.associations;
result.name = value.name;
result.options = value.options;
result.rules = value.rules;
result.setModel(value.model_name);
for (i = 0; i < value.fields.length; i++) {
field = value.fields[i];
result.addField(field.name, field.class_name, field.options);
}
return result;
});
/**
* Add a behaviour to this schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.3.0
*
* @param {string} behaviour_name
* @param {Object} options
*/
Schema.setMethod(function addBehaviour(behaviour_name, options) {
var constructor = Behaviour.getClass(behaviour_name),
property_name;
if (!constructor) {
throw new Error('Could not find Behaviour "' + behaviour_name + '"');
}
this.has_behaviours++;
if (!options) {
options = {};
}
// See under which name to store this behaviour
// This way, we can add the same behaviour multiple times
if (options.name) {
property_name = options.name;
} else {
property_name = constructor.name;
}
// Get the class constructor
constructor = Behaviour.getClass(behaviour_name);
// Store it in the schema
this.behaviours[property_name] = {
constructor: constructor,
options: options
};
// See if this behaviour listens to attachments
if (typeof constructor.attached == 'function') {
constructor.attached(this, options);
}
});
/**
* Get all indexes to check for the given record
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.3.0
*
* @param {Object} data
* @param {string} hasType Get indexes with this type only
*
* @return {Object}
*/
Schema.setMethod(function getRecordIndexes(data, hasType) {
var fieldName,
indexName,
result;
result = {};
for (fieldName in data) {
if (this.index_fields[fieldName] != null) {
indexName = this.index_fields[fieldName].name;
// Skip non alternates
if (hasType && !this.indexes[indexName].options[hasType]) {
continue;
}
result[indexName] = this.indexes[indexName];
}
}
return result;
});
/**
* Convenience method for iterating over indexes of a given record
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*
* @param {Object} data
*/
Schema.setMethod(function eachRecordIndex(data, fnc) {
Object.each(this.getRecordIndexes(data), fnc);
});
/**
* Convenience method for iterating over alternate indexes of a given record
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*
* @param {Object} data
*/
Schema.setMethod(function eachAlternateIndex(data, fnc) {
Object.each(this.getRecordIndexes(data, 'alternate'), fnc);
});
/**
* Get the datasource for this schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.0
* @version 1.4.0
*
* @return {Pledge}
*/
Schema.setMethod(function getDatasource() {
var that = this;
return Function.series(function waitForModelClass(next) {
that.afterOnce('has_model_class', next);
}, function waitForDatasources(next) {
STAGES.afterStages('datasource', function afterDs() {
let datasource;
if (that.model_class) {
datasource = Datasource.get(that.model_class.prototype.dbConfig);
if (!datasource) {
console.log('Failed to get', that.model_class.prototype.dbConfig, 'datasource?');
return next(new Error('Could not get datasource "' + that.model_class.prototype.dbConfig + '"'));
}
} else {
return next(new Error('Unable to get datasource for this sub-schema'));
}
next(null, datasource);
});
}, function done(err, result) {
if (err) {
return;
}
return result[1];
});
});
/**
* Add an association
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*
* @param {string} alias
* @param {Object} relation_config
* @param {string} modelname
* @param {Object} options
*
* @return {Object}
*/
Schema.setMethod(function addAssociation(type, relation_config, alias, modelName, options) {
if (this.name === alias) {
throw new Error('Can\'t add association with the same name as current model');
}
let is_internal = relation_config.internal === true,
is_singular = relation_config.singular === true;
let constructor,
client_doc,
doc_class,
className,
path;
let args = this.getAssociationArguments(is_internal, alias, modelName, options);
args.type = type;
alias = args.alias;
modelName = args.modelName;
options = args.options;
options.singular = is_singular;
className = this.model_class_name;
if (this.namespace) {
path = this.namespace + '.' + className;
} else {
path = className;
}
if (className) {
// Get the model constructor
constructor = Model.get(path, false);
}
if (constructor) {
doc_class = Classes.Alchemy.Document.Document.getDocumentClass(constructor);
client_doc = doc_class.getClientDocumentClass();
}
// If the key is part of this model, make sure the field is added
if (is_internal) {
if (!this.getField(options.localKey)) {
let field_options = Object.assign({}, args);
if (options && options.field_options) {
Object.assign(field_options, options.field_options);
field_options.field_options = undefined;
}
this.addAssociationFieldToSchema(options.localKey, type, field_options, doc_class, client_doc);
}
// Also add an index on this new field
this.addIndex(options.localKey, {by_convenience: true});
}
this.associations[alias] = args;
this.addAssociationFieldToSchema(alias, 'AssociationAlias', null, doc_class, client_doc);
return args;
});
/**
* Add enum values.
* Modifications to the `values` object will have no effect later.
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.3.0
*
* @param {string} name
* @param {Object} values
*/
Schema.setMethod(function addEnumValues(name, values) {
if (this.enum_values[name] == null) {
this.enum_values[name] = {};
}
Object.assign(this.enum_values[name], values);
});
/**
* Set (overwrite) enum values.
* The given `values` object will be used by reference.
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.3.0
*
* @param {string} name
* @param {Object} values
*/
Schema.setMethod(function setEnumValues(name, values) {
this.enum_values[name] = values;
});
/**
* Clone
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.1
* @version 1.3.1
*
* @return {Object}
*/
Schema.setMethod(function dryClone(wm, custom_method) {
if (!custom_method) {
custom_method = 'toShallowClone';
}
let cloned = JSON.clone(this.toDry(), custom_method, wm);
cloned = this.constructor.unDry(cloned.value);
return cloned;
});
/**
* Clone using JSON-Dry
* (Needed anyway because Deck also has a clone method)
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.1
* @version 1.3.1
*
* @return {Object}
*/
Schema.setMethod(function clone() {
return this.dryClone(new WeakMap(), 'toShallowClone');
});