alchemymvc
Version:
MVC framework for Node.js
1,798 lines (1,446 loc) • 37.1 kB
JavaScript
var nameCache = {},
mongo = alchemy.use('mongodb'),
all_prefixes = alchemy.shared('Routing.prefixes'),
fs = alchemy.use('fs'),
createdModel;
/**
* The Model class
*
* @constructor
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 1.1.0
*/
const Model = Function.inherits('Alchemy.Base', 'Alchemy.Model', function Model(options) {
this.init(options);
});
/**
* This is a wrapper class
*/
Model.makeAbstractClass();
/**
* This wrapper class starts a new group
*/
Model.startNewGroup();
/**
* Set the modelName property after class creation
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*/
Model.postInherit(function setModelName() {
let base_name = this.name;
let model_name = base_name;
let table_prefix = this.table_prefix;
let namespace = this.namespace;
if (namespace.startsWith('Alchemy.Model.') || namespace == 'Alchemy.Model') {
namespace = namespace.slice(14);
} else if (namespace.startsWith('Alchemy.Client.Model.') || namespace == 'Alchemy.Client.Model') {
namespace = namespace.slice(21);
}
this.setStatic('model_namespace', namespace, false);
let ns = namespace.replaceAll('.', '_');
if (!table_prefix && ns) {
table_prefix = ns.underscore();
}
if (ns) {
model_name = ns + '_' + model_name;
}
// The simple name of the model
this.model_name = model_name;
this.setProperty('model_name', model_name);
let table_name = base_name.tableize();
if (table_prefix) {
table_name = table_prefix + '_' + table_name;
}
this.table = table_name;
});
/**
* This is a model constructor
*
* @type {boolean}
*/
Model.setStaticProperty('model', true);
/**
* The cache duration static getter/setter
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 1.0.3
*
* @property cache_duration
* @type {string}
*/
Model.setStaticProperty(function cache_duration() {
if (this._cache_duration == null) {
this._cache_duration = alchemy.settings.data_management.model_query_cache_duration;
}
return this._cache_duration;
}, function setCacheDuration(duration) {
this._cache_duration = duration;
// @todo: reset cache
});
/**
* Get the cache object
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 1.0.3
*
* @property cache
* @type {Object}
*/
Model.setStaticProperty(function cache() {
if (this.cache_duration) {
if (this._cache) {
return this._cache;
}
this._cache = alchemy.getCache(this.name, this.cache_duration);
return this._cache;
}
return false;
}, function setCache(value) {
return this._cache = value;
});
/**
* Is this an abstract model?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.0
* @version 1.1.0
*
* @type {boolean}
*/
Model.setStaticProperty(function is_abstract() {
// Do simple is_abstract_class check
if (this.is_abstract_class != null) {
return !!this.is_abstract_class;
}
// If we need to load an external schema, it's also not abstract
if (this.prototype.load_external_schema) {
return false;
}
// See if this model has other fields than the default ones
let field_count = this.schema.array.length;
if (this.schema.has('_id')) {
field_count--;
}
if (this.schema.has('created')) {
field_count--;
}
if (this.schema.has('updated')) {
field_count--;
}
return field_count < 1;
});
/**
* Get the document class constructor
*
* @type {Alchemy.Document}
*/
Model.prepareStaticProperty('Document', function getDocumentClass() {
return Classes.Alchemy.Document.Document.getDocumentClass(this);
});
/**
* Get the client document class constructor
*
* @type {Hawkejs.Document}
*/
Model.prepareStaticProperty('ClientDocument', function getClientDocumentClass() {
return this.Document.getClientDocumentClass();
});
/**
* Set the static per-model schema
*
* @version 1.1.0
*
* @type {Schema}
*/
Model.staticCompose('schema', function createSchema(doNext) {
let model = this.compositorParent,
schema = new Classes.Alchemy.Schema();
// The base Model does not have a schema
if (model.name == 'Model') {
return false;
} else {
// Link the schema to this model
schema.setModel(model);
// Set the schema name
schema.setName(model.model_name);
if (model.prototype.add_basic_fields !== false) {
// Set default model fields immediately after this function ends
// This has to be scheduled next, because addField would call createSchema
// again, resulting in an infinite loop
doNext(function addSchemaBasics() {
model.addField('_id', 'ObjectId', {default: Field.createPathEvaluator('alchemy.ObjectId')});
model.addField('created', 'Datetime', {default: Field.createPathEvaluator('Date.create')});
model.addField('updated', 'Datetime', {default: Field.createPathEvaluator('Date.create')});
});
}
}
return schema;
}, [
'addEnumValues',
'setEnumValues',
'belongsTo',
'hasOneParent',
'hasAndBelongsToMany',
'hasMany',
'hasOneChild',
'addIndex',
'addRule',
]);
Model.setDeprecatedProperty('modelName', 'model_name');
Model.setDeprecatedProperty('blueprint', 'schema');
Model.setDeprecatedProperty('displayField', 'display_field');
/**
* The default database config to use
*
* @type {string}
*/
Model.setProperty('dbConfig', 'default');
/**
* The default field to use as display
*
* @type {string}
*/
Model.setProperty('display_field', 'title');
/**
* Translate is on by default
*
* @type {boolean}
*/
Model.setProperty('translate', true);
/**
* Set the name of the primary key field
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*/
Model.setProperty('primary_key', '_id');
/**
* Should we load the schema from the database?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.1.0
* @version 1.1.0
*/
Model.setProperty('load_external_schema', false);
/**
* Object where behaviours are stored
*
* @type {Object}
*/
Model.prepareProperty('behaviours', Object);
/**
* Associations
*
* @type {Object}
*/
Model.setProperty(function associations() {
return this.schema.associations;
});
/**
* Instance access to static cache
*
* @type {Expirable}
*/
Model.prepareProperty('cache', function cache() {
return this.constructor.cache;
});
/**
* Instance access to static schema
*
* @type {Schema}
*/
Model.setProperty(function schema() {
return this.constructor.schema;
});
/**
* Is this an abstract model?
*
* @type {boolean}
*/
Model.setProperty(function is_abstract() {
return this.constructor.is_abstract;
});
/**
* The connection
*
* @type {Object}
*/
Model.prepareProperty('datasource', function datasource() {
if (this.table) return Datasource.get(this.dbConfig);
});
/**
* The default sort options
*
* @type {Object}
*/
Model.prepareProperty('sort', function sort() {
return {[this.primary_key]: -1};
});
/**
* Check a url value
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.2.5
*
* @param {string} value The value in the url
* @param {string} name The name of the url parameter
* @param {string} field_name The name of the field to check
* @param {Conduit} conduit The optional conduit
*
* @return {Pledge}
*/
Model.setStatic(async function checkPathValue(value, name, field_name, conduit) {
var instance,
pledge,
crit;
if (!field_name) {
if (name == 'id') {
field_name = this.prototype.primary_key;
} else {
field_name = name;
}
}
if (conduit) {
instance = conduit.getModel(this);
} else {
instance = new this;
}
// Create new criteria instance
crit = instance.find();
// Look for the wanted field
crit.where(field_name).equals(value);
let result = await instance.find('first', crit);
if (result) {
let found_value = result[field_name];
if (found_value != value && !Object.alike(value, found_value)) {
conduit.rewriteRequestRouteParam(name, found_value);
}
}
return result;
});
/**
* Add a computed field to this model's schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.21
* @version 1.3.21
*
* @return {Alchemy.Field}
*/
Model.setStatic(function addComputedField(name, type, options) {
let is_new = !this.schema.has(name);
// Add it to the schema
let field = this.schema.addComputedField(name, type, options);
if (is_new) {
// Add it to the Document class
this.Document.setComputedFieldGetter(name);
// False means it should not be set on the server implementation
// (because that's where it's coming from)
// Yes, this also sets private fields on the server-side client document.
this.ClientDocument.setComputedFieldGetter(name, null, null, false);
}
return field;
});
/**
* Add a field to this model's schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.2.21
*
* @return {Alchemy.Field}
*/
Model.setStatic(function addField(name, type, options) {
let is_new = !this.schema.has(name);
// Add it to the schema
let field = this.schema.addField(name, type, options);
if (is_new) {
// Add it to the Document class
this.Document.setFieldGetter(name);
// False means it should not be set on the server implementation
// (because that's where it's coming from)
// Yes, this also sets private fields on the server-side client document.
this.ClientDocument.setFieldGetter(name, null, null, false);
}
return field;
});
/**
* Set the wanted table prefix
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.4.0
* @version 1.4.0
*/
Model.setStatic(function setTablePrefix(prefix) {
this.setStatic('table_prefix', prefix);
});
/**
* Add a behaviour to this model
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*/
Model.setStatic(function addBehaviour(behaviour_name, options) {
return this.schema.addBehaviour(behaviour_name, options);
});
/**
* Set a method on the document class
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.0.6
*/
Model.setStatic(function setDocumentMethod(name, fnc) {
var that = this;
if (typeof name == 'function') {
fnc = name;
name = fnc.name;
}
Blast.loaded(function whenLoaded() {
that.Document.setMethod(name, fnc);
});
});
/**
* Set a property on the document class
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.0.6
*/
Model.setStatic(function setDocumentProperty(name, fnc) {
var that = this,
args = arguments;
Blast.loaded(function whenLoaded() {
that.Document.setProperty.apply(that.Document, args);
});
});
/**
* Get a field
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*
* @return {FieldType}
*/
Model.setStatic(function getField(name) {
var fieldPath,
alias,
model,
split;
if (name.indexOf('.') > -1) {
split = name.split('.');
alias = name[0];
if (this.schema.associations[alias] == null) {
model = this;
fieldPath = name;
} else {
model = Model.get(this.schema.associations[alias].modelName).constructor;
split.shift();
fieldPath = split.join('.');
}
} else {
model = this;
fieldPath = name;
}
return model.schema.get(fieldPath);
});
/**
* Get the model's public configuration
* (This is used to create the client-side Model instances)
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.3.1
*/
Model.setStatic(function getClientConfig() {
const result = {
class_name : this.name,
model_name : this.model_name,
model_namespace : this.model_namespace || undefined,
schema : this.schema,
primary_key : this.prototype.primary_key,
display_field : this.prototype.display_field,
ancestors : 0,
};
if (this.super.name != 'Model') {
result.parent = this.super.model_name;
let ancestors = 0,
ancestor = this.super;
while (ancestor && ancestor.name != 'Model') {
ancestors++;
ancestor = ancestor.super;
}
result.ancestors = ancestors;
}
return result;
});
/**
* Initialize behaviours
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.4.0
*/
Model.setMethod(function initBehaviours() {
let instances = {},
behaviour,
count = 0,
key;
for (key in this.schema.behaviours) {
behaviour = this.schema.behaviours[key];
instances[key] = new behaviour.constructor(this, behaviour.options);
count++;
}
if (count) {
this.behaviours = instances;
}
});
/**
* Enable a behaviour on-the-fly
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 0.2.0
*/
Model.setMethod(function addBehaviour(behaviourname, options) {
var instance;
if (!options) {
options = {};
}
instance = Behaviour.get(behaviourname, this, options);
this.behaviours[behaviourname] = instance;
return instance;
});
/**
* Get a behaviour instance
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.3
* @version 1.0.3
*
* @param {string} name
*
* @return {Behaviour}
*/
Model.setMethod(function getBehaviour(name) {
name = name.camelize();
if (!name.endsWith('Behaviour')) {
name += 'Behaviour';
}
return this.behaviours[name];
});
/**
* Enable translations
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*/
Model.setMethod(function enableTranslations() {
this.translate = true;
});
/**
* Disable translations
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.0
*/
Model.setMethod(function disableTranslations() {
this.translate = false;
});
/**
* Aggregate
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.5.0
* @version 0.5.0
*
* @param {Array} pipeline
* @param {Function} callback
*/
Model.setMethod(function aggregate(pipeline, callback) {
this.datasource.collection(this.table, function gotCollection(err, collection) {
if (err) {
return callback(err);
}
collection.aggregate(pipeline, callback);
});
});
/**
* Translate the given records
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 1.3.0
*
* @param {Array} items
* @param {Object} options Optional options object
* @param {Function} callback
*/
Model.setMethod(function translateItems(items, options, callback) {
if (options && options instanceof Classes.Alchemy.Criteria.Criteria) {
options = options.options;
}
// No items to translate
if (!items.length) {
return callback();
}
// No fields in this schema are translatable
if (!this.schema.has_translations) {
return callback();
}
// Do nothing if there are no translatable fields
// or translate is disabled
if (!this.translate || (!this.conduit && !options.locale)) {
return callback();
}
let collection,
fieldName,
prefixes,
prefix,
record,
found,
item,
key,
i,
j;
// Get the alias we need to translate
let alias = options.forAlias || this.name;
// Get the (optional) attached conduit
let conduit = this.conduit;
// Should we use fallback translations?
const allow_fallbacks = options.allow_fallback_translations ?? alchemy.settings.data_management.allow_fallback_translations ?? false;
let use_predefined_prefixes = false;
// If prefixes are given as an option, only use those
if (options.prefixes) {
prefix = options.prefixes;
use_predefined_prefixes = true;
} else {
// Possible prefixes
prefix = [];
// Prefixes set in the options get precedence
if (options.locale && options.locale !== true) {
prefix.include(options.locale);
}
if (conduit) {
// Append the visited prefix after that (if there is one)
if (conduit.prefix) {
prefix.include(conduit.prefix);
}
// Add the active prefix
if (conduit.active_prefix) {
prefix.include(conduit.active_prefix);
}
// Append all the allowed locales after that
if (conduit.locales) {
prefix.include(conduit.locales);
}
}
// Add all available prefixes last
for (key in all_prefixes) {
prefix.push(key);
}
// The fallback prefix
prefix.push('__');
// @DEPRECATED: empty keys should no longer be allowed
prefix.push('');
if (!allow_fallbacks) {
prefix = [prefix[0]];
}
}
// Deduplicate the prefixes
prefix = Array.from(new Set(prefix));
for (i = 0; i < items.length; i++) {
item = items[i];
if (!options.ungrouped_items) {
item = item[alias];
}
// Don't translate items twice
if (item?.$translated_fields && !Object.isEmpty(item.$translated_fields)) {
continue;
}
collection = Array.cast(item);
// Clone the prefixes
prefixes = prefix.slice(0);
// If one of the query conditions searched through a translatable field,
// the prefix found should get preference
if (options.use_found_prefix && items.item_prefixes && items.item_prefixes[i]) {
prefixes.unshift(items.item_prefixes[i]);
}
if (!allow_fallbacks && !use_predefined_prefixes) {
prefixes = [prefixes[0]];
}
let field;
for (j = 0; j < collection.length; j++) {
record = collection[j];
for (fieldName in this.schema.translatable_fields) {
field = this.schema.translatable_fields[fieldName];
field.translateRecord(prefixes, record, options.allow_empty);
}
}
}
callback();
});
/**
* Create the given record if the id does not exist in the database
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 1.1.0
*
* @param {Array} list A list of all the records that need to be in the db
* @param {Function} callback
*/
Model.setMethod(function ensureIds(list, callback) {
var that = this;
list = Array.cast(list);
return Function.forEach.parallel(list, function checkEntry(entry, key, next) {
var id;
id = entry[that.primary_key];
if (!id && entry[that.name]) {
id = entry[that.name][that.primary_key];
}
if (!id) {
return next(new Classes.Alchemy.Error.Model('`Model#ensureIds()` can\'t ensure an entry without an _id'));
}
that.findById(id, function gotItem(err, result) {
if (err) {
return next(err);
}
if (result) {
return next();
}
that.save(entry, {create: true, document: false}, next);
});
}, callback);
});
/**
* Save one record
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 1.3.20
*
* @param {Document} document
* @param {Object} options
* @param {Function} callback
*
* @return {Pledge}
*/
Model.setMethod(function saveRecord(document, options, callback) {
var that = this,
saved_record,
creating,
results,
pledge,
main,
iter;
if (!document) {
pledge = Pledge.reject(new Error('Unable to save record: given document is undefined'));
pledge.done(callback);
return pledge;
}
// Normalize the arguments
if (typeof options == 'function') {
callback = options;
}
if (typeof options !== 'object') {
options = {};
}
pledge = Function.series(function doAudit(next) {
if (Object.isPlainObject(document)) {
return next(new Error('Model#saveRecord() expects a Document, not a plain object'));
}
// Look through unique indexes if no _id is present
that.auditRecord(document, options, function afterAudit(err, doc) {
if (err) {
return next(err);
}
// Is a new record being created?
creating = options.create || doc[that.primary_key] == null;
next();
});
}, function doBeforeNormalize(next) {
that.issueDataEvent('beforeNormalize', [document, options], next);
}, function emitSavingEvent(next) {
that.issueDataEvent('beforeSave', [document, options, creating], function afterSavingEvent(err, stopped) {
return next(err);
});
}, function doDatabase(next) {
if (options.debug) {
console.log('Saving document', document, 'Creating?', creating);
}
function gotRecord(err, result) {
if (err) {
return next(err);
}
saved_record = result;
next();
}
if (creating) {
that.createRecord(document, options, gotRecord);
} else {
that.updateRecord(document, options, gotRecord);
}
}, function doAssociated(next) {
let tasks = [],
assoc;
Object.each(document.$record, function eachEntry(entry, key) {
// Skip empty entries
if (!entry) {
return;
}
// Skip our own record
if (key == that.name) {
return;
}
// Get the association configuration
assoc = that.schema.associations[key];
// If the association doesn't exist, do nothing
if (!assoc) {
return;
}
// Add the saved _id
entry[assoc.options.foreignKey] = saved_record[assoc.options.localKey];
// Add the task
tasks.push(function doSave(next) {
var a_model = that.getModel(assoc.modelName);
a_model.save(entry, next);
});
});
Function.parallel(tasks, next);
}, function done(err) {
if (err) {
return;
}
return saved_record;
});
pledge.handleCallback(callback);
return pledge;
});
/**
* Look for the record id by checking the indexes
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 1.1.0
*
* @param {Document} document
* @param {Object} options
* @param {Function} callback
*/
Model.setMethod(function auditRecord(document, options, callback) {
var that = this,
results,
schema,
tasks;
if (!document) {
return callback(new Error('No record was given to audit'));
}
schema = this.schema;
if (schema && document[this.primary_key] == null && options.audit !== false) {
tasks = {};
results = {};
if (options.debug) {
console.log('Pre-save audit record', document);
}
schema.eachAlternateIndex(document, function iterIndex(index, indexName) {
if (options.debug) {
console.log('Checking alternate index', indexName);
}
tasks[indexName] = function auditIndex(next) {
var query = {},
fieldName;
for (fieldName in index.fields) {
if (document[fieldName] != null) {
query[fieldName] = document[fieldName];
// @todo: should run through the FieldType instance
if (String(query[fieldName]).isObjectId()) {
query[fieldName] = alchemy.castObjectId(query[fieldName]);
}
}
}
that.datasource.read(that, query, {}, function gotRecordInfo(err, records) {
if (err != null) {
return next(err);
}
if (records[0] != null) {
results[indexName] = records[0];
}
next();
});
};
});
Function.parallel(tasks, function doneAudit(err) {
var indexName,
record,
count,
ids;
if (err != null) {
return callback(err);
}
if (!Object.isEmpty(results)) {
count = 0;
ids = {};
for (indexName in results) {
record = results[indexName];
// First make sure this index is allowed during the audit
// If it's not, this means it should be considered a duplicate
if (options.allowedIndexes != null && !Object.hasValue(options.allowedIndexes, indexName)) {
if (callback) callback(new Error('Duplicate index found other than _id: ' + indexName), null);
return;
}
// Add the id a first time
if (ids[record[that.primary_key]] == null) {
count++;
ids[record[that.primary_key]] = true;
}
}
// If more than 1 ids are found, we can't update the item
// because we don't know which record is the actual owner
if (count > 1) {
if (callback) callback(new Error('Multiple unique records found'));
return;
}
// Use the last found record to get the id
document[that.primary_key] = record[that.primary_key];
}
if (options.debug) {
console.log('Audit done, found pk:', document[that.primary_key]);
}
callback(null, document);
});
return;
}
setImmediate(function skippedAudit() {
callback(null, document);
});
});
/**
* Turn a record into something the database will understand
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.3
* @version 1.4.0
*
* @param {Document} record
* @param {Object} options
* @param {Function} callback
*
* @return {Pledge}
*/
Model.setMethod(function convertRecordToDatasourceFormat(record, options, callback) {
if (typeof options == 'function') {
callback = options;
options = {};
}
if (!options) {
options = {};
}
let data = record.$main || record[this.model_name] || record;
// Normalize the data
data = this.compose(data, options);
let context = new Classes.Alchemy.OperationalContext.SaveDocumentToDatasource();
context.setDatasource(this.datasource);
context.setModel(this);
context.setRootData(data);
context.setSaveOptions(options);
let pledge = Swift.cast(this.datasource.toDatasource(context));
pledge.handleCallback(callback);
return pledge;
});
/**
* Process an object of datasource format
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.3
* @version 1.4.0
*
* @param {Object} ds_data
* @param {Object} options
* @param {Function} callback
*
* @return {Pledge}
*/
Model.setMethod(function processDatasourceFormat(ds_data, options, callback) {
var that = this,
pledge,
data;
if (typeof options == 'function') {
callback = options;
options = {};
}
if (!options) {
options = {};
}
pledge = Pledge.Swift.cast(this.datasource.toApp(this.schema, {}, options, ds_data));
pledge.handleCallback(callback);
return pledge;
});
/**
* Clear the cache of this and all associated models
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 1.1.6
*
* @param {boolean} associated Also nuke associated models
* @param {Branch} parent
*/
Model.setMethod(function nukeCache(associated, parent) {
let model_name,
branch,
alias;
// Nuke associated caches by default
if (typeof associated == 'undefined') {
associated = true;
}
// Create the parent branch object
if (!parent) {
branch = parent = new Classes.Branch.Data(this.name);
}
if (branch || !parent.root.seen(this.name)) {
if (!branch) {
branch = parent.append(this.name);
}
if (this.cache) {
this.cache.reset();
}
// Also nuke the cache of the client model, if it exists
if (Classes.Alchemy.Client.Model[this.constructor.name] && Classes.Alchemy.Client.Model[this.constructor.name].cache) {
Classes.Alchemy.Client.Model[this.constructor.name].cache.reset();
}
}
// Return if we don't need to nuke associated models
if (!associated) {
return;
}
for (alias in this.associations) {
model_name = this.associations[alias].modelName;
if (!parent.root.seen(model_name)) {
let assoc_model = this.getModel(model_name);
assoc_model.nukeCache(true, branch);
}
}
});
/**
* Perform a MongoDB pipeline
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.3.17
* @version 1.3.17
*
* @param {Array} pipeline
*
* @return {Promise}
*/
Model.setMethod(async function executeMongoPipeline(pipeline) {
if (typeof this.datasource.collection != 'function') {
throw new Error('The `' + this.model_name + '` model does not seem to use MongoDB, unable to perform pipeline');
}
let collection = await this.datasource.collection(this.table);
let cursor = await collection.aggregate(pipeline);
let result = await cursor.toArray();
return result;
});
/**
* Delete the given record id
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 1.4.0
*
* @param {string} id The object id
* @param {Function} callback
*
* @return {Pledge}
*/
Model.setMethod(function remove(id, callback) {
var that = this,
pledge = new Pledge(),
id;
pledge.handleCallback(callback);
if (!id) {
pledge.reject(new Error('Invalid id given!'));
return pledge;
}
if (this.datasource.supports('objectid')) {
id = alchemy.castObjectId(id);
} else {
id = String(id);
}
let query = {
[this.primary_key] : id,
};
let has_remove_events = typeof this.beforeRemove == 'function' || typeof this.afterRemove == 'function' || this.listeners('removed').length;
let doc;
let tasks = [];
if (has_remove_events) {
// Get the actual document
tasks.push(async next => {
doc = await this.findByPk(id);
if (!doc) {
return next(new Error('Unable to find document with id ' + id));
}
next();
});
tasks.push(next => {
this.callOrNext('beforeRemove', [doc], next);
});
}
Function.series(tasks, function done(err) {
if (err) {
pledge.reject(err);
return;
}
let context = new Classes.Alchemy.OperationalContext.RemoveFromDatasource();
context.setDatasource(that.datasource);
context.setModel(that);
context.setQuery(query);
Swift.done(that.datasource.remove(context), function afterRemove(err, result) {
if (err != null) {
return pledge.reject(err);
}
if (has_remove_events) {
that.issueDataEvent('afterRemove', [doc, result], () => pledge.resolve(result));
} else {
pledge.resolve(result);
}
});
});
return pledge;
});
/**
* Get all the records and perform the given task on them
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.5.0
* @version 1.2.0
*
* @param {Object} options Find options
* @param {Function} task Task to perform on each record
* @param {Function} callback Function to call when done
*/
Model.setMethod(function eachRecord(options, task, callback) {
var that = this,
parallel_limit,
available = null,
last_id = null,
pledge = new Classes.Pledge(),
index = 0;
if (typeof options == 'function') {
callback = task;
task = options;
options = {};
} else if (!options) {
options = {};
}
if (!callback) {
callback = Function.thrower;
}
// Apply default limit of 50 records per fetch
options = Object.assign({}, {limit: 50}, options);
// Get amount of tasks to do in parallel
parallel_limit = options.parallel_limit || 8;
// Sort by _id ascending
if (!options.sort) {
options.sort = {};
options.sort[this.primary_key] = 1;
}
// Make sure there is a conditions object
if (!options.conditions) {
options.conditions = {};
}
this.find('all', options, function gotRecords(err, result) {
var tasks = [];
if (!result.length) {
pledge.reportProgress(100);
pledge.resolve();
return callback(null);
}
if (available == null) {
available = result.available;
options.available = false;
} else {
result.available = available;
}
result.forEach(function eachRecord(record) {
var record_index = index++;
last_id = record[that.model_name][that.primary_key];
tasks.push(function doSave(next) {
pledge.reportProgress(((record_index - 1) / available) * 100);
task.call(that, record, record_index, next);
});
});
Function.parallel(parallel_limit, tasks, function done(err) {
if (err) {
pledge.reject(err);
return callback(err);
}
let next_options = Object.assign({}, options);
// Get records with a bigger _id than the last found
next_options.conditions[that.primary_key] = {$gt: last_id};
that.find('all', next_options, gotRecords);
});
});
return pledge;
});
/**
* Strip out private fields
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.0
* @version 1.0.0
*
* @param {Array} records
*/
Model.setMethod(function removePrivateFields(records) {
var has_private_fields,
fields = this.schema.getSorted(false),
record,
field,
i,
j;
records = Array.cast(records);
for (i = 0; i < records.length; i++) {
record = records[i];
for (j = 0; j < fields.length; j++) {
field = fields[j];
if (field.is_private) {
has_private_fields = true;
delete record[field.name];
}
}
// If there are no private fields, break loop
if (!has_private_fields) {
break;
}
}
return records;
});
/**
* Create an export stream
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.5
* @version 1.0.5
*
* @param {Stream} output
* @param {Object} options
*
* @return {Pledge}
*/
Model.setMethod(function exportToStream(output, options) {
if (!alchemy.isStream(output)) {
if (!options) {
options = output;
output = null;
}
output = options.output;
}
if (!output) {
return Pledge.reject(new Error('No target output stream has been given'));
}
if (!options) {
options = {};
}
// Only allow 1 task to run at a time
options.parallel_limit = 1;
let that = this,
name_buf = Buffer.from(this.model_name),
head_buf;
// 0x01 is a model
head_buf = Buffer.concat([Buffer.from([0x01, name_buf.length]), name_buf]);
output.write(head_buf);
return this.eachRecord(options, function eachRecord(record, index, next) {
record.exportToStream(output).done(next);
}, function done(err) {
});
});
/**
* Import from a stream
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 1.0.5
* @version 1.0.5
*
* @param {Stream} input
* @param {Object} options
*
* @return {Pledge}
*/
Model.setMethod(function importFromStream(input, options) {
if (!alchemy.isStream(input)) {
if (!options) {
options = input;
input = null;
}
input = options.input;
}
if (!input) {
return Pledge.reject(new Error('No source input stream has been given'));
}
let that = this,
current_type = null,
extra_stream,
pledge = new Pledge(),
stopped,
paused,
buffer,
value,
seen = 0,
left,
size,
doc;
input.on('data', function onData(data) {
if (stopped) {
return;
}
if (buffer) {
buffer = Buffer.concat([buffer, data]);
} else {
buffer = data;
}
handleBuffer();
});
function handleBuffer() {
if (paused) {
return;
}
if (!current_type && buffer.length < 2) {
return;
}
if (!current_type) {
current_type = buffer.readUInt8(0);
if (current_type == 0x01) {
size = buffer.readUInt8(1);
buffer = buffer.slice(2);
} else if (current_type == 0x02 && buffer.length >= 5) {
size = buffer.readUInt32BE(1);
buffer = buffer.slice(5);
} else if (current_type == 0xFF) {
size = buffer.readUInt32BE(1);
buffer = buffer.slice(5);
seen = 0;
if (!doc) {
stopped = true;
pledge.reject(new Error('Found extra import data, but no active document'));
} else {
extra_stream = new require('stream').PassThrough();
doc.extraImportFromStream(extra_stream);
}
} else {
// Not enough data? Wait
current_type = null;
return;
}
}
handleRest();
}
function handleRest() {
if (current_type == 0xFF) {
left = size - seen;
value = buffer.slice(0, left);
seen += value.length;
if (value.length == buffer.length) {
buffer = null;
} else if (value.length < buffer.length) {
buffer = buffer.slice(left);
}
extra_stream.write(value);
if (value.length == left) {
extra_stream.end();
current_type = null;
if (buffer) {
handleBuffer();
}
}
return;
}
if (buffer.length >= size) {
value = buffer.slice(0, size);
buffer = buffer.slice(size);
} else {
// Wait for next call
return;
}
if (current_type == 0x01) {
value = value.toString();
if (value == that.model_name) {
// Found name!
current_type = null;
size = 0;
} else {
stopped = true;
return pledge.reject(new Error('Model names do not match'));
}
} else if (current_type == 0x02) {
doc = that.createDocument();
input.pause();
paused = true;
doc.importFromBuffer(value).done(function done(err, result) {
if (err) {
stopped = true;
return pledge.reject(err);
}
current_type = null;
paused = false;
input.resume();
handleBuffer();
});
return;
}
if (buffer && buffer.length) {
handleBuffer();
}
}
return pledge;
});
/**
* Get a model
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 1.4.0
*
* @param {string} name
* @param {boolean} init
*
* @return {Model}
*/
Model.get = function get(name, init, options) {
if (typeof init != 'boolean') {
options = init;
init = true;
}
let path = Blast.parseClassPath(name);
let constructor = Object.path(Blast.Classes.Alchemy.Model, path) || Object.path(Blast.Classes, path);
if (!constructor) {
throw new Error('Model "' + name + '" could not be found');
}
if (!init) {
return constructor;
}
return new constructor(options);
};
/**
* Make the base Model class a global
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.0.1
* @version 0.2.0
*
* @type {Object}
*/
DEFINE('Model', Model);