UNPKG

keystone

Version:

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

273 lines (254 loc) 7.43 kB
var _ = require('lodash'); var FieldType = require('../Type'); var keystone = require('../../../'); var util = require('util'); var utils = require('keystone-utils'); var definePrototypeGetters = require('../../utils/definePrototypeGetters'); /** * Relationship FieldType Constructor * @extends Field * @api public */ function relationship (list, path, options) { this.many = (options.many) ? true : false; this.filters = options.filters; this.createInline = (options.createInline) ? true : false; this._defaultSize = 'full'; this._nativeType = keystone.mongoose.Schema.Types.ObjectId; this._underscoreMethods = ['format', 'getExpandedData']; this._properties = ['isValid', 'many', 'filters', 'createInline']; relationship.super_.call(this, list, path, options); } relationship.properName = 'Relationship'; util.inherits(relationship, FieldType); /** * Get client-side properties to pass to react field. */ relationship.prototype.getProperties = function () { var refList = this.refList; return { refList: { singular: refList.singular, plural: refList.plural, path: refList.path, key: refList.key, }, }; }; /** * Gets id and name for the related item(s) from populated values */ function expandRelatedItemData (item) { if (!item || !item.id) return undefined; return { id: item.id, name: this.refList.getDocumentName(item), }; } function truthy (value) { return value; } relationship.prototype.getExpandedData = function (item) { var value = item.get(this.path); if (this.many) { if (!value || !Array.isArray(value)) return []; return value.map(expandRelatedItemData.bind(this)).filter(truthy); } else { return expandRelatedItemData.call(this, value); } }; /** * Registers the field on the List's Mongoose Schema. */ relationship.prototype.addToSchema = function (schema) { var field = this; var def = { type: this._nativeType, ref: this.options.ref, index: (this.options.index ? true : false), required: (this.options.required ? true : false), unique: (this.options.unique ? true : false), }; this.paths = { refList: this.options.refListPath || this.path + 'RefList', }; schema.path(this.path, this.many ? [def] : def); schema.virtual(this.paths.refList).get(function () { return keystone.list(field.options.ref); }); this.bindUnderscoreMethods(); }; /** * Gets the field's data from an Item, as used by the React components */ relationship.prototype.getData = function (item) { var value = item.get(this.path); if (this.many) { return Array.isArray(value) ? value : []; } else { return value; } }; /** * Add filters to a query */ relationship.prototype.addFilterToQuery = function (filter) { var query = {}; if (!Array.isArray(filter.value)) { if (typeof filter.value === 'string' && filter.value) { filter.value = [filter.value]; } else { filter.value = []; } } if (filter.value.length) { query[this.path] = (filter.inverted) ? { $nin: filter.value } : { $in: filter.value }; } else { if (this.many) { query[this.path] = (filter.inverted) ? { $not: { $size: 0 } } : { $size: 0 }; } else { query[this.path] = (filter.inverted) ? { $ne: null } : null; } } return query; }; /** * Formats the field value */ relationship.prototype.format = function (item) { var value = item.get(this.path); // force the formatted value to be a string - unexpected things happen with ObjectIds. return this.many ? value.join(', ') : (value || '') + ''; }; /** * Asynchronously confirms that the provided value is valid * * TODO: might be a good idea to check the value provided looks like a MongoID * TODO: we're just testing for strings here, so actual MongoID Objects (from * mongoose) would fail validation. not sure if this is an issue. */ relationship.prototype.validateInput = function (data, callback) { var value = this.getValueFromData(data); var result = false; if (value === undefined || value === null || value === '') { result = true; } else { if (this.many) { if (!Array.isArray(value) && typeof value === 'string' && value.length) { value = [value]; } if (Array.isArray(value)) { result = true; } } else { if (typeof value === 'string' && value.length) { result = true; } if (typeof value === 'object' && value.id) { result = true; } } } utils.defer(callback, result); }; /** * Asynchronously confirms that the provided value is present */ relationship.prototype.validateRequiredInput = function (item, data, callback) { var value = this.getValueFromData(data); var result = false; if (value === undefined) { if (this.many) { if (item.get(this.path).length) { result = true; } } else { if (item.get(this.path)) { result = true; } } } else if (this.many) { if (!Array.isArray(value) && typeof value === 'string' && value.length) { value = [value]; } if (Array.isArray(value) && value.length) { result = true; } } else { if (value) { result = true; } } utils.defer(callback, result); }; /** * Validates that a value for this field has been provided in a data object * * Deprecated */ relationship.prototype.inputIsValid = function (data, required, item) { if (!required) return true; if (!(this.path in data) && item && ((this.many && item.get(this.path).length) || 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. * Only updates the value if it has changed. * Treats an empty string as a null value. * If data object does not contain the path field, then leave the field untouched. * falsey values such as `null` or an empty string will reset the field */ relationship.prototype.updateItem = function (item, data, callback) { if (item.populated(this.path)) { throw new Error('fieldTypes.relationship.updateItem() Error - You cannot update populated relationships.'); } var value = this.getValueFromData(data); if (value === undefined) { return process.nextTick(callback); } // Are we handling a many relationship or just one value? if (this.many) { var arr = item.get(this.path); var _old = arr.map(function (i) { return String(i); }); var _new = value; if (!utils.isArray(_new)) { _new = String(_new || '').split(','); } _new = _.compact(_new); // Only update if the lists aren't the same if (!_.isEqual(_old, _new)) { item.set(this.path, _new); } } else { // Ok, it's one value, should I do anything with it? if (value && value !== item.get(this.path)) { // If it's set and has changed, I do. item.set(this.path, value); } else if (!value && item.get(this.path)) { // If it's not set and it was set previously, I need to clear. item.set(this.path, null); } // Otherwise, ignore. } process.nextTick(callback); }; definePrototypeGetters(relationship, { // Returns true if the relationship configuration is valid isValid: function () { return keystone.list(this.options.ref) ? true : false; }, // Returns the Related List refList: function () { return keystone.list(this.options.ref); }, // Whether the field has any filters defined hasFilters: function () { return (this.filters && _.keys(this.filters).length); }, }); /* Export Field Type */ module.exports = relationship;