src/model/ModelBase.js
import ModelValidation from './ModelValidation'
import { getJSONForProperties } from './helpers/json'
import {
hasModelValidationForProperty,
pickTypeFromModelValidation,
pickEmbeddedObjectFromModelValidation,
pickOwnerFromModelValidation,
updateModelFromResponseStatus,
} from './helpers/models'
const modelValidator = ModelValidation.getModelValidation()
export const DIRTY_PROPERTY_LIST = Symbol(
'List to keep track of dirty properties'
)
/**
* Base class that supplies functionality to the Model classes
*
* @memberof module:model
*/
class ModelBase {
constructor() {
const modelValidations = { modelValidations: {} }
Object.defineProperty(this, 'modelDefinition', {
writable: true,
value: modelValidations,
})
}
/**
* @returns {Promise} Returns a promise that resolves when the model has been saved or rejected with the result from
* the `validate()` call.
*
* @definition
* Will save model as a new object to the server using a POST request. This method would generally be used if
* you're creating models with pre-specified IDs. Note that this does not check if the model is marked as dirty.
*/
create() {
return this.validate().then((validationState) => {
if (!validationState.status) {
return Promise.reject(validationState)
}
return this.modelDefinition
.saveNew(this)
.then((apiResponse) =>
updateModelFromResponseStatus.call(this, apiResponse)
)
})
}
/**
* @returns {Promise} Returns a promise that resolves when the model has been saved
* or rejects with the result from the `validate()` call.
*
* @description
* Checks if the model is dirty. When the model is dirty it will check if the values of the model are valid by calling
* `validate`. If this is correct it will attempt to save the [Model](#/model/Model) to the api.
*
* ```js
* myModel.save()
* .then((message) => console.log(message));
* ```
*/
save(includeChildren) {
// Calling save when there's nothing to be saved is a no-op
if (!this.isDirty(includeChildren)) {
return Promise.resolve({})
}
return this.validate().then((validationState) => {
if (!validationState.status) {
return Promise.reject(validationState)
}
return this.modelDefinition
.save(this)
.then((apiResponse) =>
updateModelFromResponseStatus.call(this, apiResponse)
)
})
}
/**
* @returns {Promise} Promise that resolves with an object with a status property that represents if the model
* is valid or not the fields array will return the names of the fields that are invalid.
*
* @description
* This will run the validations on the properties which have validations set. Normally these validations are defined
* through the DHIS2 schema. It will check min/max for strings/numbers etc. Additionally it will
* run model validations against the schema.
*
* ```js
* myModel.validate()
* .then(myModelStatus => {
* if (myModelStatus.status === false) {
* myModelStatus.fields.forEach((fieldName) => console.log(fieldName));
* }
* });
* ```
*
* @deprecated The api now validates the object on save, so doing the additional request to validate the object
* is not very useful anymore as the validation on POST/PUT is more extensive than the /api/schemas validation.
*/
validate() {
return new Promise((resolve, reject) => {
let validationMessages = []
function unique(current, property) {
if (property && current.indexOf(property) === -1) {
current.push(property)
}
return current
}
function asyncRemoteValidation(model) {
return modelValidator.validateAgainstSchema(model)
}
// Run async validation against the api
asyncRemoteValidation(this)
.catch((remoteMessages) => {
// Errors are ok in this case
if (Array.isArray(remoteMessages)) {
return remoteMessages
}
return Promise.reject(remoteMessages)
})
.then((remoteMessages) => {
validationMessages =
validationMessages.concat(remoteMessages)
const validationState = {
status: remoteMessages.length === 0,
fields: validationMessages
.map(
(validationMessage) =>
validationMessage.property
)
.reduce(unique, []),
messages: validationMessages,
}
resolve(validationState)
})
.catch((message) => reject(message))
})
}
// TODO: Cloning large graphs is very slow
clone() {
const modelClone = this.modelDefinition.create(
getJSONForProperties(
this,
Object.keys(this.modelDefinition.modelValidations),
true
)
)
if (this.isDirty()) {
modelClone.dirty = this.isDirty(true)
}
return modelClone
}
/**
* Deletes the object from the server.
*
* This will fire a DELETE request to have the object be removed from the system.
*
* @returns {Promise} Resolves when the object was successfully deleted.
*/
delete() {
return this.modelDefinition.delete(this)
}
/**
* Check if the model is in a dirty state and is a candidate to be saved.
*
* It will check for direct properties that have been changed and if any of the children have been changed.
*
* @param {boolean} includeChildren If set to false only the models direct properties will be checked.
* @returns {boolean} Returns true when the model is in a dirty state.
*/
isDirty(includeChildren = true) {
return (
this.dirty || (includeChildren === true && this.hasDirtyChildren())
)
}
/**
* Utility method to reset the dirty state of the object.
*
* This is called after a successful save operation was done.
*
* @returns {ModelBase} Returns itself for potential chaining
*/
resetDirtyState() {
this.dirty = false
// Also set it's children to be clean
this.getDirtyChildren().forEach((value) => {
if (value.resetDirtyState) {
value.resetDirtyState()
} else {
value.dirty = false
}
})
this[DIRTY_PROPERTY_LIST].clear()
return this
}
/**
* Returns a list of properties that have been changed.
*
* @returns {Array<string>} The names of the properties that were changed.
*/
getDirtyPropertyNames() {
return Array.from(this[DIRTY_PROPERTY_LIST].values())
}
/**
* This will return the properties that are marked as `owner: true` in the schema definition for the model.
*
* @returns {Array<any>} Returns an array of properties that are owned by the object
*/
// TODO: This name is very misleading and should probably be renamed to something like `getOwnerProperties` (would be a breaking change)
getCollectionChildren() {
return Object.keys(this)
.filter(
(propertyName) =>
this[propertyName] &&
hasModelValidationForProperty(this, propertyName) &&
pickOwnerFromModelValidation(propertyName, this)
)
.map((propertyName) => this[propertyName])
}
/**
* Gets the names of the properties that are collections on the object.
*
* These are usually the properties that contain ModelCollectionProperties.
*
* @returns {Array<string>} A list of property names that are marked as type `COLLECTION` in the schema.
*/
getCollectionChildrenPropertyNames() {
return Object.keys(this).filter(
(propertyName) =>
pickTypeFromModelValidation(propertyName, this) === 'COLLECTION'
)
}
/**
* Gets the names of the properties that are references on the object.
*
* These are usually the properties that contain a Model of a different type. (e.g DataElement -> CategoryCombo)
*
* @returns {Array<string>} A list of property names that are marked as type `REFERENCE` in the schema.
*/
getReferenceProperties() {
return Object.keys(this).filter(
(propertyName) =>
pickTypeFromModelValidation(propertyName, this) ===
'REFERENCE' &&
pickEmbeddedObjectFromModelValidation(propertyName, this) ===
false
)
}
/**
* Gets the names of the properties that are embedded objects.
*
* These the properties that are not represented by a different Model, but are just plain objects that are
* embedded within the current object.
*
* @returns {Array<string>} A list of property names of embedded objects.
*/
getEmbeddedObjectCollectionPropertyNames() {
return this.getCollectionChildrenPropertyNames().filter(
(propertyName) =>
pickEmbeddedObjectFromModelValidation(propertyName, this)
)
}
/**
* Returns a list of child properties that are marked as dirty. This uses the `getCollectionChildren()` method
* to retrieve the children properties and then checks if they are marked as dirty.
*
* The method does not check if direct properties are dirty as those are tracked on the Model itself.
*
* @returns {Array<any>}
*/
getDirtyChildren() {
return this.getCollectionChildren().filter(
(property) => property && property.dirty === true
)
}
/**
* Check if any of the Model's child collections are dirty.
*
* @returns {boolean} True when one of the children is dirty.
*/
hasDirtyChildren() {
return this.getDirtyChildren().length > 0
}
/**
* This method is generally intended to, by default, usefully serialize Model objects during JSON serialization.
*
* This method will take all the properties that are defined on the schema and create an object with the keys and
* values for those properties. This will remove any circular dependencies that could have occurred otherwise.
*
* @returns {Object}
*/
toJSON() {
return getJSONForProperties(
this,
Object.keys(this.modelDefinition.modelValidations)
)
}
}
export default ModelBase