UNPKG

alchemymvc

Version:
399 lines (317 loc) 8.45 kB
/** * The Sluggable Behaviour class * * @constructor * @extends Alchemy.Behaviour * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.1.0 * @version 1.3.16 */ var Sluggable = Function.inherits('Alchemy.Behaviour', function SluggableBehaviour(model, options) { Behaviour.call(this, model, options); // Source options this.source = options.source; this.source_field = options.source_field; this.target_field = options.target_field; this.replacement = options.replacement; if (!this.source_field) { throw new Error('No source field found for sluggable behaviour in "' + this.model.model_name + '"'); } if (!options.unique_modifier_fields) { options.unique_modifier_fields = []; } else if (!Array.isArray(options.unique_modifier_fields)) { options.unique_modifier_fields = Array.cast(options.unique_modifier_fields); } this.unique_modifier_fields = options.unique_modifier_fields; this.translatable = this.source_field.translatable; }); /** * Default slug options * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.3 * * @type {Object} */ Sluggable.setProperty('default_options', { translatable : null, target : 'slug', source : ['title', 'name'], replacement : '-' }); /** * Get the source field * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 0.2.0 * * @param {string} source * @param {Schema} schema */ Sluggable.setStatic(function getSource(source, schema) { var field, i; source = Array.cast(source); for (i = 0; i < source.length; i++) { field = schema.get(source[i]); if (field) { break; } } return field; }); /** * Listen to attachments to schema's * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.2.0 * @version 1.3.0 * * @param {Schema} schema * @param {Object} options */ Sluggable.setStatic(function attached(schema, new_options) { var field_options, options, source, target, main; if (schema.model_class) { main = schema.model_class; } else { main = schema; } // Merge the options object options = Object.assign({}, this.prototype.default_options, new_options); // Add them back into the new_options by reference options = Object.assign(new_options, options); // Get the source field source = this.getSource(options.source, schema); // Get the target field target = schema.get(options.target); // Create the target if it doesn't exist yet if (!target) { field_options = { description: 'A human-readable yet unique identifier' }; // See if the target field we're adding needs to be translatable if (options.translatable || (options.translatable == null && source && source.is_translatable)) { field_options.translatable = true; } target = main.addField(options.target, 'String', field_options); } else if (target.is_translatable) { options.translatable = true; } // Also store the fields in the options options.source_field = source; options.target_field = target; }); /** * The beforeSave * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.1.0 * @version 1.0.6 * * @param {Object} data The data that is to be saved * @param {Object} options Behaviour options * @param {boolean} creating Is this record being created? */ Sluggable.setMethod(async function beforeSave(data, options, creating) { var that = this, has_new_value, old_record, new_value, old_value; // Get the actual record data if (data[that.model.name]) { data = data[that.model.name]; } // See if there is a new value defined new_value = data[that.target_field.name]; if (typeof new_value == 'undefined') { has_new_value = false; } else { has_new_value = true; } if (!creating) { old_record = await this.model.findById(data._id); if (old_value) { old_value = old_record[that.target_field.name]; } } // If we're not creating a new document, // and the old document already has a slug, // and we're not explicitly setting a new slug value, // then do nothing if (!creating && old_value && !has_new_value) { return; } let new_data = {}; // Create a new data object, so the non-changed values are available if (old_record) { Object.assign(new_data, old_record.$main, data); } else { Object.assign(new_data, data); } let pledge = new Swift(); // Try creating a new slug that.createSlug(new_data, new_value, function createdSlug(err, result) { if (err) { return pledge.reject(err); } if (!Object.isEmpty(result)) { data[that.target_field.name] = result; } pledge.resolve(); }); return pledge; }); /** * Start the slug creating process * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.1.0 * @version 1.1.0 * * @param {Object} record * @param {string} new_value * @param {Function} callback */ Sluggable.setMethod(function createSlug(record, new_value, callback) { var that = this, source, tasks, key, i; if (typeof new_value == 'function') { callback = new_value; new_value = undefined; } if (Array.isArray(this.source)) { for (i = 0; i < this.source.length; i++) { source = record[this.source[i]]; if (source) { break; } } } else { source = record[this.source]; } if (source && typeof source == 'object') { tasks = {}; Object.each(source, function eachSource(source_title, key) { var has_value = !!record[that.target_field.name] && !!record[that.target_field.name][key]; // If there already is a slug for this prefix, // and no new one was manually provided, // just return the old one if (has_value && (!new_value || !new_value[key])) { tasks[key] = function returnExistingValue(next) { next(null, record[that.target_field.name][key]); }; return; } tasks[key] = function generateSlug(next) { if (new_value && new_value[key]) { source_title = new_value[key]; } that.generateSlug(source_title, key, record, next); }; }); return Function.parallel(tasks, callback); } else { // Only generate a new slug if it doesn't exist yet if (!record[that.target_field.name]) { return that.generateSlug(record[that.source_field.name], false, record, callback); } } callback(); }); /** * Actually generate the slug from the given string, * and look for existing slugs in the path * * @author Jelle De Loecker <jelle@elevenways.be> * @since 0.1.0 * @version 1.1.8 * * @param {string} title * @param {string} path * @param {Document} record * @param {Function} callback */ Sluggable.setMethod(function generateSlug(title, key, record, callback) { var that = this, for_record_id, baseSlug, current, model, count, item, path; if (typeof key == 'object') { callback = record; record = key; key = false; } if (typeof record == 'function') { callback = record; record = null; } if (record) { for_record_id = record._id; } if (typeof key === 'function') { callback = key; key = false; } // If no valid title is given, do nothing! if (!title) { return callback(); } baseSlug = title.slug(that.replacement || '-').toLowerCase(); current = baseSlug; model = Model.get(this.model.name); count = 1; path = this.target_field.name; if (key) { path += '.' + key; } Function.doWhile(function doWhile(next) { var conditions = {}; conditions[path] = current; if (that.unique_modifier_fields.length && record) { let value, field, i; for (i = 0; i < that.unique_modifier_fields.length; i++) { field = that.unique_modifier_fields[i]; value = record[field]; conditions[field] = value; } } model.find('first', {conditions: conditions}, function gotFirst(err, found_item) { if (err != null) { return next(err); } // Don't use the target record for duplicate checks if (found_item && for_record_id && String(for_record_id) == String(found_item._id)) { return next(); } item = found_item; next(); }); }, function check() { // As long as items have been found we keep on adding numbers if (item) { count++; current = baseSlug + (that.replacement || '-') + count; return true; } }, function finished(err) { callback(err, current); }); });