UNPKG

acklen-keystone

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

359 lines (326 loc) 10.6 kB
/*! * Module dependencies. */ var _ = require('lodash'); var assign = require('object-assign'); var di = require('asyncdi'); var marked = require('marked'); var Path = require('../../lib/path'); var utils = require('keystone-utils'); var evalDependsOn = require('../utils/evalDependsOn.js'); var definePrototypeGetters = require('../utils/definePrototypeGetters.js'); var debug = require('debug')('keystone:fields:types:Type'); var DEFAULT_OPTION_KEYS = [ 'path', 'paths', 'type', 'label', 'note', 'size', 'initial', 'required', 'col', 'noedit', 'nocol', 'nosort', 'indent', 'hidden', 'collapse', 'dependsOn', 'autoCleanup', ]; /** * Field Constructor * ================= * * Extended by fieldType Classes, should not be used directly. * * @api public */ function Field (list, path, options) { // Set field properties and options this.list = list; this._path = new Path(path); this.path = path; this.type = this.constructor.name; this.options = _.defaults({}, options, this.defaults); this.label = options.label || utils.keyToLabel(this.path); this.typeDescription = options.typeDescription || this.typeDescription || this.type; this.list.automap(this); // Warn on required fields that aren't initial if (this.options.required && this.options.initial === undefined && this.options.default === undefined && !this.options.value && !this.list.get('nocreate') && this.path !== this.list.mappings.name ) { console.error('\nError: Invalid Configuration\n\n' + 'Field (' + list.key + '.' + path + ') is required but not initial, and has no default or generated value.\n' + 'Please provide a default, remove the required setting, or set initial: false to override this error.\n'); process.exit(1); } // if dependsOn and required, set required to a function for validation if (this.options.dependsOn && this.options.required === true) { var opts = this.options; this.options.required = function () { // `this` refers to the validating document debug('validate dependsOn required', evalDependsOn(opts.dependsOn, this.toObject())); return evalDependsOn(opts.dependsOn, this.toObject()); }; } // Add the field to the schema this.addToSchema(this.list.schema); // Add pre-save handler to the list if this field watches others if (this.options.watch) { this.list.schema.pre('save', this.getPreSaveWatcher()); } // Convert notes from markdown to html var note = null; Object.defineProperty(this, 'note', { get: function () { return (note === null) ? (note = (this.options.note) ? marked(this.options.note) : '') : note; }, }); } /** * Gets the options for the Field, as used by the React components */ Field.prototype.getOptions = function () { if (!this.__options) { this.__options = {}; var optionKeys = DEFAULT_OPTION_KEYS; if (_.isArray(this._properties)) { optionKeys = optionKeys.concat(this._properties); } optionKeys.forEach(function (key) { if (this[key]) { this.__options[key] = this[key]; } else if (this.options[key]) { this.__options[key] = this.options[key]; } }, this); if (this.getProperties) { assign(this.__options, this.getProperties()); } this.__options.hasFilterMethod = this.addFilterToQuery ? true : false; this.__options.defaultValue = this.getDefaultValue(); } return this.__options; }; /** * Validates and returns the size of the field. * Defaults to deprecated 'width' option. */ Field.prototype.getSize = function () { if (!this.__size) { var size = this._fixedSize || this.options.size || this.options.width; if (size !== 'small' && size !== 'medium' && size !== 'large' && size !== 'full') { size = this._defaultSize || 'full'; } this.__size = size; } return this.__size; }; /** * Gets default value for the field, based on the option or default for the type */ Field.prototype.getDefaultValue = function () { return typeof this.options.default !== 'undefined' ? this.options.default : ''; }; /** * Gets the field's data from an Item, as used by the React components */ Field.prototype.getData = function (item) { return item.get(this.path); }; /** * Field watching implementation */ Field.prototype.getPreSaveWatcher = function () { var field = this; var applyValue; if (this.options.watch === true) { // watch == true means always apply the value method applyValue = function () { return true; }; } else { // if watch is a string, convert it to a list of paths to watch if (typeof this.options.watch === 'string') { this.options.watch = this.options.watch.split(' '); } if (typeof this.options.watch === 'function') { applyValue = this.options.watch; } else if (_.isArray(this.options.watch)) { applyValue = function (item) { var pass = false; field.options.watch.forEach(function (path) { if (item.isModified(path)) pass = true; }); return pass; }; } else if (_.isObject(this.options.watch)) { applyValue = function (item) { var pass = false; _.forEach(field.options.watch, function (value, path) { if (item.isModified(path) && item.get(path) === value) pass = true; }); return pass; }; } } if (!applyValue) { console.error('\nError: Invalid Configuration\n\n' + 'Invalid watch value (' + this.options.watch + ') provided for ' + this.list.key + '.' + this.path + ' (' + this.type + ')'); process.exit(1); } if (typeof this.options.value !== 'function') { console.error('\nError: Invalid Configuration\n\n' + 'Watch set with no value method provided for ' + this.list.key + '.' + this.path + ' (' + this.type + ')'); process.exit(1); } return function (next) { if (!applyValue(this)) { return next(); } di(field.options.value).call(this, function (err, val) { if (err) { console.error('\nError: ' + 'Watch set with value method for ' + field.list.key + '.' + field.path + ' (' + field.type + ') throws error:' + err); } else { this.set(field.path, val); } next(); }.bind(this)); }; }; module.exports = Field; /** Getter properties for the Field prototype */ definePrototypeGetters(Field, { size: function () { return this.getSize(); }, initial: function () { return this.options.initial || false; }, required: function () { return this.options.required || false; }, note: function () { return this.options.note || ''; }, col: function () { return this.options.col || false; }, noedit: function () { return this.options.noedit || false; }, nocol: function () { return this.options.nocol || false; }, nosort: function () { return this.options.nosort || false; }, collapse: function () { return this.options.collapse || false; }, hidden: function () { return this.options.hidden || false; }, dependsOn: function () { return this.options.dependsOn || false; }, }); /** * Default method to register the field on the List's Mongoose Schema. * Overridden by some fieldType Classes */ Field.prototype.addToSchema = function (schema) { var ops = (this._nativeType) ? _.defaults({ type: this._nativeType }, this.options) : this.options; schema.path(this.path, ops); this.bindUnderscoreMethods(); }; /** * Binds the methods specified by the _underscoreMethods property * Must be called by the field type's `addToSchema` method * Always includes the `update` method */ Field.prototype.bindUnderscoreMethods = function () { var field = this; (this._underscoreMethods || []).concat({ fn: 'updateItem', as: 'update' }).forEach(function (method) { if (typeof method === 'string') { method = { fn: method, as: method }; } if (typeof field[method.fn] !== 'function') { throw new Error('Invalid underscore method (' + method.fn + ') applied to ' + field.list.key + '.' + field.path + ' (' + field.type + ')'); } field.underscoreMethod(method.as, function () { var args = [this].concat(Array.prototype.slice.call(arguments)); return field[method.fn].apply(field, args); }); }); }; /** * Adds a method to the underscoreMethods collection on the field's list, * with a path prefix to match this field's path and bound to the document */ Field.prototype.underscoreMethod = function (path, fn) { this.list.underscoreMethod(this.path + '.' + path, function () { return fn.apply(this, arguments); }); }; /** * Default method to format the field value for display * Overridden by some fieldType Classes * * @api public */ Field.prototype.format = function (item) { var value = item.get(this.path); if (value === undefined) return ''; return value; }; /** * Default method to detect whether the field has been modified in an item * Overridden by some fieldType Classes * * @api public */ Field.prototype.isModified = function (item) { return item.isModified(this.path); }; /** * Checks whether a provided value for the field is in a valid format * Overridden by some fieldType Classes * * @api public */ Field.prototype.validateInput = function (data, callback) { utils.defer(callback, this.inputIsValid(data)); }; /** * Validates that a value for this field has been provided in a data object, * taking into account existing data in an item * Overridden by some fieldType Classes * * @api public */ Field.prototype.validateRequiredInput = function (item, data, callback) { utils.defer(callback, this.inputIsValid(data, true, item)); }; /** * Validates that a value for this field has been provided in a data object * Overridden by some fieldType Classes * * Not a reliable public API; use inputIsValid, which is async, instead. * This method has been deprecated. */ Field.prototype.inputIsValid = function (data, required, item) { if (!required) return true; var value = this.getValueFromData(data); if (value === undefined && item && item.get(this.path)) return true; if (typeof data[this.path] === 'string') { return (data[this.path].trim()) ? true : false; } else { return (data[this.path]) ? true : false; } }; /** * Updates the value for this field in the item from a data object * Overridden by some fieldType Classes * * @api public */ Field.prototype.updateItem = function (item, data, callback) { var value = this.getValueFromData(data); // This is a deliberate type coercion so that numbers from forms play nice if (value !== undefined && value != item.get(this.path)) { // eslint-disable-line eqeqeq item.set(this.path, value); } process.nextTick(callback); }; /** * Retrieves the value from an object, whether the path is nested or flattened * * @api public */ Field.prototype.getValueFromData = function (data, subpath) { return this._path.get(data, subpath); };