import {Definition} from "./Definition"; /** * Model properties definition type. */ export type ModelDefinition = Partial>>; /** * Model identifier type. */ export type ModelIdentifier = keyof T; /** * A Sharkitek model. */ export abstract class Model { /** * Model properties definition function. */ protected abstract SDefinition(): ModelDefinition; /** * Return the name of the model identifier property. */ protected SIdentifier(): ModelIdentifier { return undefined; } /** * Get given property definition. * @protected */ protected getPropertyDefinition(propertyName: string): Definition { return (this.SDefinition() as any)?.[propertyName]; } /** * Get the list of the model properties. * @protected */ protected getProperties(): string[] { return Object.keys(this.SDefinition()); } /** * Calling a function for a defined property. * @param propertyName - The property for which to check definition. * @param callback - The function called when the property is defined. * @param notProperty - The function called when the property is not defined. * @protected */ protected propertyWithDefinition(propertyName: string, callback: (propertyDefinition: Definition) => void, notProperty: () => void = () => {}): unknown { // Getting the current property definition. const propertyDefinition = this.getPropertyDefinition(propertyName); if (propertyDefinition) // There is a definition for the current property, calling the right callback. return callback(propertyDefinition); else // No definition for the given property, calling the right callback. return notProperty(); } /** * Calling a function for each defined property. * @param callback - The function to call. * @protected */ protected forEachModelProperty(callback: (propertyName: string, propertyDefinition: Definition) => unknown): any|void { for (const propertyName of this.getProperties()) { // For each property, checking that its type is defined and calling the callback with its type. const result = this.propertyWithDefinition(propertyName, (propertyDefinition) => { // If the property is defined, calling the function with the property name and definition. const result = callback(propertyName, propertyDefinition); // If there is a return value, returning it directly (loop is broken). if (typeof result !== "undefined") return result; }); // If there is a return value, returning it directly (loop is broken). if (typeof result !== "undefined") return result; } } /** * The original properties values. * @protected */ protected _originalProperties: Record = {}; /** * The original (serialized) object. * @protected */ protected _originalObject: any = null; /** * Determine if the model is new or not. */ isNew(): boolean { return !this._originalObject; } /** * Determine if the model is dirty or not. */ isDirty(): boolean { return this.forEachModelProperty((propertyName, propertyDefinition) => ( // For each property, checking if it is different. propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName]) // There is a difference, we should return false. ? true // There is no difference, returning nothing. : undefined )) === true; } /** * Get model identifier. */ getIdentifier(): unknown { return (this as any)[this.SIdentifier()]; } /** * Set current properties values as original values. */ resetDiff() { this.forEachModelProperty((propertyName, propertyDefinition) => { // For each property, set its original value to its current property value. this._originalProperties[propertyName] = (this as any)[propertyName]; propertyDefinition.type.resetDiff((this as any)[propertyName]); }); } /** * Serialize the difference between current model state and original one. */ serializeDiff(): any { // Creating a serialized object. const serializedDiff: any = {}; this.forEachModelProperty((propertyName, propertyDefinition) => { // For each defined model property, adding it to the serialized object if it has changed. if (this.SIdentifier() == propertyName || propertyDefinition.type.propertyHasChanged(this._originalProperties[propertyName], (this as any)[propertyName])) // Adding the current property to the serialized object if it is the identifier or its value has changed. serializedDiff[propertyName] = propertyDefinition.type.serializeDiff((this as any)[propertyName]); }) return serializedDiff; // Returning the serialized object. } /** * Get difference between original values and current ones, then reset it. * Similar to call `serializeDiff()` then `resetDiff()`. */ save(): any { // Get the difference. const diff = this.serializeDiff(); // Once the difference has been gotten, reset it. this.resetDiff(); return diff; // Return the difference. } /** * Serialize the model. */ serialize(): any { // Creating a serialized object. const serializedObject: any = {}; this.forEachModelProperty((propertyName, propertyDefinition) => { // For each defined model property, adding it to the serialized object. serializedObject[propertyName] = propertyDefinition.type.serialize((this as any)[propertyName]); }); return serializedObject; // Returning the serialized object. } /** * Special operations on parse. * @protected */ protected parse(): void {} // Nothing by default. TODO: create an event system to create functions like "beforeDeserialization" or "afterDeserialization". /** * Deserialize the model. */ deserialize(serializedObject: any): THIS { this.forEachModelProperty((propertyName, propertyDefinition) => { // For each defined model property, assigning its deserialized value to the model. (this as any)[propertyName] = propertyDefinition.type.deserialize(serializedObject[propertyName]); }); // Reset original property values. this.resetDiff(); this._originalObject = serializedObject; // The model is not a new one, but loaded from a deserialized one. return this as unknown as THIS; // Returning this, after deserialization. } }