@onehat/data
Version:
JS data modeling package with adapters for many storage mediums.
539 lines (458 loc) • 13.3 kB
JavaScript
/** @module Property */
import EventEmitter from '@onehat/events';
import Formatters from '../Util/Formatters.js';
import _ from 'lodash';
/**
* Base class representing a Property
* This class should not be instantiated directly.
* Rather, instantiate a subclass, like StringProperty
* @extends EventEmitter
* @fires ['change', 'changeValidity', 'destroy']
*/
export default class Property extends EventEmitter {
static defaults = {
/**
* @member {string} name - Could be anything, but OneHat's convention is to use
* the model name pluralized and underscored, followed by two underscores,
* followed by the field name singular and underscored (e.g. 'groups_users__id')
* This convention allows us to have multiple models' data in a single Entity, all flattened
*/
name: null,
/**
* @member {boolean} allowNull - Is the property required to have a value?
* Defaults to true.
*/
allowNull: true,
/**
* @member {function} parse - Custom parse function that overrides
* Property.parse.
* Takes one argument:
* - rawValue {any} - The raw value to parse
*
* Note: If you use standard function notation for the parse function,
* then the Property and Entity (with all other parsed properties and original data)
* are available inside the function as:
* - Property: this
* - Entity: this.getEntity()
* - Original Data: this.getEntity().getOriginalData()
*
* Returns 'parsedValue'
* @private
*/
// parse: null,
/**
* @member {(string|string[])} depends - Other properties this property
* depends upon for its custom "parse" function.
* @private
*/
depends: null,
/**
* @member {string} mapping - JS dot-notation path
* (e.g. "user.username") for how to access the rawValue which will be
* given to parse(), based on an Entity's _originalData object.
* @private
* @readonly
*/
mapping: null,
/**
* @member {boolean} submitAsString - Whether to submit value as a string, rather than a primitive or complex type
*/
submitAsString: false,
/**
* @member {boolean} isSortable - Whether this property type is sortable
*/
isSortable: true,
/**
* @member {boolean} isTempId - Whether this property's ID is temporary
*/
isTempId: false,
// ##################################################################
// #### These next properties are only for OneBuild repositories ####
// ##################################################################
/**
* @member {boolean} isVirtual - Whether this property represents a virtual field on server
*/
isVirtual: false,
/**
* @member {string} title - The human-readable title for this property
*/
title: null,
/**
* @member {string} tooltip - The human-readable tooltip for this property
*/
tooltip: null,
/**
* @member {string} fieldGroup - The field group for this property
*/
fieldGroup: null,
/**
* @member {boolean} isForeignModel - Whether this property belongs to a foreign model
*/
isForeignModel: false,
/**
* @member {object} filterType - The UI filter type of this property
*/
filterType: null,
/**
* @member {boolean} isFilteringDisabled - Whether this property is disabled for UI filtering
*/
isFilteringDisabled: false,
/**
* @member {object} viewerType - The UI viewer type of this property
*/
viewerType: null,
/**
* @member {object} editorType - The UI editor type of this property
*/
editorType: null,
/**
* @member {boolean} isEditingDisabled - Whether this property is disabled for UI editing
*/
isEditingDisabled: false,
/**
* @member {any} defaultValue - Default value for this property if none is supplied
* @private
*/
defaultValue: null,
/**
* @member {string} formatter - The name of the formatter to use for this property
* @private
*/
formatter: null,
};
/**
* @constructor
* @param {object} config - Object with key/value pairs that define this Property
*/
constructor(config = {}, entity) {
super(...arguments);
_.merge(this, Property.defaults, config);
this._originalConfig = config;
this.registerEvents([
'change',
'changeValidity',
'destroy',
]);
/**
* @member {object} entity - Entity object
* @private
*/
this._entity = entity;
/**
* @member {any} rawValue - The raw value supplied to this property, *before* any parsing was applied
*/
this.rawValue = null;
/**
* @member {any} parsedValue - The value for this property, *after* any parsing was applied
*/
this.parsedValue = null;
/**
* @member {boolean} isDestroyed - Whether this object has been destroyed
* @private
*/
this.isDestroyed = false;
}
// ______ __ __
// / ____/__ / /_/ /____ __________
// / / __/ _ \/ __/ __/ _ \/ ___/ ___/
// / /_/ / __/ /_/ /_/ __/ / (__ )
// \____/\___/\__/\__/\___/_/ /____/
/**
*
* @param {object} defaults - Optional defaults to merge with Property.defaults
* @returns {object} Merged defaults
*/
static getStaticDefaults(defaults = {}) {
return _.merge({}, Property.defaults, defaults);
}
/**
* Gets default value. Can be overridden to get dynamic default value.
* @return {any} defaultValue
*/
getDefaultValue() {
let value = null;
if (!_.isNil(this.defaultValue)) {
value = this.defaultValue;
}
if (_.isFunction(value)) {
value = value();
}
return value;
}
/**
* Gets "raw" value
* @return {any} rawValue
*/
getRawValue() {
if (this.isDestroyed) {
throw Error('this.getRawValue is no longer valid. Property has been destroyed.');
}
return this.rawValue;
}
/**
* Gets "raw" value in its parsed form
* @return {any} rawValue
*/
getParsedRawValue() {
if (this.isDestroyed) {
throw Error('this.getParsedRawValue is no longer valid. Property has been destroyed.');
}
return this.parse(this.rawValue);
}
/**
* Gets "parsed" value, without any formatting applied
* @return {any} parsedValue
*/
getParsedValue() {
if (this.isDestroyed) {
throw Error('this.getParsedValue is no longer valid. Property has been destroyed.');
}
return this.parsedValue;
}
/**
* Gets parsed value, formatted for submission to server
* @return {any} parsedValue
*/
getSubmitValue() {
if (this.isDestroyed) {
throw Error('this.getSubmitValue is no longer valid. Property has been destroyed.');
}
if (this.submitAsString) {
return String(this.parsedValue);
}
return this.parsedValue;
}
/**
* Gets parsed value, formatted for submission to server
* @return {any} submitValue
*/
get submitValue() {
if (this.isDestroyed) {
throw Error('this.submitValue is no longer valid. Property has been destroyed.');
}
return this.getSubmitValue();
}
/**
* Gets parsed value, formatted for displaying to user
* @return {any} _displayValue
*/
getDisplayValue() {
if (this.isDestroyed) {
throw Error('this.getDisplayValue is no longer valid. Property has been destroyed.');
}
if (this.formatter) {
return Formatters[this.formatter](this.parsedValue);
}
return this.parsedValue;
}
/**
* Gets parsed value, formatted for displaying to user
* @return {any} displayValue
*/
get displayValue() {
if (this.isDestroyed) {
throw Error('this.displayValue is no longer valid. Property has been destroyed.');
}
return this.getDisplayValue();
}
/**
* Gets whether or not the Property has a mapping.
* Used by Entity.
* @return {boolean}
*/
get hasMapping() {
if (this.isDestroyed) {
throw Error('this.hasMapping is no longer valid. Property has been destroyed.');
}
return !!this.mapping;
}
/**
* Gets whether or not the Property depends on any other properties
* for its local "parse" function.
* Used by Entity.
* @return {boolean}
*/
get hasDepends() {
if (this.isDestroyed) {
throw Error('this.hasDepends is no longer valid. Property has been destroyed.');
}
return !!this.depends;
}
/**
* Gets whether or not the Property is an "ID" property for the Entity it's assigned to
* @return {boolean} isIdProperty
*/
get isIdProperty() {
if (this.isDestroyed) {
throw Error('this.isIdProperty is no longer valid. Property has been destroyed.');
}
const entity = this.getEntity();
if (!entity || !entity.getIdProperty) {
return false;
}
return entity.getIdProperty() === this;
}
/**
* Gets whether or not the Property is a "Display" property for the Entity it's assigned to
* @return {boolean} isDisplayProperty
*/
get isDisplayProperty() {
if (this.isDestroyed) {
throw Error('this.isDisplayProperty is no longer valid. Property has been destroyed.');
}
const entity = this.getEntity();
if (!entity || !entity.getDisplayProperty) {
return false;
}
return entity.getDisplayProperty() === this;
}
/**
* Gets the model name from this property
* NOTE: Only for OneBuild repositories!
* @return {any} submitValue
*/
get modelName() {
if (this.isDestroyed) {
throw Error('this.modelName is no longer valid. Property has been destroyed.');
}
if (!this.name.match(/__/)) {
throw Error('this.name is not in the correct format for modelName.');
}
const
matches = this.name.match(/^([\w_]+)__/),
modelName = matches[1];
return modelName;
}
// _____ __ __
// / ___/___ / /_/ /____ __________
// \__ \/ _ \/ __/ __/ _ \/ ___/ ___/
// ___/ / __/ /_/ /_/ __/ / (__ )
// /____/\___/\__/\__/\___/_/ /____/
/**
* Sets the parsedValue for this Property.
* Any mapping for this property has already taken place.
* i.e. The rawValue to parse *is the mapped value.*
* @param {any} rawValue - Value to parse
* @return {boolean} isChanged - Whether or not the parsedValue was changed
*/
setValue(rawValue) {
if (this.isDestroyed) {
throw Error('this.setValue is no longer valid. Property has been destroyed.');
}
const oldValue = this.parsedValue;
let newValue = this.parse(rawValue);
// Special case: ID properties that are normally integers
// but which use strings as temporary values (e.g. "TEMP-123")
if (this.isIdProperty && this.type === 'int' && _.isString(rawValue)) {
// Allow this to stay as a string
newValue = rawValue;
}
if (!this.allowNull && _.isNil(newValue)) {
throw new Error('Value for ' + this.name + ' cannot be null.');
}
let isChanged = !_.isEqual(oldValue, newValue);
if (isChanged) {
this.rawValue = rawValue;
this.parsedValue = newValue;
this.emit('change', this, oldValue, newValue);
}
return isChanged;
}
/**
* Performs the actual parsing conversion, but *does not* set anything on the property.
* Default function. Meant to be overridden with subclass.
* @param {any} value - Value to parse
* @return {any} value - Parsed value. NOTE: for the Property base class, no parsing actually takes place!
*/
parse(value) {
if (this.isDestroyed) {
throw Error('this.parse is no longer valid. Property has been destroyed.');
}
if (_.isNil(value)) {
return null;
}
return value;
}
/**
* Sets the formatter for this Property.
* @param {*} formatter
*/
setFormatter(formatter) {
if (this.isDestroyed) {
throw Error('this.setFormatter is no longer valid. Property has been destroyed.');
}
this.formatter = formatter;
}
// __ ____ _ ___ __ _
// / / / / /_(_) (_) /_(_)__ _____
// / / / / __/ / / / __/ / _ \/ ___/
// / /_/ / /_/ / / / /_/ / __(__ )
// \____/\__/_/_/_/\__/_/\___/____/
/**
* Sets value of submitAsString
* @param {boolean} bool - New value of submitAsString
*/
setSubmitAsString(bool) {
if (this.isDestroyed) {
throw Error('this.setSubmitAsString is no longer valid. Property has been destroyed.');
}
this.submitAsString = bool;
}
/**
* Gets the Entity object
* @return {object} _entity - Entity
*/
getEntity() {
if (this.isDestroyed) {
throw Error('this.getEntity is no longer valid. Property has been destroyed.');
}
return this._entity;
}
/**
* Gets the className of this Property type.
* @return {string} className
*/
getClassName() {
if (this.isDestroyed) {
throw Error('this.getClassName is no longer valid. Property has been destroyed.');
}
return this.__proto__.constructor.className;
}
/**
* Gets the mapped name of this Property.
* @return {string} name
*/
getMapping() {
if (this.isDestroyed) {
throw Error('this.getMapping is no longer valid. Property has been destroyed.');
}
return this.mapping;
}
/**
* Destroy this object.
* - Removes all circular references to parent objects
* - Removes child objects
* - Removes event listeners
* @fires destroy
*/
destroy() {
// parent objects
this._entity = null;
// child objects
this._originalConfig = null;
this.rawValue = null;
this.parsedValue = null;
this.emit('destroy');
this.isDestroyed = true;
// listeners
this.removeAllListeners();
}
toString() {
if (this.isDestroyed) {
throw Error('this.toString is no longer valid. Property has been destroyed.');
}
return 'Property {' + this.name + '} - ' + this.getDisplayValue();
}
};
Property.className = 'Auto';
Property.type = 'auto';